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_media::{ColorMetadata, EncodeError};
13use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef};
14use rsmpeg::avutil::AVRational;
15use rsmpeg::error::RsmpegError;
16use rsmpeg::ffi;
17
18#[derive(Copy, Clone, Debug, PartialEq)]
20pub struct AvEncodeConfig {
21 pub video: VideoEncodeConfig,
23 pub audio_sample_rate: u32,
25 pub audio_channels: u16,
27 pub audio_codec: NativeAudioCodec,
29 pub audio_bit_rate: u32,
31}
32
33#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
35#[non_exhaustive]
36pub enum NativeVideoCodec {
37 H264,
39 Hevc,
41 Vp9,
43 Av1,
45 Mpeg4Part2,
47}
48
49impl NativeVideoCodec {
50 #[must_use]
52 pub const fn as_str(self) -> &'static str {
53 match self {
54 Self::H264 => "h264",
55 Self::Hevc => "hevc",
56 Self::Vp9 => "vp9",
57 Self::Av1 => "av1",
58 Self::Mpeg4Part2 => "mpeg4-part2",
59 }
60 }
61
62 #[must_use]
67 pub const fn model(self) -> alpha2_media::VideoCodec {
68 match self {
69 Self::H264 => alpha2_media::VideoCodec::H264,
70 Self::Hevc => alpha2_media::VideoCodec::Hevc,
71 Self::Vp9 => alpha2_media::VideoCodec::Vp9,
72 Self::Av1 => alpha2_media::VideoCodec::Av1,
73 Self::Mpeg4Part2 => alpha2_media::VideoCodec::Mpeg4Part2,
74 }
75 }
76
77 #[must_use]
79 pub const fn from_model(codec: alpha2_media::VideoCodec) -> Self {
80 match codec {
81 alpha2_media::VideoCodec::H264 => Self::H264,
82 alpha2_media::VideoCodec::Hevc => Self::Hevc,
83 alpha2_media::VideoCodec::Vp9 => Self::Vp9,
84 alpha2_media::VideoCodec::Av1 => Self::Av1,
85 alpha2_media::VideoCodec::Mpeg4Part2 => Self::Mpeg4Part2,
86 }
87 }
88
89 pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
90 match self {
91 Self::H264 => ffi::AV_CODEC_ID_H264,
92 Self::Hevc => ffi::AV_CODEC_ID_HEVC,
93 Self::Vp9 => ffi::AV_CODEC_ID_VP9,
94 Self::Av1 => ffi::AV_CODEC_ID_AV1,
95 Self::Mpeg4Part2 => ffi::AV_CODEC_ID_MPEG4,
96 }
97 }
98}
99
100#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
102#[non_exhaustive]
103pub enum NativeAudioCodec {
104 Aac,
106 Opus,
108}
109
110impl NativeAudioCodec {
111 pub const ALL: [Self; 2] = [Self::Aac, Self::Opus];
113
114 #[must_use]
116 pub const fn as_str(self) -> &'static str {
117 match self {
118 Self::Aac => "aac",
119 Self::Opus => "opus",
120 }
121 }
122
123 #[must_use]
125 pub const fn model(self) -> alpha2_media::AudioCodec {
126 match self {
127 Self::Aac => alpha2_media::AudioCodec::Aac,
128 Self::Opus => alpha2_media::AudioCodec::Opus,
129 }
130 }
131
132 #[must_use]
134 pub const fn from_model(codec: alpha2_media::AudioCodec) -> Self {
135 match codec {
136 alpha2_media::AudioCodec::Aac => Self::Aac,
137 alpha2_media::AudioCodec::Opus => Self::Opus,
138 }
139 }
140
141 pub(crate) const fn encoder_name(self) -> &'static std::ffi::CStr {
145 match self {
146 Self::Aac => c"aac",
147 Self::Opus => c"libopus",
148 }
149 }
150}
151
152pub(crate) fn audio_encoder(codec: NativeAudioCodec) -> Option<AVCodecRef<'static>> {
155 AVCodec::find_encoder_by_name(codec.encoder_name()).filter(|encoder| {
156 u32::try_from(encoder.capabilities).unwrap_or(0) & ffi::AV_CODEC_CAP_EXPERIMENTAL == 0
157 })
158}
159
160#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
170#[non_exhaustive]
171pub enum NativeEncoderClass {
172 Software,
174 Hardware,
176 Hybrid,
180}
181
182impl NativeEncoderClass {
183 #[must_use]
185 pub const fn as_str(self) -> &'static str {
186 match self {
187 Self::Software => "software",
188 Self::Hardware => "hardware",
189 Self::Hybrid => "hybrid",
190 }
191 }
192
193 #[must_use]
199 pub const fn modes(self, hybrid_hardware: bool) -> &'static [bool] {
200 match self {
201 Self::Hardware => &[true],
202 Self::Hybrid if hybrid_hardware => &[true, false],
203 Self::Software | Self::Hybrid => &[false],
204 }
205 }
206}
207
208pub(crate) struct VideoEncoderCandidate {
211 pub(crate) encoder: AVCodecRef<'static>,
212 pub(crate) on_hardware: bool,
213}
214
215impl VideoEncoderCandidate {
216 pub(crate) fn name(&self) -> String {
217 self.encoder.name().to_string_lossy().into_owned()
218 }
219}
220
221#[must_use]
231pub fn hybrid_encoders_use_hardware() -> bool {
232 std::env::var("ALPHA2_VIDEO_ENCODER_HYBRID")
233 .is_ok_and(|value| value.eq_ignore_ascii_case("hardware"))
234}
235
236#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
238#[non_exhaustive]
239pub enum NativeEncoderRejection {
240 Dimensions,
242 OpenFailed,
244 OptionsIgnored,
246}
247
248impl NativeEncoderRejection {
249 #[must_use]
251 pub const fn as_str(self) -> &'static str {
252 match self {
253 Self::Dimensions => "dimensions",
254 Self::OpenFailed => "open-failed",
255 Self::OptionsIgnored => "options-ignored",
256 }
257 }
258}
259
260#[derive(Clone, Debug, Eq, PartialEq)]
262pub struct NativeVideoEncoderFallback {
263 pub encoder_name: String,
265 pub hardware_accelerated: bool,
268 pub reason: NativeEncoderRejection,
270 pub detail: String,
272}
273
274#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct NativeVideoEncoderSelection {
281 pub encoder_name: String,
283 pub class: NativeEncoderClass,
285 pub hardware_accelerated: bool,
287 pub rejected: Vec<NativeVideoEncoderFallback>,
289 pub rate_control: crate::AppliedRateControl,
291 pub quantizer_refused: Option<String>,
293 pub software_threads: Option<i64>,
296}
297
298#[derive(Clone, Debug, Eq, PartialEq)]
300pub struct NativeVideoEncoderCapability {
301 pub codec: NativeVideoCodec,
303 pub encoder_name: String,
305 pub class: NativeEncoderClass,
307 pub hardware_accelerated: bool,
310 pub preferred: bool,
312 pub minimum_width: u32,
314 pub minimum_height: u32,
316 pub library: Option<NativeEncoderLibrary>,
319}
320
321#[derive(Clone, Debug, Eq, PartialEq)]
323pub struct NativeEncoderLibrary {
324 pub provider: &'static str,
326 pub version: String,
328 pub path: String,
330 pub notice: &'static str,
332}
333
334impl NativeEncoderLibrary {
335 fn openh264(library: &crate::OpenH264Library) -> Self {
336 Self {
337 provider: library.provider().as_str(),
338 version: library.version().to_owned(),
339 path: library.path().to_owned(),
340 notice: crate::OPENH264_NOTICE,
341 }
342 }
343}
344
345#[derive(Clone, Debug, Eq, PartialEq)]
347pub struct NativeAudioEncoderCapability {
348 pub codec: NativeAudioCodec,
350 pub encoder_name: String,
352}
353
354#[derive(Clone, Debug, Eq, PartialEq)]
360pub struct NativeCodecCapabilities {
361 pub video: Vec<NativeVideoEncoderCapability>,
364 pub audio: Vec<NativeAudioEncoderCapability>,
367}
368
369#[must_use]
378pub fn codec_capabilities() -> NativeCodecCapabilities {
379 let video = [
380 NativeVideoCodec::H264,
381 NativeVideoCodec::Hevc,
382 NativeVideoCodec::Vp9,
383 NativeVideoCodec::Av1,
384 NativeVideoCodec::Mpeg4Part2,
385 ]
386 .into_iter()
387 .flat_map(video_encoder_capabilities)
388 .collect();
389 NativeCodecCapabilities {
390 video,
391 audio: audio_encoder_capabilities(),
392 }
393}
394
395#[must_use]
398pub fn audio_encoder_capabilities() -> Vec<NativeAudioEncoderCapability> {
399 NativeAudioCodec::ALL
400 .into_iter()
401 .filter_map(|codec| {
402 let encoder = audio_encoder(codec)?;
403 Some(NativeAudioEncoderCapability {
404 codec,
405 encoder_name: encoder.name().to_string_lossy().into_owned(),
406 })
407 })
408 .collect()
409}
410
411#[must_use]
418pub fn video_encoder_capabilities(codec: NativeVideoCodec) -> Vec<NativeVideoEncoderCapability> {
419 available_video_encoders(codec)
420 .into_iter()
421 .enumerate()
422 .map(|(index, choice)| {
423 let encoder_name = choice.name();
424 let (minimum_width, minimum_height) = encoder_minimum_dimensions(&encoder_name);
425 NativeVideoEncoderCapability {
426 codec,
427 class: choice.class(),
428 hardware_accelerated: choice.on_hardware(),
429 preferred: index == 0,
430 encoder_name,
431 minimum_width,
432 minimum_height,
433 library: match &choice {
434 VideoEncoderChoice::Ffmpeg(_) => None,
435 VideoEncoderChoice::OpenH264(library) => {
436 Some(NativeEncoderLibrary::openh264(library))
437 }
438 },
439 }
440 })
441 .collect()
442}
443
444pub(crate) enum VideoEncoderChoice {
446 Ffmpeg(VideoEncoderCandidate),
447 OpenH264(std::sync::Arc<crate::OpenH264Library>),
448}
449
450impl VideoEncoderChoice {
451 pub(crate) fn name(&self) -> String {
452 match self {
453 Self::Ffmpeg(candidate) => candidate.name(),
454 Self::OpenH264(_) => crate::OPENH264_ENCODER_NAME.to_owned(),
455 }
456 }
457
458 pub(crate) const fn on_hardware(&self) -> bool {
459 match self {
460 Self::Ffmpeg(candidate) => candidate.on_hardware,
461 Self::OpenH264(_) => false,
462 }
463 }
464
465 pub(crate) fn class(&self) -> NativeEncoderClass {
466 match self {
467 Self::Ffmpeg(candidate) => encoder_class(&candidate.encoder),
468 Self::OpenH264(_) => NativeEncoderClass::Software,
469 }
470 }
471}
472
473pub(crate) fn available_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderChoice> {
477 let mut choices = registered_video_encoders(codec)
478 .into_iter()
479 .filter(encoder_is_compatible)
480 .map(VideoEncoderChoice::Ffmpeg)
481 .collect::<Vec<_>>();
482 if codec == NativeVideoCodec::H264
483 && let Some(library) = crate::openh264_status().library()
484 {
485 choices.push(VideoEncoderChoice::OpenH264(std::sync::Arc::clone(library)));
486 }
487 choices
488}
489
490#[derive(Clone, Debug, Eq, PartialEq)]
492pub struct NativeHardwareDecodeCapability {
493 pub codec: NativeVideoCodec,
495 pub decoder_name: Option<String>,
497 pub hardware_config: bool,
503 pub software_decoder: Option<String>,
507}
508
509#[derive(Clone, Debug, Eq, PartialEq)]
511pub struct NativeHardwareDecodeCapabilities {
512 pub backend: &'static str,
514 pub codecs: Vec<NativeHardwareDecodeCapability>,
516}
517
518#[must_use]
520pub const fn hardware_decode_backend() -> &'static str {
521 #[cfg(feature = "hwdecode")]
522 {
523 #[cfg(target_os = "linux")]
524 {
525 "vulkan"
526 }
527 #[cfg(target_os = "macos")]
528 {
529 "videotoolbox"
530 }
531 #[cfg(target_os = "windows")]
532 {
533 "d3d12va"
534 }
535 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
536 {
537 "none"
538 }
539 }
540 #[cfg(not(feature = "hwdecode"))]
541 {
542 "none"
543 }
544}
545
546#[must_use]
548pub fn hardware_decode_capabilities() -> NativeHardwareDecodeCapabilities {
549 let codecs = [
550 NativeVideoCodec::H264,
551 NativeVideoCodec::Hevc,
552 NativeVideoCodec::Av1,
553 NativeVideoCodec::Vp9,
554 ]
555 .into_iter()
556 .map(|codec| {
557 let decoder = AVCodec::find_decoder(codec.codec_id());
558 NativeHardwareDecodeCapability {
559 codec,
560 decoder_name: decoder
561 .as_ref()
562 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
563 hardware_config: crate::decode::registered_video_decoders(codec.codec_id())
564 .any(|decoder| decoder_has_hardware_config(&decoder)),
565 software_decoder: crate::decode::software_video_decoder(codec.codec_id())
566 .ok()
567 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
568 }
569 })
570 .collect();
571 NativeHardwareDecodeCapabilities {
572 backend: hardware_decode_backend(),
573 codecs,
574 }
575}
576
577#[allow(unused_variables)]
579fn decoder_has_hardware_config(decoder: &AVCodecRef<'_>) -> bool {
580 #[cfg(all(
581 any(target_os = "linux", target_os = "macos", target_os = "windows"),
582 feature = "hwdecode"
583 ))]
584 {
585 #[cfg(target_os = "linux")]
586 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_VULKAN, ffi::AV_PIX_FMT_VULKAN);
587 #[cfg(target_os = "macos")]
588 let (device_type, pixel_format) = (
589 ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
590 ffi::AV_PIX_FMT_VIDEOTOOLBOX,
591 );
592 #[cfg(target_os = "windows")]
593 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_D3D12VA, ffi::AV_PIX_FMT_D3D12);
594 (0..)
595 .map_while(|index| decoder.hw_config(index))
596 .any(|config| config.device_type == device_type && config.pix_fmt == pixel_format)
597 }
598 #[cfg(not(all(
599 any(target_os = "linux", target_os = "macos", target_os = "windows"),
600 feature = "hwdecode"
601 )))]
602 {
603 false
604 }
605}
606
607#[cfg(test)]
611pub(crate) fn test_skips_encoder(name: &std::ffi::CStr) -> bool {
612 static SKIPPED: LazyLock<Vec<String>> = LazyLock::new(|| {
613 std::env::var("ALPHA2_TEST_SKIP_ENCODERS")
614 .unwrap_or_default()
615 .split(',')
616 .map(str::trim)
617 .filter(|name| !name.is_empty())
618 .map(str::to_owned)
619 .collect()
620 });
621 let name = name.to_string_lossy();
622 SKIPPED.iter().any(|skipped| *skipped == name)
623}
624
625#[cfg(not(test))]
626const fn test_skips_encoder(_name: &std::ffi::CStr) -> bool {
627 false
628}
629
630pub(crate) fn registered_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderCandidate> {
632 let hybrid_hardware = hybrid_encoders_use_hardware();
633 let mut candidates = AVCodec::iterate()
634 .filter(|encoder| {
635 encoder.id == codec.codec_id()
636 && !test_skips_encoder(encoder.name())
637 && AVCodec::find_encoder_by_name(encoder.name())
638 .is_some_and(|found| found.as_ptr() == encoder.as_ptr())
639 })
640 .flat_map(|encoder| {
641 encoder_class(&encoder)
642 .modes(hybrid_hardware)
643 .iter()
644 .filter_map(move |&on_hardware| {
645 AVCodec::find_encoder_by_name(encoder.name()).map(|encoder| {
646 VideoEncoderCandidate {
647 encoder,
648 on_hardware,
649 }
650 })
651 })
652 })
653 .collect::<Vec<_>>();
654 candidates.sort_by_key(|candidate| !candidate.on_hardware);
656 candidates
657}
658
659pub(crate) fn encoder_class(encoder: &AVCodec) -> NativeEncoderClass {
662 let name = encoder.name().to_string_lossy();
663 declared_encoder_class(&name, u32::try_from(encoder.capabilities).unwrap_or(0))
664}
665
666const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
677
678pub(crate) fn declared_encoder_class(name: &str, capabilities: u32) -> NativeEncoderClass {
682 if capabilities & ffi::AV_CODEC_CAP_HYBRID != 0 {
683 NativeEncoderClass::Hybrid
684 } else if capabilities & ffi::AV_CODEC_CAP_HARDWARE != 0
685 || MISDECLARED_HARDWARE_ENCODERS.contains(&name)
686 {
687 NativeEncoderClass::Hardware
688 } else {
689 NativeEncoderClass::Software
690 }
691}
692
693pub(crate) fn encoder_is_compatible(candidate: &VideoEncoderCandidate) -> bool {
694 let name = candidate.name();
695 registration_is_compatible(candidate, &name) && encoder_runs_here(candidate, &name)
696}
697
698fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
701 if candidate
704 .encoder
705 .pix_fmts()
706 .is_some_and(|formats| !formats.contains(&ffi::AV_PIX_FMT_YUV420P))
707 {
708 return false;
709 }
710 !candidate.on_hardware || hardware_device_node(name) != Some(false)
711}
712
713pub(crate) fn needs_open_probe(on_hardware: bool, name: &str) -> bool {
722 (on_hardware && (hardware_device_node(name).is_none() || name.ends_with("_nvenc")))
723 || name.ends_with("_mf")
724 || name.ends_with("_videotoolbox")
725}
726
727fn encoder_runs_here(candidate: &VideoEncoderCandidate, name: &str) -> bool {
752 probe_outcome(candidate, name, true).unwrap_or(false)
753}
754
755fn probe_outcome(candidate: &VideoEncoderCandidate, name: &str, probe: bool) -> Option<bool> {
759 static RUNS: LazyLock<Mutex<HashMap<(String, bool), bool>>> = LazyLock::new(Mutex::default);
760 if !needs_open_probe(candidate.on_hardware, name) {
761 return Some(true);
762 }
763 let key = (name.to_owned(), candidate.on_hardware);
764 let mut runs = RUNS
765 .lock()
766 .unwrap_or_else(std::sync::PoisonError::into_inner);
767 if let Some(runs_here) = runs.get(&key) {
768 return Some(*runs_here);
769 }
770 if !probe {
771 return None;
772 }
773 let runs_here = match probe_software_encode(candidate, name, 256, 144, None).0 {
774 None => true,
775 Some(error) => {
776 alpha2_os::diagnostic!(
777 "alpha2 native: video encoder {name} {} is registered but did not encode a \
778 frame on this machine, so it is not offered that way: {error}",
779 mode_label(candidate.on_hardware)
780 );
781 false
782 }
783 };
784 runs.insert(key, runs_here);
785 Some(runs_here)
786}
787
788#[must_use]
795pub fn video_encoders_without_probing() -> (Vec<NativeVideoEncoderCapability>, Vec<String>) {
796 let mut unprobed = Vec::new();
797 let mut listed = Vec::new();
798 for codec in [
799 NativeVideoCodec::H264,
800 NativeVideoCodec::Hevc,
801 NativeVideoCodec::Vp9,
802 NativeVideoCodec::Av1,
803 NativeVideoCodec::Mpeg4Part2,
804 ] {
805 for candidate in registered_video_encoders(codec) {
806 let name = candidate.name();
807 if !registration_is_compatible(&candidate, &name) {
808 continue;
809 }
810 match probe_outcome(&candidate, &name, false) {
811 Some(true) => {
812 let (minimum_width, minimum_height) = encoder_minimum_dimensions(&name);
813 listed.push(NativeVideoEncoderCapability {
814 codec,
815 class: encoder_class(&candidate.encoder),
816 hardware_accelerated: candidate.on_hardware,
817 preferred: !listed
818 .iter()
819 .any(|listed: &NativeVideoEncoderCapability| listed.codec == codec),
820 encoder_name: name,
821 minimum_width,
822 minimum_height,
823 library: None,
824 });
825 }
826 Some(false) => {}
827 None => unprobed.push(format!("{name} {}", mode_label(candidate.on_hardware))),
828 }
829 }
830 }
831 (listed, unprobed)
832}
833
834pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
836 if on_hardware { "on GPU" } else { "on CPU" }
837}
838
839#[cfg(target_os = "linux")]
850fn hardware_device_node(name: &str) -> Option<bool> {
851 if name.ends_with("_nvenc") {
852 return Some(Path::new("/dev/nvidia0").exists() || Path::new("/dev/nvidiactl").exists());
853 }
854 if name.ends_with("_vaapi") || name.ends_with("_qsv") || name.ends_with("_amf") {
855 return Some(fs::read_dir("/dev/dri").is_ok_and(|entries| {
856 entries.flatten().any(|entry| {
857 entry
858 .file_name()
859 .to_str()
860 .is_some_and(|name| name.starts_with("renderD") || name.starts_with("card"))
861 })
862 }));
863 }
864 if name.ends_with("_v4l2m2m") {
865 return Some((0..64).any(|index| Path::new(&format!("/dev/video{index}")).exists()));
866 }
867 None
868}
869
870#[cfg(not(target_os = "linux"))]
873const fn hardware_device_node(_name: &str) -> Option<bool> {
874 None
875}
876
877#[must_use]
885pub fn no_video_encoder_message(
886 codec: NativeVideoCodec,
887 offered: &[NativeVideoEncoderCapability],
888 gpu_output_unavailable: Option<&str>,
889) -> String {
890 let identity = crate::ffmpeg_runtime_identity();
891 let inventory = offered
892 .iter()
893 .filter(|capability| capability.library.is_none())
894 .map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
895 .collect::<Vec<_>>()
896 .join(", ");
897 let mut message = format!("no usable {} encoder: ", codec.as_str());
898 if let Some(reason) = gpu_output_unavailable {
899 let _ = write!(message, "GPU output path unavailable ({reason}); ");
900 }
901 let _ = write!(
902 message,
903 "the linked FFmpeg runtime ({}, pinned SDK {}) offers [{}]",
904 identity.version,
905 identity.pinned_sdk_release,
906 if inventory.is_empty() {
907 "none"
908 } else {
909 inventory.as_str()
910 }
911 );
912 if codec != NativeVideoCodec::H264 {
913 return message;
914 }
915 let status = crate::openh264_status();
916 if let Some(reason) = status.reason() {
917 let _ = write!(
918 message,
919 "; OpenH264 is {}: {reason}",
920 status.as_str().replace('-', " ")
921 );
922 }
923 let _ = write!(message, ". To export H.264 here: {}", h264_encoder_remedy());
924 message
925}
926
927#[must_use]
931pub fn h264_encoder_remedy() -> String {
932 let openh264 = match crate::openh264_status() {
933 crate::OpenH264Status::Disabled => {
934 "unset ALPHA2_OPENH264=0 to use the installed OpenH264 encoder".to_owned()
935 }
936 _ => format!(
937 "install the OpenH264 encoder ({})",
938 crate::OPENH264_INSTALL_COMMAND
939 ),
940 };
941 format!(
942 "{openh264}, use a thin runtime with an FFmpeg 8 that has libx264, or export VP9 (.webm) \
943 or AV1 instead"
944 )
945}
946
947fn unavailable_codec_error(codec: NativeVideoCodec) -> EncodeError {
950 EncodeError::Adapter(no_video_encoder_message(
951 codec,
952 &codec_capabilities().video,
953 None,
954 ))
955}
956
957pub(crate) fn video_encoder_candidates(
963 codec: NativeVideoCodec,
964 requested_name: Option<&str>,
965) -> Result<Vec<VideoEncoderChoice>, EncodeError> {
966 if requested_name == Some(crate::OPENH264_ENCODER_NAME) {
967 if codec != NativeVideoCodec::H264 {
968 return Err(EncodeError::Adapter(format!(
969 "video encoder {} does not implement {}",
970 crate::OPENH264_ENCODER_NAME,
971 codec.as_str()
972 )));
973 }
974 let status = crate::openh264_status();
975 return match status.library() {
976 Some(library) => Ok(vec![VideoEncoderChoice::OpenH264(std::sync::Arc::clone(
977 library,
978 ))]),
979 None => Err(EncodeError::Adapter(format!(
980 "video encoder {} is unavailable: {}{}",
981 crate::OPENH264_ENCODER_NAME,
982 status.reason().unwrap_or_default(),
983 status
984 .remedy()
985 .map(|remedy| format!("; {remedy}"))
986 .unwrap_or_default()
987 ))),
988 };
989 }
990 if let Some(name) = requested_name {
991 let encoder = select_video_encoder(codec, Some(name))?;
992 return Ok(registered_video_encoders(codec)
993 .into_iter()
994 .filter(|candidate| candidate.encoder.as_ptr() == encoder.as_ptr())
995 .filter(encoder_is_compatible)
996 .map(VideoEncoderChoice::Ffmpeg)
997 .collect());
998 }
999 let candidates = available_video_encoders(codec);
1000 if candidates.is_empty() {
1001 return Err(unavailable_codec_error(codec));
1002 }
1003 Ok(candidates)
1004}
1005
1006pub(crate) fn select_video_encoder(
1010 codec: NativeVideoCodec,
1011 requested_name: Option<&str>,
1012) -> Result<AVCodecRef<'static>, EncodeError> {
1013 if let Some(name) = requested_name {
1014 let name_c = CString::new(name)
1015 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1016 let encoder = AVCodec::find_encoder_by_name(&name_c)
1017 .ok_or_else(|| EncodeError::Adapter(format!("video encoder {name} is unavailable")))?;
1018 if encoder.id != codec.codec_id() {
1019 return Err(EncodeError::Adapter(format!(
1020 "video encoder {name} does not implement {}",
1021 codec.as_str()
1022 )));
1023 }
1024 if !video_encoder_capabilities(codec)
1025 .iter()
1026 .any(|capability| capability.encoder_name == name)
1027 {
1028 return Err(EncodeError::Adapter(format!(
1029 "video encoder {name} is not compatible with the native encode path"
1030 )));
1031 }
1032 return Ok(encoder);
1033 }
1034
1035 let capability = video_encoder_capabilities(codec)
1036 .into_iter()
1037 .find(|capability| capability.library.is_none())
1038 .ok_or_else(|| unavailable_codec_error(codec))?;
1039 let name = CString::new(capability.encoder_name)
1040 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1041 AVCodec::find_encoder_by_name(&name).ok_or_else(|| unavailable_codec_error(codec))
1042}
1043
1044pub(crate) struct VideoEncoderOpenRequest<'a> {
1046 pub config: &'a VideoEncodeConfig,
1048 pub requested_name: Option<&'a str>,
1050 pub threads: SoftwareEncoderThreads<'a>,
1053 pub global_header: bool,
1055 pub width: i32,
1057 pub height: i32,
1059}
1060
1061pub(crate) struct SoftwareEncoderThreads<'a> {
1072 decide: &'a dyn Fn() -> i64,
1073 decided: std::cell::OnceCell<i64>,
1074}
1075
1076impl<'a> SoftwareEncoderThreads<'a> {
1077 pub(crate) fn new(decide: &'a dyn Fn() -> i64) -> Self {
1079 Self {
1080 decide,
1081 decided: std::cell::OnceCell::new(),
1082 }
1083 }
1084
1085 pub(crate) fn get(&self) -> i64 {
1087 *self.decided.get_or_init(|| (self.decide)().max(1))
1088 }
1089}
1090
1091pub(crate) fn open_video_encoder(
1104 request: &VideoEncoderOpenRequest<'_>,
1105) -> Result<
1106 (
1107 crate::video_backend::VideoEncoderBackend,
1108 NativeVideoEncoderSelection,
1109 ),
1110 EncodeError,
1111> {
1112 let candidates = video_encoder_candidates(request.config.codec, request.requested_name)?;
1113 open_first_video_encoder(request, candidates)
1114}
1115
1116struct OpenedVideoEncoder {
1118 backend: crate::video_backend::VideoEncoderBackend,
1119 rate_control: crate::AppliedRateControl,
1120 quantizer_refused: Option<String>,
1121 software_threads: Option<i64>,
1122}
1123
1124pub(crate) fn open_first_video_encoder(
1127 request: &VideoEncoderOpenRequest<'_>,
1128 candidates: Vec<VideoEncoderChoice>,
1129) -> Result<
1130 (
1131 crate::video_backend::VideoEncoderBackend,
1132 NativeVideoEncoderSelection,
1133 ),
1134 EncodeError,
1135> {
1136 let mut rejected = Vec::new();
1137 let last = candidates.len().saturating_sub(1);
1138 for (index, candidate) in candidates.into_iter().enumerate() {
1139 let name = candidate.name();
1140 let class = candidate.class();
1141 let opened = match &candidate {
1142 VideoEncoderChoice::Ffmpeg(candidate) => {
1143 open_one_video_encoder(request, candidate, &name).map(
1144 |(encoder, rate_control, quantizer_refused)| OpenedVideoEncoder {
1145 backend: crate::video_backend::VideoEncoderBackend::Ffmpeg(encoder),
1146 rate_control,
1147 quantizer_refused,
1148 software_threads: (!candidate.on_hardware).then(|| request.threads.get()),
1149 },
1150 )
1151 }
1152 VideoEncoderChoice::OpenH264(library) => open_openh264(request, library),
1153 };
1154 match opened {
1155 Ok(opened) => {
1156 return Ok((
1157 opened.backend,
1158 NativeVideoEncoderSelection {
1159 encoder_name: name,
1160 class,
1161 hardware_accelerated: candidate.on_hardware(),
1162 rejected,
1163 rate_control: opened.rate_control,
1164 quantizer_refused: opened.quantizer_refused,
1165 software_threads: opened.software_threads,
1166 },
1167 ));
1168 }
1169 Err((reason, error)) => {
1170 let detail = error.to_string();
1171 if index == last {
1172 let earlier = rejected
1173 .iter()
1174 .map(|fallback: &NativeVideoEncoderFallback| {
1175 format!(
1176 "{} {} ({}): {}",
1177 fallback.encoder_name,
1178 mode_label(fallback.hardware_accelerated),
1179 fallback.reason.as_str(),
1180 fallback.detail
1181 )
1182 })
1183 .collect::<Vec<_>>();
1184 let prefix = if earlier.is_empty() {
1185 String::new()
1186 } else {
1187 format!("after {}; ", earlier.join("; "))
1188 };
1189 return Err(EncodeError::Adapter(format!(
1190 "{prefix}{name} {} ({}): {detail}",
1191 mode_label(candidate.on_hardware()),
1192 reason.as_str()
1193 )));
1194 }
1195 alpha2_os::diagnostic!(
1196 "alpha2 native export: video encoder {name} {} ({}) rejected: {detail}",
1197 mode_label(candidate.on_hardware()),
1198 reason.as_str()
1199 );
1200 rejected.push(NativeVideoEncoderFallback {
1201 encoder_name: name,
1202 hardware_accelerated: candidate.on_hardware(),
1203 reason,
1204 detail,
1205 });
1206 }
1207 }
1208 }
1209 Err(unavailable_codec_error(request.config.codec))
1210}
1211
1212fn open_openh264(
1220 request: &VideoEncoderOpenRequest<'_>,
1221 library: &crate::OpenH264Library,
1222) -> Result<OpenedVideoEncoder, (NativeEncoderRejection, EncodeError)> {
1223 let config = request.config;
1224 validate_encoder_dimensions(crate::OPENH264_ENCODER_NAME, config.width, config.height)
1225 .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1226 let open_failed = |message: String| {
1227 (
1228 NativeEncoderRejection::OpenFailed,
1229 EncodeError::Adapter(message),
1230 )
1231 };
1232 if !request.global_header {
1233 return Err(open_failed(
1234 "OpenH264 writes its parameter sets into the stream header, which this container \
1235 lacks"
1236 .to_owned(),
1237 ));
1238 }
1239 let (rate, rate_control) = crate::rate_control::openh264_rate(config.rate_control);
1240 let time_base = encoder_time_base(config.frame_rate, None)
1241 .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1242 let _ = request.threads.get();
1243 let encoder = crate::openh264::OpenH264Encoder::open(
1244 library,
1245 &crate::openh264::OpenH264Settings {
1246 width: config.width,
1247 height: config.height,
1248 frame_rate: config.frame_rate,
1249 color: config.color,
1250 sample_aspect_ratio: config.display.sample_aspect_ratio,
1251 rate,
1252 keyframe_interval: u32::try_from(keyframe_interval(config.frame_rate)).unwrap_or(1),
1253 },
1254 time_base,
1255 )
1256 .map_err(open_failed)?;
1257 let threads = i64::from(encoder.threads());
1258 Ok(OpenedVideoEncoder {
1259 backend: crate::video_backend::VideoEncoderBackend::OpenH264(Box::new(encoder)),
1260 rate_control,
1261 quantizer_refused: None,
1262 software_threads: Some(threads),
1263 })
1264}
1265
1266fn open_one_video_encoder(
1273 request: &VideoEncoderOpenRequest<'_>,
1274 candidate: &VideoEncoderCandidate,
1275 name: &str,
1276) -> Result<
1277 (AVCodecContext, crate::AppliedRateControl, Option<String>),
1278 (NativeEncoderRejection, EncodeError),
1279> {
1280 validate_encoder_dimensions(name, request.config.width, request.config.height)
1281 .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1282 let refused = match crate::rate_control::quantizer_request(name, request.config.rate_control) {
1283 None => None,
1284 Some(Err(reason)) => Some(reason),
1285 Some(Ok(quantized)) => {
1286 match open_video_encoder_context(request, candidate, name, Some(&quantized)) {
1287 Ok(encoder) => return Ok((encoder, crate::AppliedRateControl::Quantizer, None)),
1288 Err((_, error)) => Some(format!("{} refused: {error}", quantized.describe())),
1289 }
1290 }
1291 };
1292 if let Some(reason) = &refused {
1293 alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
1294 }
1295 open_video_encoder_context(request, candidate, name, None)
1296 .map(|encoder| (encoder, crate::AppliedRateControl::Bitrate, refused))
1297}
1298
1299fn open_video_encoder_context(
1300 request: &VideoEncoderOpenRequest<'_>,
1301 candidate: &VideoEncoderCandidate,
1302 name: &str,
1303 quantized: Option<&crate::rate_control::MechanismRequest>,
1304) -> Result<AVCodecContext, (NativeEncoderRejection, EncodeError)> {
1305 let mut encoder = AVCodecContext::new(&candidate.encoder);
1306 encoder.set_width(request.width);
1307 encoder.set_height(request.height);
1308 encoder.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
1309 let rate = request.config.frame_rate;
1310 let time_base = encoder_time_base(rate, max_time_base_denominator(candidate.encoder.id))
1311 .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1312 encoder.set_time_base(time_base);
1313 let sar = request.config.display.sample_aspect_ratio;
1315 if sar != alpha2_media::SampleAspectRatio::default() {
1316 encoder.set_sample_aspect_ratio(rsmpeg::avutil::ra(
1317 i32::try_from(sar.num).unwrap_or(1),
1318 i32::try_from(sar.den).unwrap_or(1),
1319 ));
1320 }
1321 encoder.set_framerate(rsmpeg::avutil::ra(
1322 i32::try_from(rate.numerator()).unwrap_or(i32::MAX),
1323 i32::try_from(rate.denominator()).unwrap_or(1),
1324 ));
1325 encoder.set_bit_rate(quantized.map_or_else(
1326 || i64::try_from(request.config.rate_control.bitrate).unwrap_or(i64::MAX),
1327 |quantized| quantized.bit_rate,
1328 ));
1329 encoder.set_gop_size(keyframe_interval(request.config.frame_rate));
1330 encoder.set_max_b_frames(encoder_max_b_frames(name));
1331 if request.global_header {
1332 encoder.set_flags(
1333 encoder.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
1334 );
1335 }
1336 let threads = if candidate.on_hardware {
1339 0
1340 } else {
1341 request.threads.get()
1342 };
1343 let base = video_encoder_options(
1344 request.config.color,
1345 &candidate.encoder,
1346 candidate.on_hardware,
1347 threads,
1348 request.config.width,
1349 );
1350 let options = match quantized {
1351 Some(quantized) => quantized.dictionary(base),
1352 None => crate::rate_control::rate_control_options(name, request.config.rate_control, base),
1353 };
1354 let unused =
1355 alpha2_ffmpeg_bridge::open_codec_context(&mut encoder, options).map_err(|error| {
1356 let hint = match quantized {
1357 Some(_) => "",
1358 None => crate::rate_control::constant_rate_hint(name, request.config.rate_control),
1359 };
1360 let error = match encode_adapter_error(error) {
1361 EncodeError::Adapter(message) if !hint.is_empty() => {
1362 EncodeError::Adapter(format!("{message}{hint}"))
1363 }
1364 error => error,
1365 };
1366 (NativeEncoderRejection::OpenFailed, error)
1367 })?;
1368 reject_unused_encode_options(unused)
1369 .map_err(|error| (NativeEncoderRejection::OptionsIgnored, error))?;
1370 Ok(encoder)
1371}
1372
1373pub(crate) fn keyframe_interval(frame_rate: alpha2_time::FrameRate) -> i32 {
1384 let frames = (2 * u64::from(frame_rate.numerator()))
1385 .div_ceil(u64::from(frame_rate.denominator()))
1386 .max(1);
1387 i32::try_from(frames).unwrap_or(i32::MAX)
1388}
1389
1390pub(crate) const fn max_time_base_denominator(codec: ffi::AVCodecID) -> Option<u32> {
1395 match codec {
1396 ffi::AV_CODEC_ID_MPEG4 | ffi::AV_CODEC_ID_H263 | ffi::AV_CODEC_ID_H263P => Some(65_535),
1397 _ => None,
1398 }
1399}
1400
1401pub(crate) fn encoder_time_base(
1412 rate: alpha2_time::FrameRate,
1413 limit: Option<u32>,
1414) -> Result<AVRational, EncodeError> {
1415 let out_of_range = || EncodeError::Adapter("frame rate is out of range".into());
1416 let (num, den) = (u64::from(rate.numerator()), u64::from(rate.denominator()));
1417 let exact = || {
1418 Ok(rsmpeg::avutil::ra(
1419 i32::try_from(den).map_err(|_| out_of_range())?,
1420 i32::try_from(num).map_err(|_| out_of_range())?,
1421 ))
1422 };
1423 let Some(limit) = limit.map(u64::from).filter(|limit| num > *limit) else {
1424 return exact();
1425 };
1426 let mut best: Option<(u64, u64, u64)> = None;
1431 for units in num.div_ceil(den).max(1)..=limit {
1432 let q = num / gcd_u64(num, den * units);
1433 let (error, scale) = (q / 2, q * units);
1434 if best.is_none_or(|(_, best_error, best_scale)| {
1435 u128::from(error) * u128::from(best_scale) < u128::from(best_error) * u128::from(scale)
1436 }) {
1437 best = Some((units, error, scale));
1438 }
1439 }
1440 let (units, _, _) = best.ok_or_else(out_of_range)?;
1441 Ok(rsmpeg::avutil::ra(
1442 1,
1443 i32::try_from(units).map_err(|_| out_of_range())?,
1444 ))
1445}
1446
1447const fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
1448 while b != 0 {
1449 (a, b) = (b, a % b);
1450 }
1451 a
1452}
1453
1454pub(crate) fn encoder_max_b_frames(name: &str) -> i32 {
1456 if name.ends_with("_videotoolbox") {
1460 0
1461 } else {
1462 2
1463 }
1464}
1465
1466pub(crate) fn encoder_minimum_dimensions(name: &str) -> (u32, u32) {
1468 if name == "av1_nvenc" {
1471 (146, 128)
1472 } else if name.ends_with("_nvenc") {
1473 (146, 50)
1474 } else {
1475 (2, 2)
1476 }
1477}
1478
1479pub(crate) fn validate_encoder_dimensions(
1480 encoder_name: &str,
1481 width: u32,
1482 height: u32,
1483) -> Result<(), EncodeError> {
1484 let (minimum_width, minimum_height) = encoder_minimum_dimensions(encoder_name);
1485 if width < minimum_width || height < minimum_height {
1486 Err(EncodeError::Adapter(format!(
1487 "encoder {encoder_name} requires at least {minimum_width}x{minimum_height}, received {width}x{height}"
1488 )))
1489 } else {
1490 Ok(())
1491 }
1492}
1493
1494#[derive(Clone, Debug, Eq, PartialEq)]
1496pub struct NativeEncoderHardwareConfig {
1497 pub pixel_format: String,
1499 pub device_type: String,
1501 pub methods: i32,
1503 pub needs_hardware_frames: bool,
1505}
1506
1507#[derive(Clone, Debug, Eq, PartialEq)]
1514#[allow(clippy::struct_excessive_bools)]
1516pub struct NativeVideoEncoderProbe {
1517 pub codec: NativeVideoCodec,
1519 pub encoder_name: String,
1521 pub class: NativeEncoderClass,
1523 pub hardware_accelerated: bool,
1525 pub capability_bits: u32,
1527 pub pixel_formats: Vec<String>,
1529 pub hardware_configs: Vec<NativeEncoderHardwareConfig>,
1531 pub accepts_yuv420p: bool,
1533 pub requires_hardware_frames: bool,
1536 pub advertised: bool,
1538 pub minimum_size: (u32, u32),
1540 pub software_open_error: Option<String>,
1543 pub software_packets: u32,
1545 pub rate_control: Vec<NativeRateControlProbe>,
1547}
1548
1549#[derive(Clone, Debug, Eq, PartialEq)]
1551pub struct NativeRateControlProbe {
1552 pub mechanism: crate::RateControlMechanism,
1554 pub options: Option<String>,
1557 pub outcome: Result<u32, String>,
1560 pub device: Option<String>,
1564}
1565
1566pub(crate) const fn probe_quantizer(codec: NativeVideoCodec, software: bool) -> u32 {
1570 match codec {
1571 NativeVideoCodec::H264 | NativeVideoCodec::Hevc => 26,
1572 NativeVideoCodec::Av1 | NativeVideoCodec::Vp9 => {
1573 if software {
1574 32
1575 } else {
1576 100
1577 }
1578 }
1579 NativeVideoCodec::Mpeg4Part2 => 4,
1580 }
1581}
1582
1583#[must_use]
1593#[allow(clippy::too_many_lines)]
1594pub fn probe_video_encoders(width: u32, height: u32) -> Vec<NativeVideoEncoderProbe> {
1595 let advertised = codec_capabilities();
1596 [
1597 NativeVideoCodec::H264,
1598 NativeVideoCodec::Hevc,
1599 NativeVideoCodec::Vp9,
1600 NativeVideoCodec::Av1,
1601 NativeVideoCodec::Mpeg4Part2,
1602 ]
1603 .into_iter()
1604 .flat_map(|codec| {
1605 registered_video_encoders(codec)
1606 .into_iter()
1607 .map(move |candidate| (codec, candidate))
1608 })
1609 .map(|(codec, candidate)| {
1610 let encoder = &candidate.encoder;
1611 let encoder_name = candidate.name();
1612 let pixel_formats = encoder.pix_fmts().map_or_else(Vec::new, |formats| {
1613 formats
1614 .iter()
1615 .map(|format| {
1616 alpha2_ffmpeg_bridge::pixel_format_name(*format)
1617 .unwrap_or_else(|| "unknown".to_owned())
1618 })
1619 .collect()
1620 });
1621 let hardware_configs = (0..)
1622 .map_while(|index| encoder.hw_config(index))
1623 .map(|config| NativeEncoderHardwareConfig {
1624 pixel_format: alpha2_ffmpeg_bridge::pixel_format_name(config.pix_fmt)
1625 .unwrap_or_else(|| "unknown".to_owned()),
1626 device_type: alpha2_ffmpeg_bridge::hardware_device_type_name(config.device_type)
1627 .unwrap_or_else(|| "none".to_owned()),
1628 methods: config.methods,
1629 needs_hardware_frames: crate::decode::has_hw_config_method(
1630 config.methods,
1631 ffi::AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX,
1632 ),
1633 })
1634 .collect::<Vec<_>>();
1635 let accepts_yuv420p = encoder
1636 .pix_fmts()
1637 .is_none_or(|formats| formats.contains(&ffi::AV_PIX_FMT_YUV420P));
1638 let (software_open_error, software_packets) =
1639 probe_software_encode(&candidate, &encoder_name, width, height, None);
1640 let requires_hardware_frames = !accepts_yuv420p
1641 && hardware_configs
1642 .iter()
1643 .any(|config| config.needs_hardware_frames);
1644 let rate_control = crate::RateControlMechanism::ALL
1645 .into_iter()
1646 .map(|mechanism| {
1647 let request = crate::rate_control::mechanism_request(
1648 &encoder_name,
1649 mechanism,
1650 2_000_000,
1651 if encoder_name.ends_with("_d3d12va") {
1653 26
1654 } else {
1655 probe_quantizer(
1656 codec,
1657 encoder_class(encoder) == NativeEncoderClass::Software,
1658 )
1659 },
1660 );
1661 let mut device = None;
1662 let outcome = match &request {
1663 None => Err("no known option for this encoder family".to_owned()),
1664 Some(request) if requires_hardware_frames => probe_hardware_frames_encode(
1665 encoder,
1666 &encoder_name,
1667 (width, height),
1668 mechanism,
1669 request,
1670 )
1671 .map(|(packets, on)| {
1672 device = Some(on);
1673 packets
1674 }),
1675 Some(request) => {
1676 match probe_software_encode(
1677 &candidate,
1678 &encoder_name,
1679 width,
1680 height,
1681 Some(request),
1682 ) {
1683 (None, packets) => Ok(packets),
1684 (Some(error), _) => Err(error),
1685 }
1686 }
1687 };
1688 NativeRateControlProbe {
1689 mechanism,
1690 options: request
1691 .as_ref()
1692 .map(crate::rate_control::MechanismRequest::describe),
1693 outcome,
1694 device,
1695 }
1696 })
1697 .collect();
1698 NativeVideoEncoderProbe {
1699 codec,
1700 class: encoder_class(encoder),
1701 hardware_accelerated: candidate.on_hardware,
1702 capability_bits: u32::try_from(encoder.capabilities).unwrap_or(0),
1703 advertised: advertised.video.iter().any(|capability| {
1704 capability.codec == codec
1705 && capability.encoder_name == encoder_name
1706 && capability.hardware_accelerated == candidate.on_hardware
1707 }),
1708 minimum_size: encoder_minimum_dimensions(&encoder_name),
1709 requires_hardware_frames,
1710 accepts_yuv420p,
1711 pixel_formats,
1712 hardware_configs,
1713 encoder_name,
1714 software_open_error,
1715 software_packets,
1716 rate_control,
1717 }
1718 })
1719 .collect()
1720}
1721
1722#[allow(unused_variables)]
1738fn probe_hardware_frames_encode(
1739 encoder: &AVCodecRef<'static>,
1740 name: &str,
1741 (width, height): (u32, u32),
1742 mechanism: crate::RateControlMechanism,
1743 request: &crate::rate_control::MechanismRequest,
1744) -> Result<(u32, String), String> {
1745 #[cfg(all(target_os = "linux", feature = "hwencode"))]
1746 if name.ends_with("_vulkan") {
1747 return crate::hwencode::probe_vulkan_rate_control(encoder, width, height, request);
1748 }
1749 #[cfg(all(target_os = "windows", feature = "hwencode"))]
1750 if name.ends_with("_d3d12va") {
1751 return crate::hwencode::probe_d3d12va_rate_control(
1752 name, width, height, mechanism, request,
1753 );
1754 }
1755 Err("takes hardware frames this probe cannot supply".to_owned())
1756}
1757
1758fn probe_software_encode(
1760 candidate: &VideoEncoderCandidate,
1761 name: &str,
1762 width: u32,
1763 height: u32,
1764 request: Option<&crate::rate_control::MechanismRequest>,
1765) -> (Option<String>, u32) {
1766 let Ok(coded_width) = i32::try_from(width) else {
1767 return (Some("width is out of range".to_owned()), 0);
1768 };
1769 let Ok(coded_height) = i32::try_from(height) else {
1770 return (Some("height is out of range".to_owned()), 0);
1771 };
1772 if let Err(error) = validate_encoder_dimensions(name, width, height) {
1773 return (Some(error.to_string()), 0);
1774 }
1775 let encoder = &candidate.encoder;
1776 let mut context = AVCodecContext::new(encoder);
1777 context.set_width(coded_width);
1778 context.set_height(coded_height);
1779 context.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
1780 let time_base = rsmpeg::avutil::ra(1, 30);
1781 context.set_time_base(time_base);
1782 context.set_framerate(rsmpeg::avutil::ra(30, 1));
1783 context.set_bit_rate(request.map_or(2_000_000, |request| request.bit_rate));
1784 context.set_gop_size(12);
1785 context.set_max_b_frames(encoder_max_b_frames(name));
1786 let options = video_encoder_options(
1787 ColorMetadata::EXPORT,
1788 encoder,
1789 candidate.on_hardware,
1790 1,
1791 width,
1792 );
1793 let options = match request {
1794 Some(request) => request.dictionary(options),
1795 None => options,
1796 };
1797 match alpha2_ffmpeg_bridge::open_codec_context(&mut context, options) {
1798 Err(error) => return (Some(error.to_string()), 0),
1799 Ok(unused) => {
1800 if let Err(error) = reject_unused_encode_options(unused) {
1801 return (Some(error.to_string()), 0);
1802 }
1803 }
1804 }
1805 let Some(image) =
1806 rsmpeg::avutil::AVImage::new(ffi::AV_PIX_FMT_YUV420P, coded_width, coded_height, 1)
1807 else {
1808 return (Some("cannot allocate a probe frame".to_owned()), 0);
1809 };
1810 let mut frame = rsmpeg::avutil::AVFrameWithImage::new(image);
1811 frame.set_pts(0);
1812 frame.set_time_base(time_base);
1813 if let Err(error) = context.send_frame(Some(&frame)) {
1814 return (Some(error.to_string()), 0);
1815 }
1816 if let Err(error) = context.send_frame(None) {
1817 return (Some(error.to_string()), 0);
1818 }
1819 let mut packets = 0;
1820 loop {
1821 match context.receive_packet() {
1822 Ok(_) => packets += 1,
1823 Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
1824 Err(error) => return (Some(error.to_string()), packets),
1825 }
1826 }
1827 if packets == 0 {
1828 return (
1829 Some("opened but produced no packet from a software frame".to_owned()),
1830 0,
1831 );
1832 }
1833 (None, packets)
1834}