Skip to main content

alpha2_native/
capabilities.rs

1//! Codec capability discovery and encoder selection.
2
3use 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/// Configuration for transactional audio/video export.
18#[derive(Copy, Clone, Debug, PartialEq)]
19pub struct AvEncodeConfig {
20    /// Video encoder settings.
21    pub video: VideoEncodeConfig,
22    /// Project audio sample rate.
23    pub audio_sample_rate: u32,
24    /// Audio channel count.
25    pub audio_channels: u16,
26    /// Audio codec selected by the downstream host.
27    pub audio_codec: NativeAudioCodec,
28    /// Resolved audio bitrate in bits per second; the host resolves the level.
29    pub audio_bit_rate: u32,
30}
31
32/// MP4 video codecs implemented by the native encode sinks.
33#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
34#[non_exhaustive]
35pub enum NativeVideoCodec {
36    /// H.264/AVC, preferred for broad browser, Windows, and Apple playback.
37    H264,
38    /// H.265/HEVC in MP4.
39    Hevc,
40    /// VP9 in MP4 when the linked `FFmpeg` build provides a VP9 encoder.
41    Vp9,
42    /// AV1 in MP4 when the linked `FFmpeg` build provides an AV1 encoder.
43    Av1,
44    /// MPEG-4 Part 2, retained for compatibility with existing alpha2 hosts.
45    Mpeg4Part2,
46}
47
48impl NativeVideoCodec {
49    /// Stable identifier suitable for an application protocol or settings file.
50    #[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    /// The engine's codec this encoder family implements.
62    ///
63    /// The rate mapping is keyed on [`alpha2_media::VideoCodec`] alone, so
64    /// there is one front door to it; this is the native side's way in.
65    #[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/// MP4 audio codecs implemented by the native audio/video sink.
88#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
89#[non_exhaustive]
90pub enum NativeAudioCodec {
91    /// AAC-LC compatible audio.
92    Aac,
93}
94
95impl NativeAudioCodec {
96    /// Stable identifier suitable for an application protocol or settings file.
97    #[must_use]
98    pub const fn as_str(self) -> &'static str {
99        match self {
100            Self::Aac => "aac",
101        }
102    }
103
104    /// The engine's codec this encoder family implements.
105    #[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/// How a concrete encoder executes, as the linked runtime declares it.
120///
121/// Taken from capability bits rather than the name suffix:
122/// `AV_CODEC_CAP_HARDWARE` and `AV_CODEC_CAP_HYBRID` are set by the encoder
123/// itself, so a build that adds an encoder alpha2 has never heard of is still
124/// classified correctly, and a hybrid encoder is not described as though it
125/// were one or the other.
126/// The one exception is a short list of encoders the pinned runtime
127/// misdeclares (`declared_encoder_class`).
128#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
129#[non_exhaustive]
130pub enum NativeEncoderClass {
131    /// Runs on the CPU and draws on the software codec thread budget.
132    Software,
133    /// Runs on a hardware engine and ignores the thread budget.
134    Hardware,
135    /// Can do either. `MediaFoundation`'s `_mf` encoders are the shipped case:
136    /// `hw_encoding=0` enumerates a synchronous software transform and
137    /// `hw_encoding=1` an asynchronous hardware one.
138    Hybrid,
139}
140
141impl NativeEncoderClass {
142    /// Stable identifier for diagnostics and the export result.
143    #[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    /// The ways alpha2 runs an encoder of this class, most preferred first:
153    /// `true` on the GPU, `false` on the CPU. A hybrid encoder runs on the
154    /// CPU unless `hybrid_hardware` asks for the GPU, and then still has the
155    /// CPU behind it, because a machine without a hardware transform cannot
156    /// run the GPU mode at all.
157    #[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
167/// One way to run a registered encoder: the encoder, and whether alpha2
168/// configures it to encode on the GPU. Only a hybrid encoder has two.
169pub(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/// Whether hybrid encoders are configured to run on the GPU.
181///
182/// `h264_mf` and its siblings default to `hw_encoding=0`, which enumerates
183/// `MFT_ENUM_FLAG_SYNCMFT` (the synchronous *software* transform category),
184/// so the shipped default is a CPU encoder that correctly draws on the codec
185/// thread budget. `ALPHA2_VIDEO_ENCODER_HYBRID=hardware` tries `hw_encoding=1`
186/// first, which asks `MediaFoundation` for a hardware MFT, and keeps the
187/// software transform of the same encoder behind it for the SKUs and VMs
188/// that have none: `FFmpeg` does not fall back from one to the other itself.
189#[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/// Why one candidate encoder was not the one an export opened.
196#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
197#[non_exhaustive]
198pub enum NativeEncoderRejection {
199    /// The encoder refuses an output this small.
200    Dimensions,
201    /// `avcodec_open2` failed: no device, no licence, unsupported profile.
202    OpenFailed,
203    /// The encoder silently ignored an option alpha2 requires it to honour.
204    OptionsIgnored,
205}
206
207impl NativeEncoderRejection {
208    /// Stable identifier for diagnostics and the export result.
209    #[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/// One candidate encoder an export tried and moved past.
220#[derive(Clone, Debug, Eq, PartialEq)]
221pub struct NativeVideoEncoderFallback {
222    /// `FFmpeg` short name of the candidate.
223    pub encoder_name: String,
224    /// Whether the candidate was configured to encode on the GPU. A hybrid
225    /// encoder can be tried on the GPU and then on the CPU.
226    pub hardware_accelerated: bool,
227    /// Typed reason it was rejected.
228    pub reason: NativeEncoderRejection,
229    /// The runtime's own message, kept verbatim.
230    pub detail: String,
231}
232
233/// The encoder an export actually opened, and what it passed over.
234///
235/// Advertisement is not instantiation: a platform can register an encoder it
236/// cannot open on this machine. Every export reports what it opened rather
237/// than letting the capability list imply it.
238#[derive(Clone, Debug, Eq, PartialEq)]
239pub struct NativeVideoEncoderSelection {
240    /// `FFmpeg` short name of the encoder that opened.
241    pub encoder_name: String,
242    /// How that encoder executes.
243    pub class: NativeEncoderClass,
244    /// Whether it is configured to encode on the GPU.
245    pub hardware_accelerated: bool,
246    /// Candidates tried first, in preference order.
247    pub rejected: Vec<NativeVideoEncoderFallback>,
248    /// Whether the encoder runs the rate control's quantizer or its bitrate.
249    pub rate_control: crate::AppliedRateControl,
250    /// Why a quantizer that was asked for is not the one running.
251    pub quantizer_refused: Option<String>,
252    /// `FFmpeg` workers the encoder was opened with when it runs on the CPU;
253    /// `None` on the GPU, which takes no share of the software budget.
254    pub software_threads: Option<i64>,
255}
256
257/// One linked video encoder available to the MP4 sink.
258#[derive(Clone, Debug, Eq, PartialEq)]
259pub struct NativeVideoEncoderCapability {
260    /// Stable logical codec requested through [`VideoEncodeConfig`].
261    pub codec: NativeVideoCodec,
262    /// Concrete `FFmpeg` encoder implementation for this codec.
263    pub encoder_name: String,
264    /// How the linked runtime declares this encoder executes.
265    pub class: NativeEncoderClass,
266    /// Whether this entry runs the encoder on the GPU. A hybrid encoder is
267    /// listed once per way it runs here ([`hybrid_encoders_use_hardware`]).
268    pub hardware_accelerated: bool,
269    /// Whether this is the default encoder for its logical codec.
270    pub preferred: bool,
271    /// Smallest even width accepted by this concrete encoder.
272    pub minimum_width: u32,
273    /// Smallest even height accepted by this concrete encoder.
274    pub minimum_height: u32,
275}
276
277/// One linked audio encoder available to the MP4 sink.
278#[derive(Clone, Debug, Eq, PartialEq)]
279pub struct NativeAudioEncoderCapability {
280    /// Stable logical codec requested through [`AvEncodeConfig`].
281    pub codec: NativeAudioCodec,
282    /// Concrete `FFmpeg` encoder selected for that codec on this host.
283    pub encoder_name: String,
284}
285
286/// Runtime codec inventory for the native MP4 sinks.
287#[derive(Clone, Debug, Eq, PartialEq)]
288pub struct NativeCodecCapabilities {
289    /// Video encoders accepted by [`NativeVideoEncodeSink`](crate::NativeVideoEncodeSink) and
290    /// [`NativeAvEncodeSink`](crate::NativeAvEncodeSink).
291    pub mp4_video: Vec<NativeVideoEncoderCapability>,
292    /// Audio encoders accepted by [`NativeAvEncodeSink`](crate::NativeAvEncodeSink).
293    pub mp4_audio: Vec<NativeAudioEncoderCapability>,
294}
295
296/// Reports the linked `FFmpeg` encoders usable by alpha2's native MP4 sinks.
297///
298/// A logical codec may have multiple entries, and a hybrid encoder one per
299/// way it runs. Hardware entries appear first when they accept the native
300/// YUV420P path and their required device is present; exactly one entry per
301/// codec is marked preferred.
302#[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/// One codec's hardware decode registration for this platform's backend.
349#[derive(Clone, Debug, Eq, PartialEq)]
350pub struct NativeHardwareDecodeCapability {
351    /// Logical codec.
352    pub codec: NativeVideoCodec,
353    /// Concrete `FFmpeg` decoder implementation, when one is linked.
354    pub decoder_name: Option<String>,
355    /// Whether that decoder advertises this platform's hardware configuration.
356    ///
357    /// This is registration only. Whether a device, profile, and surface
358    /// format actually work is decided when a stream is opened, and every
359    /// export reports which path its frames took.
360    pub hardware_config: bool,
361    /// The decoder a software session opens for the codec, or `None` when
362    /// only a hardware session can decode it (AV1 on a runtime without
363    /// libdav1d).
364    pub software_decoder: Option<String>,
365}
366
367/// Hardware decode registration reported by the linked `FFmpeg` build.
368#[derive(Clone, Debug, Eq, PartialEq)]
369pub struct NativeHardwareDecodeCapabilities {
370    /// Stable backend identifier, or `none` on a build without a GPU path.
371    pub backend: &'static str,
372    /// One entry per codec alpha2 can present from a decoder surface.
373    pub codecs: Vec<NativeHardwareDecodeCapability>,
374}
375
376/// Backend this build decodes with, independent of any device being present.
377#[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/// Reports which codecs the linked `FFmpeg` registers for this backend.
405#[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/// Whether `decoder` advertises this platform's hardware device configuration.
436#[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
465/// Every way to run each encoder registered for `codec`, GPU ones first.
466pub(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    // Stable sort: FFmpeg's registration order within each tier, hardware first.
489    candidates.sort_by_key(|candidate| !candidate.on_hardware);
490    candidates
491}
492
493/// Classifies `encoder` from the capability bits it declares
494/// ([`declared_encoder_class`]).
495pub(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
500/// Encoders the pinned runtime registers as software although they run only
501/// on a hardware engine.
502///
503/// `FFmpeg` 8.0 and 8.1 declare `h264_videotoolbox` with neither
504/// `AV_CODEC_CAP_HARDWARE` nor `AV_CODEC_CAP_HYBRID`, unlike
505/// `hevc_videotoolbox`, yet opens it with `allow_sw=0`
506/// (`RequireHardwareAcceleratedVideoEncoder`), so it runs on the Mac's
507/// hardware encoder or not at all. An entry applies only while the runtime
508/// declares neither bit: a runtime that declares one is taken at its word,
509/// and the entry can go once no supported runtime needs it.
510const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
511
512/// The class of the encoder `name` that declares `capabilities`: the bits it
513/// declares, except that an encoder in [`MISDECLARED_HARDWARE_ENCODERS`]
514/// declaring neither is `Hardware`.
515pub(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
532/// What the registration alone rules out: an encoder the YUV420P sink cannot
533/// feed, and a hardware one whose device has no node here.
534fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
535    // The native sink submits software YUV420P frames directly. Hardware wrappers
536    // requiring an AVHWFramesContext must not be advertised.
537    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
547/// Whether the encoder `name`, run on the GPU when `on_hardware` is set, is
548/// listed only after an open probe ([`encoder_runs_here`]): a hardware
549/// encoder with no device node to stand for its device, and an `_mf` or
550/// `_videotoolbox` one either way it runs. A `VideoToolbox` encoder opens only
551/// where the Mac has a hardware encoder, and a runtime may declare one
552/// software (`FFmpeg` 8.1's `h264_videotoolbox`, which the class corrects),
553/// so the name keeps it probed whatever its class.
554pub(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
560/// Whether `encoder` encodes a frame on this machine, for the encoders whose
561/// registration does not say.
562///
563/// Registration says an encoder was compiled in, not that this machine runs
564/// it. `MediaFoundation` registers every `_mf` encoder whether or not Windows
565/// has a transform that runs it: on one runner `hevc_mf` found the HEVC
566/// Video Extension, opened, and failed its first frame with `E_FAIL` in
567/// every rate mode, and `av1_mf` found no transform at all. A hardware
568/// encoder is the same claim about a device: `h264_videotoolbox` registers in
569/// a headless macOS VM with no encoder to open, and NVENC on a Windows
570/// machine without the driver. Advertised on registration alone, they would
571/// be preferred and exports could only fall through. So an `_mf` or
572/// `_videotoolbox` encoder either way it runs, and a hardware one with no
573/// device node to stand for its device ([`hardware_device_node`]), is
574/// listed only after it has encoded one 256x144 frame with the options an
575/// export sends, once per process for each way it runs (a machine with no
576/// hardware transform still runs the software one); a failure is logged once.
577/// Any other software encoder is taken at its registration, and any failure
578/// when an export opens a listed encoder falls through to the next candidate.
579fn 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
603/// How a log line names the way a candidate runs.
604pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
605    if on_hardware { "on GPU" } else { "on CPU" }
606}
607
608/// Whether the device a hardware encoder needs has a node here, for the
609/// encoder families Linux knows a node for; `None` for the rest.
610///
611/// A node is the evidence Linux takes for those families instead of an open
612/// probe ([`encoder_runs_here`]): the probe would be exact, but each NVENC one
613/// sets up a CUDA context, and probing the three NVENC encoders added 1.27 s
614/// to every host process's startup on an RTX 4090 (1.56 s against 0.29 s),
615/// for a runtime the pinned Linux SDK is not: it has no NVENC. A node whose
616/// device still does not open costs the export one failed open, and the
617/// encoder it falls through to is given the split for what it is.
618#[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/// Off Linux there is no device node to look at: whether the device is there
640/// is the open probe's to answer ([`encoder_runs_here`]).
641#[cfg(not(target_os = "linux"))]
642const fn hardware_device_node(_name: &str) -> Option<bool> {
643    None
644}
645
646/// Names the loaded runtime and everything it does register for `codec`.
647///
648/// "h264 encoder is unavailable" reads like a bug in alpha2. The runtime is the
649/// thing that decides, so the message says which runtime answered and what it
650/// offered instead.
651fn 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
672/// Encoders to try for `codec`, most preferred first.
673///
674/// A named request is that encoder alone: a caller that pinned an encoder
675/// wants that encoder or an error, not a quiet substitution. A hybrid
676/// encoder is still tried each way it runs here.
677pub(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
699/// The one encoder an export of `codec` opens without a fallback: the pinned
700/// `requested_name`, which must implement `codec` and be advertised for the
701/// YUV420P sink, else the codec's preferred advertised encoder.
702pub(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
739/// What one sink needs to open its video encoder.
740pub(crate) struct VideoEncoderOpenRequest<'a> {
741    /// Output format the encoder is configured for.
742    pub config: &'a VideoEncodeConfig,
743    /// Concrete encoder the caller pinned, if any.
744    pub requested_name: Option<&'a str>,
745    /// Software worker budget, asked for only when a candidate that runs on
746    /// the CPU is opened; a hardware candidate takes none.
747    pub threads: SoftwareEncoderThreads<'a>,
748    /// Whether the container wants the extradata in the stream header.
749    pub global_header: bool,
750    /// Coded size, already range-checked by the caller.
751    pub width: i32,
752    /// Coded size, already range-checked by the caller.
753    pub height: i32,
754}
755
756/// The worker count a software video encoder opens with, decided only when
757/// one is opened.
758///
759/// An export divides its CPU between decode and the encoder, and which way it
760/// divides depends on whether the encoder runs on the CPU at all. That is
761/// known only once a candidate opens: the preferred one can be a hardware
762/// encoder that fails `avcodec_open2` and falls through to a software one.
763/// A count decided before the open would give that fallback the hardware
764/// split's share of the encoder, which is none, and it would run on one
765/// worker. The caller passes how to decide instead; it is asked at most once.
766pub(crate) struct SoftwareEncoderThreads<'a> {
767    decide: &'a dyn Fn() -> i64,
768    decided: std::cell::OnceCell<i64>,
769}
770
771impl<'a> SoftwareEncoderThreads<'a> {
772    /// The count `decide` returns, asked for when a software candidate opens.
773    pub(crate) fn new(decide: &'a dyn Fn() -> i64) -> Self {
774        Self {
775            decide,
776            decided: std::cell::OnceCell::new(),
777        }
778    }
779
780    /// The decided count, at least one worker.
781    pub(crate) fn get(&self) -> i64 {
782        *self.decided.get_or_init(|| (self.decide)().max(1))
783    }
784}
785
786/// Opens the first candidate encoder that this machine can actually run.
787///
788/// `avcodec_open2` is where a registered encoder becomes a running one, and
789/// the two are not the same claim: a Windows SKU or VM without the
790/// `MediaFoundation` H.264 MFT registers `h264_mf` and cannot open it, and a
791/// headless container registers `h264_videotoolbox` and cannot open it. Each
792/// rejection is recorded with a typed reason and carried on the sink so the
793/// run can report which encoder it ended up on.
794///
795/// # Errors
796/// Returns the last candidate's failure, prefixed with every earlier one, when
797/// no candidate opens.
798pub(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
805/// Opens the first of `candidates` that runs, recording why each one before
806/// it did not; when none does, the error names every candidate's reason.
807pub(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
874/// Opens `candidate`, on the rate control's quantizer when it has one and the
875/// encoder accepts it, otherwise on its bitrate.
876///
877/// A refused quantizer is not a refused encoder: the bitrate is the rate
878/// control's own fallback, so the export continues on it and the selection
879/// says so rather than failing over to another candidate.
880fn 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    // Square pixels are what an undeclared ratio means; only another is written.
922    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    // A hardware candidate never asks, so an export whose GPU encoder opens never decides a
945    // software split it would not use.
946    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
980/// Frames between forced keyframes: two seconds of output.
981///
982/// libavcodec's own `gop_size` default is 12, which at 24 fps is a keyframe
983/// every half second. Nothing in alpha2 seeks or thumbnails its own output, so the
984/// interval is only a rate question. Measured at a fixed rate on the 4K
985/// showcase, 12 s of the colour clip, PSNR-Y: going from 12 to two seconds
986/// (48) gained 0.53-0.64 dB on `h264_vulkan` and 0.67-0.77 dB on
987/// `h264_nvenc`; going on to ten seconds (240) gained a further 0.11-0.12 dB
988/// on `h264_vulkan`. Two seconds is where nearly all of that is and a player
989/// can still start or seek within two seconds of any point.
990pub(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
997/// The largest time base denominator encoders of `codec` can write, `None`
998/// for a codec without one. MPEG-4 Part 2 and H.263 store it as the 16-bit
999/// `vop_time_increment_resolution`, so `FFmpeg`'s encoders refuse a time base
1000/// of 1001/120000 (119.88 fps).
1001pub(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
1008/// The time base an encoder of frames at `rate` runs in: the frame length
1009/// itself (1001/30000 for 29.97 fps), when its denominator is within `limit`.
1010/// Otherwise 1/D for the D within `limit` that rounds frame times the least
1011/// (the largest error over every frame), so a rate a time base of this
1012/// encoder cannot hold keeps its frame count and frame times, each rounded
1013/// by under one unit. 119.88 fps (120000/1001) is 1/64000: frame times are
1014/// off by at most 7/960000 s.
1015///
1016/// # Errors
1017/// For a rate whose terms do not fit an `FFmpeg` rational.
1018pub(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    // Frame k is at k*den/num s, k*den*D/num units of 1/D. Its fractional part is a multiple of
1034    // 1/q, q = num / gcd(num, den*D), so the largest rounding is floor(q/2)/q units,
1035    // floor(q/2)/(q*D) s.
1036    // A unit no longer than a frame keeps every frame its own time.
1037    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
1061/// `max_b_frames` for encoder `name`.
1062pub(crate) fn encoder_max_b_frames(name: &str) -> i32 {
1063    // VideoToolbox's default is no frame reordering. Forcing B-frames makes support
1064    // device-dependent and can yield invalid packet timestamps on otherwise valid hardware
1065    // sessions, so preserve that native default.
1066    if name.ends_with("_videotoolbox") {
1067        0
1068    } else {
1069        2
1070    }
1071}
1072
1073/// The smallest even width and height encoder `name` accepts.
1074pub(crate) fn encoder_minimum_dimensions(name: &str) -> (u32, u32) {
1075    // NVIDIA AV1 requires at least 145x128. Its H.264/HEVC encoders reject dimensions below 145x49.
1076    // alpha2 exposes even 4:2:0 dimensions.
1077    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/// One hardware configuration a video encoder declares.
1102#[derive(Clone, Debug, Eq, PartialEq)]
1103pub struct NativeEncoderHardwareConfig {
1104    /// Hardware pixel format this configuration carries frames in.
1105    pub pixel_format: String,
1106    /// `FFmpeg` hardware device type backing it.
1107    pub device_type: String,
1108    /// `AV_CODEC_HW_CONFIG_METHOD_*` bits.
1109    pub methods: i32,
1110    /// Whether the encoder takes its frames through an `AVHWFramesContext`.
1111    pub needs_hardware_frames: bool,
1112}
1113
1114/// What one registered video encoder declares, and what happened when alpha2
1115/// opened it on this machine.
1116///
1117/// Registration, advertisement and instantiation are three different claims,
1118/// and only the third is the one an export depends on. This probe makes all
1119/// three observable in one place, per platform, without an export.
1120#[derive(Clone, Debug, Eq, PartialEq)]
1121// Independent observations of one encoder, reported side by side, not a state.
1122#[allow(clippy::struct_excessive_bools)]
1123pub struct NativeVideoEncoderProbe {
1124    /// Logical codec this encoder implements.
1125    pub codec: NativeVideoCodec,
1126    /// `FFmpeg` short name.
1127    pub encoder_name: String,
1128    /// Class taken from the encoder's own capability bits.
1129    pub class: NativeEncoderClass,
1130    /// Whether this row runs the encoder on the GPU.
1131    pub hardware_accelerated: bool,
1132    /// Raw `AV_CODEC_CAP_*` bits, so an unexpected class can be traced.
1133    pub capability_bits: u32,
1134    /// Pixel formats the encoder declares, or empty when it declares none.
1135    pub pixel_formats: Vec<String>,
1136    /// Hardware configurations the encoder declares.
1137    pub hardware_configs: Vec<NativeEncoderHardwareConfig>,
1138    /// Whether `AV_PIX_FMT_YUV420P` is among the declared formats.
1139    pub accepts_yuv420p: bool,
1140    /// Whether every declared format is a hardware one, so a software frame
1141    /// can never be sent to it. This is what keeps `h264_vulkan` unadvertised.
1142    pub requires_hardware_frames: bool,
1143    /// Whether [`codec_capabilities`] advertises it for the YUV420P sink.
1144    pub advertised: bool,
1145    /// Smallest frame the encoder accepts, as alpha2 records it.
1146    pub minimum_size: (u32, u32),
1147    /// `None` when the encoder opened and produced at least one packet from a
1148    /// software YUV420P frame; otherwise why it did not.
1149    pub software_open_error: Option<String>,
1150    /// Packets drained from that attempt.
1151    pub software_packets: u32,
1152    /// Which rate-control mechanisms opened, and with what options.
1153    pub rate_control: Vec<NativeRateControlProbe>,
1154}
1155
1156/// Whether one encoder opened with one rate-control mechanism.
1157#[derive(Clone, Debug, Eq, PartialEq)]
1158pub struct NativeRateControlProbe {
1159    /// The mechanism asked for.
1160    pub mechanism: crate::RateControlMechanism,
1161    /// The options that carried it, or `None` where alpha2 knows no spelling for
1162    /// it on this encoder's family.
1163    pub options: Option<String>,
1164    /// Packets drained when it opened, consumed every option and encoded a
1165    /// frame; otherwise why not.
1166    pub outcome: Result<u32, String>,
1167    /// The device a hardware-frames encoder was probed on: a Vulkan device
1168    /// index for `*_vulkan`, and for `*_d3d12va` the DX12 adapter that
1169    /// answered (`DX12 adapter <index> (<name>)`).
1170    pub device: Option<String>,
1171}
1172
1173/// A quantizer in the middle of `codec`'s scale, for a probe open.
1174pub(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/// Opens every registered video encoder and reports what it actually does.
1192///
1193/// The name of an encoder says nothing about whether this machine can run it:
1194/// a Windows SKU without the `MediaFoundation` H.264 MFT registers `h264_mf`
1195/// and cannot open it, and `h264_vulkan` is registered on every Linux build
1196/// and cannot take a software frame at all. Each candidate is opened at
1197/// `width`x`height` and fed one YUV420P frame, so the report distinguishes
1198/// "registered", "advertised" and "ran". A hybrid encoder is a row for each
1199/// way it runs here.
1200#[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                    // D3D12VA takes 0-52 for every codec, AV1 included.
1260                    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/// Opens an encoder that only takes hardware frames, with `request`.
1331///
1332/// `h264_vulkan` is the shipped Linux encoder and cannot take a software
1333/// frame at all, so its rate-control surface is only observable through a
1334/// device. `FFmpeg` creates its own Vulkan device here, with whatever
1335/// video-encode queue the physical device offers, the same capability the
1336/// export's shared device is checked for.
1337///
1338/// The D3D12VA encoders are Windows' GPU-path encoders and take D3D12
1339/// frames only. Their rows open on a DX12 adapter's device, the first one
1340/// whose D3D12 Video Encode reports the encoder, and a mode that device does
1341/// not list is refused with its reason rather than opened.
1342///
1343/// Returns the packets and the device that answered: a Vulkan device's
1344/// index, or a DX12 adapter's index and name.
1345#[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
1366/// Opens `candidate` and feeds it one software YUV420P frame.
1367fn 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}