1use std::collections::HashMap;
4use std::ffi::CString;
5use std::fmt::Write as _;
6use std::sync::{LazyLock, Mutex};
7#[cfg(target_os = "linux")]
8use std::{fs, path::Path};
9
10use crate::convert::{encode_adapter_error, reject_unused_encode_options, video_encoder_options};
11use crate::video_encode::VideoEncodeConfig;
12use alpha2_ffmpeg_bridge::{EncoderContext, LogLine};
13use alpha2_media::{ColorMetadata, EncodeError, EncoderRefusal, OpenFailureKind};
14use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef};
15use rsmpeg::avutil::AVRational;
16use rsmpeg::error::RsmpegError;
17use rsmpeg::ffi;
18
19#[derive(Copy, Clone, Debug, PartialEq)]
21pub struct AvEncodeConfig {
22 pub video: VideoEncodeConfig,
24 pub audio_sample_rate: u32,
26 pub audio_channels: u16,
28 pub audio_codec: NativeAudioCodec,
30 pub audio_bit_rate: u32,
32}
33
34#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
36#[non_exhaustive]
37pub enum NativeVideoCodec {
38 H264,
40 Hevc,
42 Vp9,
44 Av1,
46 Mpeg4Part2,
48 ProRes,
50}
51
52impl NativeVideoCodec {
53 #[must_use]
55 pub const fn as_str(self) -> &'static str {
56 match self {
57 Self::H264 => "h264",
58 Self::Hevc => "hevc",
59 Self::Vp9 => "vp9",
60 Self::Av1 => "av1",
61 Self::Mpeg4Part2 => "mpeg4-part2",
62 Self::ProRes => "prores",
63 }
64 }
65
66 #[must_use]
69 pub const fn base_layout(self) -> alpha2_media::PixelLayout {
70 match self {
71 Self::ProRes => alpha2_media::PixelLayout {
72 chroma: alpha2_media::Chroma::Yuv422,
73 bit_depth: alpha2_media::BitDepth::Ten,
74 alpha: false,
75 planes: alpha2_media::PlaneLayout::Planar,
76 },
77 _ => alpha2_media::PixelLayout::YUV420P,
78 }
79 }
80
81 #[must_use]
83 pub const fn model(self) -> alpha2_media::VideoCodec {
84 match self {
85 Self::H264 => alpha2_media::VideoCodec::H264,
86 Self::Hevc => alpha2_media::VideoCodec::Hevc,
87 Self::Vp9 => alpha2_media::VideoCodec::Vp9,
88 Self::Av1 => alpha2_media::VideoCodec::Av1,
89 Self::Mpeg4Part2 => alpha2_media::VideoCodec::Mpeg4Part2,
90 Self::ProRes => alpha2_media::VideoCodec::ProRes,
91 }
92 }
93
94 #[must_use]
96 pub const fn from_model(codec: alpha2_media::VideoCodec) -> Self {
97 match codec {
98 alpha2_media::VideoCodec::H264 => Self::H264,
99 alpha2_media::VideoCodec::Hevc => Self::Hevc,
100 alpha2_media::VideoCodec::Vp9 => Self::Vp9,
101 alpha2_media::VideoCodec::Av1 => Self::Av1,
102 alpha2_media::VideoCodec::Mpeg4Part2 => Self::Mpeg4Part2,
103 alpha2_media::VideoCodec::ProRes => Self::ProRes,
104 }
105 }
106
107 pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
108 match self {
109 Self::H264 => ffi::AV_CODEC_ID_H264,
110 Self::Hevc => ffi::AV_CODEC_ID_HEVC,
111 Self::Vp9 => ffi::AV_CODEC_ID_VP9,
112 Self::Av1 => ffi::AV_CODEC_ID_AV1,
113 Self::Mpeg4Part2 => ffi::AV_CODEC_ID_MPEG4,
114 Self::ProRes => ffi::AV_CODEC_ID_PRORES,
115 }
116 }
117}
118
119#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
121#[non_exhaustive]
122pub enum NativeAudioCodec {
123 Aac,
125 Opus,
127}
128
129impl NativeAudioCodec {
130 pub const ALL: [Self; 2] = [Self::Aac, Self::Opus];
132
133 #[must_use]
135 pub const fn as_str(self) -> &'static str {
136 match self {
137 Self::Aac => "aac",
138 Self::Opus => "opus",
139 }
140 }
141
142 #[must_use]
144 pub const fn model(self) -> alpha2_media::AudioCodec {
145 match self {
146 Self::Aac => alpha2_media::AudioCodec::Aac,
147 Self::Opus => alpha2_media::AudioCodec::Opus,
148 }
149 }
150
151 #[must_use]
153 pub const fn from_model(codec: alpha2_media::AudioCodec) -> Self {
154 match codec {
155 alpha2_media::AudioCodec::Aac => Self::Aac,
156 alpha2_media::AudioCodec::Opus => Self::Opus,
157 }
158 }
159
160 pub(crate) const fn encoder_name(self) -> &'static std::ffi::CStr {
163 match self {
164 Self::Aac => c"aac",
165 Self::Opus => c"libopus",
166 }
167 }
168}
169
170pub(crate) fn audio_encoder(codec: NativeAudioCodec) -> Option<AVCodecRef<'static>> {
172 AVCodec::find_encoder_by_name(codec.encoder_name()).filter(|encoder| {
173 u32::try_from(encoder.capabilities).unwrap_or(0) & ffi::AV_CODEC_CAP_EXPERIMENTAL == 0
174 })
175}
176
177#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
183#[non_exhaustive]
184pub enum NativeEncoderClass {
185 Software,
187 Hardware,
189 Hybrid,
192}
193
194impl NativeEncoderClass {
195 #[must_use]
197 pub const fn as_str(self) -> &'static str {
198 match self {
199 Self::Software => "software",
200 Self::Hardware => "hardware",
201 Self::Hybrid => "hybrid",
202 }
203 }
204
205 #[must_use]
208 pub const fn modes(self, hybrid_hardware: bool) -> &'static [bool] {
209 match self {
210 Self::Hardware => &[true],
211 Self::Hybrid if hybrid_hardware => &[true, false],
212 Self::Software | Self::Hybrid => &[false],
213 }
214 }
215}
216
217pub(crate) struct VideoEncoderCandidate {
219 pub(crate) encoder: AVCodecRef<'static>,
220 pub(crate) on_hardware: bool,
221}
222
223impl VideoEncoderCandidate {
224 pub(crate) fn name(&self) -> String {
225 self.encoder.name().to_string_lossy().into_owned()
226 }
227
228 pub(crate) fn codec(&self) -> NativeVideoCodec {
229 [
230 NativeVideoCodec::H264,
231 NativeVideoCodec::Hevc,
232 NativeVideoCodec::Vp9,
233 NativeVideoCodec::Av1,
234 NativeVideoCodec::Mpeg4Part2,
235 NativeVideoCodec::ProRes,
236 ]
237 .into_iter()
238 .find(|codec| codec.codec_id() == self.encoder.id)
239 .unwrap_or(NativeVideoCodec::H264)
240 }
241}
242
243#[must_use]
249pub fn hybrid_encoders_use_hardware() -> bool {
250 std::env::var("ALPHA2_VIDEO_ENCODER_HYBRID")
251 .is_ok_and(|value| value.eq_ignore_ascii_case("hardware"))
252}
253
254#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
256#[non_exhaustive]
257pub enum NativeEncoderRejection {
258 Dimensions,
260 OpenFailed,
262 OptionsIgnored,
264 PixelFormat,
266}
267
268impl NativeEncoderRejection {
269 #[must_use]
271 pub const fn as_str(self) -> &'static str {
272 match self {
273 Self::Dimensions => "dimensions",
274 Self::OpenFailed => "open-failed",
275 Self::OptionsIgnored => "options-ignored",
276 Self::PixelFormat => "pixel-format",
277 }
278 }
279}
280
281#[derive(Clone, Debug, Eq, PartialEq)]
283pub struct NativeVideoEncoderFallback {
284 pub encoder_name: String,
286 pub hardware_accelerated: bool,
288 pub reason: NativeEncoderRejection,
290 pub kind: OpenFailureKind,
292 pub detail: String,
294}
295
296impl NativeVideoEncoderFallback {
297 #[must_use]
299 pub fn refusal(&self) -> EncoderRefusal {
300 EncoderRefusal {
301 encoder: self.encoder_name.clone(),
302 hardware_accelerated: self.hardware_accelerated,
303 kind: self.kind,
304 reason: self.detail.clone(),
305 }
306 }
307}
308
309#[derive(Clone, Debug, Eq, PartialEq)]
313pub struct NativeVideoEncoderSelection {
314 pub encoder_name: String,
316 pub class: NativeEncoderClass,
318 pub hardware_accelerated: bool,
320 pub rejected: Vec<NativeVideoEncoderFallback>,
322 pub rate_control: crate::AppliedRateControl,
324 pub quantizer_refused: Option<String>,
326 pub software_threads: Option<i64>,
328 pub pixels: alpha2_media::PixelLayout,
331}
332
333#[derive(Clone, Debug, Eq, PartialEq)]
335pub struct NativeVideoEncoderCapability {
336 pub codec: NativeVideoCodec,
338 pub encoder_name: String,
340 pub class: NativeEncoderClass,
342 pub hardware_accelerated: bool,
344 pub preferred: bool,
346 pub minimum_width: u32,
348 pub minimum_height: u32,
350 pub library: Option<NativeEncoderLibrary>,
352}
353
354#[derive(Clone, Debug, Eq, PartialEq)]
356pub struct NativeEncoderLibrary {
357 pub provider: &'static str,
359 pub version: String,
361 pub path: String,
363 pub notice: &'static str,
365}
366
367impl NativeEncoderLibrary {
368 fn openh264(library: &crate::OpenH264Library) -> Self {
369 Self {
370 provider: library.provider().as_str(),
371 version: library.version().to_owned(),
372 path: library.path().to_owned(),
373 notice: crate::OPENH264_NOTICE,
374 }
375 }
376}
377
378#[derive(Clone, Debug, Eq, PartialEq)]
380pub struct NativeAudioEncoderCapability {
381 pub codec: NativeAudioCodec,
383 pub encoder_name: String,
385}
386
387#[derive(Clone, Debug, Eq, PartialEq)]
392pub struct NativeCodecCapabilities {
393 pub video: Vec<NativeVideoEncoderCapability>,
396 pub audio: Vec<NativeAudioEncoderCapability>,
399}
400
401#[must_use]
407pub fn codec_capabilities() -> NativeCodecCapabilities {
408 let video = [
409 NativeVideoCodec::H264,
410 NativeVideoCodec::Hevc,
411 NativeVideoCodec::Vp9,
412 NativeVideoCodec::Av1,
413 NativeVideoCodec::Mpeg4Part2,
414 NativeVideoCodec::ProRes,
415 ]
416 .into_iter()
417 .flat_map(|codec| video_encoder_inventory(codec).usable)
418 .collect();
419 NativeCodecCapabilities {
420 video,
421 audio: audio_encoder_capabilities(),
422 }
423}
424
425#[must_use]
427pub fn encodes_pixels(
428 capabilities: &NativeCodecCapabilities,
429 codec: NativeVideoCodec,
430 layout: alpha2_media::PixelLayout,
431) -> bool {
432 capabilities
433 .video
434 .iter()
435 .filter(|capability| capability.codec == codec)
436 .any(|capability| {
437 capability.encoder_name == crate::OPENH264_ENCODER_NAME && layout.is_yuv420p()
438 || std::ffi::CString::new(capability.encoder_name.as_str())
439 .ok()
440 .and_then(|name| AVCodec::find_encoder_by_name(&name))
441 .is_some_and(|encoder| encoder_layout(&encoder, layout).is_some())
442 })
443}
444
445#[must_use]
447pub fn audio_encoder_capabilities() -> Vec<NativeAudioEncoderCapability> {
448 NativeAudioCodec::ALL
449 .into_iter()
450 .filter_map(|codec| {
451 let encoder = audio_encoder(codec)?;
452 Some(NativeAudioEncoderCapability {
453 codec,
454 encoder_name: encoder.name().to_string_lossy().into_owned(),
455 })
456 })
457 .collect()
458}
459
460#[derive(Clone, Debug, Default, Eq, PartialEq)]
462pub struct EncoderInventory {
463 pub usable: Vec<NativeVideoEncoderCapability>,
465 pub refused: Vec<EncoderRefusal>,
467}
468
469#[must_use]
473pub fn video_encoder_inventory(codec: NativeVideoCodec) -> EncoderInventory {
474 let (choices, refused) = available_video_encoders(codec);
475 let usable = choices
476 .into_iter()
477 .enumerate()
478 .map(|(index, choice)| {
479 let encoder_name = choice.name();
480 let (minimum_width, minimum_height) = encoder_minimum_dimensions(&encoder_name);
481 NativeVideoEncoderCapability {
482 codec,
483 class: choice.class(),
484 hardware_accelerated: choice.on_hardware(),
485 preferred: index == 0,
486 encoder_name,
487 minimum_width,
488 minimum_height,
489 library: match &choice {
490 VideoEncoderChoice::Ffmpeg(_) => None,
491 VideoEncoderChoice::OpenH264(library) => {
492 Some(NativeEncoderLibrary::openh264(library))
493 }
494 },
495 }
496 })
497 .collect();
498 EncoderInventory { usable, refused }
499}
500
501pub(crate) enum VideoEncoderChoice {
503 Ffmpeg(VideoEncoderCandidate),
504 OpenH264(std::sync::Arc<crate::OpenH264Library>),
505}
506
507impl VideoEncoderChoice {
508 pub(crate) fn name(&self) -> String {
509 match self {
510 Self::Ffmpeg(candidate) => candidate.name(),
511 Self::OpenH264(_) => crate::OPENH264_ENCODER_NAME.to_owned(),
512 }
513 }
514
515 pub(crate) const fn on_hardware(&self) -> bool {
516 match self {
517 Self::Ffmpeg(candidate) => candidate.on_hardware,
518 Self::OpenH264(_) => false,
519 }
520 }
521
522 pub(crate) fn class(&self) -> NativeEncoderClass {
523 match self {
524 Self::Ffmpeg(candidate) => encoder_class(&candidate.encoder),
525 Self::OpenH264(_) => NativeEncoderClass::Software,
526 }
527 }
528}
529
530pub(crate) fn available_video_encoders(
533 codec: NativeVideoCodec,
534) -> (Vec<VideoEncoderChoice>, Vec<EncoderRefusal>) {
535 let (mut choices, refused) = compatible_encoders(registered_video_encoders(codec));
536 if codec == NativeVideoCodec::H264
537 && let Some(library) = crate::openh264_status().library()
538 {
539 choices.push(VideoEncoderChoice::OpenH264(std::sync::Arc::clone(library)));
540 }
541 (choices, refused)
542}
543
544fn compatible_encoders(
547 candidates: Vec<VideoEncoderCandidate>,
548) -> (Vec<VideoEncoderChoice>, Vec<EncoderRefusal>) {
549 let mut choices = Vec::new();
550 let mut refused = Vec::new();
551 for candidate in candidates {
552 let name = candidate.name();
553 if !registration_is_compatible(&candidate, &name) {
554 continue;
555 }
556 match encoder_runs_here(&candidate, &name) {
557 Ok(()) => choices.push(VideoEncoderChoice::Ffmpeg(candidate)),
558 Err(refusal) => refused.push(refusal),
559 }
560 }
561 (choices, refused)
562}
563
564#[derive(Clone, Debug, Eq, PartialEq)]
566pub struct NativeHardwareDecodeCapability {
567 pub codec: NativeVideoCodec,
569 pub decoder_name: Option<String>,
571 pub hardware_config: bool,
575 pub software_decoder: Option<String>,
578}
579
580#[derive(Clone, Debug, Eq, PartialEq)]
582pub struct NativeHardwareDecodeCapabilities {
583 pub backend: &'static str,
585 pub codecs: Vec<NativeHardwareDecodeCapability>,
587}
588
589#[must_use]
591pub const fn hardware_decode_backend() -> &'static str {
592 #[cfg(feature = "hwdecode")]
593 {
594 #[cfg(target_os = "linux")]
595 {
596 "vulkan"
597 }
598 #[cfg(target_os = "macos")]
599 {
600 "videotoolbox"
601 }
602 #[cfg(target_os = "windows")]
603 {
604 "d3d12va"
605 }
606 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
607 {
608 "none"
609 }
610 }
611 #[cfg(not(feature = "hwdecode"))]
612 {
613 "none"
614 }
615}
616
617#[must_use]
619pub fn hardware_decode_capabilities() -> NativeHardwareDecodeCapabilities {
620 let codecs = [
621 NativeVideoCodec::H264,
622 NativeVideoCodec::Hevc,
623 NativeVideoCodec::Av1,
624 NativeVideoCodec::Vp9,
625 ]
626 .into_iter()
627 .map(|codec| {
628 let decoder = AVCodec::find_decoder(codec.codec_id());
629 NativeHardwareDecodeCapability {
630 codec,
631 decoder_name: decoder
632 .as_ref()
633 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
634 hardware_config: crate::decode::registered_video_decoders(codec.codec_id())
635 .any(|decoder| decoder_has_hardware_config(&decoder)),
636 software_decoder: crate::decode::software_video_decoder(codec.codec_id())
637 .ok()
638 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
639 }
640 })
641 .collect();
642 NativeHardwareDecodeCapabilities {
643 backend: hardware_decode_backend(),
644 codecs,
645 }
646}
647
648#[allow(unused_variables)]
650fn decoder_has_hardware_config(decoder: &AVCodecRef<'_>) -> bool {
651 #[cfg(all(
652 any(target_os = "linux", target_os = "macos", target_os = "windows"),
653 feature = "hwdecode"
654 ))]
655 {
656 #[cfg(target_os = "linux")]
657 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_VULKAN, ffi::AV_PIX_FMT_VULKAN);
658 #[cfg(target_os = "macos")]
659 let (device_type, pixel_format) = (
660 ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
661 ffi::AV_PIX_FMT_VIDEOTOOLBOX,
662 );
663 #[cfg(target_os = "windows")]
664 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_D3D12VA, ffi::AV_PIX_FMT_D3D12);
665 (0..)
666 .map_while(|index| decoder.hw_config(index))
667 .any(|config| config.device_type == device_type && config.pix_fmt == pixel_format)
668 }
669 #[cfg(not(all(
670 any(target_os = "linux", target_os = "macos", target_os = "windows"),
671 feature = "hwdecode"
672 )))]
673 {
674 false
675 }
676}
677
678#[cfg(test)]
681pub(crate) fn test_skips_encoder(name: &std::ffi::CStr) -> bool {
682 static SKIPPED: LazyLock<Vec<String>> = LazyLock::new(|| {
683 std::env::var("ALPHA2_TEST_SKIP_ENCODERS")
684 .unwrap_or_default()
685 .split(',')
686 .map(str::trim)
687 .filter(|name| !name.is_empty())
688 .map(str::to_owned)
689 .collect()
690 });
691 let name = name.to_string_lossy();
692 SKIPPED.iter().any(|skipped| *skipped == name)
693}
694
695#[cfg(not(test))]
696const fn test_skips_encoder(_name: &std::ffi::CStr) -> bool {
697 false
698}
699
700pub(crate) fn registered_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderCandidate> {
702 let hybrid_hardware = hybrid_encoders_use_hardware();
703 let mut candidates = AVCodec::iterate()
704 .filter(|encoder| {
705 encoder.id == codec.codec_id()
706 && (codec != NativeVideoCodec::ProRes || encoder.name() == c"prores_ks")
708 && !test_skips_encoder(encoder.name())
709 && AVCodec::find_encoder_by_name(encoder.name())
710 .is_some_and(|found| found.as_ptr() == encoder.as_ptr())
711 })
712 .flat_map(|encoder| {
713 encoder_class(&encoder)
714 .modes(hybrid_hardware)
715 .iter()
716 .filter_map(move |&on_hardware| {
717 AVCodec::find_encoder_by_name(encoder.name()).map(|encoder| {
718 VideoEncoderCandidate {
719 encoder,
720 on_hardware,
721 }
722 })
723 })
724 })
725 .collect::<Vec<_>>();
726 candidates.sort_by_key(|candidate| !candidate.on_hardware);
728 candidates
729}
730
731pub(crate) fn encoder_class(encoder: &AVCodec) -> NativeEncoderClass {
733 let name = encoder.name().to_string_lossy();
734 declared_encoder_class(&name, u32::try_from(encoder.capabilities).unwrap_or(0))
735}
736
737const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
743
744pub(crate) fn declared_encoder_class(name: &str, capabilities: u32) -> NativeEncoderClass {
747 if capabilities & ffi::AV_CODEC_CAP_HYBRID != 0 {
748 NativeEncoderClass::Hybrid
749 } else if capabilities & ffi::AV_CODEC_CAP_HARDWARE != 0
750 || MISDECLARED_HARDWARE_ENCODERS.contains(&name)
751 {
752 NativeEncoderClass::Hardware
753 } else {
754 NativeEncoderClass::Software
755 }
756}
757
758#[cfg(test)]
759pub(crate) fn encoder_is_compatible(candidate: &VideoEncoderCandidate) -> bool {
760 let name = candidate.name();
761 registration_is_compatible(candidate, &name) && encoder_runs_here(candidate, &name).is_ok()
762}
763
764fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
767 let codec = candidate.codec();
769 if encoder_layout(&candidate.encoder, codec.base_layout()).is_none() {
770 return false;
771 }
772 !candidate.on_hardware || hardware_device_node(name) != Some(false)
773}
774
775pub(crate) fn encoder_layout(
778 encoder: &AVCodec,
779 layout: alpha2_media::PixelLayout,
780) -> Option<alpha2_media::PixelLayout> {
781 let Some(formats) = encoder.pix_fmts() else {
782 return Some(layout);
783 };
784 [layout, layout.as_p010()]
785 .into_iter()
786 .find(|candidate| formats.contains(&crate::convert::pixel_format(*candidate)))
787}
788
789pub(crate) fn needs_open_probe(on_hardware: bool, name: &str) -> bool {
795 (on_hardware && (hardware_device_node(name).is_none() || name.ends_with("_nvenc")))
796 || name.ends_with("_mf")
797 || name.ends_with("_videotoolbox")
798}
799
800fn encoder_runs_here(candidate: &VideoEncoderCandidate, name: &str) -> Result<(), EncoderRefusal> {
811 probe_outcome(candidate, name, true).unwrap_or(Ok(()))
812}
813
814#[derive(Clone, Debug, Eq, PartialEq)]
816pub(crate) enum ProbeVerdict {
817 Runs,
819 Refused(OpenFailureKind, String),
821}
822
823pub(crate) const UNCLASSIFIED_FAILURES_KEPT: u8 = 3;
827
828#[derive(Debug, Default)]
830pub(crate) struct ProbeEntry {
831 verdict: Option<ProbeVerdict>,
832 unclassified_failures: u8,
833}
834
835pub(crate) type ProbeCache = Mutex<HashMap<(String, bool), ProbeEntry>>;
837
838fn probe_outcome(
841 candidate: &VideoEncoderCandidate,
842 name: &str,
843 probe: bool,
844) -> Option<Result<(), EncoderRefusal>> {
845 static VERDICTS: LazyLock<ProbeCache> = LazyLock::new(Mutex::default);
846 if !needs_open_probe(candidate.on_hardware, name) {
847 return Some(Ok(()));
848 }
849 cached_probe(
850 &VERDICTS,
851 name,
852 candidate.on_hardware,
853 probe.then_some(|| probe_open(candidate, name)),
854 )
855}
856
857pub(crate) fn cached_probe(
861 cache: &ProbeCache,
862 name: &str,
863 on_hardware: bool,
864 probe: Option<impl FnOnce() -> Result<(), (OpenFailureKind, String)>>,
865) -> Option<Result<(), EncoderRefusal>> {
866 let refusal = |kind, reason| EncoderRefusal {
867 encoder: name.to_owned(),
868 hardware_accelerated: on_hardware,
869 kind,
870 reason,
871 };
872 let mut entries = cache
874 .lock()
875 .unwrap_or_else(std::sync::PoisonError::into_inner);
876 let entry = entries.entry((name.to_owned(), on_hardware)).or_default();
877 match &entry.verdict {
878 Some(ProbeVerdict::Runs) => return Some(Ok(())),
879 Some(ProbeVerdict::Refused(kind, reason)) => {
880 return Some(Err(refusal(*kind, reason.clone())));
881 }
882 None => {}
883 }
884 let outcome = probe?();
885 entry.unclassified_failures = match &outcome {
886 Err((OpenFailureKind::Unknown, _)) => entry.unclassified_failures.saturating_add(1),
887 _ => 0,
888 };
889 entry.verdict = match &outcome {
890 Ok(()) => Some(ProbeVerdict::Runs),
891 Err((kind @ (OpenFailureKind::Absent | OpenFailureKind::Unsuitable), reason)) => {
892 Some(ProbeVerdict::Refused(*kind, reason.clone()))
893 }
894 Err((OpenFailureKind::Unknown, reason))
895 if entry.unclassified_failures >= UNCLASSIFIED_FAILURES_KEPT =>
896 {
897 Some(ProbeVerdict::Refused(
898 OpenFailureKind::Unknown,
899 reason.clone(),
900 ))
901 }
902 Err(_) => None,
903 };
904 if let Err((kind, error)) = &outcome {
905 alpha2_os::diagnostic!(
906 "alpha2 native: video encoder {name} {} is registered but did not encode a frame on \
907 this machine, so it is not offered that way ({}{}): {error}",
908 mode_label(on_hardware),
909 kind.as_str(),
910 if entry.verdict.is_some() {
911 ""
912 } else {
913 "; asked again at the next export"
914 }
915 );
916 }
917 Some(outcome.map_err(|(kind, reason)| refusal(kind, reason)))
918}
919
920fn probe_open(
922 candidate: &VideoEncoderCandidate,
923 name: &str,
924) -> Result<(), (OpenFailureKind, String)> {
925 let mark = alpha2_ffmpeg_bridge::log_mark();
926 match probe_software_encode(candidate, name, 256, 144, None).0 {
927 None => Ok(()),
928 Some(error) => {
929 let lines = alpha2_ffmpeg_bridge::log_since(mark, &[name]);
930 Err((
931 classify_open_failure(name, &error, &lines),
932 format!("{error}{}", alpha2_ffmpeg_bridge::log_detail(&lines)),
933 ))
934 }
935 }
936}
937
938pub(crate) fn classify_open_failure(
945 encoder: &str,
946 error: &str,
947 lines: &[LogLine],
948) -> OpenFailureKind {
949 let said =
950 |words: &str| error.contains(words) || lines.iter().any(|line| line.text.contains(words));
951 if encoder.ends_with("_nvenc") {
952 return match lines.iter().find_map(|line| nvenc_status(&line.text)) {
957 Some(10 | 13 | 18 | 21) => OpenFailureKind::Busy,
958 Some(1..=5) => OpenFailureKind::Absent,
959 None if said("No capable devices found") || said("Cannot load") => {
960 OpenFailureKind::Absent
961 }
962 _ => OpenFailureKind::Unknown,
963 };
964 }
965 if encoder.ends_with("_vulkan") {
966 let busy = lines.iter().any(|line| {
967 line.text.contains("Unable to create video session")
968 && (line.text.contains("VK_ERROR_OUT_OF_DEVICE_MEMORY")
969 || line.text.contains("VK_ERROR_TOO_MANY_OBJECTS"))
970 });
971 return if busy {
972 OpenFailureKind::Busy
973 } else {
974 OpenFailureKind::Unknown
975 };
976 }
977 if encoder.ends_with("_videotoolbox") {
978 if said("-12915") {
980 return OpenFailureKind::Busy;
981 }
982 if said("-12908") {
983 return OpenFailureKind::Absent;
984 }
985 return OpenFailureKind::Unknown;
986 }
987 if encoder.ends_with("_mf")
989 && (said("could not find any MFT") || said("E_FAIL") || said("80004005"))
990 {
991 return OpenFailureKind::Absent;
992 }
993 OpenFailureKind::Unknown
994}
995
996fn nvenc_status(line: &str) -> Option<u32> {
998 line.match_indices('(').find_map(|(start, _)| {
999 let rest = &line[start + 1..];
1000 let end = rest.find(')')?;
1001 let digits = &rest[..end];
1002 (!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()))
1003 .then(|| digits.parse().ok())
1004 .flatten()
1005 })
1006}
1007
1008#[must_use]
1011pub fn video_encoders_without_probing() -> (Vec<NativeVideoEncoderCapability>, Vec<String>) {
1012 let mut unprobed = Vec::new();
1013 let mut listed = Vec::new();
1014 for codec in [
1015 NativeVideoCodec::H264,
1016 NativeVideoCodec::Hevc,
1017 NativeVideoCodec::Vp9,
1018 NativeVideoCodec::Av1,
1019 NativeVideoCodec::Mpeg4Part2,
1020 NativeVideoCodec::ProRes,
1021 ] {
1022 for candidate in registered_video_encoders(codec) {
1023 let name = candidate.name();
1024 if !registration_is_compatible(&candidate, &name) {
1025 continue;
1026 }
1027 match probe_outcome(&candidate, &name, false) {
1028 Some(Ok(())) => {
1029 let (minimum_width, minimum_height) = encoder_minimum_dimensions(&name);
1030 listed.push(NativeVideoEncoderCapability {
1031 codec,
1032 class: encoder_class(&candidate.encoder),
1033 hardware_accelerated: candidate.on_hardware,
1034 preferred: !listed
1035 .iter()
1036 .any(|listed: &NativeVideoEncoderCapability| listed.codec == codec),
1037 encoder_name: name,
1038 minimum_width,
1039 minimum_height,
1040 library: None,
1041 });
1042 }
1043 Some(Err(_)) => {}
1044 None => unprobed.push(format!("{name} {}", mode_label(candidate.on_hardware))),
1045 }
1046 }
1047 }
1048 (listed, unprobed)
1049}
1050
1051pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
1052 if on_hardware { "on GPU" } else { "on CPU" }
1053}
1054
1055#[cfg(target_os = "linux")]
1061fn hardware_device_node(name: &str) -> Option<bool> {
1062 if name.ends_with("_nvenc") {
1063 return Some(Path::new("/dev/nvidia0").exists() || Path::new("/dev/nvidiactl").exists());
1064 }
1065 if name.ends_with("_vaapi") || name.ends_with("_qsv") || name.ends_with("_amf") {
1066 return Some(fs::read_dir("/dev/dri").is_ok_and(|entries| {
1067 entries.flatten().any(|entry| {
1068 entry
1069 .file_name()
1070 .to_str()
1071 .is_some_and(|name| name.starts_with("renderD") || name.starts_with("card"))
1072 })
1073 }));
1074 }
1075 if name.ends_with("_v4l2m2m") {
1076 return Some((0..64).any(|index| Path::new(&format!("/dev/video{index}")).exists()));
1077 }
1078 None
1079}
1080
1081#[cfg(not(target_os = "linux"))]
1083const fn hardware_device_node(_name: &str) -> Option<bool> {
1084 None
1085}
1086
1087#[must_use]
1092pub fn no_video_encoder_message(
1093 codec: NativeVideoCodec,
1094 offered: &[NativeVideoEncoderCapability],
1095 gpu_output_unavailable: Option<&str>,
1096) -> String {
1097 let identity = crate::ffmpeg_runtime_identity();
1098 let inventory = offered
1099 .iter()
1100 .filter(|capability| capability.library.is_none())
1101 .map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
1102 .collect::<Vec<_>>()
1103 .join(", ");
1104 let mut message = format!("no usable {} encoder: ", codec.as_str());
1105 if let Some(reason) = gpu_output_unavailable {
1106 let _ = write!(message, "GPU output path unavailable ({reason}); ");
1107 }
1108 let _ = write!(
1109 message,
1110 "the linked FFmpeg runtime ({}, pinned SDK {}) offers [{}]",
1111 identity.version,
1112 identity.pinned_sdk_release,
1113 if inventory.is_empty() {
1114 "none"
1115 } else {
1116 inventory.as_str()
1117 }
1118 );
1119 if codec != NativeVideoCodec::H264 {
1120 return message;
1121 }
1122 let status = crate::openh264_status();
1123 if let Some(reason) = status.reason() {
1124 let _ = write!(
1125 message,
1126 "; OpenH264 is {}: {reason}",
1127 status.as_str().replace('-', " ")
1128 );
1129 }
1130 let _ = write!(message, ". To export H.264 here: {}", h264_encoder_remedy());
1131 message
1132}
1133
1134#[must_use]
1137pub fn h264_encoder_remedy() -> String {
1138 let openh264 = match crate::openh264_status() {
1139 crate::OpenH264Status::Disabled => {
1140 "unset ALPHA2_OPENH264=0 to use the installed OpenH264 encoder".to_owned()
1141 }
1142 _ => format!(
1143 "install the OpenH264 encoder ({})",
1144 crate::OPENH264_INSTALL_COMMAND
1145 ),
1146 };
1147 format!(
1148 "{openh264}, build alpha2-host from source against an FFmpeg 8 that has libx264, or \
1149 export VP9 (.webm) or AV1 instead"
1150 )
1151}
1152
1153fn unavailable_codec_error(codec: NativeVideoCodec, refused: Vec<EncoderRefusal>) -> EncodeError {
1156 let message = no_video_encoder_message(codec, &codec_capabilities().video, None);
1157 if refused.is_empty() {
1158 return EncodeError::Adapter(message);
1159 }
1160 EncodeError::EncodersRefused {
1161 message: format!("{}; {message}", refusal_list(&refused)),
1162 refusals: refused,
1163 }
1164}
1165
1166fn refusal_list(refusals: &[EncoderRefusal]) -> String {
1168 refusals
1169 .iter()
1170 .map(|refusal| {
1171 format!(
1172 "{} {} ({}): {}",
1173 refusal.encoder,
1174 mode_label(refusal.hardware_accelerated),
1175 refusal.kind.as_str(),
1176 refusal.reason
1177 )
1178 })
1179 .collect::<Vec<_>>()
1180 .join("; ")
1181}
1182
1183#[cfg(test)]
1185pub(crate) fn video_encoder_candidates(
1186 codec: NativeVideoCodec,
1187 requested_name: Option<&str>,
1188) -> Result<Vec<VideoEncoderChoice>, EncodeError> {
1189 video_encoder_candidates_and_refusals(codec, requested_name).map(|(candidates, _)| candidates)
1190}
1191
1192fn video_encoder_candidates_and_refusals(
1196 codec: NativeVideoCodec,
1197 requested_name: Option<&str>,
1198) -> Result<(Vec<VideoEncoderChoice>, Vec<EncoderRefusal>), EncodeError> {
1199 if requested_name == Some(crate::OPENH264_ENCODER_NAME) {
1200 if codec != NativeVideoCodec::H264 {
1201 return Err(EncodeError::Adapter(format!(
1202 "video encoder {} does not implement {}",
1203 crate::OPENH264_ENCODER_NAME,
1204 codec.as_str()
1205 )));
1206 }
1207 let status = crate::openh264_status();
1208 return match status.library() {
1209 Some(library) => Ok((
1210 vec![VideoEncoderChoice::OpenH264(std::sync::Arc::clone(library))],
1211 Vec::new(),
1212 )),
1213 None => Err(EncodeError::Adapter(format!(
1214 "video encoder {} is unavailable: {}{}",
1215 crate::OPENH264_ENCODER_NAME,
1216 status.reason().unwrap_or_default(),
1217 status
1218 .remedy()
1219 .map(|remedy| format!("; {remedy}"))
1220 .unwrap_or_default()
1221 ))),
1222 };
1223 }
1224 if let Some(name) = requested_name {
1225 let encoder = select_video_encoder(codec, Some(name))?;
1226 let (candidates, refused) = compatible_encoders(
1227 registered_video_encoders(codec)
1228 .into_iter()
1229 .filter(|candidate| candidate.encoder.as_ptr() == encoder.as_ptr())
1230 .collect(),
1231 );
1232 if candidates.is_empty() && !refused.is_empty() {
1233 return Err(EncodeError::EncodersRefused {
1234 message: refusal_list(&refused),
1235 refusals: refused,
1236 });
1237 }
1238 return Ok((candidates, refused));
1239 }
1240 let (candidates, refused) = available_video_encoders(codec);
1241 if candidates.is_empty() {
1242 return Err(unavailable_codec_error(codec, refused));
1243 }
1244 Ok((candidates, refused))
1245}
1246
1247pub(crate) fn select_video_encoder(
1250 codec: NativeVideoCodec,
1251 requested_name: Option<&str>,
1252) -> Result<AVCodecRef<'static>, EncodeError> {
1253 if let Some(name) = requested_name {
1254 let name_c = CString::new(name)
1255 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1256 let encoder = AVCodec::find_encoder_by_name(&name_c)
1257 .ok_or_else(|| EncodeError::Adapter(format!("video encoder {name} is unavailable")))?;
1258 if encoder.id != codec.codec_id() {
1259 return Err(EncodeError::Adapter(format!(
1260 "video encoder {name} does not implement {}",
1261 codec.as_str()
1262 )));
1263 }
1264 let inventory = video_encoder_inventory(codec);
1265 if !inventory
1266 .usable
1267 .iter()
1268 .any(|capability| capability.encoder_name == name)
1269 {
1270 let refused = inventory
1271 .refused
1272 .into_iter()
1273 .filter(|refusal| refusal.encoder == name)
1274 .collect::<Vec<_>>();
1275 if !refused.is_empty() {
1276 return Err(EncodeError::EncodersRefused {
1277 message: refusal_list(&refused),
1278 refusals: refused,
1279 });
1280 }
1281 return Err(EncodeError::Adapter(format!(
1282 "video encoder {name} is not compatible with the native encode path"
1283 )));
1284 }
1285 return Ok(encoder);
1286 }
1287
1288 let inventory = video_encoder_inventory(codec);
1289 let Some(capability) = inventory
1290 .usable
1291 .into_iter()
1292 .find(|capability| capability.library.is_none())
1293 else {
1294 return Err(unavailable_codec_error(codec, inventory.refused));
1295 };
1296 let name = CString::new(capability.encoder_name)
1297 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1298 AVCodec::find_encoder_by_name(&name).ok_or_else(|| unavailable_codec_error(codec, Vec::new()))
1299}
1300
1301pub(crate) struct VideoEncoderOpenRequest<'a> {
1303 pub config: &'a VideoEncodeConfig,
1305 pub requested_name: Option<&'a str>,
1307 pub threads: SoftwareEncoderThreads<'a>,
1309 pub global_header: bool,
1311 pub width: i32,
1313 pub height: i32,
1315}
1316
1317pub(crate) struct SoftwareEncoderThreads<'a> {
1323 decide: &'a dyn Fn(Option<usize>) -> i64,
1324 decided: std::cell::OnceCell<i64>,
1325}
1326
1327impl<'a> SoftwareEncoderThreads<'a> {
1328 pub(crate) fn new(decide: &'a dyn Fn(Option<usize>) -> i64) -> Self {
1331 Self {
1332 decide,
1333 decided: std::cell::OnceCell::new(),
1334 }
1335 }
1336
1337 pub(crate) fn get(&self) -> i64 {
1339 self.get_fixed(None)
1340 }
1341
1342 pub(crate) fn get_fixed(&self, fixed: Option<usize>) -> i64 {
1344 *self.decided.get_or_init(|| (self.decide)(fixed).max(1))
1345 }
1346}
1347
1348pub(crate) fn open_video_encoder(
1356 request: &VideoEncoderOpenRequest<'_>,
1357) -> Result<
1358 (
1359 crate::video_backend::VideoEncoderBackend,
1360 NativeVideoEncoderSelection,
1361 ),
1362 EncodeError,
1363> {
1364 let (candidates, refused) =
1365 video_encoder_candidates_and_refusals(request.config.codec, request.requested_name)?;
1366 open_first_video_encoder(request, candidates).map_err(|error| match error {
1367 EncodeError::EncodersRefused {
1369 message,
1370 refusals: opened,
1371 } if !refused.is_empty() => EncodeError::EncodersRefused {
1372 message: format!("{}; {message}", refusal_list(&refused)),
1373 refusals: refused.into_iter().chain(opened).collect(),
1374 },
1375 error => error,
1376 })
1377}
1378
1379struct OpenedVideoEncoder {
1381 backend: crate::video_backend::VideoEncoderBackend,
1382 rate_control: crate::AppliedRateControl,
1383 quantizer_refused: Option<String>,
1384 software_threads: Option<i64>,
1385 pixels: alpha2_media::PixelLayout,
1386}
1387
1388pub(crate) fn open_first_video_encoder(
1390 request: &VideoEncoderOpenRequest<'_>,
1391 candidates: Vec<VideoEncoderChoice>,
1392) -> Result<
1393 (
1394 crate::video_backend::VideoEncoderBackend,
1395 NativeVideoEncoderSelection,
1396 ),
1397 EncodeError,
1398> {
1399 let mut rejected = Vec::new();
1400 let last = candidates.len().saturating_sub(1);
1401 for (index, candidate) in candidates.into_iter().enumerate() {
1402 let name = candidate.name();
1403 let class = candidate.class();
1404 let mark = alpha2_ffmpeg_bridge::log_mark();
1405 let opened = match &candidate {
1406 VideoEncoderChoice::Ffmpeg(candidate) => {
1407 open_one_video_encoder(request, candidate, &name).map(
1408 |(encoder, rate_control, quantizer_refused, pixels)| OpenedVideoEncoder {
1409 backend: crate::video_backend::VideoEncoderBackend::Ffmpeg(encoder),
1410 rate_control,
1411 quantizer_refused,
1412 software_threads: (!candidate.on_hardware).then(|| request.threads.get()),
1413 pixels,
1414 },
1415 )
1416 }
1417 VideoEncoderChoice::OpenH264(library) => open_openh264(request, library),
1418 };
1419 match opened {
1420 Ok(opened) => {
1421 return Ok((
1422 opened.backend,
1423 NativeVideoEncoderSelection {
1424 encoder_name: name,
1425 class,
1426 hardware_accelerated: candidate.on_hardware(),
1427 rejected,
1428 rate_control: opened.rate_control,
1429 quantizer_refused: opened.quantizer_refused,
1430 software_threads: opened.software_threads,
1431 pixels: opened.pixels,
1432 },
1433 ));
1434 }
1435 Err((reason, error)) => {
1436 let (kind, detail) = rejection_kind(&name, reason, &error, mark);
1437 if index == last {
1438 let earlier = rejected
1439 .iter()
1440 .map(|fallback: &NativeVideoEncoderFallback| {
1441 format!(
1442 "{} {} ({}): {}",
1443 fallback.encoder_name,
1444 mode_label(fallback.hardware_accelerated),
1445 fallback.reason.as_str(),
1446 fallback.detail
1447 )
1448 })
1449 .collect::<Vec<_>>();
1450 let prefix = if earlier.is_empty() {
1451 String::new()
1452 } else {
1453 format!("after {}; ", earlier.join("; "))
1454 };
1455 let message = format!(
1456 "{prefix}{name} {} ({}): {detail}",
1457 mode_label(candidate.on_hardware()),
1458 reason.as_str()
1459 );
1460 let refusals = rejected
1461 .iter()
1462 .map(NativeVideoEncoderFallback::refusal)
1463 .chain([EncoderRefusal {
1464 encoder: name,
1465 hardware_accelerated: candidate.on_hardware(),
1466 kind,
1467 reason: detail,
1468 }])
1469 .collect();
1470 return Err(EncodeError::EncodersRefused { message, refusals });
1471 }
1472 alpha2_os::diagnostic!(
1473 "alpha2 native export: video encoder {name} {} ({}) rejected: {detail}",
1474 mode_label(candidate.on_hardware()),
1475 reason.as_str()
1476 );
1477 rejected.push(NativeVideoEncoderFallback {
1478 encoder_name: name,
1479 hardware_accelerated: candidate.on_hardware(),
1480 reason,
1481 kind,
1482 detail,
1483 });
1484 }
1485 }
1486 }
1487 Err(unavailable_codec_error(request.config.codec, Vec::new()))
1488}
1489
1490fn rejection_kind(
1493 name: &str,
1494 reason: NativeEncoderRejection,
1495 error: &EncodeError,
1496 mark: alpha2_ffmpeg_bridge::LogMark,
1497) -> (OpenFailureKind, String) {
1498 match reason {
1499 NativeEncoderRejection::OpenFailed => {
1500 let lines = alpha2_ffmpeg_bridge::log_since(mark, &[name]);
1501 let detail = error.to_string();
1502 (
1503 classify_open_failure(name, &detail, &lines),
1504 format!("{detail}{}", alpha2_ffmpeg_bridge::log_detail(&lines)),
1505 )
1506 }
1507 NativeEncoderRejection::Dimensions
1508 | NativeEncoderRejection::OptionsIgnored
1509 | NativeEncoderRejection::PixelFormat => (OpenFailureKind::Unsuitable, error.to_string()),
1510 }
1511}
1512
1513fn open_openh264(
1518 request: &VideoEncoderOpenRequest<'_>,
1519 library: &crate::OpenH264Library,
1520) -> Result<OpenedVideoEncoder, (NativeEncoderRejection, EncodeError)> {
1521 let config = request.config;
1522 if !config.pixels.is_yuv420p() {
1523 return Err((
1524 NativeEncoderRejection::PixelFormat,
1525 EncodeError::Adapter(format!(
1526 "OpenH264 writes 8-bit 4:2:0 only, not {}",
1527 config.pixels.ffmpeg_name()
1528 )),
1529 ));
1530 }
1531 validate_encoder_dimensions(crate::OPENH264_ENCODER_NAME, config.width, config.height)
1532 .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1533 let open_failed = |message: String| {
1534 (
1535 NativeEncoderRejection::OpenFailed,
1536 EncodeError::Adapter(message),
1537 )
1538 };
1539 if !request.global_header {
1540 return Err(open_failed(
1541 "OpenH264 writes its parameter sets into the stream header, which this container \
1542 lacks"
1543 .to_owned(),
1544 ));
1545 }
1546 let (rate, rate_control) = crate::rate_control::openh264_rate(config.rate_control);
1547 let time_base = encoder_time_base(config.frame_rate, None)
1548 .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1549 let _ = request
1551 .threads
1552 .get_fixed(Some(usize::from(crate::openh264::thread_count(
1553 crate::openh264::slice_count(config.height),
1554 ))));
1555 let encoder = crate::openh264::OpenH264Encoder::open(
1556 library,
1557 &crate::openh264::OpenH264Settings {
1558 width: config.width,
1559 height: config.height,
1560 frame_rate: config.frame_rate,
1561 color: config.color,
1562 sample_aspect_ratio: config.display.sample_aspect_ratio,
1563 rate,
1564 keyframe_interval: u32::try_from(keyframe_interval(config.frame_rate)).unwrap_or(1),
1565 },
1566 time_base,
1567 )
1568 .map_err(open_failed)?;
1569 let threads = i64::from(encoder.threads());
1570 Ok(OpenedVideoEncoder {
1571 backend: crate::video_backend::VideoEncoderBackend::OpenH264(Box::new(encoder)),
1572 rate_control,
1573 quantizer_refused: None,
1574 software_threads: Some(threads),
1575 pixels: alpha2_media::PixelLayout::YUV420P,
1576 })
1577}
1578
1579type OpenedContext = (
1581 EncoderContext,
1582 crate::AppliedRateControl,
1583 Option<String>,
1584 alpha2_media::PixelLayout,
1585);
1586
1587fn open_one_video_encoder(
1592 request: &VideoEncoderOpenRequest<'_>,
1593 candidate: &VideoEncoderCandidate,
1594 name: &str,
1595) -> Result<OpenedContext, (NativeEncoderRejection, EncodeError)> {
1596 let pixels = encoder_layout(&candidate.encoder, request.config.pixels).ok_or_else(|| {
1597 (
1598 NativeEncoderRejection::PixelFormat,
1599 EncodeError::Adapter(format!(
1600 "{name} does not take {} pictures",
1601 request.config.pixels.ffmpeg_name()
1602 )),
1603 )
1604 })?;
1605 validate_encoder_dimensions(name, request.config.width, request.config.height)
1606 .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1607 let refused = match crate::rate_control::quantizer_request(name, request.config.rate_control) {
1608 None => None,
1609 Some(Err(reason)) => Some(reason),
1610 Some(Ok(quantized)) => {
1611 match open_video_encoder_context(request, candidate, name, Some(&quantized), pixels) {
1612 Ok(encoder) => {
1613 let applied = crate::AppliedRateControl::of_quantizer_request(name);
1614 return Ok((encoder, applied, None, pixels));
1615 }
1616 Err((_, error)) => Some(format!("{} refused: {error}", quantized.describe())),
1617 }
1618 }
1619 };
1620 if let Some(reason) = &refused {
1621 alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
1622 }
1623 open_video_encoder_context(request, candidate, name, None, pixels)
1624 .map(|encoder| (encoder, crate::AppliedRateControl::Bitrate, refused, pixels))
1625}
1626
1627fn open_video_encoder_context(
1628 request: &VideoEncoderOpenRequest<'_>,
1629 candidate: &VideoEncoderCandidate,
1630 name: &str,
1631 quantized: Option<&crate::rate_control::MechanismRequest>,
1632 pixels: alpha2_media::PixelLayout,
1633) -> Result<EncoderContext, (NativeEncoderRejection, EncodeError)> {
1634 let mut encoder = EncoderContext::from(AVCodecContext::new(&candidate.encoder));
1635 encoder.set_width(request.width);
1636 encoder.set_height(request.height);
1637 encoder.set_pix_fmt(crate::convert::pixel_format(pixels));
1638 let rate = request.config.frame_rate;
1639 let time_base = encoder_time_base(rate, max_time_base_denominator(candidate.encoder.id))
1640 .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1641 encoder.set_time_base(time_base);
1642 let sar = request.config.display.sample_aspect_ratio;
1644 if sar != alpha2_media::SampleAspectRatio::default() {
1645 encoder.set_sample_aspect_ratio(rsmpeg::avutil::ra(
1646 i32::try_from(sar.num).unwrap_or(1),
1647 i32::try_from(sar.den).unwrap_or(1),
1648 ));
1649 }
1650 encoder.set_framerate(rsmpeg::avutil::ra(
1651 i32::try_from(rate.numerator()).unwrap_or(i32::MAX),
1652 i32::try_from(rate.denominator()).unwrap_or(1),
1653 ));
1654 encoder.set_bit_rate(quantized.map_or_else(
1655 || i64::try_from(request.config.rate_control.bitrate).unwrap_or(i64::MAX),
1656 |quantized| quantized.bit_rate,
1657 ));
1658 encoder.set_gop_size(keyframe_interval(request.config.frame_rate));
1659 encoder.set_max_b_frames(encoder_max_b_frames(name));
1660 if request.global_header {
1661 let flags = encoder.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0);
1662 encoder.set_flags(flags);
1663 }
1664 let threads = if candidate.on_hardware {
1666 0
1667 } else {
1668 request.threads.get()
1669 };
1670 let mut base = video_encoder_options(
1671 request.config.color,
1672 &candidate.encoder,
1673 candidate.on_hardware,
1674 threads,
1675 request.config.width,
1676 );
1677 if candidate.codec() == NativeVideoCodec::ProRes {
1678 for (key, value) in crate::convert::prores_options(pixels) {
1679 base = crate::convert::set_encoder_option(base, key, &value);
1680 }
1681 }
1682 let options = match quantized {
1683 Some(quantized) => quantized.dictionary(base),
1684 None => crate::rate_control::rate_control_options(name, request.config.rate_control, base),
1685 };
1686 let unused =
1687 alpha2_ffmpeg_bridge::open_codec_context(&mut encoder, options).map_err(|error| {
1688 let hint = match quantized {
1689 Some(_) => "",
1690 None => crate::rate_control::constant_rate_hint(name, request.config.rate_control),
1691 };
1692 let error = match encode_adapter_error(error) {
1693 EncodeError::Adapter(message) if !hint.is_empty() => {
1694 EncodeError::Adapter(format!("{message}{hint}"))
1695 }
1696 error => error,
1697 };
1698 (NativeEncoderRejection::OpenFailed, error)
1699 })?;
1700 reject_unused_encode_options(unused)
1701 .map_err(|error| (NativeEncoderRejection::OptionsIgnored, error))?;
1702 Ok(encoder)
1703}
1704
1705pub(crate) fn keyframe_interval(frame_rate: alpha2_time::FrameRate) -> i32 {
1711 let frames = (2 * u64::from(frame_rate.numerator()))
1712 .div_ceil(u64::from(frame_rate.denominator()))
1713 .max(1);
1714 i32::try_from(frames).unwrap_or(i32::MAX)
1715}
1716
1717pub(crate) const fn max_time_base_denominator(codec: ffi::AVCodecID) -> Option<u32> {
1720 match codec {
1721 ffi::AV_CODEC_ID_MPEG4 | ffi::AV_CODEC_ID_H263 | ffi::AV_CODEC_ID_H263P => Some(65_535),
1722 _ => None,
1723 }
1724}
1725
1726pub(crate) fn encoder_time_base(
1733 rate: alpha2_time::FrameRate,
1734 limit: Option<u32>,
1735) -> Result<AVRational, EncodeError> {
1736 let out_of_range = || EncodeError::Adapter("frame rate is out of range".into());
1737 let (num, den) = (u64::from(rate.numerator()), u64::from(rate.denominator()));
1738 let exact = || {
1739 Ok(rsmpeg::avutil::ra(
1740 i32::try_from(den).map_err(|_| out_of_range())?,
1741 i32::try_from(num).map_err(|_| out_of_range())?,
1742 ))
1743 };
1744 let Some(limit) = limit.map(u64::from).filter(|limit| num > *limit) else {
1745 return exact();
1746 };
1747 let mut best: Option<(u64, u64, u64)> = None;
1751 for units in num.div_ceil(den).max(1)..=limit {
1752 let q = num / gcd_u64(num, den * units);
1753 let (error, scale) = (q / 2, q * units);
1754 if best.is_none_or(|(_, best_error, best_scale)| {
1755 u128::from(error) * u128::from(best_scale) < u128::from(best_error) * u128::from(scale)
1756 }) {
1757 best = Some((units, error, scale));
1758 }
1759 }
1760 let (units, _, _) = best.ok_or_else(out_of_range)?;
1761 Ok(rsmpeg::avutil::ra(
1762 1,
1763 i32::try_from(units).map_err(|_| out_of_range())?,
1764 ))
1765}
1766
1767const fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
1768 while b != 0 {
1769 (a, b) = (b, a % b);
1770 }
1771 a
1772}
1773
1774pub(crate) fn encoder_max_b_frames(name: &str) -> i32 {
1775 if name.ends_with("_videotoolbox") {
1778 0
1779 } else {
1780 2
1781 }
1782}
1783
1784#[must_use]
1786pub fn encoder_minimum_dimensions(name: &str) -> (u32, u32) {
1787 if name == "av1_nvenc" {
1789 (146, 128)
1790 } else if name.ends_with("_nvenc") {
1791 (146, 50)
1792 } else {
1793 (2, 2)
1794 }
1795}
1796
1797pub(crate) fn validate_encoder_dimensions(
1798 encoder_name: &str,
1799 width: u32,
1800 height: u32,
1801) -> Result<(), EncodeError> {
1802 let (minimum_width, minimum_height) = encoder_minimum_dimensions(encoder_name);
1803 if width < minimum_width || height < minimum_height {
1804 Err(EncodeError::Adapter(format!(
1805 "encoder {encoder_name} requires at least {minimum_width}x{minimum_height}, received {width}x{height}"
1806 )))
1807 } else {
1808 Ok(())
1809 }
1810}
1811
1812#[derive(Clone, Debug, Eq, PartialEq)]
1814pub struct NativeEncoderHardwareConfig {
1815 pub pixel_format: String,
1817 pub device_type: String,
1819 pub methods: i32,
1821 pub needs_hardware_frames: bool,
1823}
1824
1825#[derive(Clone, Debug, Eq, PartialEq)]
1829#[allow(clippy::struct_excessive_bools)]
1831pub struct NativeVideoEncoderProbe {
1832 pub codec: NativeVideoCodec,
1834 pub encoder_name: String,
1836 pub class: NativeEncoderClass,
1838 pub hardware_accelerated: bool,
1840 pub capability_bits: u32,
1842 pub pixel_formats: Vec<String>,
1844 pub hardware_configs: Vec<NativeEncoderHardwareConfig>,
1846 pub accepts_yuv420p: bool,
1848 pub requires_hardware_frames: bool,
1850 pub advertised: bool,
1852 pub minimum_size: (u32, u32),
1854 pub software_open_error: Option<String>,
1856 pub software_packets: u32,
1858 pub rate_control: Vec<NativeRateControlProbe>,
1860}
1861
1862#[derive(Clone, Debug, Eq, PartialEq)]
1864pub struct NativeRateControlProbe {
1865 pub mechanism: crate::RateControlMechanism,
1867 pub options: Option<String>,
1869 pub outcome: Result<u32, String>,
1871 pub device: Option<String>,
1874}
1875
1876pub(crate) const fn probe_quantizer(codec: NativeVideoCodec, software: bool) -> u32 {
1879 match codec {
1880 NativeVideoCodec::H264 | NativeVideoCodec::Hevc => 26,
1881 NativeVideoCodec::Av1 | NativeVideoCodec::Vp9 => {
1882 if software {
1883 32
1884 } else {
1885 100
1886 }
1887 }
1888 NativeVideoCodec::Mpeg4Part2 => 4,
1889 NativeVideoCodec::ProRes => 0,
1891 }
1892}
1893
1894#[must_use]
1899#[allow(clippy::too_many_lines)]
1900pub fn probe_video_encoders(width: u32, height: u32) -> Vec<NativeVideoEncoderProbe> {
1901 let advertised = codec_capabilities();
1902 [
1903 NativeVideoCodec::H264,
1904 NativeVideoCodec::Hevc,
1905 NativeVideoCodec::Vp9,
1906 NativeVideoCodec::Av1,
1907 NativeVideoCodec::Mpeg4Part2,
1908 NativeVideoCodec::ProRes,
1909 ]
1910 .into_iter()
1911 .flat_map(|codec| {
1912 registered_video_encoders(codec)
1913 .into_iter()
1914 .map(move |candidate| (codec, candidate))
1915 })
1916 .map(|(codec, candidate)| {
1917 let encoder = &candidate.encoder;
1918 let encoder_name = candidate.name();
1919 let pixel_formats = encoder.pix_fmts().map_or_else(Vec::new, |formats| {
1920 formats
1921 .iter()
1922 .map(|format| {
1923 alpha2_ffmpeg_bridge::pixel_format_name(*format)
1924 .unwrap_or_else(|| "unknown".to_owned())
1925 })
1926 .collect()
1927 });
1928 let hardware_configs = (0..)
1929 .map_while(|index| encoder.hw_config(index))
1930 .map(|config| NativeEncoderHardwareConfig {
1931 pixel_format: alpha2_ffmpeg_bridge::pixel_format_name(config.pix_fmt)
1932 .unwrap_or_else(|| "unknown".to_owned()),
1933 device_type: alpha2_ffmpeg_bridge::hardware_device_type_name(config.device_type)
1934 .unwrap_or_else(|| "none".to_owned()),
1935 methods: config.methods,
1936 needs_hardware_frames: crate::decode::has_hw_config_method(
1937 config.methods,
1938 ffi::AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX,
1939 ),
1940 })
1941 .collect::<Vec<_>>();
1942 let accepts_yuv420p = encoder
1943 .pix_fmts()
1944 .is_none_or(|formats| formats.contains(&ffi::AV_PIX_FMT_YUV420P));
1945 let (software_open_error, software_packets) =
1946 probe_software_encode(&candidate, &encoder_name, width, height, None);
1947 let requires_hardware_frames = !accepts_yuv420p
1948 && hardware_configs
1949 .iter()
1950 .any(|config| config.needs_hardware_frames);
1951 let rate_control = crate::RateControlMechanism::ALL
1952 .into_iter()
1953 .map(|mechanism| {
1954 let request = crate::rate_control::mechanism_request(
1955 &encoder_name,
1956 mechanism,
1957 2_000_000,
1958 if encoder_name.ends_with("_d3d12va") {
1960 26
1961 } else {
1962 probe_quantizer(
1963 codec,
1964 encoder_class(encoder) == NativeEncoderClass::Software,
1965 )
1966 },
1967 );
1968 let mut device = None;
1969 let outcome = match &request {
1970 None => Err("no known option for this encoder family".to_owned()),
1971 Some(request) if requires_hardware_frames => probe_hardware_frames_encode(
1972 encoder,
1973 &encoder_name,
1974 (width, height),
1975 mechanism,
1976 request,
1977 )
1978 .map(|(packets, on)| {
1979 device = Some(on);
1980 packets
1981 }),
1982 Some(request) => {
1983 match probe_software_encode(
1984 &candidate,
1985 &encoder_name,
1986 width,
1987 height,
1988 Some(request),
1989 ) {
1990 (None, packets) => Ok(packets),
1991 (Some(error), _) => Err(error),
1992 }
1993 }
1994 };
1995 NativeRateControlProbe {
1996 mechanism,
1997 options: request
1998 .as_ref()
1999 .map(crate::rate_control::MechanismRequest::describe),
2000 outcome,
2001 device,
2002 }
2003 })
2004 .collect();
2005 NativeVideoEncoderProbe {
2006 codec,
2007 class: encoder_class(encoder),
2008 hardware_accelerated: candidate.on_hardware,
2009 capability_bits: u32::try_from(encoder.capabilities).unwrap_or(0),
2010 advertised: advertised.video.iter().any(|capability| {
2011 capability.codec == codec
2012 && capability.encoder_name == encoder_name
2013 && capability.hardware_accelerated == candidate.on_hardware
2014 }),
2015 minimum_size: encoder_minimum_dimensions(&encoder_name),
2016 requires_hardware_frames,
2017 accepts_yuv420p,
2018 pixel_formats,
2019 hardware_configs,
2020 encoder_name,
2021 software_open_error,
2022 software_packets,
2023 rate_control,
2024 }
2025 })
2026 .collect()
2027}
2028
2029#[allow(unused_variables)]
2037fn probe_hardware_frames_encode(
2038 encoder: &AVCodecRef<'static>,
2039 name: &str,
2040 (width, height): (u32, u32),
2041 mechanism: crate::RateControlMechanism,
2042 request: &crate::rate_control::MechanismRequest,
2043) -> Result<(u32, String), String> {
2044 #[cfg(all(target_os = "linux", feature = "hwencode"))]
2045 if name.ends_with("_vulkan") {
2046 return crate::hwencode::probe_vulkan_rate_control(encoder, width, height, request);
2047 }
2048 #[cfg(all(target_os = "windows", feature = "hwencode"))]
2049 if name.ends_with("_d3d12va") {
2050 return crate::hwencode::probe_d3d12va_rate_control(
2051 name, width, height, mechanism, request,
2052 );
2053 }
2054 Err("takes hardware frames this probe cannot supply".to_owned())
2055}
2056
2057fn probe_software_encode(
2059 candidate: &VideoEncoderCandidate,
2060 name: &str,
2061 width: u32,
2062 height: u32,
2063 request: Option<&crate::rate_control::MechanismRequest>,
2064) -> (Option<String>, u32) {
2065 let Ok(coded_width) = i32::try_from(width) else {
2066 return (Some("width is out of range".to_owned()), 0);
2067 };
2068 let Ok(coded_height) = i32::try_from(height) else {
2069 return (Some("height is out of range".to_owned()), 0);
2070 };
2071 if let Err(error) = validate_encoder_dimensions(name, width, height) {
2072 return (Some(error.to_string()), 0);
2073 }
2074 let encoder = &candidate.encoder;
2075 let base = candidate.codec().base_layout();
2076 let format =
2077 encoder_layout(encoder, base).map_or(ffi::AV_PIX_FMT_YUV420P, crate::convert::pixel_format);
2078 let mut context = EncoderContext::from(AVCodecContext::new(encoder));
2079 context.set_width(coded_width);
2080 context.set_height(coded_height);
2081 context.set_pix_fmt(format);
2082 let time_base = rsmpeg::avutil::ra(1, 30);
2083 context.set_time_base(time_base);
2084 context.set_framerate(rsmpeg::avutil::ra(30, 1));
2085 context.set_bit_rate(request.map_or(2_000_000, |request| request.bit_rate));
2086 context.set_gop_size(12);
2087 context.set_max_b_frames(encoder_max_b_frames(name));
2088 let options = video_encoder_options(
2089 ColorMetadata::EXPORT,
2090 encoder,
2091 candidate.on_hardware,
2092 1,
2093 width,
2094 );
2095 let options = match request {
2096 Some(request) => request.dictionary(options),
2097 None => options,
2098 };
2099 match alpha2_ffmpeg_bridge::open_codec_context(&mut context, options) {
2100 Err(error) => return (Some(error.to_string()), 0),
2101 Ok(unused) => {
2102 if let Err(error) = reject_unused_encode_options(unused) {
2103 return (Some(error.to_string()), 0);
2104 }
2105 }
2106 }
2107 let Some(image) = rsmpeg::avutil::AVImage::new(format, coded_width, coded_height, 1) else {
2108 return (Some("cannot allocate a probe frame".to_owned()), 0);
2109 };
2110 let mut frame = rsmpeg::avutil::AVFrameWithImage::new(image);
2111 frame.set_pts(0);
2112 frame.set_time_base(time_base);
2113 if let Err(error) = context.send_frame(Some(&frame)) {
2114 return (Some(error.to_string()), 0);
2115 }
2116 if let Err(error) = context.send_frame(None) {
2117 return (Some(error.to_string()), 0);
2118 }
2119 let mut packets = 0;
2120 loop {
2121 match context.receive_packet() {
2122 Ok(_) => packets += 1,
2123 Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
2124 Err(error) => return (Some(error.to_string()), packets),
2125 }
2126 }
2127 if packets == 0 {
2128 return (
2129 Some("opened but produced no packet from a software frame".to_owned()),
2130 0,
2131 );
2132 }
2133 (None, packets)
2134}