1use std::collections::HashMap;
4use std::ffi::CString;
5use std::fmt::Write as _;
6use std::sync::{LazyLock, Mutex};
7#[cfg(target_os = "linux")]
8use std::{fs, path::Path};
9
10use crate::convert::{encode_adapter_error, reject_unused_encode_options, video_encoder_options};
11use crate::video_encode::VideoEncodeConfig;
12use alpha2_ffmpeg_bridge::EncoderContext;
13use alpha2_media::{ColorMetadata, EncodeError};
14use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef};
15use rsmpeg::avutil::AVRational;
16use rsmpeg::error::RsmpegError;
17use rsmpeg::ffi;
18
19#[derive(Copy, Clone, Debug, PartialEq)]
21pub struct AvEncodeConfig {
22 pub video: VideoEncodeConfig,
24 pub audio_sample_rate: u32,
26 pub audio_channels: u16,
28 pub audio_codec: NativeAudioCodec,
30 pub audio_bit_rate: u32,
32}
33
34#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
36#[non_exhaustive]
37pub enum NativeVideoCodec {
38 H264,
40 Hevc,
42 Vp9,
44 Av1,
46 Mpeg4Part2,
48}
49
50impl NativeVideoCodec {
51 #[must_use]
53 pub const fn as_str(self) -> &'static str {
54 match self {
55 Self::H264 => "h264",
56 Self::Hevc => "hevc",
57 Self::Vp9 => "vp9",
58 Self::Av1 => "av1",
59 Self::Mpeg4Part2 => "mpeg4-part2",
60 }
61 }
62
63 #[must_use]
68 pub const fn model(self) -> alpha2_media::VideoCodec {
69 match self {
70 Self::H264 => alpha2_media::VideoCodec::H264,
71 Self::Hevc => alpha2_media::VideoCodec::Hevc,
72 Self::Vp9 => alpha2_media::VideoCodec::Vp9,
73 Self::Av1 => alpha2_media::VideoCodec::Av1,
74 Self::Mpeg4Part2 => alpha2_media::VideoCodec::Mpeg4Part2,
75 }
76 }
77
78 #[must_use]
80 pub const fn from_model(codec: alpha2_media::VideoCodec) -> Self {
81 match codec {
82 alpha2_media::VideoCodec::H264 => Self::H264,
83 alpha2_media::VideoCodec::Hevc => Self::Hevc,
84 alpha2_media::VideoCodec::Vp9 => Self::Vp9,
85 alpha2_media::VideoCodec::Av1 => Self::Av1,
86 alpha2_media::VideoCodec::Mpeg4Part2 => Self::Mpeg4Part2,
87 }
88 }
89
90 pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
91 match self {
92 Self::H264 => ffi::AV_CODEC_ID_H264,
93 Self::Hevc => ffi::AV_CODEC_ID_HEVC,
94 Self::Vp9 => ffi::AV_CODEC_ID_VP9,
95 Self::Av1 => ffi::AV_CODEC_ID_AV1,
96 Self::Mpeg4Part2 => ffi::AV_CODEC_ID_MPEG4,
97 }
98 }
99}
100
101#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
103#[non_exhaustive]
104pub enum NativeAudioCodec {
105 Aac,
107 Opus,
109}
110
111impl NativeAudioCodec {
112 pub const ALL: [Self; 2] = [Self::Aac, Self::Opus];
114
115 #[must_use]
117 pub const fn as_str(self) -> &'static str {
118 match self {
119 Self::Aac => "aac",
120 Self::Opus => "opus",
121 }
122 }
123
124 #[must_use]
126 pub const fn model(self) -> alpha2_media::AudioCodec {
127 match self {
128 Self::Aac => alpha2_media::AudioCodec::Aac,
129 Self::Opus => alpha2_media::AudioCodec::Opus,
130 }
131 }
132
133 #[must_use]
135 pub const fn from_model(codec: alpha2_media::AudioCodec) -> Self {
136 match codec {
137 alpha2_media::AudioCodec::Aac => Self::Aac,
138 alpha2_media::AudioCodec::Opus => Self::Opus,
139 }
140 }
141
142 pub(crate) const fn encoder_name(self) -> &'static std::ffi::CStr {
146 match self {
147 Self::Aac => c"aac",
148 Self::Opus => c"libopus",
149 }
150 }
151}
152
153pub(crate) fn audio_encoder(codec: NativeAudioCodec) -> Option<AVCodecRef<'static>> {
156 AVCodec::find_encoder_by_name(codec.encoder_name()).filter(|encoder| {
157 u32::try_from(encoder.capabilities).unwrap_or(0) & ffi::AV_CODEC_CAP_EXPERIMENTAL == 0
158 })
159}
160
161#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
171#[non_exhaustive]
172pub enum NativeEncoderClass {
173 Software,
175 Hardware,
177 Hybrid,
181}
182
183impl NativeEncoderClass {
184 #[must_use]
186 pub const fn as_str(self) -> &'static str {
187 match self {
188 Self::Software => "software",
189 Self::Hardware => "hardware",
190 Self::Hybrid => "hybrid",
191 }
192 }
193
194 #[must_use]
200 pub const fn modes(self, hybrid_hardware: bool) -> &'static [bool] {
201 match self {
202 Self::Hardware => &[true],
203 Self::Hybrid if hybrid_hardware => &[true, false],
204 Self::Software | Self::Hybrid => &[false],
205 }
206 }
207}
208
209pub(crate) struct VideoEncoderCandidate {
212 pub(crate) encoder: AVCodecRef<'static>,
213 pub(crate) on_hardware: bool,
214}
215
216impl VideoEncoderCandidate {
217 pub(crate) fn name(&self) -> String {
218 self.encoder.name().to_string_lossy().into_owned()
219 }
220}
221
222#[must_use]
232pub fn hybrid_encoders_use_hardware() -> bool {
233 std::env::var("ALPHA2_VIDEO_ENCODER_HYBRID")
234 .is_ok_and(|value| value.eq_ignore_ascii_case("hardware"))
235}
236
237#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
239#[non_exhaustive]
240pub enum NativeEncoderRejection {
241 Dimensions,
243 OpenFailed,
245 OptionsIgnored,
247}
248
249impl NativeEncoderRejection {
250 #[must_use]
252 pub const fn as_str(self) -> &'static str {
253 match self {
254 Self::Dimensions => "dimensions",
255 Self::OpenFailed => "open-failed",
256 Self::OptionsIgnored => "options-ignored",
257 }
258 }
259}
260
261#[derive(Clone, Debug, Eq, PartialEq)]
263pub struct NativeVideoEncoderFallback {
264 pub encoder_name: String,
266 pub hardware_accelerated: bool,
269 pub reason: NativeEncoderRejection,
271 pub detail: String,
273}
274
275#[derive(Clone, Debug, Eq, PartialEq)]
281pub struct NativeVideoEncoderSelection {
282 pub encoder_name: String,
284 pub class: NativeEncoderClass,
286 pub hardware_accelerated: bool,
288 pub rejected: Vec<NativeVideoEncoderFallback>,
290 pub rate_control: crate::AppliedRateControl,
292 pub quantizer_refused: Option<String>,
294 pub software_threads: Option<i64>,
297}
298
299#[derive(Clone, Debug, Eq, PartialEq)]
301pub struct NativeVideoEncoderCapability {
302 pub codec: NativeVideoCodec,
304 pub encoder_name: String,
306 pub class: NativeEncoderClass,
308 pub hardware_accelerated: bool,
311 pub preferred: bool,
313 pub minimum_width: u32,
315 pub minimum_height: u32,
317 pub library: Option<NativeEncoderLibrary>,
320}
321
322#[derive(Clone, Debug, Eq, PartialEq)]
324pub struct NativeEncoderLibrary {
325 pub provider: &'static str,
327 pub version: String,
329 pub path: String,
331 pub notice: &'static str,
333}
334
335impl NativeEncoderLibrary {
336 fn openh264(library: &crate::OpenH264Library) -> Self {
337 Self {
338 provider: library.provider().as_str(),
339 version: library.version().to_owned(),
340 path: library.path().to_owned(),
341 notice: crate::OPENH264_NOTICE,
342 }
343 }
344}
345
346#[derive(Clone, Debug, Eq, PartialEq)]
348pub struct NativeAudioEncoderCapability {
349 pub codec: NativeAudioCodec,
351 pub encoder_name: String,
353}
354
355#[derive(Clone, Debug, Eq, PartialEq)]
361pub struct NativeCodecCapabilities {
362 pub video: Vec<NativeVideoEncoderCapability>,
365 pub audio: Vec<NativeAudioEncoderCapability>,
368}
369
370#[must_use]
379pub fn codec_capabilities() -> NativeCodecCapabilities {
380 let video = [
381 NativeVideoCodec::H264,
382 NativeVideoCodec::Hevc,
383 NativeVideoCodec::Vp9,
384 NativeVideoCodec::Av1,
385 NativeVideoCodec::Mpeg4Part2,
386 ]
387 .into_iter()
388 .flat_map(video_encoder_capabilities)
389 .collect();
390 NativeCodecCapabilities {
391 video,
392 audio: audio_encoder_capabilities(),
393 }
394}
395
396#[must_use]
399pub fn audio_encoder_capabilities() -> Vec<NativeAudioEncoderCapability> {
400 NativeAudioCodec::ALL
401 .into_iter()
402 .filter_map(|codec| {
403 let encoder = audio_encoder(codec)?;
404 Some(NativeAudioEncoderCapability {
405 codec,
406 encoder_name: encoder.name().to_string_lossy().into_owned(),
407 })
408 })
409 .collect()
410}
411
412#[must_use]
419pub fn video_encoder_capabilities(codec: NativeVideoCodec) -> Vec<NativeVideoEncoderCapability> {
420 available_video_encoders(codec)
421 .into_iter()
422 .enumerate()
423 .map(|(index, choice)| {
424 let encoder_name = choice.name();
425 let (minimum_width, minimum_height) = encoder_minimum_dimensions(&encoder_name);
426 NativeVideoEncoderCapability {
427 codec,
428 class: choice.class(),
429 hardware_accelerated: choice.on_hardware(),
430 preferred: index == 0,
431 encoder_name,
432 minimum_width,
433 minimum_height,
434 library: match &choice {
435 VideoEncoderChoice::Ffmpeg(_) => None,
436 VideoEncoderChoice::OpenH264(library) => {
437 Some(NativeEncoderLibrary::openh264(library))
438 }
439 },
440 }
441 })
442 .collect()
443}
444
445pub(crate) enum VideoEncoderChoice {
447 Ffmpeg(VideoEncoderCandidate),
448 OpenH264(std::sync::Arc<crate::OpenH264Library>),
449}
450
451impl VideoEncoderChoice {
452 pub(crate) fn name(&self) -> String {
453 match self {
454 Self::Ffmpeg(candidate) => candidate.name(),
455 Self::OpenH264(_) => crate::OPENH264_ENCODER_NAME.to_owned(),
456 }
457 }
458
459 pub(crate) const fn on_hardware(&self) -> bool {
460 match self {
461 Self::Ffmpeg(candidate) => candidate.on_hardware,
462 Self::OpenH264(_) => false,
463 }
464 }
465
466 pub(crate) fn class(&self) -> NativeEncoderClass {
467 match self {
468 Self::Ffmpeg(candidate) => encoder_class(&candidate.encoder),
469 Self::OpenH264(_) => NativeEncoderClass::Software,
470 }
471 }
472}
473
474pub(crate) fn available_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderChoice> {
478 let mut choices = registered_video_encoders(codec)
479 .into_iter()
480 .filter(encoder_is_compatible)
481 .map(VideoEncoderChoice::Ffmpeg)
482 .collect::<Vec<_>>();
483 if codec == NativeVideoCodec::H264
484 && let Some(library) = crate::openh264_status().library()
485 {
486 choices.push(VideoEncoderChoice::OpenH264(std::sync::Arc::clone(library)));
487 }
488 choices
489}
490
491#[derive(Clone, Debug, Eq, PartialEq)]
493pub struct NativeHardwareDecodeCapability {
494 pub codec: NativeVideoCodec,
496 pub decoder_name: Option<String>,
498 pub hardware_config: bool,
504 pub software_decoder: Option<String>,
508}
509
510#[derive(Clone, Debug, Eq, PartialEq)]
512pub struct NativeHardwareDecodeCapabilities {
513 pub backend: &'static str,
515 pub codecs: Vec<NativeHardwareDecodeCapability>,
517}
518
519#[must_use]
521pub const fn hardware_decode_backend() -> &'static str {
522 #[cfg(feature = "hwdecode")]
523 {
524 #[cfg(target_os = "linux")]
525 {
526 "vulkan"
527 }
528 #[cfg(target_os = "macos")]
529 {
530 "videotoolbox"
531 }
532 #[cfg(target_os = "windows")]
533 {
534 "d3d12va"
535 }
536 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
537 {
538 "none"
539 }
540 }
541 #[cfg(not(feature = "hwdecode"))]
542 {
543 "none"
544 }
545}
546
547#[must_use]
549pub fn hardware_decode_capabilities() -> NativeHardwareDecodeCapabilities {
550 let codecs = [
551 NativeVideoCodec::H264,
552 NativeVideoCodec::Hevc,
553 NativeVideoCodec::Av1,
554 NativeVideoCodec::Vp9,
555 ]
556 .into_iter()
557 .map(|codec| {
558 let decoder = AVCodec::find_decoder(codec.codec_id());
559 NativeHardwareDecodeCapability {
560 codec,
561 decoder_name: decoder
562 .as_ref()
563 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
564 hardware_config: crate::decode::registered_video_decoders(codec.codec_id())
565 .any(|decoder| decoder_has_hardware_config(&decoder)),
566 software_decoder: crate::decode::software_video_decoder(codec.codec_id())
567 .ok()
568 .map(|decoder| decoder.name().to_string_lossy().into_owned()),
569 }
570 })
571 .collect();
572 NativeHardwareDecodeCapabilities {
573 backend: hardware_decode_backend(),
574 codecs,
575 }
576}
577
578#[allow(unused_variables)]
580fn decoder_has_hardware_config(decoder: &AVCodecRef<'_>) -> bool {
581 #[cfg(all(
582 any(target_os = "linux", target_os = "macos", target_os = "windows"),
583 feature = "hwdecode"
584 ))]
585 {
586 #[cfg(target_os = "linux")]
587 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_VULKAN, ffi::AV_PIX_FMT_VULKAN);
588 #[cfg(target_os = "macos")]
589 let (device_type, pixel_format) = (
590 ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
591 ffi::AV_PIX_FMT_VIDEOTOOLBOX,
592 );
593 #[cfg(target_os = "windows")]
594 let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_D3D12VA, ffi::AV_PIX_FMT_D3D12);
595 (0..)
596 .map_while(|index| decoder.hw_config(index))
597 .any(|config| config.device_type == device_type && config.pix_fmt == pixel_format)
598 }
599 #[cfg(not(all(
600 any(target_os = "linux", target_os = "macos", target_os = "windows"),
601 feature = "hwdecode"
602 )))]
603 {
604 false
605 }
606}
607
608#[cfg(test)]
612pub(crate) fn test_skips_encoder(name: &std::ffi::CStr) -> bool {
613 static SKIPPED: LazyLock<Vec<String>> = LazyLock::new(|| {
614 std::env::var("ALPHA2_TEST_SKIP_ENCODERS")
615 .unwrap_or_default()
616 .split(',')
617 .map(str::trim)
618 .filter(|name| !name.is_empty())
619 .map(str::to_owned)
620 .collect()
621 });
622 let name = name.to_string_lossy();
623 SKIPPED.iter().any(|skipped| *skipped == name)
624}
625
626#[cfg(not(test))]
627const fn test_skips_encoder(_name: &std::ffi::CStr) -> bool {
628 false
629}
630
631pub(crate) fn registered_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderCandidate> {
633 let hybrid_hardware = hybrid_encoders_use_hardware();
634 let mut candidates = AVCodec::iterate()
635 .filter(|encoder| {
636 encoder.id == codec.codec_id()
637 && !test_skips_encoder(encoder.name())
638 && AVCodec::find_encoder_by_name(encoder.name())
639 .is_some_and(|found| found.as_ptr() == encoder.as_ptr())
640 })
641 .flat_map(|encoder| {
642 encoder_class(&encoder)
643 .modes(hybrid_hardware)
644 .iter()
645 .filter_map(move |&on_hardware| {
646 AVCodec::find_encoder_by_name(encoder.name()).map(|encoder| {
647 VideoEncoderCandidate {
648 encoder,
649 on_hardware,
650 }
651 })
652 })
653 })
654 .collect::<Vec<_>>();
655 candidates.sort_by_key(|candidate| !candidate.on_hardware);
657 candidates
658}
659
660pub(crate) fn encoder_class(encoder: &AVCodec) -> NativeEncoderClass {
663 let name = encoder.name().to_string_lossy();
664 declared_encoder_class(&name, u32::try_from(encoder.capabilities).unwrap_or(0))
665}
666
667const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
678
679pub(crate) fn declared_encoder_class(name: &str, capabilities: u32) -> NativeEncoderClass {
683 if capabilities & ffi::AV_CODEC_CAP_HYBRID != 0 {
684 NativeEncoderClass::Hybrid
685 } else if capabilities & ffi::AV_CODEC_CAP_HARDWARE != 0
686 || MISDECLARED_HARDWARE_ENCODERS.contains(&name)
687 {
688 NativeEncoderClass::Hardware
689 } else {
690 NativeEncoderClass::Software
691 }
692}
693
694pub(crate) fn encoder_is_compatible(candidate: &VideoEncoderCandidate) -> bool {
695 let name = candidate.name();
696 registration_is_compatible(candidate, &name) && encoder_runs_here(candidate, &name)
697}
698
699fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
702 if candidate
705 .encoder
706 .pix_fmts()
707 .is_some_and(|formats| !formats.contains(&ffi::AV_PIX_FMT_YUV420P))
708 {
709 return false;
710 }
711 !candidate.on_hardware || hardware_device_node(name) != Some(false)
712}
713
714pub(crate) fn needs_open_probe(on_hardware: bool, name: &str) -> bool {
723 (on_hardware && (hardware_device_node(name).is_none() || name.ends_with("_nvenc")))
724 || name.ends_with("_mf")
725 || name.ends_with("_videotoolbox")
726}
727
728fn encoder_runs_here(candidate: &VideoEncoderCandidate, name: &str) -> bool {
753 probe_outcome(candidate, name, true).unwrap_or(false)
754}
755
756fn probe_outcome(candidate: &VideoEncoderCandidate, name: &str, probe: bool) -> Option<bool> {
760 static RUNS: LazyLock<Mutex<HashMap<(String, bool), bool>>> = LazyLock::new(Mutex::default);
761 if !needs_open_probe(candidate.on_hardware, name) {
762 return Some(true);
763 }
764 let key = (name.to_owned(), candidate.on_hardware);
765 let mut runs = RUNS
766 .lock()
767 .unwrap_or_else(std::sync::PoisonError::into_inner);
768 if let Some(runs_here) = runs.get(&key) {
769 return Some(*runs_here);
770 }
771 if !probe {
772 return None;
773 }
774 let runs_here = match probe_software_encode(candidate, name, 256, 144, None).0 {
775 None => true,
776 Some(error) => {
777 alpha2_os::diagnostic!(
778 "alpha2 native: video encoder {name} {} is registered but did not encode a \
779 frame on this machine, so it is not offered that way: {error}",
780 mode_label(candidate.on_hardware)
781 );
782 false
783 }
784 };
785 runs.insert(key, runs_here);
786 Some(runs_here)
787}
788
789#[must_use]
796pub fn video_encoders_without_probing() -> (Vec<NativeVideoEncoderCapability>, Vec<String>) {
797 let mut unprobed = Vec::new();
798 let mut listed = Vec::new();
799 for codec in [
800 NativeVideoCodec::H264,
801 NativeVideoCodec::Hevc,
802 NativeVideoCodec::Vp9,
803 NativeVideoCodec::Av1,
804 NativeVideoCodec::Mpeg4Part2,
805 ] {
806 for candidate in registered_video_encoders(codec) {
807 let name = candidate.name();
808 if !registration_is_compatible(&candidate, &name) {
809 continue;
810 }
811 match probe_outcome(&candidate, &name, false) {
812 Some(true) => {
813 let (minimum_width, minimum_height) = encoder_minimum_dimensions(&name);
814 listed.push(NativeVideoEncoderCapability {
815 codec,
816 class: encoder_class(&candidate.encoder),
817 hardware_accelerated: candidate.on_hardware,
818 preferred: !listed
819 .iter()
820 .any(|listed: &NativeVideoEncoderCapability| listed.codec == codec),
821 encoder_name: name,
822 minimum_width,
823 minimum_height,
824 library: None,
825 });
826 }
827 Some(false) => {}
828 None => unprobed.push(format!("{name} {}", mode_label(candidate.on_hardware))),
829 }
830 }
831 }
832 (listed, unprobed)
833}
834
835pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
837 if on_hardware { "on GPU" } else { "on CPU" }
838}
839
840#[cfg(target_os = "linux")]
851fn hardware_device_node(name: &str) -> Option<bool> {
852 if name.ends_with("_nvenc") {
853 return Some(Path::new("/dev/nvidia0").exists() || Path::new("/dev/nvidiactl").exists());
854 }
855 if name.ends_with("_vaapi") || name.ends_with("_qsv") || name.ends_with("_amf") {
856 return Some(fs::read_dir("/dev/dri").is_ok_and(|entries| {
857 entries.flatten().any(|entry| {
858 entry
859 .file_name()
860 .to_str()
861 .is_some_and(|name| name.starts_with("renderD") || name.starts_with("card"))
862 })
863 }));
864 }
865 if name.ends_with("_v4l2m2m") {
866 return Some((0..64).any(|index| Path::new(&format!("/dev/video{index}")).exists()));
867 }
868 None
869}
870
871#[cfg(not(target_os = "linux"))]
874const fn hardware_device_node(_name: &str) -> Option<bool> {
875 None
876}
877
878#[must_use]
886pub fn no_video_encoder_message(
887 codec: NativeVideoCodec,
888 offered: &[NativeVideoEncoderCapability],
889 gpu_output_unavailable: Option<&str>,
890) -> String {
891 let identity = crate::ffmpeg_runtime_identity();
892 let inventory = offered
893 .iter()
894 .filter(|capability| capability.library.is_none())
895 .map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
896 .collect::<Vec<_>>()
897 .join(", ");
898 let mut message = format!("no usable {} encoder: ", codec.as_str());
899 if let Some(reason) = gpu_output_unavailable {
900 let _ = write!(message, "GPU output path unavailable ({reason}); ");
901 }
902 let _ = write!(
903 message,
904 "the linked FFmpeg runtime ({}, pinned SDK {}) offers [{}]",
905 identity.version,
906 identity.pinned_sdk_release,
907 if inventory.is_empty() {
908 "none"
909 } else {
910 inventory.as_str()
911 }
912 );
913 if codec != NativeVideoCodec::H264 {
914 return message;
915 }
916 let status = crate::openh264_status();
917 if let Some(reason) = status.reason() {
918 let _ = write!(
919 message,
920 "; OpenH264 is {}: {reason}",
921 status.as_str().replace('-', " ")
922 );
923 }
924 let _ = write!(message, ". To export H.264 here: {}", h264_encoder_remedy());
925 message
926}
927
928#[must_use]
932pub fn h264_encoder_remedy() -> String {
933 let openh264 = match crate::openh264_status() {
934 crate::OpenH264Status::Disabled => {
935 "unset ALPHA2_OPENH264=0 to use the installed OpenH264 encoder".to_owned()
936 }
937 _ => format!(
938 "install the OpenH264 encoder ({})",
939 crate::OPENH264_INSTALL_COMMAND
940 ),
941 };
942 format!(
943 "{openh264}, use a thin runtime with an FFmpeg 8 that has libx264, or export VP9 (.webm) \
944 or AV1 instead"
945 )
946}
947
948fn unavailable_codec_error(codec: NativeVideoCodec) -> EncodeError {
951 EncodeError::Adapter(no_video_encoder_message(
952 codec,
953 &codec_capabilities().video,
954 None,
955 ))
956}
957
958pub(crate) fn video_encoder_candidates(
964 codec: NativeVideoCodec,
965 requested_name: Option<&str>,
966) -> Result<Vec<VideoEncoderChoice>, EncodeError> {
967 if requested_name == Some(crate::OPENH264_ENCODER_NAME) {
968 if codec != NativeVideoCodec::H264 {
969 return Err(EncodeError::Adapter(format!(
970 "video encoder {} does not implement {}",
971 crate::OPENH264_ENCODER_NAME,
972 codec.as_str()
973 )));
974 }
975 let status = crate::openh264_status();
976 return match status.library() {
977 Some(library) => Ok(vec![VideoEncoderChoice::OpenH264(std::sync::Arc::clone(
978 library,
979 ))]),
980 None => Err(EncodeError::Adapter(format!(
981 "video encoder {} is unavailable: {}{}",
982 crate::OPENH264_ENCODER_NAME,
983 status.reason().unwrap_or_default(),
984 status
985 .remedy()
986 .map(|remedy| format!("; {remedy}"))
987 .unwrap_or_default()
988 ))),
989 };
990 }
991 if let Some(name) = requested_name {
992 let encoder = select_video_encoder(codec, Some(name))?;
993 return Ok(registered_video_encoders(codec)
994 .into_iter()
995 .filter(|candidate| candidate.encoder.as_ptr() == encoder.as_ptr())
996 .filter(encoder_is_compatible)
997 .map(VideoEncoderChoice::Ffmpeg)
998 .collect());
999 }
1000 let candidates = available_video_encoders(codec);
1001 if candidates.is_empty() {
1002 return Err(unavailable_codec_error(codec));
1003 }
1004 Ok(candidates)
1005}
1006
1007pub(crate) fn select_video_encoder(
1011 codec: NativeVideoCodec,
1012 requested_name: Option<&str>,
1013) -> Result<AVCodecRef<'static>, EncodeError> {
1014 if let Some(name) = requested_name {
1015 let name_c = CString::new(name)
1016 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1017 let encoder = AVCodec::find_encoder_by_name(&name_c)
1018 .ok_or_else(|| EncodeError::Adapter(format!("video encoder {name} is unavailable")))?;
1019 if encoder.id != codec.codec_id() {
1020 return Err(EncodeError::Adapter(format!(
1021 "video encoder {name} does not implement {}",
1022 codec.as_str()
1023 )));
1024 }
1025 if !video_encoder_capabilities(codec)
1026 .iter()
1027 .any(|capability| capability.encoder_name == name)
1028 {
1029 return Err(EncodeError::Adapter(format!(
1030 "video encoder {name} is not compatible with the native encode path"
1031 )));
1032 }
1033 return Ok(encoder);
1034 }
1035
1036 let capability = video_encoder_capabilities(codec)
1037 .into_iter()
1038 .find(|capability| capability.library.is_none())
1039 .ok_or_else(|| unavailable_codec_error(codec))?;
1040 let name = CString::new(capability.encoder_name)
1041 .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1042 AVCodec::find_encoder_by_name(&name).ok_or_else(|| unavailable_codec_error(codec))
1043}
1044
1045pub(crate) struct VideoEncoderOpenRequest<'a> {
1047 pub config: &'a VideoEncodeConfig,
1049 pub requested_name: Option<&'a str>,
1051 pub threads: SoftwareEncoderThreads<'a>,
1054 pub global_header: bool,
1056 pub width: i32,
1058 pub height: i32,
1060}
1061
1062pub(crate) struct SoftwareEncoderThreads<'a> {
1073 decide: &'a dyn Fn() -> i64,
1074 decided: std::cell::OnceCell<i64>,
1075}
1076
1077impl<'a> SoftwareEncoderThreads<'a> {
1078 pub(crate) fn new(decide: &'a dyn Fn() -> i64) -> Self {
1080 Self {
1081 decide,
1082 decided: std::cell::OnceCell::new(),
1083 }
1084 }
1085
1086 pub(crate) fn get(&self) -> i64 {
1088 *self.decided.get_or_init(|| (self.decide)().max(1))
1089 }
1090}
1091
1092pub(crate) fn open_video_encoder(
1105 request: &VideoEncoderOpenRequest<'_>,
1106) -> Result<
1107 (
1108 crate::video_backend::VideoEncoderBackend,
1109 NativeVideoEncoderSelection,
1110 ),
1111 EncodeError,
1112> {
1113 let candidates = video_encoder_candidates(request.config.codec, request.requested_name)?;
1114 open_first_video_encoder(request, candidates)
1115}
1116
1117struct OpenedVideoEncoder {
1119 backend: crate::video_backend::VideoEncoderBackend,
1120 rate_control: crate::AppliedRateControl,
1121 quantizer_refused: Option<String>,
1122 software_threads: Option<i64>,
1123}
1124
1125pub(crate) fn open_first_video_encoder(
1128 request: &VideoEncoderOpenRequest<'_>,
1129 candidates: Vec<VideoEncoderChoice>,
1130) -> Result<
1131 (
1132 crate::video_backend::VideoEncoderBackend,
1133 NativeVideoEncoderSelection,
1134 ),
1135 EncodeError,
1136> {
1137 let mut rejected = Vec::new();
1138 let last = candidates.len().saturating_sub(1);
1139 for (index, candidate) in candidates.into_iter().enumerate() {
1140 let name = candidate.name();
1141 let class = candidate.class();
1142 let opened = match &candidate {
1143 VideoEncoderChoice::Ffmpeg(candidate) => {
1144 open_one_video_encoder(request, candidate, &name).map(
1145 |(encoder, rate_control, quantizer_refused)| OpenedVideoEncoder {
1146 backend: crate::video_backend::VideoEncoderBackend::Ffmpeg(encoder),
1147 rate_control,
1148 quantizer_refused,
1149 software_threads: (!candidate.on_hardware).then(|| request.threads.get()),
1150 },
1151 )
1152 }
1153 VideoEncoderChoice::OpenH264(library) => open_openh264(request, library),
1154 };
1155 match opened {
1156 Ok(opened) => {
1157 return Ok((
1158 opened.backend,
1159 NativeVideoEncoderSelection {
1160 encoder_name: name,
1161 class,
1162 hardware_accelerated: candidate.on_hardware(),
1163 rejected,
1164 rate_control: opened.rate_control,
1165 quantizer_refused: opened.quantizer_refused,
1166 software_threads: opened.software_threads,
1167 },
1168 ));
1169 }
1170 Err((reason, error)) => {
1171 let detail = error.to_string();
1172 if index == last {
1173 let earlier = rejected
1174 .iter()
1175 .map(|fallback: &NativeVideoEncoderFallback| {
1176 format!(
1177 "{} {} ({}): {}",
1178 fallback.encoder_name,
1179 mode_label(fallback.hardware_accelerated),
1180 fallback.reason.as_str(),
1181 fallback.detail
1182 )
1183 })
1184 .collect::<Vec<_>>();
1185 let prefix = if earlier.is_empty() {
1186 String::new()
1187 } else {
1188 format!("after {}; ", earlier.join("; "))
1189 };
1190 return Err(EncodeError::Adapter(format!(
1191 "{prefix}{name} {} ({}): {detail}",
1192 mode_label(candidate.on_hardware()),
1193 reason.as_str()
1194 )));
1195 }
1196 alpha2_os::diagnostic!(
1197 "alpha2 native export: video encoder {name} {} ({}) rejected: {detail}",
1198 mode_label(candidate.on_hardware()),
1199 reason.as_str()
1200 );
1201 rejected.push(NativeVideoEncoderFallback {
1202 encoder_name: name,
1203 hardware_accelerated: candidate.on_hardware(),
1204 reason,
1205 detail,
1206 });
1207 }
1208 }
1209 }
1210 Err(unavailable_codec_error(request.config.codec))
1211}
1212
1213fn open_openh264(
1221 request: &VideoEncoderOpenRequest<'_>,
1222 library: &crate::OpenH264Library,
1223) -> Result<OpenedVideoEncoder, (NativeEncoderRejection, EncodeError)> {
1224 let config = request.config;
1225 validate_encoder_dimensions(crate::OPENH264_ENCODER_NAME, config.width, config.height)
1226 .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1227 let open_failed = |message: String| {
1228 (
1229 NativeEncoderRejection::OpenFailed,
1230 EncodeError::Adapter(message),
1231 )
1232 };
1233 if !request.global_header {
1234 return Err(open_failed(
1235 "OpenH264 writes its parameter sets into the stream header, which this container \
1236 lacks"
1237 .to_owned(),
1238 ));
1239 }
1240 let (rate, rate_control) = crate::rate_control::openh264_rate(config.rate_control);
1241 let time_base = encoder_time_base(config.frame_rate, None)
1242 .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1243 let _ = request.threads.get();
1244 let encoder = crate::openh264::OpenH264Encoder::open(
1245 library,
1246 &crate::openh264::OpenH264Settings {
1247 width: config.width,
1248 height: config.height,
1249 frame_rate: config.frame_rate,
1250 color: config.color,
1251 sample_aspect_ratio: config.display.sample_aspect_ratio,
1252 rate,
1253 keyframe_interval: u32::try_from(keyframe_interval(config.frame_rate)).unwrap_or(1),
1254 },
1255 time_base,
1256 )
1257 .map_err(open_failed)?;
1258 let threads = i64::from(encoder.threads());
1259 Ok(OpenedVideoEncoder {
1260 backend: crate::video_backend::VideoEncoderBackend::OpenH264(Box::new(encoder)),
1261 rate_control,
1262 quantizer_refused: None,
1263 software_threads: Some(threads),
1264 })
1265}
1266
1267fn open_one_video_encoder(
1274 request: &VideoEncoderOpenRequest<'_>,
1275 candidate: &VideoEncoderCandidate,
1276 name: &str,
1277) -> Result<
1278 (EncoderContext, crate::AppliedRateControl, Option<String>),
1279 (NativeEncoderRejection, EncodeError),
1280> {
1281 validate_encoder_dimensions(name, request.config.width, request.config.height)
1282 .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1283 let refused = match crate::rate_control::quantizer_request(name, request.config.rate_control) {
1284 None => None,
1285 Some(Err(reason)) => Some(reason),
1286 Some(Ok(quantized)) => {
1287 match open_video_encoder_context(request, candidate, name, Some(&quantized)) {
1288 Ok(encoder) => return Ok((encoder, crate::AppliedRateControl::Quantizer, None)),
1289 Err((_, error)) => Some(format!("{} refused: {error}", quantized.describe())),
1290 }
1291 }
1292 };
1293 if let Some(reason) = &refused {
1294 alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
1295 }
1296 open_video_encoder_context(request, candidate, name, None)
1297 .map(|encoder| (encoder, crate::AppliedRateControl::Bitrate, refused))
1298}
1299
1300fn open_video_encoder_context(
1301 request: &VideoEncoderOpenRequest<'_>,
1302 candidate: &VideoEncoderCandidate,
1303 name: &str,
1304 quantized: Option<&crate::rate_control::MechanismRequest>,
1305) -> Result<EncoderContext, (NativeEncoderRejection, EncodeError)> {
1306 let mut encoder = EncoderContext::from(AVCodecContext::new(&candidate.encoder));
1307 encoder.set_width(request.width);
1308 encoder.set_height(request.height);
1309 encoder.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
1310 let rate = request.config.frame_rate;
1311 let time_base = encoder_time_base(rate, max_time_base_denominator(candidate.encoder.id))
1312 .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1313 encoder.set_time_base(time_base);
1314 let sar = request.config.display.sample_aspect_ratio;
1316 if sar != alpha2_media::SampleAspectRatio::default() {
1317 encoder.set_sample_aspect_ratio(rsmpeg::avutil::ra(
1318 i32::try_from(sar.num).unwrap_or(1),
1319 i32::try_from(sar.den).unwrap_or(1),
1320 ));
1321 }
1322 encoder.set_framerate(rsmpeg::avutil::ra(
1323 i32::try_from(rate.numerator()).unwrap_or(i32::MAX),
1324 i32::try_from(rate.denominator()).unwrap_or(1),
1325 ));
1326 encoder.set_bit_rate(quantized.map_or_else(
1327 || i64::try_from(request.config.rate_control.bitrate).unwrap_or(i64::MAX),
1328 |quantized| quantized.bit_rate,
1329 ));
1330 encoder.set_gop_size(keyframe_interval(request.config.frame_rate));
1331 encoder.set_max_b_frames(encoder_max_b_frames(name));
1332 if request.global_header {
1333 let flags = encoder.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0);
1334 encoder.set_flags(flags);
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 = EncoderContext::from(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}