1use std::collections::HashMap;
4use std::ffi::CString;
5use std::sync::{LazyLock, Mutex};
6#[cfg(target_os = "linux")]
7use std::{fs, path::Path};
8
9use crate::convert::{encode_adapter_error, reject_unused_encode_options, video_encoder_options};
10use crate::video_encode::VideoEncodeConfig;
11use alpha2_media::{ColorMetadata, EncodeError};
12use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef};
13use rsmpeg::avutil::AVRational;
14use rsmpeg::error::RsmpegError;
15use rsmpeg::ffi;
16
17#[derive(Copy, Clone, Debug, PartialEq)]
19pub struct AvEncodeConfig {
20 pub video: VideoEncodeConfig,
22 pub audio_sample_rate: u32,
24 pub audio_channels: u16,
26 pub audio_codec: NativeAudioCodec,
28 pub audio_bit_rate: u32,
30}
31
32#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
34#[non_exhaustive]
35pub enum NativeVideoCodec {
36 H264,
38 Hevc,
40 Vp9,
42 Av1,
44 Mpeg4Part2,
46}
47
48impl NativeVideoCodec {
49 #[must_use]
51 pub const fn as_str(self) -> &'static str {
52 match self {
53 Self::H264 => "h264",
54 Self::Hevc => "hevc",
55 Self::Vp9 => "vp9",
56 Self::Av1 => "av1",
57 Self::Mpeg4Part2 => "mpeg4-part2",
58 }
59 }
60
61 #[must_use]
66 pub const fn model(self) -> alpha2_media::VideoCodec {
67 match self {
68 Self::H264 => alpha2_media::VideoCodec::H264,
69 Self::Hevc => alpha2_media::VideoCodec::Hevc,
70 Self::Vp9 => alpha2_media::VideoCodec::Vp9,
71 Self::Av1 => alpha2_media::VideoCodec::Av1,
72 Self::Mpeg4Part2 => alpha2_media::VideoCodec::Mpeg4Part2,
73 }
74 }
75
76 #[must_use]
78 pub const fn from_model(codec: alpha2_media::VideoCodec) -> Self {
79 match codec {
80 alpha2_media::VideoCodec::H264 => Self::H264,
81 alpha2_media::VideoCodec::Hevc => Self::Hevc,
82 alpha2_media::VideoCodec::Vp9 => Self::Vp9,
83 alpha2_media::VideoCodec::Av1 => Self::Av1,
84 alpha2_media::VideoCodec::Mpeg4Part2 => Self::Mpeg4Part2,
85 }
86 }
87
88 pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
89 match self {
90 Self::H264 => ffi::AV_CODEC_ID_H264,
91 Self::Hevc => ffi::AV_CODEC_ID_HEVC,
92 Self::Vp9 => ffi::AV_CODEC_ID_VP9,
93 Self::Av1 => ffi::AV_CODEC_ID_AV1,
94 Self::Mpeg4Part2 => ffi::AV_CODEC_ID_MPEG4,
95 }
96 }
97}
98
99#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
101#[non_exhaustive]
102pub enum NativeAudioCodec {
103 Aac,
105 Opus,
107}
108
109impl NativeAudioCodec {
110 pub const ALL: [Self; 2] = [Self::Aac, Self::Opus];
112
113 #[must_use]
115 pub const fn as_str(self) -> &'static str {
116 match self {
117 Self::Aac => "aac",
118 Self::Opus => "opus",
119 }
120 }
121
122 #[must_use]
124 pub const fn model(self) -> alpha2_media::AudioCodec {
125 match self {
126 Self::Aac => alpha2_media::AudioCodec::Aac,
127 Self::Opus => alpha2_media::AudioCodec::Opus,
128 }
129 }
130
131 #[must_use]
133 pub const fn from_model(codec: alpha2_media::AudioCodec) -> Self {
134 match codec {
135 alpha2_media::AudioCodec::Aac => Self::Aac,
136 alpha2_media::AudioCodec::Opus => Self::Opus,
137 }
138 }
139
140 pub(crate) const fn encoder_name(self) -> &'static std::ffi::CStr {
144 match self {
145 Self::Aac => c"aac",
146 Self::Opus => c"libopus",
147 }
148 }
149}
150
151pub(crate) fn audio_encoder(codec: NativeAudioCodec) -> Option<AVCodecRef<'static>> {
154 AVCodec::find_encoder_by_name(codec.encoder_name()).filter(|encoder| {
155 u32::try_from(encoder.capabilities).unwrap_or(0) & ffi::AV_CODEC_CAP_EXPERIMENTAL == 0
156 })
157}
158
159#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
169#[non_exhaustive]
170pub enum NativeEncoderClass {
171 Software,
173 Hardware,
175 Hybrid,
179}
180
181impl NativeEncoderClass {
182 #[must_use]
184 pub const fn as_str(self) -> &'static str {
185 match self {
186 Self::Software => "software",
187 Self::Hardware => "hardware",
188 Self::Hybrid => "hybrid",
189 }
190 }
191
192 #[must_use]
198 pub const fn modes(self, hybrid_hardware: bool) -> &'static [bool] {
199 match self {
200 Self::Hardware => &[true],
201 Self::Hybrid if hybrid_hardware => &[true, false],
202 Self::Software | Self::Hybrid => &[false],
203 }
204 }
205}
206
207pub(crate) struct VideoEncoderCandidate {
210 pub(crate) encoder: AVCodecRef<'static>,
211 pub(crate) on_hardware: bool,
212}
213
214impl VideoEncoderCandidate {
215 pub(crate) fn name(&self) -> String {
216 self.encoder.name().to_string_lossy().into_owned()
217 }
218}
219
220#[must_use]
230pub fn hybrid_encoders_use_hardware() -> bool {
231 std::env::var("ALPHA2_VIDEO_ENCODER_HYBRID")
232 .is_ok_and(|value| value.eq_ignore_ascii_case("hardware"))
233}
234
235#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
237#[non_exhaustive]
238pub enum NativeEncoderRejection {
239 Dimensions,
241 OpenFailed,
243 OptionsIgnored,
245}
246
247impl NativeEncoderRejection {
248 #[must_use]
250 pub const fn as_str(self) -> &'static str {
251 match self {
252 Self::Dimensions => "dimensions",
253 Self::OpenFailed => "open-failed",
254 Self::OptionsIgnored => "options-ignored",
255 }
256 }
257}
258
259#[derive(Clone, Debug, Eq, PartialEq)]
261pub struct NativeVideoEncoderFallback {
262 pub encoder_name: String,
264 pub hardware_accelerated: bool,
267 pub reason: NativeEncoderRejection,
269 pub detail: String,
271}
272
273#[derive(Clone, Debug, Eq, PartialEq)]
279pub struct NativeVideoEncoderSelection {
280 pub encoder_name: String,
282 pub class: NativeEncoderClass,
284 pub hardware_accelerated: bool,
286 pub rejected: Vec<NativeVideoEncoderFallback>,
288 pub rate_control: crate::AppliedRateControl,
290 pub quantizer_refused: Option<String>,
292 pub software_threads: Option<i64>,
295}
296
297#[derive(Clone, Debug, Eq, PartialEq)]
299pub struct NativeVideoEncoderCapability {
300 pub codec: NativeVideoCodec,
302 pub encoder_name: String,
304 pub class: NativeEncoderClass,
306 pub hardware_accelerated: bool,
309 pub preferred: bool,
311 pub minimum_width: u32,
313 pub minimum_height: u32,
315}
316
317#[derive(Clone, Debug, Eq, PartialEq)]
319pub struct NativeAudioEncoderCapability {
320 pub codec: NativeAudioCodec,
322 pub encoder_name: String,
324}
325
326#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct NativeCodecCapabilities {
333 pub video: Vec<NativeVideoEncoderCapability>,
336 pub audio: Vec<NativeAudioEncoderCapability>,
339}
340
341#[must_use]
348pub fn codec_capabilities() -> NativeCodecCapabilities {
349 let video = [
350 NativeVideoCodec::H264,
351 NativeVideoCodec::Hevc,
352 NativeVideoCodec::Vp9,
353 NativeVideoCodec::Av1,
354 NativeVideoCodec::Mpeg4Part2,
355 ]
356 .into_iter()
357 .flat_map(|codec| {
358 registered_video_encoders(codec)
359 .into_iter()
360 .filter(encoder_is_compatible)
361 .enumerate()
362 .map(move |(index, candidate)| {
363 let encoder_name = candidate.name();
364 let (minimum_width, minimum_height) = encoder_minimum_dimensions(&encoder_name);
365 NativeVideoEncoderCapability {
366 codec,
367 class: encoder_class(&candidate.encoder),
368 hardware_accelerated: candidate.on_hardware,
369 preferred: index == 0,
370 encoder_name,
371 minimum_width,
372 minimum_height,
373 }
374 })
375 })
376 .collect();
377 let audio = NativeAudioCodec::ALL
378 .into_iter()
379 .filter_map(|codec| {
380 let encoder = audio_encoder(codec)?;
381 Some(NativeAudioEncoderCapability {
382 codec,
383 encoder_name: encoder.name().to_string_lossy().into_owned(),
384 })
385 })
386 .collect();
387 NativeCodecCapabilities { video, audio }
388}
389
390#[derive(Clone, Debug, Eq, PartialEq)]
392pub struct NativeHardwareDecodeCapability {
393 pub codec: NativeVideoCodec,
395 pub decoder_name: Option<String>,
397 pub hardware_config: bool,
403 pub software_decoder: Option<String>,
407}
408
409#[derive(Clone, Debug, Eq, PartialEq)]
411pub struct NativeHardwareDecodeCapabilities {
412 pub backend: &'static str,
414 pub codecs: Vec<NativeHardwareDecodeCapability>,
416}
417
418#[must_use]
420pub const fn hardware_decode_backend() -> &'static str {
421 #[cfg(feature = "hwdecode")]
422 {
423 #[cfg(target_os = "linux")]
424 {
425 "vulkan"
426 }
427 #[cfg(target_os = "macos")]
428 {
429 "videotoolbox"
430 }
431 #[cfg(target_os = "windows")]
432 {
433 "d3d12va"
434 }
435 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
436 {
437 "none"
438 }
439 }
440 #[cfg(not(feature = "hwdecode"))]
441 {
442 "none"
443 }
444}
445
446#[must_use]
448pub fn hardware_decode_capabilities() -> NativeHardwareDecodeCapabilities {
449 let codecs = [
450 NativeVideoCodec::H264,
451 NativeVideoCodec::Hevc,
452 NativeVideoCodec::Av1,
453 NativeVideoCodec::Vp9,
454 ]
455 .into_iter()
456 .map(|codec| {
457 let decoder = AVCodec::find_decoder(codec.codec_id());
458 NativeHardwareDecodeCapability {
459 codec,
460 decoder_name: decoder
461 .as_ref()
462 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
463 hardware_config: crate::decode::registered_video_decoders(codec.codec_id())
464 .any(|decoder| decoder_has_hardware_config(&decoder)),
465 software_decoder: crate::decode::software_video_decoder(codec.codec_id())
466 .ok()
467 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
468 }
469 })
470 .collect();
471 NativeHardwareDecodeCapabilities {
472 backend: hardware_decode_backend(),
473 codecs,
474 }
475}
476
477#[allow(unused_variables)]
479fn decoder_has_hardware_config(decoder: &AVCodecRef<'_>) -> bool {
480 #[cfg(all(
481 any(target_os = "linux", target_os = "macos", target_os = "windows"),
482 feature = "hwdecode"
483 ))]
484 {
485 #[cfg(target_os = "linux")]
486 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_VULKAN, ffi::AV_PIX_FMT_VULKAN);
487 #[cfg(target_os = "macos")]
488 let (device_type, pixel_format) = (
489 ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
490 ffi::AV_PIX_FMT_VIDEOTOOLBOX,
491 );
492 #[cfg(target_os = "windows")]
493 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_D3D12VA, ffi::AV_PIX_FMT_D3D12);
494 (0..)
495 .map_while(|index| decoder.hw_config(index))
496 .any(|config| config.device_type == device_type && config.pix_fmt == pixel_format)
497 }
498 #[cfg(not(all(
499 any(target_os = "linux", target_os = "macos", target_os = "windows"),
500 feature = "hwdecode"
501 )))]
502 {
503 false
504 }
505}
506
507#[cfg(test)]
511pub(crate) fn test_skips_encoder(name: &std::ffi::CStr) -> bool {
512 static SKIPPED: LazyLock<Vec<String>> = LazyLock::new(|| {
513 std::env::var("ALPHA2_TEST_SKIP_ENCODERS")
514 .unwrap_or_default()
515 .split(',')
516 .map(str::trim)
517 .filter(|name| !name.is_empty())
518 .map(str::to_owned)
519 .collect()
520 });
521 let name = name.to_string_lossy();
522 SKIPPED.iter().any(|skipped| *skipped == name)
523}
524
525#[cfg(not(test))]
526const fn test_skips_encoder(_name: &std::ffi::CStr) -> bool {
527 false
528}
529
530pub(crate) fn registered_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderCandidate> {
532 let hybrid_hardware = hybrid_encoders_use_hardware();
533 let mut candidates = AVCodec::iterate()
534 .filter(|encoder| {
535 encoder.id == codec.codec_id()
536 && !test_skips_encoder(encoder.name())
537 && AVCodec::find_encoder_by_name(encoder.name())
538 .is_some_and(|found| found.as_ptr() == encoder.as_ptr())
539 })
540 .flat_map(|encoder| {
541 encoder_class(&encoder)
542 .modes(hybrid_hardware)
543 .iter()
544 .filter_map(move |&on_hardware| {
545 AVCodec::find_encoder_by_name(encoder.name()).map(|encoder| {
546 VideoEncoderCandidate {
547 encoder,
548 on_hardware,
549 }
550 })
551 })
552 })
553 .collect::<Vec<_>>();
554 candidates.sort_by_key(|candidate| !candidate.on_hardware);
556 candidates
557}
558
559pub(crate) fn encoder_class(encoder: &AVCodec) -> NativeEncoderClass {
562 let name = encoder.name().to_string_lossy();
563 declared_encoder_class(&name, u32::try_from(encoder.capabilities).unwrap_or(0))
564}
565
566const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
577
578pub(crate) fn declared_encoder_class(name: &str, capabilities: u32) -> NativeEncoderClass {
582 if capabilities & ffi::AV_CODEC_CAP_HYBRID != 0 {
583 NativeEncoderClass::Hybrid
584 } else if capabilities & ffi::AV_CODEC_CAP_HARDWARE != 0
585 || MISDECLARED_HARDWARE_ENCODERS.contains(&name)
586 {
587 NativeEncoderClass::Hardware
588 } else {
589 NativeEncoderClass::Software
590 }
591}
592
593pub(crate) fn encoder_is_compatible(candidate: &VideoEncoderCandidate) -> bool {
594 let name = candidate.name();
595 registration_is_compatible(candidate, &name) && encoder_runs_here(candidate, &name)
596}
597
598fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
601 if candidate
604 .encoder
605 .pix_fmts()
606 .is_some_and(|formats| !formats.contains(&ffi::AV_PIX_FMT_YUV420P))
607 {
608 return false;
609 }
610 !candidate.on_hardware || hardware_device_node(name) != Some(false)
611}
612
613pub(crate) fn needs_open_probe(on_hardware: bool, name: &str) -> bool {
621 (on_hardware && hardware_device_node(name).is_none())
622 || name.ends_with("_mf")
623 || name.ends_with("_videotoolbox")
624}
625
626fn encoder_runs_here(candidate: &VideoEncoderCandidate, name: &str) -> bool {
646 static RUNS: LazyLock<Mutex<HashMap<(String, bool), bool>>> = LazyLock::new(Mutex::default);
647 if !needs_open_probe(candidate.on_hardware, name) {
648 return true;
649 }
650 let key = (name.to_owned(), candidate.on_hardware);
651 let mut runs = RUNS
652 .lock()
653 .unwrap_or_else(std::sync::PoisonError::into_inner);
654 *runs.entry(key).or_insert_with(|| {
655 match probe_software_encode(candidate, name, 256, 144, None).0 {
656 None => true,
657 Some(error) => {
658 alpha2_os::diagnostic!(
659 "alpha2 native: video encoder {name} {} is registered but did not encode a \
660 frame on this machine, so it is not offered that way: {error}",
661 mode_label(candidate.on_hardware)
662 );
663 false
664 }
665 }
666 })
667}
668
669pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
671 if on_hardware { "on GPU" } else { "on CPU" }
672}
673
674#[cfg(target_os = "linux")]
685fn hardware_device_node(name: &str) -> Option<bool> {
686 if name.ends_with("_nvenc") {
687 return Some(Path::new("/dev/nvidia0").exists() || Path::new("/dev/nvidiactl").exists());
688 }
689 if name.ends_with("_vaapi") || name.ends_with("_qsv") || name.ends_with("_amf") {
690 return Some(fs::read_dir("/dev/dri").is_ok_and(|entries| {
691 entries.flatten().any(|entry| {
692 entry
693 .file_name()
694 .to_str()
695 .is_some_and(|name| name.starts_with("renderD") || name.starts_with("card"))
696 })
697 }));
698 }
699 if name.ends_with("_v4l2m2m") {
700 return Some((0..64).any(|index| Path::new(&format!("/dev/video{index}")).exists()));
701 }
702 None
703}
704
705#[cfg(not(target_os = "linux"))]
708const fn hardware_device_node(_name: &str) -> Option<bool> {
709 None
710}
711
712fn unavailable_codec_error(codec: NativeVideoCodec) -> EncodeError {
718 let identity = crate::ffmpeg_runtime_identity();
719 let inventory = codec_capabilities()
720 .video
721 .iter()
722 .map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
723 .collect::<Vec<_>>()
724 .join(", ");
725 let inventory = if inventory.is_empty() {
726 "none".to_owned()
727 } else {
728 inventory
729 };
730 EncodeError::Adapter(format!(
731 "the linked FFmpeg runtime ({}, pinned SDK {}) registers no usable {} encoder; it offers [{inventory}]",
732 identity.version,
733 identity.pinned_sdk_release,
734 codec.as_str()
735 ))
736}
737
738pub(crate) fn video_encoder_candidates(
744 codec: NativeVideoCodec,
745 requested_name: Option<&str>,
746) -> Result<Vec<VideoEncoderCandidate>, EncodeError> {
747 if let Some(name) = requested_name {
748 let encoder = select_video_encoder(codec, Some(name))?;
749 return Ok(registered_video_encoders(codec)
750 .into_iter()
751 .filter(|candidate| candidate.encoder.as_ptr() == encoder.as_ptr())
752 .filter(encoder_is_compatible)
753 .collect());
754 }
755 let candidates = registered_video_encoders(codec)
756 .into_iter()
757 .filter(encoder_is_compatible)
758 .collect::<Vec<_>>();
759 if candidates.is_empty() {
760 return Err(unavailable_codec_error(codec));
761 }
762 Ok(candidates)
763}
764
765pub(crate) fn select_video_encoder(
769 codec: NativeVideoCodec,
770 requested_name: Option<&str>,
771) -> Result<AVCodecRef<'static>, EncodeError> {
772 if let Some(name) = requested_name {
773 let name_c = CString::new(name)
774 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
775 let encoder = AVCodec::find_encoder_by_name(&name_c)
776 .ok_or_else(|| EncodeError::Adapter(format!("video encoder {name} is unavailable")))?;
777 if encoder.id != codec.codec_id() {
778 return Err(EncodeError::Adapter(format!(
779 "video encoder {name} does not implement {}",
780 codec.as_str()
781 )));
782 }
783 if !codec_capabilities()
784 .video
785 .iter()
786 .any(|capability| capability.codec == codec && capability.encoder_name == name)
787 {
788 return Err(EncodeError::Adapter(format!(
789 "video encoder {name} is not compatible with the native encode path"
790 )));
791 }
792 return Ok(encoder);
793 }
794
795 let capability = codec_capabilities()
796 .video
797 .into_iter()
798 .find(|capability| capability.codec == codec)
799 .ok_or_else(|| unavailable_codec_error(codec))?;
800 let name = CString::new(capability.encoder_name)
801 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
802 AVCodec::find_encoder_by_name(&name).ok_or_else(|| unavailable_codec_error(codec))
803}
804
805pub(crate) struct VideoEncoderOpenRequest<'a> {
807 pub config: &'a VideoEncodeConfig,
809 pub requested_name: Option<&'a str>,
811 pub threads: SoftwareEncoderThreads<'a>,
814 pub global_header: bool,
816 pub width: i32,
818 pub height: i32,
820}
821
822pub(crate) struct SoftwareEncoderThreads<'a> {
833 decide: &'a dyn Fn() -> i64,
834 decided: std::cell::OnceCell<i64>,
835}
836
837impl<'a> SoftwareEncoderThreads<'a> {
838 pub(crate) fn new(decide: &'a dyn Fn() -> i64) -> Self {
840 Self {
841 decide,
842 decided: std::cell::OnceCell::new(),
843 }
844 }
845
846 pub(crate) fn get(&self) -> i64 {
848 *self.decided.get_or_init(|| (self.decide)().max(1))
849 }
850}
851
852pub(crate) fn open_video_encoder(
865 request: &VideoEncoderOpenRequest<'_>,
866) -> Result<(AVCodecContext, NativeVideoEncoderSelection), EncodeError> {
867 let candidates = video_encoder_candidates(request.config.codec, request.requested_name)?;
868 open_first_video_encoder(request, candidates)
869}
870
871pub(crate) fn open_first_video_encoder(
874 request: &VideoEncoderOpenRequest<'_>,
875 candidates: Vec<VideoEncoderCandidate>,
876) -> Result<(AVCodecContext, NativeVideoEncoderSelection), EncodeError> {
877 let mut rejected = Vec::new();
878 let last = candidates.len().saturating_sub(1);
879 for (index, candidate) in candidates.into_iter().enumerate() {
880 let name = candidate.name();
881 let class = encoder_class(&candidate.encoder);
882 match open_one_video_encoder(request, &candidate, &name) {
883 Ok((encoder, rate_control, quantizer_refused)) => {
884 return Ok((
885 encoder,
886 NativeVideoEncoderSelection {
887 encoder_name: name,
888 class,
889 hardware_accelerated: candidate.on_hardware,
890 rejected,
891 rate_control,
892 quantizer_refused,
893 software_threads: (!candidate.on_hardware).then(|| request.threads.get()),
894 },
895 ));
896 }
897 Err((reason, error)) => {
898 let detail = error.to_string();
899 if index == last {
900 let earlier = rejected
901 .iter()
902 .map(|fallback: &NativeVideoEncoderFallback| {
903 format!(
904 "{} {} ({}): {}",
905 fallback.encoder_name,
906 mode_label(fallback.hardware_accelerated),
907 fallback.reason.as_str(),
908 fallback.detail
909 )
910 })
911 .collect::<Vec<_>>();
912 let prefix = if earlier.is_empty() {
913 String::new()
914 } else {
915 format!("after {}; ", earlier.join("; "))
916 };
917 return Err(EncodeError::Adapter(format!(
918 "{prefix}{name} {} ({}): {detail}",
919 mode_label(candidate.on_hardware),
920 reason.as_str()
921 )));
922 }
923 alpha2_os::diagnostic!(
924 "alpha2 native export: video encoder {name} {} ({}) rejected: {detail}",
925 mode_label(candidate.on_hardware),
926 reason.as_str()
927 );
928 rejected.push(NativeVideoEncoderFallback {
929 encoder_name: name,
930 hardware_accelerated: candidate.on_hardware,
931 reason,
932 detail,
933 });
934 }
935 }
936 }
937 Err(unavailable_codec_error(request.config.codec))
938}
939
940fn open_one_video_encoder(
947 request: &VideoEncoderOpenRequest<'_>,
948 candidate: &VideoEncoderCandidate,
949 name: &str,
950) -> Result<
951 (AVCodecContext, crate::AppliedRateControl, Option<String>),
952 (NativeEncoderRejection, EncodeError),
953> {
954 validate_encoder_dimensions(name, request.config.width, request.config.height)
955 .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
956 let refused = match crate::rate_control::quantizer_request(name, request.config.rate_control) {
957 None => None,
958 Some(Err(reason)) => Some(reason),
959 Some(Ok(quantized)) => {
960 match open_video_encoder_context(request, candidate, name, Some(&quantized)) {
961 Ok(encoder) => return Ok((encoder, crate::AppliedRateControl::Quantizer, None)),
962 Err((_, error)) => Some(format!("{} refused: {error}", quantized.describe())),
963 }
964 }
965 };
966 if let Some(reason) = &refused {
967 alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
968 }
969 open_video_encoder_context(request, candidate, name, None)
970 .map(|encoder| (encoder, crate::AppliedRateControl::Bitrate, refused))
971}
972
973fn open_video_encoder_context(
974 request: &VideoEncoderOpenRequest<'_>,
975 candidate: &VideoEncoderCandidate,
976 name: &str,
977 quantized: Option<&crate::rate_control::MechanismRequest>,
978) -> Result<AVCodecContext, (NativeEncoderRejection, EncodeError)> {
979 let mut encoder = AVCodecContext::new(&candidate.encoder);
980 encoder.set_width(request.width);
981 encoder.set_height(request.height);
982 encoder.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
983 let rate = request.config.frame_rate;
984 let time_base = encoder_time_base(rate, max_time_base_denominator(candidate.encoder.id))
985 .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
986 encoder.set_time_base(time_base);
987 let sar = request.config.display.sample_aspect_ratio;
989 if sar != alpha2_media::SampleAspectRatio::default() {
990 encoder.set_sample_aspect_ratio(rsmpeg::avutil::ra(
991 i32::try_from(sar.num).unwrap_or(1),
992 i32::try_from(sar.den).unwrap_or(1),
993 ));
994 }
995 encoder.set_framerate(rsmpeg::avutil::ra(
996 i32::try_from(rate.numerator()).unwrap_or(i32::MAX),
997 i32::try_from(rate.denominator()).unwrap_or(1),
998 ));
999 encoder.set_bit_rate(quantized.map_or_else(
1000 || i64::try_from(request.config.rate_control.bitrate).unwrap_or(i64::MAX),
1001 |quantized| quantized.bit_rate,
1002 ));
1003 encoder.set_gop_size(keyframe_interval(request.config.frame_rate));
1004 encoder.set_max_b_frames(encoder_max_b_frames(name));
1005 if request.global_header {
1006 encoder.set_flags(
1007 encoder.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
1008 );
1009 }
1010 let threads = if candidate.on_hardware {
1013 0
1014 } else {
1015 request.threads.get()
1016 };
1017 let base = video_encoder_options(
1018 request.config.color,
1019 &candidate.encoder,
1020 candidate.on_hardware,
1021 threads,
1022 request.config.width,
1023 );
1024 let options = match quantized {
1025 Some(quantized) => quantized.dictionary(base),
1026 None => crate::rate_control::rate_control_options(name, request.config.rate_control, base),
1027 };
1028 let unused =
1029 alpha2_ffmpeg_bridge::open_codec_context(&mut encoder, options).map_err(|error| {
1030 let hint = match quantized {
1031 Some(_) => "",
1032 None => crate::rate_control::constant_rate_hint(name, request.config.rate_control),
1033 };
1034 let error = match encode_adapter_error(error) {
1035 EncodeError::Adapter(message) if !hint.is_empty() => {
1036 EncodeError::Adapter(format!("{message}{hint}"))
1037 }
1038 error => error,
1039 };
1040 (NativeEncoderRejection::OpenFailed, error)
1041 })?;
1042 reject_unused_encode_options(unused)
1043 .map_err(|error| (NativeEncoderRejection::OptionsIgnored, error))?;
1044 Ok(encoder)
1045}
1046
1047pub(crate) fn keyframe_interval(frame_rate: alpha2_time::FrameRate) -> i32 {
1058 let frames = (2 * u64::from(frame_rate.numerator()))
1059 .div_ceil(u64::from(frame_rate.denominator()))
1060 .max(1);
1061 i32::try_from(frames).unwrap_or(i32::MAX)
1062}
1063
1064pub(crate) const fn max_time_base_denominator(codec: ffi::AVCodecID) -> Option<u32> {
1069 match codec {
1070 ffi::AV_CODEC_ID_MPEG4 | ffi::AV_CODEC_ID_H263 | ffi::AV_CODEC_ID_H263P => Some(65_535),
1071 _ => None,
1072 }
1073}
1074
1075pub(crate) fn encoder_time_base(
1086 rate: alpha2_time::FrameRate,
1087 limit: Option<u32>,
1088) -> Result<AVRational, EncodeError> {
1089 let out_of_range = || EncodeError::Adapter("frame rate is out of range".into());
1090 let (num, den) = (u64::from(rate.numerator()), u64::from(rate.denominator()));
1091 let exact = || {
1092 Ok(rsmpeg::avutil::ra(
1093 i32::try_from(den).map_err(|_| out_of_range())?,
1094 i32::try_from(num).map_err(|_| out_of_range())?,
1095 ))
1096 };
1097 let Some(limit) = limit.map(u64::from).filter(|limit| num > *limit) else {
1098 return exact();
1099 };
1100 let mut best: Option<(u64, u64, u64)> = None;
1105 for units in num.div_ceil(den).max(1)..=limit {
1106 let q = num / gcd_u64(num, den * units);
1107 let (error, scale) = (q / 2, q * units);
1108 if best.is_none_or(|(_, best_error, best_scale)| {
1109 u128::from(error) * u128::from(best_scale) < u128::from(best_error) * u128::from(scale)
1110 }) {
1111 best = Some((units, error, scale));
1112 }
1113 }
1114 let (units, _, _) = best.ok_or_else(out_of_range)?;
1115 Ok(rsmpeg::avutil::ra(
1116 1,
1117 i32::try_from(units).map_err(|_| out_of_range())?,
1118 ))
1119}
1120
1121const fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
1122 while b != 0 {
1123 (a, b) = (b, a % b);
1124 }
1125 a
1126}
1127
1128pub(crate) fn encoder_max_b_frames(name: &str) -> i32 {
1130 if name.ends_with("_videotoolbox") {
1134 0
1135 } else {
1136 2
1137 }
1138}
1139
1140pub(crate) fn encoder_minimum_dimensions(name: &str) -> (u32, u32) {
1142 if name == "av1_nvenc" {
1145 (146, 128)
1146 } else if name.ends_with("_nvenc") {
1147 (146, 50)
1148 } else {
1149 (2, 2)
1150 }
1151}
1152
1153pub(crate) fn validate_encoder_dimensions(
1154 encoder_name: &str,
1155 width: u32,
1156 height: u32,
1157) -> Result<(), EncodeError> {
1158 let (minimum_width, minimum_height) = encoder_minimum_dimensions(encoder_name);
1159 if width < minimum_width || height < minimum_height {
1160 Err(EncodeError::Adapter(format!(
1161 "encoder {encoder_name} requires at least {minimum_width}x{minimum_height}, received {width}x{height}"
1162 )))
1163 } else {
1164 Ok(())
1165 }
1166}
1167
1168#[derive(Clone, Debug, Eq, PartialEq)]
1170pub struct NativeEncoderHardwareConfig {
1171 pub pixel_format: String,
1173 pub device_type: String,
1175 pub methods: i32,
1177 pub needs_hardware_frames: bool,
1179}
1180
1181#[derive(Clone, Debug, Eq, PartialEq)]
1188#[allow(clippy::struct_excessive_bools)]
1190pub struct NativeVideoEncoderProbe {
1191 pub codec: NativeVideoCodec,
1193 pub encoder_name: String,
1195 pub class: NativeEncoderClass,
1197 pub hardware_accelerated: bool,
1199 pub capability_bits: u32,
1201 pub pixel_formats: Vec<String>,
1203 pub hardware_configs: Vec<NativeEncoderHardwareConfig>,
1205 pub accepts_yuv420p: bool,
1207 pub requires_hardware_frames: bool,
1210 pub advertised: bool,
1212 pub minimum_size: (u32, u32),
1214 pub software_open_error: Option<String>,
1217 pub software_packets: u32,
1219 pub rate_control: Vec<NativeRateControlProbe>,
1221}
1222
1223#[derive(Clone, Debug, Eq, PartialEq)]
1225pub struct NativeRateControlProbe {
1226 pub mechanism: crate::RateControlMechanism,
1228 pub options: Option<String>,
1231 pub outcome: Result<u32, String>,
1234 pub device: Option<String>,
1238}
1239
1240pub(crate) const fn probe_quantizer(codec: NativeVideoCodec, software: bool) -> u32 {
1244 match codec {
1245 NativeVideoCodec::H264 | NativeVideoCodec::Hevc => 26,
1246 NativeVideoCodec::Av1 | NativeVideoCodec::Vp9 => {
1247 if software {
1248 32
1249 } else {
1250 100
1251 }
1252 }
1253 NativeVideoCodec::Mpeg4Part2 => 4,
1254 }
1255}
1256
1257#[must_use]
1267#[allow(clippy::too_many_lines)]
1268pub fn probe_video_encoders(width: u32, height: u32) -> Vec<NativeVideoEncoderProbe> {
1269 let advertised = codec_capabilities();
1270 [
1271 NativeVideoCodec::H264,
1272 NativeVideoCodec::Hevc,
1273 NativeVideoCodec::Vp9,
1274 NativeVideoCodec::Av1,
1275 NativeVideoCodec::Mpeg4Part2,
1276 ]
1277 .into_iter()
1278 .flat_map(|codec| {
1279 registered_video_encoders(codec)
1280 .into_iter()
1281 .map(move |candidate| (codec, candidate))
1282 })
1283 .map(|(codec, candidate)| {
1284 let encoder = &candidate.encoder;
1285 let encoder_name = candidate.name();
1286 let pixel_formats = encoder.pix_fmts().map_or_else(Vec::new, |formats| {
1287 formats
1288 .iter()
1289 .map(|format| {
1290 alpha2_ffmpeg_bridge::pixel_format_name(*format)
1291 .unwrap_or_else(|| "unknown".to_owned())
1292 })
1293 .collect()
1294 });
1295 let hardware_configs = (0..)
1296 .map_while(|index| encoder.hw_config(index))
1297 .map(|config| NativeEncoderHardwareConfig {
1298 pixel_format: alpha2_ffmpeg_bridge::pixel_format_name(config.pix_fmt)
1299 .unwrap_or_else(|| "unknown".to_owned()),
1300 device_type: alpha2_ffmpeg_bridge::hardware_device_type_name(config.device_type)
1301 .unwrap_or_else(|| "none".to_owned()),
1302 methods: config.methods,
1303 needs_hardware_frames: crate::decode::has_hw_config_method(
1304 config.methods,
1305 ffi::AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX,
1306 ),
1307 })
1308 .collect::<Vec<_>>();
1309 let accepts_yuv420p = encoder
1310 .pix_fmts()
1311 .is_none_or(|formats| formats.contains(&ffi::AV_PIX_FMT_YUV420P));
1312 let (software_open_error, software_packets) =
1313 probe_software_encode(&candidate, &encoder_name, width, height, None);
1314 let requires_hardware_frames = !accepts_yuv420p
1315 && hardware_configs
1316 .iter()
1317 .any(|config| config.needs_hardware_frames);
1318 let rate_control = crate::RateControlMechanism::ALL
1319 .into_iter()
1320 .map(|mechanism| {
1321 let request = crate::rate_control::mechanism_request(
1322 &encoder_name,
1323 mechanism,
1324 2_000_000,
1325 if encoder_name.ends_with("_d3d12va") {
1327 26
1328 } else {
1329 probe_quantizer(
1330 codec,
1331 encoder_class(encoder) == NativeEncoderClass::Software,
1332 )
1333 },
1334 );
1335 let mut device = None;
1336 let outcome = match &request {
1337 None => Err("no known option for this encoder family".to_owned()),
1338 Some(request) if requires_hardware_frames => probe_hardware_frames_encode(
1339 encoder,
1340 &encoder_name,
1341 (width, height),
1342 mechanism,
1343 request,
1344 )
1345 .map(|(packets, on)| {
1346 device = Some(on);
1347 packets
1348 }),
1349 Some(request) => {
1350 match probe_software_encode(
1351 &candidate,
1352 &encoder_name,
1353 width,
1354 height,
1355 Some(request),
1356 ) {
1357 (None, packets) => Ok(packets),
1358 (Some(error), _) => Err(error),
1359 }
1360 }
1361 };
1362 NativeRateControlProbe {
1363 mechanism,
1364 options: request
1365 .as_ref()
1366 .map(crate::rate_control::MechanismRequest::describe),
1367 outcome,
1368 device,
1369 }
1370 })
1371 .collect();
1372 NativeVideoEncoderProbe {
1373 codec,
1374 class: encoder_class(encoder),
1375 hardware_accelerated: candidate.on_hardware,
1376 capability_bits: u32::try_from(encoder.capabilities).unwrap_or(0),
1377 advertised: advertised.video.iter().any(|capability| {
1378 capability.codec == codec
1379 && capability.encoder_name == encoder_name
1380 && capability.hardware_accelerated == candidate.on_hardware
1381 }),
1382 minimum_size: encoder_minimum_dimensions(&encoder_name),
1383 requires_hardware_frames,
1384 accepts_yuv420p,
1385 pixel_formats,
1386 hardware_configs,
1387 encoder_name,
1388 software_open_error,
1389 software_packets,
1390 rate_control,
1391 }
1392 })
1393 .collect()
1394}
1395
1396#[allow(unused_variables)]
1412fn probe_hardware_frames_encode(
1413 encoder: &AVCodecRef<'static>,
1414 name: &str,
1415 (width, height): (u32, u32),
1416 mechanism: crate::RateControlMechanism,
1417 request: &crate::rate_control::MechanismRequest,
1418) -> Result<(u32, String), String> {
1419 #[cfg(all(target_os = "linux", feature = "hwencode"))]
1420 if name.ends_with("_vulkan") {
1421 return crate::hwencode::probe_vulkan_rate_control(encoder, width, height, request);
1422 }
1423 #[cfg(all(target_os = "windows", feature = "hwencode"))]
1424 if name.ends_with("_d3d12va") {
1425 return crate::hwencode::probe_d3d12va_rate_control(
1426 name, width, height, mechanism, request,
1427 );
1428 }
1429 Err("takes hardware frames this probe cannot supply".to_owned())
1430}
1431
1432fn probe_software_encode(
1434 candidate: &VideoEncoderCandidate,
1435 name: &str,
1436 width: u32,
1437 height: u32,
1438 request: Option<&crate::rate_control::MechanismRequest>,
1439) -> (Option<String>, u32) {
1440 let Ok(coded_width) = i32::try_from(width) else {
1441 return (Some("width is out of range".to_owned()), 0);
1442 };
1443 let Ok(coded_height) = i32::try_from(height) else {
1444 return (Some("height is out of range".to_owned()), 0);
1445 };
1446 if let Err(error) = validate_encoder_dimensions(name, width, height) {
1447 return (Some(error.to_string()), 0);
1448 }
1449 let encoder = &candidate.encoder;
1450 let mut context = AVCodecContext::new(encoder);
1451 context.set_width(coded_width);
1452 context.set_height(coded_height);
1453 context.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
1454 let time_base = rsmpeg::avutil::ra(1, 30);
1455 context.set_time_base(time_base);
1456 context.set_framerate(rsmpeg::avutil::ra(30, 1));
1457 context.set_bit_rate(request.map_or(2_000_000, |request| request.bit_rate));
1458 context.set_gop_size(12);
1459 context.set_max_b_frames(encoder_max_b_frames(name));
1460 let options = video_encoder_options(
1461 ColorMetadata::EXPORT,
1462 encoder,
1463 candidate.on_hardware,
1464 1,
1465 width,
1466 );
1467 let options = match request {
1468 Some(request) => request.dictionary(options),
1469 None => options,
1470 };
1471 match alpha2_ffmpeg_bridge::open_codec_context(&mut context, options) {
1472 Err(error) => return (Some(error.to_string()), 0),
1473 Ok(unused) => {
1474 if let Err(error) = reject_unused_encode_options(unused) {
1475 return (Some(error.to_string()), 0);
1476 }
1477 }
1478 }
1479 let Some(image) =
1480 rsmpeg::avutil::AVImage::new(ffi::AV_PIX_FMT_YUV420P, coded_width, coded_height, 1)
1481 else {
1482 return (Some("cannot allocate a probe frame".to_owned()), 0);
1483 };
1484 let mut frame = rsmpeg::avutil::AVFrameWithImage::new(image);
1485 frame.set_pts(0);
1486 frame.set_time_base(time_base);
1487 if let Err(error) = context.send_frame(Some(&frame)) {
1488 return (Some(error.to_string()), 0);
1489 }
1490 if let Err(error) = context.send_frame(None) {
1491 return (Some(error.to_string()), 0);
1492 }
1493 let mut packets = 0;
1494 loop {
1495 match context.receive_packet() {
1496 Ok(_) => packets += 1,
1497 Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
1498 Err(error) => return (Some(error.to_string()), packets),
1499 }
1500 }
1501 if packets == 0 {
1502 return (
1503 Some("opened but produced no packet from a software frame".to_owned()),
1504 0,
1505 );
1506 }
1507 (None, packets)
1508}