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 pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
77 match self {
78 Self::H264 => ffi::AV_CODEC_ID_H264,
79 Self::Hevc => ffi::AV_CODEC_ID_HEVC,
80 Self::Vp9 => ffi::AV_CODEC_ID_VP9,
81 Self::Av1 => ffi::AV_CODEC_ID_AV1,
82 Self::Mpeg4Part2 => ffi::AV_CODEC_ID_MPEG4,
83 }
84 }
85}
86
87#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
89#[non_exhaustive]
90pub enum NativeAudioCodec {
91 Aac,
93}
94
95impl NativeAudioCodec {
96 #[must_use]
98 pub const fn as_str(self) -> &'static str {
99 match self {
100 Self::Aac => "aac",
101 }
102 }
103
104 #[must_use]
106 pub const fn model(self) -> alpha2_media::AudioCodec {
107 match self {
108 Self::Aac => alpha2_media::AudioCodec::Aac,
109 }
110 }
111
112 pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
113 match self {
114 Self::Aac => ffi::AV_CODEC_ID_AAC,
115 }
116 }
117}
118
119#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
129#[non_exhaustive]
130pub enum NativeEncoderClass {
131 Software,
133 Hardware,
135 Hybrid,
139}
140
141impl NativeEncoderClass {
142 #[must_use]
144 pub const fn as_str(self) -> &'static str {
145 match self {
146 Self::Software => "software",
147 Self::Hardware => "hardware",
148 Self::Hybrid => "hybrid",
149 }
150 }
151
152 #[must_use]
158 pub const fn modes(self, hybrid_hardware: bool) -> &'static [bool] {
159 match self {
160 Self::Hardware => &[true],
161 Self::Hybrid if hybrid_hardware => &[true, false],
162 Self::Software | Self::Hybrid => &[false],
163 }
164 }
165}
166
167pub(crate) struct VideoEncoderCandidate {
170 pub(crate) encoder: AVCodecRef<'static>,
171 pub(crate) on_hardware: bool,
172}
173
174impl VideoEncoderCandidate {
175 pub(crate) fn name(&self) -> String {
176 self.encoder.name().to_string_lossy().into_owned()
177 }
178}
179
180#[must_use]
190pub fn hybrid_encoders_use_hardware() -> bool {
191 std::env::var("ALPHA2_VIDEO_ENCODER_HYBRID")
192 .is_ok_and(|value| value.eq_ignore_ascii_case("hardware"))
193}
194
195#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
197#[non_exhaustive]
198pub enum NativeEncoderRejection {
199 Dimensions,
201 OpenFailed,
203 OptionsIgnored,
205}
206
207impl NativeEncoderRejection {
208 #[must_use]
210 pub const fn as_str(self) -> &'static str {
211 match self {
212 Self::Dimensions => "dimensions",
213 Self::OpenFailed => "open-failed",
214 Self::OptionsIgnored => "options-ignored",
215 }
216 }
217}
218
219#[derive(Clone, Debug, Eq, PartialEq)]
221pub struct NativeVideoEncoderFallback {
222 pub encoder_name: String,
224 pub hardware_accelerated: bool,
227 pub reason: NativeEncoderRejection,
229 pub detail: String,
231}
232
233#[derive(Clone, Debug, Eq, PartialEq)]
239pub struct NativeVideoEncoderSelection {
240 pub encoder_name: String,
242 pub class: NativeEncoderClass,
244 pub hardware_accelerated: bool,
246 pub rejected: Vec<NativeVideoEncoderFallback>,
248 pub rate_control: crate::AppliedRateControl,
250 pub quantizer_refused: Option<String>,
252 pub software_threads: Option<i64>,
255}
256
257#[derive(Clone, Debug, Eq, PartialEq)]
259pub struct NativeVideoEncoderCapability {
260 pub codec: NativeVideoCodec,
262 pub encoder_name: String,
264 pub class: NativeEncoderClass,
266 pub hardware_accelerated: bool,
269 pub preferred: bool,
271 pub minimum_width: u32,
273 pub minimum_height: u32,
275}
276
277#[derive(Clone, Debug, Eq, PartialEq)]
279pub struct NativeAudioEncoderCapability {
280 pub codec: NativeAudioCodec,
282 pub encoder_name: String,
284}
285
286#[derive(Clone, Debug, Eq, PartialEq)]
288pub struct NativeCodecCapabilities {
289 pub mp4_video: Vec<NativeVideoEncoderCapability>,
292 pub mp4_audio: Vec<NativeAudioEncoderCapability>,
294}
295
296#[must_use]
303pub fn codec_capabilities() -> NativeCodecCapabilities {
304 let mp4_video = [
305 NativeVideoCodec::H264,
306 NativeVideoCodec::Hevc,
307 NativeVideoCodec::Vp9,
308 NativeVideoCodec::Av1,
309 NativeVideoCodec::Mpeg4Part2,
310 ]
311 .into_iter()
312 .flat_map(|codec| {
313 registered_video_encoders(codec)
314 .into_iter()
315 .filter(encoder_is_compatible)
316 .enumerate()
317 .map(move |(index, candidate)| {
318 let encoder_name = candidate.name();
319 let (minimum_width, minimum_height) = encoder_minimum_dimensions(&encoder_name);
320 NativeVideoEncoderCapability {
321 codec,
322 class: encoder_class(&candidate.encoder),
323 hardware_accelerated: candidate.on_hardware,
324 preferred: index == 0,
325 encoder_name,
326 minimum_width,
327 minimum_height,
328 }
329 })
330 })
331 .collect();
332 let mp4_audio = [NativeAudioCodec::Aac]
333 .into_iter()
334 .filter_map(|codec| {
335 let encoder = AVCodec::find_encoder(codec.codec_id())?;
336 Some(NativeAudioEncoderCapability {
337 codec,
338 encoder_name: encoder.name().to_string_lossy().into_owned(),
339 })
340 })
341 .collect();
342 NativeCodecCapabilities {
343 mp4_video,
344 mp4_audio,
345 }
346}
347
348#[derive(Clone, Debug, Eq, PartialEq)]
350pub struct NativeHardwareDecodeCapability {
351 pub codec: NativeVideoCodec,
353 pub decoder_name: Option<String>,
355 pub hardware_config: bool,
361 pub software_decoder: Option<String>,
365}
366
367#[derive(Clone, Debug, Eq, PartialEq)]
369pub struct NativeHardwareDecodeCapabilities {
370 pub backend: &'static str,
372 pub codecs: Vec<NativeHardwareDecodeCapability>,
374}
375
376#[must_use]
378pub const fn hardware_decode_backend() -> &'static str {
379 #[cfg(feature = "hwdecode")]
380 {
381 #[cfg(target_os = "linux")]
382 {
383 "vulkan"
384 }
385 #[cfg(target_os = "macos")]
386 {
387 "videotoolbox"
388 }
389 #[cfg(target_os = "windows")]
390 {
391 "d3d12va"
392 }
393 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
394 {
395 "none"
396 }
397 }
398 #[cfg(not(feature = "hwdecode"))]
399 {
400 "none"
401 }
402}
403
404#[must_use]
406pub fn hardware_decode_capabilities() -> NativeHardwareDecodeCapabilities {
407 let codecs = [
408 NativeVideoCodec::H264,
409 NativeVideoCodec::Hevc,
410 NativeVideoCodec::Av1,
411 NativeVideoCodec::Vp9,
412 ]
413 .into_iter()
414 .map(|codec| {
415 let decoder = AVCodec::find_decoder(codec.codec_id());
416 NativeHardwareDecodeCapability {
417 codec,
418 decoder_name: decoder
419 .as_ref()
420 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
421 hardware_config: crate::decode::registered_video_decoders(codec.codec_id())
422 .any(|decoder| decoder_has_hardware_config(&decoder)),
423 software_decoder: crate::decode::software_video_decoder(codec.codec_id())
424 .ok()
425 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
426 }
427 })
428 .collect();
429 NativeHardwareDecodeCapabilities {
430 backend: hardware_decode_backend(),
431 codecs,
432 }
433}
434
435#[allow(unused_variables)]
437fn decoder_has_hardware_config(decoder: &AVCodecRef<'_>) -> bool {
438 #[cfg(all(
439 any(target_os = "linux", target_os = "macos", target_os = "windows"),
440 feature = "hwdecode"
441 ))]
442 {
443 #[cfg(target_os = "linux")]
444 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_VULKAN, ffi::AV_PIX_FMT_VULKAN);
445 #[cfg(target_os = "macos")]
446 let (device_type, pixel_format) = (
447 ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
448 ffi::AV_PIX_FMT_VIDEOTOOLBOX,
449 );
450 #[cfg(target_os = "windows")]
451 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_D3D12VA, ffi::AV_PIX_FMT_D3D12);
452 (0..)
453 .map_while(|index| decoder.hw_config(index))
454 .any(|config| config.device_type == device_type && config.pix_fmt == pixel_format)
455 }
456 #[cfg(not(all(
457 any(target_os = "linux", target_os = "macos", target_os = "windows"),
458 feature = "hwdecode"
459 )))]
460 {
461 false
462 }
463}
464
465pub(crate) fn registered_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderCandidate> {
467 let hybrid_hardware = hybrid_encoders_use_hardware();
468 let mut candidates = AVCodec::iterate()
469 .filter(|encoder| {
470 encoder.id == codec.codec_id()
471 && AVCodec::find_encoder_by_name(encoder.name())
472 .is_some_and(|found| found.as_ptr() == encoder.as_ptr())
473 })
474 .flat_map(|encoder| {
475 encoder_class(&encoder)
476 .modes(hybrid_hardware)
477 .iter()
478 .filter_map(move |&on_hardware| {
479 AVCodec::find_encoder_by_name(encoder.name()).map(|encoder| {
480 VideoEncoderCandidate {
481 encoder,
482 on_hardware,
483 }
484 })
485 })
486 })
487 .collect::<Vec<_>>();
488 candidates.sort_by_key(|candidate| !candidate.on_hardware);
490 candidates
491}
492
493pub(crate) fn encoder_class(encoder: &AVCodec) -> NativeEncoderClass {
496 let name = encoder.name().to_string_lossy();
497 declared_encoder_class(&name, u32::try_from(encoder.capabilities).unwrap_or(0))
498}
499
500const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
511
512pub(crate) fn declared_encoder_class(name: &str, capabilities: u32) -> NativeEncoderClass {
516 if capabilities & ffi::AV_CODEC_CAP_HYBRID != 0 {
517 NativeEncoderClass::Hybrid
518 } else if capabilities & ffi::AV_CODEC_CAP_HARDWARE != 0
519 || MISDECLARED_HARDWARE_ENCODERS.contains(&name)
520 {
521 NativeEncoderClass::Hardware
522 } else {
523 NativeEncoderClass::Software
524 }
525}
526
527pub(crate) fn encoder_is_compatible(candidate: &VideoEncoderCandidate) -> bool {
528 let name = candidate.name();
529 registration_is_compatible(candidate, &name) && encoder_runs_here(candidate, &name)
530}
531
532fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
535 if candidate
538 .encoder
539 .pix_fmts()
540 .is_some_and(|formats| !formats.contains(&ffi::AV_PIX_FMT_YUV420P))
541 {
542 return false;
543 }
544 !candidate.on_hardware || hardware_device_node(name) != Some(false)
545}
546
547pub(crate) fn needs_open_probe(on_hardware: bool, name: &str) -> bool {
555 (on_hardware && hardware_device_node(name).is_none())
556 || name.ends_with("_mf")
557 || name.ends_with("_videotoolbox")
558}
559
560fn encoder_runs_here(candidate: &VideoEncoderCandidate, name: &str) -> bool {
580 static RUNS: LazyLock<Mutex<HashMap<(String, bool), bool>>> = LazyLock::new(Mutex::default);
581 if !needs_open_probe(candidate.on_hardware, name) {
582 return true;
583 }
584 let key = (name.to_owned(), candidate.on_hardware);
585 let mut runs = RUNS
586 .lock()
587 .unwrap_or_else(std::sync::PoisonError::into_inner);
588 *runs.entry(key).or_insert_with(|| {
589 match probe_software_encode(candidate, name, 256, 144, None).0 {
590 None => true,
591 Some(error) => {
592 alpha2_os::diagnostic!(
593 "alpha2 native: video encoder {name} {} is registered but did not encode a \
594 frame on this machine, so it is not offered that way: {error}",
595 mode_label(candidate.on_hardware)
596 );
597 false
598 }
599 }
600 })
601}
602
603pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
605 if on_hardware { "on GPU" } else { "on CPU" }
606}
607
608#[cfg(target_os = "linux")]
619fn hardware_device_node(name: &str) -> Option<bool> {
620 if name.ends_with("_nvenc") {
621 return Some(Path::new("/dev/nvidia0").exists() || Path::new("/dev/nvidiactl").exists());
622 }
623 if name.ends_with("_vaapi") || name.ends_with("_qsv") || name.ends_with("_amf") {
624 return Some(fs::read_dir("/dev/dri").is_ok_and(|entries| {
625 entries.flatten().any(|entry| {
626 entry
627 .file_name()
628 .to_str()
629 .is_some_and(|name| name.starts_with("renderD") || name.starts_with("card"))
630 })
631 }));
632 }
633 if name.ends_with("_v4l2m2m") {
634 return Some((0..64).any(|index| Path::new(&format!("/dev/video{index}")).exists()));
635 }
636 None
637}
638
639#[cfg(not(target_os = "linux"))]
642const fn hardware_device_node(_name: &str) -> Option<bool> {
643 None
644}
645
646fn unavailable_codec_error(codec: NativeVideoCodec) -> EncodeError {
652 let identity = crate::ffmpeg_runtime_identity();
653 let inventory = codec_capabilities()
654 .mp4_video
655 .iter()
656 .map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
657 .collect::<Vec<_>>()
658 .join(", ");
659 let inventory = if inventory.is_empty() {
660 "none".to_owned()
661 } else {
662 inventory
663 };
664 EncodeError::Adapter(format!(
665 "the linked FFmpeg runtime ({}, pinned SDK {}) registers no usable {} encoder; it offers [{inventory}]",
666 identity.version,
667 identity.pinned_sdk_release,
668 codec.as_str()
669 ))
670}
671
672pub(crate) fn video_encoder_candidates(
678 codec: NativeVideoCodec,
679 requested_name: Option<&str>,
680) -> Result<Vec<VideoEncoderCandidate>, EncodeError> {
681 if let Some(name) = requested_name {
682 let encoder = select_video_encoder(codec, Some(name))?;
683 return Ok(registered_video_encoders(codec)
684 .into_iter()
685 .filter(|candidate| candidate.encoder.as_ptr() == encoder.as_ptr())
686 .filter(encoder_is_compatible)
687 .collect());
688 }
689 let candidates = registered_video_encoders(codec)
690 .into_iter()
691 .filter(encoder_is_compatible)
692 .collect::<Vec<_>>();
693 if candidates.is_empty() {
694 return Err(unavailable_codec_error(codec));
695 }
696 Ok(candidates)
697}
698
699pub(crate) fn select_video_encoder(
703 codec: NativeVideoCodec,
704 requested_name: Option<&str>,
705) -> Result<AVCodecRef<'static>, EncodeError> {
706 if let Some(name) = requested_name {
707 let name_c = CString::new(name)
708 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
709 let encoder = AVCodec::find_encoder_by_name(&name_c)
710 .ok_or_else(|| EncodeError::Adapter(format!("video encoder {name} is unavailable")))?;
711 if encoder.id != codec.codec_id() {
712 return Err(EncodeError::Adapter(format!(
713 "video encoder {name} does not implement {}",
714 codec.as_str()
715 )));
716 }
717 if !codec_capabilities()
718 .mp4_video
719 .iter()
720 .any(|capability| capability.codec == codec && capability.encoder_name == name)
721 {
722 return Err(EncodeError::Adapter(format!(
723 "video encoder {name} is not compatible with the native encode path"
724 )));
725 }
726 return Ok(encoder);
727 }
728
729 let capability = codec_capabilities()
730 .mp4_video
731 .into_iter()
732 .find(|capability| capability.codec == codec)
733 .ok_or_else(|| unavailable_codec_error(codec))?;
734 let name = CString::new(capability.encoder_name)
735 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
736 AVCodec::find_encoder_by_name(&name).ok_or_else(|| unavailable_codec_error(codec))
737}
738
739pub(crate) struct VideoEncoderOpenRequest<'a> {
741 pub config: &'a VideoEncodeConfig,
743 pub requested_name: Option<&'a str>,
745 pub threads: SoftwareEncoderThreads<'a>,
748 pub global_header: bool,
750 pub width: i32,
752 pub height: i32,
754}
755
756pub(crate) struct SoftwareEncoderThreads<'a> {
767 decide: &'a dyn Fn() -> i64,
768 decided: std::cell::OnceCell<i64>,
769}
770
771impl<'a> SoftwareEncoderThreads<'a> {
772 pub(crate) fn new(decide: &'a dyn Fn() -> i64) -> Self {
774 Self {
775 decide,
776 decided: std::cell::OnceCell::new(),
777 }
778 }
779
780 pub(crate) fn get(&self) -> i64 {
782 *self.decided.get_or_init(|| (self.decide)().max(1))
783 }
784}
785
786pub(crate) fn open_video_encoder(
799 request: &VideoEncoderOpenRequest<'_>,
800) -> Result<(AVCodecContext, NativeVideoEncoderSelection), EncodeError> {
801 let candidates = video_encoder_candidates(request.config.codec, request.requested_name)?;
802 open_first_video_encoder(request, candidates)
803}
804
805pub(crate) fn open_first_video_encoder(
808 request: &VideoEncoderOpenRequest<'_>,
809 candidates: Vec<VideoEncoderCandidate>,
810) -> Result<(AVCodecContext, NativeVideoEncoderSelection), EncodeError> {
811 let mut rejected = Vec::new();
812 let last = candidates.len().saturating_sub(1);
813 for (index, candidate) in candidates.into_iter().enumerate() {
814 let name = candidate.name();
815 let class = encoder_class(&candidate.encoder);
816 match open_one_video_encoder(request, &candidate, &name) {
817 Ok((encoder, rate_control, quantizer_refused)) => {
818 return Ok((
819 encoder,
820 NativeVideoEncoderSelection {
821 encoder_name: name,
822 class,
823 hardware_accelerated: candidate.on_hardware,
824 rejected,
825 rate_control,
826 quantizer_refused,
827 software_threads: (!candidate.on_hardware).then(|| request.threads.get()),
828 },
829 ));
830 }
831 Err((reason, error)) => {
832 let detail = error.to_string();
833 if index == last {
834 let earlier = rejected
835 .iter()
836 .map(|fallback: &NativeVideoEncoderFallback| {
837 format!(
838 "{} {} ({}): {}",
839 fallback.encoder_name,
840 mode_label(fallback.hardware_accelerated),
841 fallback.reason.as_str(),
842 fallback.detail
843 )
844 })
845 .collect::<Vec<_>>();
846 let prefix = if earlier.is_empty() {
847 String::new()
848 } else {
849 format!("after {}; ", earlier.join("; "))
850 };
851 return Err(EncodeError::Adapter(format!(
852 "{prefix}{name} {} ({}): {detail}",
853 mode_label(candidate.on_hardware),
854 reason.as_str()
855 )));
856 }
857 alpha2_os::diagnostic!(
858 "alpha2 native export: video encoder {name} {} ({}) rejected: {detail}",
859 mode_label(candidate.on_hardware),
860 reason.as_str()
861 );
862 rejected.push(NativeVideoEncoderFallback {
863 encoder_name: name,
864 hardware_accelerated: candidate.on_hardware,
865 reason,
866 detail,
867 });
868 }
869 }
870 }
871 Err(unavailable_codec_error(request.config.codec))
872}
873
874fn open_one_video_encoder(
881 request: &VideoEncoderOpenRequest<'_>,
882 candidate: &VideoEncoderCandidate,
883 name: &str,
884) -> Result<
885 (AVCodecContext, crate::AppliedRateControl, Option<String>),
886 (NativeEncoderRejection, EncodeError),
887> {
888 validate_encoder_dimensions(name, request.config.width, request.config.height)
889 .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
890 let refused = match crate::rate_control::quantizer_request(name, request.config.rate_control) {
891 None => None,
892 Some(Err(reason)) => Some(reason),
893 Some(Ok(quantized)) => {
894 match open_video_encoder_context(request, candidate, name, Some(&quantized)) {
895 Ok(encoder) => return Ok((encoder, crate::AppliedRateControl::Quantizer, None)),
896 Err((_, error)) => Some(format!("{} refused: {error}", quantized.describe())),
897 }
898 }
899 };
900 if let Some(reason) = &refused {
901 alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
902 }
903 open_video_encoder_context(request, candidate, name, None)
904 .map(|encoder| (encoder, crate::AppliedRateControl::Bitrate, refused))
905}
906
907fn open_video_encoder_context(
908 request: &VideoEncoderOpenRequest<'_>,
909 candidate: &VideoEncoderCandidate,
910 name: &str,
911 quantized: Option<&crate::rate_control::MechanismRequest>,
912) -> Result<AVCodecContext, (NativeEncoderRejection, EncodeError)> {
913 let mut encoder = AVCodecContext::new(&candidate.encoder);
914 encoder.set_width(request.width);
915 encoder.set_height(request.height);
916 encoder.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
917 let rate = request.config.frame_rate;
918 let time_base = encoder_time_base(rate, max_time_base_denominator(candidate.encoder.id))
919 .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
920 encoder.set_time_base(time_base);
921 let sar = request.config.display.sample_aspect_ratio;
923 if sar != alpha2_media::SampleAspectRatio::default() {
924 encoder.set_sample_aspect_ratio(rsmpeg::avutil::ra(
925 i32::try_from(sar.num).unwrap_or(1),
926 i32::try_from(sar.den).unwrap_or(1),
927 ));
928 }
929 encoder.set_framerate(rsmpeg::avutil::ra(
930 i32::try_from(rate.numerator()).unwrap_or(i32::MAX),
931 i32::try_from(rate.denominator()).unwrap_or(1),
932 ));
933 encoder.set_bit_rate(quantized.map_or_else(
934 || i64::try_from(request.config.rate_control.bitrate).unwrap_or(i64::MAX),
935 |quantized| quantized.bit_rate,
936 ));
937 encoder.set_gop_size(keyframe_interval(request.config.frame_rate));
938 encoder.set_max_b_frames(encoder_max_b_frames(name));
939 if request.global_header {
940 encoder.set_flags(
941 encoder.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
942 );
943 }
944 let threads = if candidate.on_hardware {
947 0
948 } else {
949 request.threads.get()
950 };
951 let base = video_encoder_options(
952 request.config.color,
953 &candidate.encoder,
954 candidate.on_hardware,
955 threads,
956 );
957 let options = match quantized {
958 Some(quantized) => quantized.dictionary(base),
959 None => crate::rate_control::rate_control_options(name, request.config.rate_control, base),
960 };
961 let unused =
962 alpha2_ffmpeg_bridge::open_codec_context(&mut encoder, options).map_err(|error| {
963 let hint = match quantized {
964 Some(_) => "",
965 None => crate::rate_control::constant_rate_hint(name, request.config.rate_control),
966 };
967 let error = match encode_adapter_error(error) {
968 EncodeError::Adapter(message) if !hint.is_empty() => {
969 EncodeError::Adapter(format!("{message}{hint}"))
970 }
971 error => error,
972 };
973 (NativeEncoderRejection::OpenFailed, error)
974 })?;
975 reject_unused_encode_options(unused)
976 .map_err(|error| (NativeEncoderRejection::OptionsIgnored, error))?;
977 Ok(encoder)
978}
979
980pub(crate) fn keyframe_interval(frame_rate: alpha2_time::FrameRate) -> i32 {
991 let frames = (2 * u64::from(frame_rate.numerator()))
992 .div_ceil(u64::from(frame_rate.denominator()))
993 .max(1);
994 i32::try_from(frames).unwrap_or(i32::MAX)
995}
996
997pub(crate) const fn max_time_base_denominator(codec: ffi::AVCodecID) -> Option<u32> {
1002 match codec {
1003 ffi::AV_CODEC_ID_MPEG4 | ffi::AV_CODEC_ID_H263 | ffi::AV_CODEC_ID_H263P => Some(65_535),
1004 _ => None,
1005 }
1006}
1007
1008pub(crate) fn encoder_time_base(
1019 rate: alpha2_time::FrameRate,
1020 limit: Option<u32>,
1021) -> Result<AVRational, EncodeError> {
1022 let out_of_range = || EncodeError::Adapter("frame rate is out of range".into());
1023 let (num, den) = (u64::from(rate.numerator()), u64::from(rate.denominator()));
1024 let exact = || {
1025 Ok(rsmpeg::avutil::ra(
1026 i32::try_from(den).map_err(|_| out_of_range())?,
1027 i32::try_from(num).map_err(|_| out_of_range())?,
1028 ))
1029 };
1030 let Some(limit) = limit.map(u64::from).filter(|limit| num > *limit) else {
1031 return exact();
1032 };
1033 let mut best: Option<(u64, u64, u64)> = None;
1038 for units in num.div_ceil(den).max(1)..=limit {
1039 let q = num / gcd_u64(num, den * units);
1040 let (error, scale) = (q / 2, q * units);
1041 if best.is_none_or(|(_, best_error, best_scale)| {
1042 u128::from(error) * u128::from(best_scale) < u128::from(best_error) * u128::from(scale)
1043 }) {
1044 best = Some((units, error, scale));
1045 }
1046 }
1047 let (units, _, _) = best.ok_or_else(out_of_range)?;
1048 Ok(rsmpeg::avutil::ra(
1049 1,
1050 i32::try_from(units).map_err(|_| out_of_range())?,
1051 ))
1052}
1053
1054const fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
1055 while b != 0 {
1056 (a, b) = (b, a % b);
1057 }
1058 a
1059}
1060
1061pub(crate) fn encoder_max_b_frames(name: &str) -> i32 {
1063 if name.ends_with("_videotoolbox") {
1067 0
1068 } else {
1069 2
1070 }
1071}
1072
1073pub(crate) fn encoder_minimum_dimensions(name: &str) -> (u32, u32) {
1075 if name == "av1_nvenc" {
1078 (146, 128)
1079 } else if name.ends_with("_nvenc") {
1080 (146, 50)
1081 } else {
1082 (2, 2)
1083 }
1084}
1085
1086pub(crate) fn validate_encoder_dimensions(
1087 encoder_name: &str,
1088 width: u32,
1089 height: u32,
1090) -> Result<(), EncodeError> {
1091 let (minimum_width, minimum_height) = encoder_minimum_dimensions(encoder_name);
1092 if width < minimum_width || height < minimum_height {
1093 Err(EncodeError::Adapter(format!(
1094 "encoder {encoder_name} requires at least {minimum_width}x{minimum_height}, received {width}x{height}"
1095 )))
1096 } else {
1097 Ok(())
1098 }
1099}
1100
1101#[derive(Clone, Debug, Eq, PartialEq)]
1103pub struct NativeEncoderHardwareConfig {
1104 pub pixel_format: String,
1106 pub device_type: String,
1108 pub methods: i32,
1110 pub needs_hardware_frames: bool,
1112}
1113
1114#[derive(Clone, Debug, Eq, PartialEq)]
1121#[allow(clippy::struct_excessive_bools)]
1123pub struct NativeVideoEncoderProbe {
1124 pub codec: NativeVideoCodec,
1126 pub encoder_name: String,
1128 pub class: NativeEncoderClass,
1130 pub hardware_accelerated: bool,
1132 pub capability_bits: u32,
1134 pub pixel_formats: Vec<String>,
1136 pub hardware_configs: Vec<NativeEncoderHardwareConfig>,
1138 pub accepts_yuv420p: bool,
1140 pub requires_hardware_frames: bool,
1143 pub advertised: bool,
1145 pub minimum_size: (u32, u32),
1147 pub software_open_error: Option<String>,
1150 pub software_packets: u32,
1152 pub rate_control: Vec<NativeRateControlProbe>,
1154}
1155
1156#[derive(Clone, Debug, Eq, PartialEq)]
1158pub struct NativeRateControlProbe {
1159 pub mechanism: crate::RateControlMechanism,
1161 pub options: Option<String>,
1164 pub outcome: Result<u32, String>,
1167 pub device: Option<String>,
1171}
1172
1173pub(crate) const fn probe_quantizer(
1175 codec: NativeVideoCodec,
1176 encoder_name_is_libavcodec: bool,
1177) -> u32 {
1178 match codec {
1179 NativeVideoCodec::H264 | NativeVideoCodec::Hevc => 26,
1180 NativeVideoCodec::Av1 | NativeVideoCodec::Vp9 => {
1181 if encoder_name_is_libavcodec {
1182 10
1183 } else {
1184 100
1185 }
1186 }
1187 NativeVideoCodec::Mpeg4Part2 => 4,
1188 }
1189}
1190
1191#[must_use]
1201#[allow(clippy::too_many_lines)]
1202pub fn probe_video_encoders(width: u32, height: u32) -> Vec<NativeVideoEncoderProbe> {
1203 let advertised = codec_capabilities();
1204 [
1205 NativeVideoCodec::H264,
1206 NativeVideoCodec::Hevc,
1207 NativeVideoCodec::Vp9,
1208 NativeVideoCodec::Av1,
1209 NativeVideoCodec::Mpeg4Part2,
1210 ]
1211 .into_iter()
1212 .flat_map(|codec| {
1213 registered_video_encoders(codec)
1214 .into_iter()
1215 .map(move |candidate| (codec, candidate))
1216 })
1217 .map(|(codec, candidate)| {
1218 let encoder = &candidate.encoder;
1219 let encoder_name = candidate.name();
1220 let pixel_formats = encoder.pix_fmts().map_or_else(Vec::new, |formats| {
1221 formats
1222 .iter()
1223 .map(|format| {
1224 alpha2_ffmpeg_bridge::pixel_format_name(*format)
1225 .unwrap_or_else(|| "unknown".to_owned())
1226 })
1227 .collect()
1228 });
1229 let hardware_configs = (0..)
1230 .map_while(|index| encoder.hw_config(index))
1231 .map(|config| NativeEncoderHardwareConfig {
1232 pixel_format: alpha2_ffmpeg_bridge::pixel_format_name(config.pix_fmt)
1233 .unwrap_or_else(|| "unknown".to_owned()),
1234 device_type: alpha2_ffmpeg_bridge::hardware_device_type_name(config.device_type)
1235 .unwrap_or_else(|| "none".to_owned()),
1236 methods: config.methods,
1237 needs_hardware_frames: crate::decode::has_hw_config_method(
1238 config.methods,
1239 ffi::AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX,
1240 ),
1241 })
1242 .collect::<Vec<_>>();
1243 let accepts_yuv420p = encoder
1244 .pix_fmts()
1245 .is_none_or(|formats| formats.contains(&ffi::AV_PIX_FMT_YUV420P));
1246 let (software_open_error, software_packets) =
1247 probe_software_encode(&candidate, &encoder_name, width, height, None);
1248 let requires_hardware_frames = !accepts_yuv420p
1249 && hardware_configs
1250 .iter()
1251 .any(|config| config.needs_hardware_frames);
1252 let rate_control = crate::RateControlMechanism::ALL
1253 .into_iter()
1254 .map(|mechanism| {
1255 let request = crate::rate_control::mechanism_request(
1256 &encoder_name,
1257 mechanism,
1258 2_000_000,
1259 if encoder_name.ends_with("_d3d12va") {
1261 26
1262 } else {
1263 probe_quantizer(
1264 codec,
1265 encoder_class(encoder) == NativeEncoderClass::Software,
1266 )
1267 },
1268 );
1269 let mut device = None;
1270 let outcome = match &request {
1271 None => Err("no known option for this encoder family".to_owned()),
1272 Some(request) if requires_hardware_frames => probe_hardware_frames_encode(
1273 encoder,
1274 &encoder_name,
1275 (width, height),
1276 mechanism,
1277 request,
1278 )
1279 .map(|(packets, on)| {
1280 device = Some(on);
1281 packets
1282 }),
1283 Some(request) => {
1284 match probe_software_encode(
1285 &candidate,
1286 &encoder_name,
1287 width,
1288 height,
1289 Some(request),
1290 ) {
1291 (None, packets) => Ok(packets),
1292 (Some(error), _) => Err(error),
1293 }
1294 }
1295 };
1296 NativeRateControlProbe {
1297 mechanism,
1298 options: request
1299 .as_ref()
1300 .map(crate::rate_control::MechanismRequest::describe),
1301 outcome,
1302 device,
1303 }
1304 })
1305 .collect();
1306 NativeVideoEncoderProbe {
1307 codec,
1308 class: encoder_class(encoder),
1309 hardware_accelerated: candidate.on_hardware,
1310 capability_bits: u32::try_from(encoder.capabilities).unwrap_or(0),
1311 advertised: advertised.mp4_video.iter().any(|capability| {
1312 capability.codec == codec
1313 && capability.encoder_name == encoder_name
1314 && capability.hardware_accelerated == candidate.on_hardware
1315 }),
1316 minimum_size: encoder_minimum_dimensions(&encoder_name),
1317 requires_hardware_frames,
1318 accepts_yuv420p,
1319 pixel_formats,
1320 hardware_configs,
1321 encoder_name,
1322 software_open_error,
1323 software_packets,
1324 rate_control,
1325 }
1326 })
1327 .collect()
1328}
1329
1330#[allow(unused_variables)]
1346fn probe_hardware_frames_encode(
1347 encoder: &AVCodecRef<'static>,
1348 name: &str,
1349 (width, height): (u32, u32),
1350 mechanism: crate::RateControlMechanism,
1351 request: &crate::rate_control::MechanismRequest,
1352) -> Result<(u32, String), String> {
1353 #[cfg(all(target_os = "linux", feature = "hwencode"))]
1354 if name.ends_with("_vulkan") {
1355 return crate::hwencode::probe_vulkan_rate_control(encoder, width, height, request);
1356 }
1357 #[cfg(all(target_os = "windows", feature = "hwencode"))]
1358 if name.ends_with("_d3d12va") {
1359 return crate::hwencode::probe_d3d12va_rate_control(
1360 name, width, height, mechanism, request,
1361 );
1362 }
1363 Err("takes hardware frames this probe cannot supply".to_owned())
1364}
1365
1366fn probe_software_encode(
1368 candidate: &VideoEncoderCandidate,
1369 name: &str,
1370 width: u32,
1371 height: u32,
1372 request: Option<&crate::rate_control::MechanismRequest>,
1373) -> (Option<String>, u32) {
1374 let Ok(coded_width) = i32::try_from(width) else {
1375 return (Some("width is out of range".to_owned()), 0);
1376 };
1377 let Ok(coded_height) = i32::try_from(height) else {
1378 return (Some("height is out of range".to_owned()), 0);
1379 };
1380 if let Err(error) = validate_encoder_dimensions(name, width, height) {
1381 return (Some(error.to_string()), 0);
1382 }
1383 let encoder = &candidate.encoder;
1384 let mut context = AVCodecContext::new(encoder);
1385 context.set_width(coded_width);
1386 context.set_height(coded_height);
1387 context.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
1388 let time_base = rsmpeg::avutil::ra(1, 30);
1389 context.set_time_base(time_base);
1390 context.set_framerate(rsmpeg::avutil::ra(30, 1));
1391 context.set_bit_rate(request.map_or(2_000_000, |request| request.bit_rate));
1392 context.set_gop_size(12);
1393 context.set_max_b_frames(encoder_max_b_frames(name));
1394 let options = video_encoder_options(ColorMetadata::EXPORT, encoder, candidate.on_hardware, 1);
1395 let options = match request {
1396 Some(request) => request.dictionary(options),
1397 None => options,
1398 };
1399 match alpha2_ffmpeg_bridge::open_codec_context(&mut context, options) {
1400 Err(error) => return (Some(error.to_string()), 0),
1401 Ok(unused) => {
1402 if let Err(error) = reject_unused_encode_options(unused) {
1403 return (Some(error.to_string()), 0);
1404 }
1405 }
1406 }
1407 let Some(image) =
1408 rsmpeg::avutil::AVImage::new(ffi::AV_PIX_FMT_YUV420P, coded_width, coded_height, 1)
1409 else {
1410 return (Some("cannot allocate a probe frame".to_owned()), 0);
1411 };
1412 let mut frame = rsmpeg::avutil::AVFrameWithImage::new(image);
1413 frame.set_pts(0);
1414 frame.set_time_base(time_base);
1415 if let Err(error) = context.send_frame(Some(&frame)) {
1416 return (Some(error.to_string()), 0);
1417 }
1418 if let Err(error) = context.send_frame(None) {
1419 return (Some(error.to_string()), 0);
1420 }
1421 let mut packets = 0;
1422 loop {
1423 match context.receive_packet() {
1424 Ok(_) => packets += 1,
1425 Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
1426 Err(error) => return (Some(error.to_string()), packets),
1427 }
1428 }
1429 if packets == 0 {
1430 return (
1431 Some("opened but produced no packet from a software frame".to_owned()),
1432 0,
1433 );
1434 }
1435 (None, packets)
1436}