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::fmt::Write as _;
6use std::sync::{LazyLock, Mutex};
7#[cfg(target_os = "linux")]
8use std::{fs, path::Path};
9
10use crate::convert::{encode_adapter_error, reject_unused_encode_options, video_encoder_options};
11use crate::video_encode::VideoEncodeConfig;
12use alpha2_media::{ColorMetadata, EncodeError};
13use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef};
14use rsmpeg::avutil::AVRational;
15use rsmpeg::error::RsmpegError;
16use rsmpeg::ffi;
17
18/// Configuration for transactional audio/video export.
19#[derive(Copy, Clone, Debug, PartialEq)]
20pub struct AvEncodeConfig {
21    /// Video encoder settings.
22    pub video: VideoEncodeConfig,
23    /// Project audio sample rate.
24    pub audio_sample_rate: u32,
25    /// Audio channel count.
26    pub audio_channels: u16,
27    /// Audio codec selected by the downstream host.
28    pub audio_codec: NativeAudioCodec,
29    /// Resolved audio bitrate in bits per second; the host resolves the level.
30    pub audio_bit_rate: u32,
31}
32
33/// Video codecs implemented by the native encode sinks.
34#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
35#[non_exhaustive]
36pub enum NativeVideoCodec {
37    /// H.264/AVC, preferred for broad browser, Windows, and Apple playback.
38    H264,
39    /// H.265/HEVC.
40    Hevc,
41    /// VP9, when the linked `FFmpeg` build provides a VP9 encoder.
42    Vp9,
43    /// AV1, when the linked `FFmpeg` build provides an AV1 encoder.
44    Av1,
45    /// MPEG-4 Part 2, retained for compatibility with existing alpha2 hosts.
46    Mpeg4Part2,
47}
48
49impl NativeVideoCodec {
50    /// Stable identifier suitable for an application protocol or settings file.
51    #[must_use]
52    pub const fn as_str(self) -> &'static str {
53        match self {
54            Self::H264 => "h264",
55            Self::Hevc => "hevc",
56            Self::Vp9 => "vp9",
57            Self::Av1 => "av1",
58            Self::Mpeg4Part2 => "mpeg4-part2",
59        }
60    }
61
62    /// The engine's codec this encoder family implements.
63    ///
64    /// The rate mapping is keyed on [`alpha2_media::VideoCodec`] alone, so
65    /// there is one front door to it; this is the native side's way in.
66    #[must_use]
67    pub const fn model(self) -> alpha2_media::VideoCodec {
68        match self {
69            Self::H264 => alpha2_media::VideoCodec::H264,
70            Self::Hevc => alpha2_media::VideoCodec::Hevc,
71            Self::Vp9 => alpha2_media::VideoCodec::Vp9,
72            Self::Av1 => alpha2_media::VideoCodec::Av1,
73            Self::Mpeg4Part2 => alpha2_media::VideoCodec::Mpeg4Part2,
74        }
75    }
76
77    /// The native codec for the engine's `codec`.
78    #[must_use]
79    pub const fn from_model(codec: alpha2_media::VideoCodec) -> Self {
80        match codec {
81            alpha2_media::VideoCodec::H264 => Self::H264,
82            alpha2_media::VideoCodec::Hevc => Self::Hevc,
83            alpha2_media::VideoCodec::Vp9 => Self::Vp9,
84            alpha2_media::VideoCodec::Av1 => Self::Av1,
85            alpha2_media::VideoCodec::Mpeg4Part2 => Self::Mpeg4Part2,
86        }
87    }
88
89    pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
90        match self {
91            Self::H264 => ffi::AV_CODEC_ID_H264,
92            Self::Hevc => ffi::AV_CODEC_ID_HEVC,
93            Self::Vp9 => ffi::AV_CODEC_ID_VP9,
94            Self::Av1 => ffi::AV_CODEC_ID_AV1,
95            Self::Mpeg4Part2 => ffi::AV_CODEC_ID_MPEG4,
96        }
97    }
98}
99
100/// Audio codecs implemented by the native encode sinks.
101#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
102#[non_exhaustive]
103pub enum NativeAudioCodec {
104    /// AAC-LC compatible audio, through `FFmpeg`'s `aac`.
105    Aac,
106    /// Opus, through `libopus`.
107    Opus,
108}
109
110impl NativeAudioCodec {
111    /// Every codec, in the order the engine lists them.
112    pub const ALL: [Self; 2] = [Self::Aac, Self::Opus];
113
114    /// Stable identifier suitable for an application protocol or settings file.
115    #[must_use]
116    pub const fn as_str(self) -> &'static str {
117        match self {
118            Self::Aac => "aac",
119            Self::Opus => "opus",
120        }
121    }
122
123    /// The engine's codec this encoder family implements.
124    #[must_use]
125    pub const fn model(self) -> alpha2_media::AudioCodec {
126        match self {
127            Self::Aac => alpha2_media::AudioCodec::Aac,
128            Self::Opus => alpha2_media::AudioCodec::Opus,
129        }
130    }
131
132    /// The native codec for the engine's `codec`.
133    #[must_use]
134    pub const fn from_model(codec: alpha2_media::AudioCodec) -> Self {
135        match codec {
136            alpha2_media::AudioCodec::Aac => Self::Aac,
137            alpha2_media::AudioCodec::Opus => Self::Opus,
138        }
139    }
140
141    /// The one encoder alpha2 opens for the codec: `FFmpeg`'s `aac`, and the
142    /// reference `libopus` for Opus. `FFmpeg`'s own `opus` encoder is
143    /// experimental and is never a candidate.
144    pub(crate) const fn encoder_name(self) -> &'static std::ffi::CStr {
145        match self {
146            Self::Aac => c"aac",
147            Self::Opus => c"libopus",
148        }
149    }
150}
151
152/// The encoder this runtime opens for `codec`, or `None` when it links none,
153/// or only one marked experimental.
154pub(crate) fn audio_encoder(codec: NativeAudioCodec) -> Option<AVCodecRef<'static>> {
155    AVCodec::find_encoder_by_name(codec.encoder_name()).filter(|encoder| {
156        u32::try_from(encoder.capabilities).unwrap_or(0) & ffi::AV_CODEC_CAP_EXPERIMENTAL == 0
157    })
158}
159
160/// How a concrete encoder executes, as the linked runtime declares it.
161///
162/// Taken from capability bits rather than the name suffix:
163/// `AV_CODEC_CAP_HARDWARE` and `AV_CODEC_CAP_HYBRID` are set by the encoder
164/// itself, so a build that adds an encoder alpha2 has never heard of is still
165/// classified correctly, and a hybrid encoder is not described as though it
166/// were one or the other.
167/// The one exception is a short list of encoders the pinned runtime
168/// misdeclares (`declared_encoder_class`).
169#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
170#[non_exhaustive]
171pub enum NativeEncoderClass {
172    /// Runs on the CPU and draws on the software codec thread budget.
173    Software,
174    /// Runs on a hardware engine and ignores the thread budget.
175    Hardware,
176    /// Can do either. `MediaFoundation`'s `_mf` encoders are the shipped case:
177    /// `hw_encoding=0` enumerates a synchronous software transform and
178    /// `hw_encoding=1` an asynchronous hardware one.
179    Hybrid,
180}
181
182impl NativeEncoderClass {
183    /// Stable identifier for diagnostics and the export result.
184    #[must_use]
185    pub const fn as_str(self) -> &'static str {
186        match self {
187            Self::Software => "software",
188            Self::Hardware => "hardware",
189            Self::Hybrid => "hybrid",
190        }
191    }
192
193    /// The ways alpha2 runs an encoder of this class, most preferred first:
194    /// `true` on the GPU, `false` on the CPU. A hybrid encoder runs on the
195    /// CPU unless `hybrid_hardware` asks for the GPU, and then still has the
196    /// CPU behind it, because a machine without a hardware transform cannot
197    /// run the GPU mode at all.
198    #[must_use]
199    pub const fn modes(self, hybrid_hardware: bool) -> &'static [bool] {
200        match self {
201            Self::Hardware => &[true],
202            Self::Hybrid if hybrid_hardware => &[true, false],
203            Self::Software | Self::Hybrid => &[false],
204        }
205    }
206}
207
208/// One way to run a registered encoder: the encoder, and whether alpha2
209/// configures it to encode on the GPU. Only a hybrid encoder has two.
210pub(crate) struct VideoEncoderCandidate {
211    pub(crate) encoder: AVCodecRef<'static>,
212    pub(crate) on_hardware: bool,
213}
214
215impl VideoEncoderCandidate {
216    pub(crate) fn name(&self) -> String {
217        self.encoder.name().to_string_lossy().into_owned()
218    }
219}
220
221/// Whether hybrid encoders are configured to run on the GPU.
222///
223/// `h264_mf` and its siblings default to `hw_encoding=0`, which enumerates
224/// `MFT_ENUM_FLAG_SYNCMFT` (the synchronous *software* transform category),
225/// so the shipped default is a CPU encoder that correctly draws on the codec
226/// thread budget. `ALPHA2_VIDEO_ENCODER_HYBRID=hardware` tries `hw_encoding=1`
227/// first, which asks `MediaFoundation` for a hardware MFT, and keeps the
228/// software transform of the same encoder behind it for the SKUs and VMs
229/// that have none: `FFmpeg` does not fall back from one to the other itself.
230#[must_use]
231pub fn hybrid_encoders_use_hardware() -> bool {
232    std::env::var("ALPHA2_VIDEO_ENCODER_HYBRID")
233        .is_ok_and(|value| value.eq_ignore_ascii_case("hardware"))
234}
235
236/// Why one candidate encoder was not the one an export opened.
237#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
238#[non_exhaustive]
239pub enum NativeEncoderRejection {
240    /// The encoder refuses an output this small.
241    Dimensions,
242    /// `avcodec_open2` failed: no device, no licence, unsupported profile.
243    OpenFailed,
244    /// The encoder silently ignored an option alpha2 requires it to honour.
245    OptionsIgnored,
246}
247
248impl NativeEncoderRejection {
249    /// Stable identifier for diagnostics and the export result.
250    #[must_use]
251    pub const fn as_str(self) -> &'static str {
252        match self {
253            Self::Dimensions => "dimensions",
254            Self::OpenFailed => "open-failed",
255            Self::OptionsIgnored => "options-ignored",
256        }
257    }
258}
259
260/// One candidate encoder an export tried and moved past.
261#[derive(Clone, Debug, Eq, PartialEq)]
262pub struct NativeVideoEncoderFallback {
263    /// `FFmpeg` short name of the candidate.
264    pub encoder_name: String,
265    /// Whether the candidate was configured to encode on the GPU. A hybrid
266    /// encoder can be tried on the GPU and then on the CPU.
267    pub hardware_accelerated: bool,
268    /// Typed reason it was rejected.
269    pub reason: NativeEncoderRejection,
270    /// The runtime's own message, kept verbatim.
271    pub detail: String,
272}
273
274/// The encoder an export actually opened, and what it passed over.
275///
276/// Advertisement is not instantiation: a platform can register an encoder it
277/// cannot open on this machine. Every export reports what it opened rather
278/// than letting the capability list imply it.
279#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct NativeVideoEncoderSelection {
281    /// `FFmpeg` short name of the encoder that opened.
282    pub encoder_name: String,
283    /// How that encoder executes.
284    pub class: NativeEncoderClass,
285    /// Whether it is configured to encode on the GPU.
286    pub hardware_accelerated: bool,
287    /// Candidates tried first, in preference order.
288    pub rejected: Vec<NativeVideoEncoderFallback>,
289    /// Whether the encoder runs the rate control's quantizer or its bitrate.
290    pub rate_control: crate::AppliedRateControl,
291    /// Why a quantizer that was asked for is not the one running.
292    pub quantizer_refused: Option<String>,
293    /// `FFmpeg` workers the encoder was opened with when it runs on the CPU;
294    /// `None` on the GPU, which takes no share of the software budget.
295    pub software_threads: Option<i64>,
296}
297
298/// One linked video encoder available to the native sinks.
299#[derive(Clone, Debug, Eq, PartialEq)]
300pub struct NativeVideoEncoderCapability {
301    /// Stable logical codec requested through [`VideoEncodeConfig`].
302    pub codec: NativeVideoCodec,
303    /// Concrete `FFmpeg` encoder implementation for this codec.
304    pub encoder_name: String,
305    /// How the linked runtime declares this encoder executes.
306    pub class: NativeEncoderClass,
307    /// Whether this entry runs the encoder on the GPU. A hybrid encoder is
308    /// listed once per way it runs here ([`hybrid_encoders_use_hardware`]).
309    pub hardware_accelerated: bool,
310    /// Whether this is the default encoder for its logical codec.
311    pub preferred: bool,
312    /// Smallest even width accepted by this concrete encoder.
313    pub minimum_width: u32,
314    /// Smallest even height accepted by this concrete encoder.
315    pub minimum_height: u32,
316    /// The library behind an encoder that is not `FFmpeg`'s, which the user
317    /// installed: `None` for `FFmpeg`'s own.
318    pub library: Option<NativeEncoderLibrary>,
319}
320
321/// A run-time loaded encoder library ([`crate::OPENH264_ENCODER_NAME`]).
322#[derive(Clone, Debug, Eq, PartialEq)]
323pub struct NativeEncoderLibrary {
324    /// Who provided it ([`crate::OpenH264Provider::as_str`]).
325    pub provider: &'static str,
326    /// The version it reports.
327    pub version: String,
328    /// Where it was loaded from.
329    pub path: String,
330    /// The notice its licence requires be shown.
331    pub notice: &'static str,
332}
333
334impl NativeEncoderLibrary {
335    fn openh264(library: &crate::OpenH264Library) -> Self {
336        Self {
337            provider: library.provider().as_str(),
338            version: library.version().to_owned(),
339            path: library.path().to_owned(),
340            notice: crate::OPENH264_NOTICE,
341        }
342    }
343}
344
345/// One linked audio encoder available to the native sinks.
346#[derive(Clone, Debug, Eq, PartialEq)]
347pub struct NativeAudioEncoderCapability {
348    /// Stable logical codec requested through [`AvEncodeConfig`].
349    pub codec: NativeAudioCodec,
350    /// Concrete `FFmpeg` encoder selected for that codec on this host.
351    pub encoder_name: String,
352}
353
354/// Runtime codec inventory for the native sinks.
355///
356/// Which containers offer which codec is the engine's policy
357/// ([`alpha2_media::Container`]), the same on every runtime; this says which
358/// of those codecs this runtime can encode.
359#[derive(Clone, Debug, Eq, PartialEq)]
360pub struct NativeCodecCapabilities {
361    /// Video encoders accepted by [`NativeVideoEncodeSink`](crate::NativeVideoEncodeSink) and
362    /// [`NativeAvEncodeSink`](crate::NativeAvEncodeSink).
363    pub video: Vec<NativeVideoEncoderCapability>,
364    /// Audio encoders accepted by [`NativeAvEncodeSink`](crate::NativeAvEncodeSink) and the
365    /// GPU-resident and packet-copy exports.
366    pub audio: Vec<NativeAudioEncoderCapability>,
367}
368
369/// Reports the encoders usable by alpha2's native sinks: the linked
370/// `FFmpeg`'s, and `OpenH264` when the user installed it.
371///
372/// A logical codec may have multiple entries, and a hybrid encoder one per
373/// way it runs. Hardware entries appear first when they accept the native
374/// YUV420P path and their required device is present, then software ones in
375/// `FFmpeg`'s order, then `OpenH264`; exactly one entry per codec is marked
376/// preferred.
377#[must_use]
378pub fn codec_capabilities() -> NativeCodecCapabilities {
379    let video = [
380        NativeVideoCodec::H264,
381        NativeVideoCodec::Hevc,
382        NativeVideoCodec::Vp9,
383        NativeVideoCodec::Av1,
384        NativeVideoCodec::Mpeg4Part2,
385    ]
386    .into_iter()
387    .flat_map(video_encoder_capabilities)
388    .collect();
389    NativeCodecCapabilities {
390        video,
391        audio: audio_encoder_capabilities(),
392    }
393}
394
395/// The audio encoders [`codec_capabilities`] lists, without looking at any
396/// video encoder.
397#[must_use]
398pub fn audio_encoder_capabilities() -> Vec<NativeAudioEncoderCapability> {
399    NativeAudioCodec::ALL
400        .into_iter()
401        .filter_map(|codec| {
402            let encoder = audio_encoder(codec)?;
403            Some(NativeAudioEncoderCapability {
404                codec,
405                encoder_name: encoder.name().to_string_lossy().into_owned(),
406            })
407        })
408        .collect()
409}
410
411/// The encoders usable for `codec` alone, as [`codec_capabilities`] lists
412/// them.
413///
414/// An export needs only its own codec's, and an encoder whose registration
415/// does not prove it runs here is opened to find out
416/// ([`encoder_runs_here`]): asking for one codec opens only that codec's.
417#[must_use]
418pub fn video_encoder_capabilities(codec: NativeVideoCodec) -> Vec<NativeVideoEncoderCapability> {
419    available_video_encoders(codec)
420        .into_iter()
421        .enumerate()
422        .map(|(index, choice)| {
423            let encoder_name = choice.name();
424            let (minimum_width, minimum_height) = encoder_minimum_dimensions(&encoder_name);
425            NativeVideoEncoderCapability {
426                codec,
427                class: choice.class(),
428                hardware_accelerated: choice.on_hardware(),
429                preferred: index == 0,
430                encoder_name,
431                minimum_width,
432                minimum_height,
433                library: match &choice {
434                    VideoEncoderChoice::Ffmpeg(_) => None,
435                    VideoEncoderChoice::OpenH264(library) => {
436                        Some(NativeEncoderLibrary::openh264(library))
437                    }
438                },
439            }
440        })
441        .collect()
442}
443
444/// One encoder an export can try: a registered `FFmpeg` one, or `OpenH264`.
445pub(crate) enum VideoEncoderChoice {
446    Ffmpeg(VideoEncoderCandidate),
447    OpenH264(std::sync::Arc<crate::OpenH264Library>),
448}
449
450impl VideoEncoderChoice {
451    pub(crate) fn name(&self) -> String {
452        match self {
453            Self::Ffmpeg(candidate) => candidate.name(),
454            Self::OpenH264(_) => crate::OPENH264_ENCODER_NAME.to_owned(),
455        }
456    }
457
458    pub(crate) const fn on_hardware(&self) -> bool {
459        match self {
460            Self::Ffmpeg(candidate) => candidate.on_hardware,
461            Self::OpenH264(_) => false,
462        }
463    }
464
465    pub(crate) fn class(&self) -> NativeEncoderClass {
466        match self {
467            Self::Ffmpeg(candidate) => encoder_class(&candidate.encoder),
468            Self::OpenH264(_) => NativeEncoderClass::Software,
469        }
470    }
471}
472
473/// Every encoder for `codec` this machine runs, most preferred first:
474/// `FFmpeg`'s, then `OpenH264` for H.264 when it loaded. `OpenH264` comes last
475/// because `libx264`, where a runtime has it, compresses better.
476pub(crate) fn available_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderChoice> {
477    let mut choices = registered_video_encoders(codec)
478        .into_iter()
479        .filter(encoder_is_compatible)
480        .map(VideoEncoderChoice::Ffmpeg)
481        .collect::<Vec<_>>();
482    if codec == NativeVideoCodec::H264
483        && let Some(library) = crate::openh264_status().library()
484    {
485        choices.push(VideoEncoderChoice::OpenH264(std::sync::Arc::clone(library)));
486    }
487    choices
488}
489
490/// One codec's hardware decode registration for this platform's backend.
491#[derive(Clone, Debug, Eq, PartialEq)]
492pub struct NativeHardwareDecodeCapability {
493    /// Logical codec.
494    pub codec: NativeVideoCodec,
495    /// Concrete `FFmpeg` decoder implementation, when one is linked.
496    pub decoder_name: Option<String>,
497    /// Whether that decoder advertises this platform's hardware configuration.
498    ///
499    /// This is registration only. Whether a device, profile, and surface
500    /// format actually work is decided when a stream is opened, and every
501    /// export reports which path its frames took.
502    pub hardware_config: bool,
503    /// The decoder a software session opens for the codec, or `None` when
504    /// only a hardware session can decode it (AV1 on a runtime without
505    /// libdav1d).
506    pub software_decoder: Option<String>,
507}
508
509/// Hardware decode registration reported by the linked `FFmpeg` build.
510#[derive(Clone, Debug, Eq, PartialEq)]
511pub struct NativeHardwareDecodeCapabilities {
512    /// Stable backend identifier, or `none` on a build without a GPU path.
513    pub backend: &'static str,
514    /// One entry per codec alpha2 can present from a decoder surface.
515    pub codecs: Vec<NativeHardwareDecodeCapability>,
516}
517
518/// Backend this build decodes with, independent of any device being present.
519#[must_use]
520pub const fn hardware_decode_backend() -> &'static str {
521    #[cfg(feature = "hwdecode")]
522    {
523        #[cfg(target_os = "linux")]
524        {
525            "vulkan"
526        }
527        #[cfg(target_os = "macos")]
528        {
529            "videotoolbox"
530        }
531        #[cfg(target_os = "windows")]
532        {
533            "d3d12va"
534        }
535        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
536        {
537            "none"
538        }
539    }
540    #[cfg(not(feature = "hwdecode"))]
541    {
542        "none"
543    }
544}
545
546/// Reports which codecs the linked `FFmpeg` registers for this backend.
547#[must_use]
548pub fn hardware_decode_capabilities() -> NativeHardwareDecodeCapabilities {
549    let codecs = [
550        NativeVideoCodec::H264,
551        NativeVideoCodec::Hevc,
552        NativeVideoCodec::Av1,
553        NativeVideoCodec::Vp9,
554    ]
555    .into_iter()
556    .map(|codec| {
557        let decoder = AVCodec::find_decoder(codec.codec_id());
558        NativeHardwareDecodeCapability {
559            codec,
560            decoder_name: decoder
561                .as_ref()
562                .map(|decoder| decoder.name().to_string_lossy().into_owned()),
563            hardware_config: crate::decode::registered_video_decoders(codec.codec_id())
564                .any(|decoder| decoder_has_hardware_config(&decoder)),
565            software_decoder: crate::decode::software_video_decoder(codec.codec_id())
566                .ok()
567                .map(|decoder| decoder.name().to_string_lossy().into_owned()),
568        }
569    })
570    .collect();
571    NativeHardwareDecodeCapabilities {
572        backend: hardware_decode_backend(),
573        codecs,
574    }
575}
576
577/// Whether `decoder` advertises this platform's hardware device configuration.
578#[allow(unused_variables)]
579fn decoder_has_hardware_config(decoder: &AVCodecRef<'_>) -> bool {
580    #[cfg(all(
581        any(target_os = "linux", target_os = "macos", target_os = "windows"),
582        feature = "hwdecode"
583    ))]
584    {
585        #[cfg(target_os = "linux")]
586        let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_VULKAN, ffi::AV_PIX_FMT_VULKAN);
587        #[cfg(target_os = "macos")]
588        let (device_type, pixel_format) = (
589            ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
590            ffi::AV_PIX_FMT_VIDEOTOOLBOX,
591        );
592        #[cfg(target_os = "windows")]
593        let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_D3D12VA, ffi::AV_PIX_FMT_D3D12);
594        (0..)
595            .map_while(|index| decoder.hw_config(index))
596            .any(|config| config.device_type == device_type && config.pix_fmt == pixel_format)
597    }
598    #[cfg(not(all(
599        any(target_os = "linux", target_os = "macos", target_os = "windows"),
600        feature = "hwdecode"
601    )))]
602    {
603        false
604    }
605}
606
607/// Test builds only: the encoders `ALPHA2_TEST_SKIP_ENCODERS` (names, comma separated) hides
608/// from this run, as though the runtime registered none of them. The valgrind run sets it for
609/// `libsvtav1`, whose own code reads memory it never wrote; no production build reads it.
610#[cfg(test)]
611pub(crate) fn test_skips_encoder(name: &std::ffi::CStr) -> bool {
612    static SKIPPED: LazyLock<Vec<String>> = LazyLock::new(|| {
613        std::env::var("ALPHA2_TEST_SKIP_ENCODERS")
614            .unwrap_or_default()
615            .split(',')
616            .map(str::trim)
617            .filter(|name| !name.is_empty())
618            .map(str::to_owned)
619            .collect()
620    });
621    let name = name.to_string_lossy();
622    SKIPPED.iter().any(|skipped| *skipped == name)
623}
624
625#[cfg(not(test))]
626const fn test_skips_encoder(_name: &std::ffi::CStr) -> bool {
627    false
628}
629
630/// Every way to run each encoder registered for `codec`, GPU ones first.
631pub(crate) fn registered_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderCandidate> {
632    let hybrid_hardware = hybrid_encoders_use_hardware();
633    let mut candidates = AVCodec::iterate()
634        .filter(|encoder| {
635            encoder.id == codec.codec_id()
636                && !test_skips_encoder(encoder.name())
637                && AVCodec::find_encoder_by_name(encoder.name())
638                    .is_some_and(|found| found.as_ptr() == encoder.as_ptr())
639        })
640        .flat_map(|encoder| {
641            encoder_class(&encoder)
642                .modes(hybrid_hardware)
643                .iter()
644                .filter_map(move |&on_hardware| {
645                    AVCodec::find_encoder_by_name(encoder.name()).map(|encoder| {
646                        VideoEncoderCandidate {
647                            encoder,
648                            on_hardware,
649                        }
650                    })
651                })
652        })
653        .collect::<Vec<_>>();
654    // Stable sort: FFmpeg's registration order within each tier, hardware first.
655    candidates.sort_by_key(|candidate| !candidate.on_hardware);
656    candidates
657}
658
659/// Classifies `encoder` from the capability bits it declares
660/// ([`declared_encoder_class`]).
661pub(crate) fn encoder_class(encoder: &AVCodec) -> NativeEncoderClass {
662    let name = encoder.name().to_string_lossy();
663    declared_encoder_class(&name, u32::try_from(encoder.capabilities).unwrap_or(0))
664}
665
666/// Encoders the pinned runtime registers as software although they run only
667/// on a hardware engine.
668///
669/// `FFmpeg` 8.0 and 8.1 declare `h264_videotoolbox` with neither
670/// `AV_CODEC_CAP_HARDWARE` nor `AV_CODEC_CAP_HYBRID`, unlike
671/// `hevc_videotoolbox`, yet opens it with `allow_sw=0`
672/// (`RequireHardwareAcceleratedVideoEncoder`), so it runs on the Mac's
673/// hardware encoder or not at all. An entry applies only while the runtime
674/// declares neither bit: a runtime that declares one is taken at its word,
675/// and the entry can go once no supported runtime needs it.
676const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
677
678/// The class of the encoder `name` that declares `capabilities`: the bits it
679/// declares, except that an encoder in [`MISDECLARED_HARDWARE_ENCODERS`]
680/// declaring neither is `Hardware`.
681pub(crate) fn declared_encoder_class(name: &str, capabilities: u32) -> NativeEncoderClass {
682    if capabilities & ffi::AV_CODEC_CAP_HYBRID != 0 {
683        NativeEncoderClass::Hybrid
684    } else if capabilities & ffi::AV_CODEC_CAP_HARDWARE != 0
685        || MISDECLARED_HARDWARE_ENCODERS.contains(&name)
686    {
687        NativeEncoderClass::Hardware
688    } else {
689        NativeEncoderClass::Software
690    }
691}
692
693pub(crate) fn encoder_is_compatible(candidate: &VideoEncoderCandidate) -> bool {
694    let name = candidate.name();
695    registration_is_compatible(candidate, &name) && encoder_runs_here(candidate, &name)
696}
697
698/// What the registration alone rules out: an encoder the YUV420P sink cannot
699/// feed, and a hardware one whose device has no node here.
700fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
701    // The native sink submits software YUV420P frames directly. Hardware wrappers
702    // requiring an AVHWFramesContext must not be advertised.
703    if candidate
704        .encoder
705        .pix_fmts()
706        .is_some_and(|formats| !formats.contains(&ffi::AV_PIX_FMT_YUV420P))
707    {
708        return false;
709    }
710    !candidate.on_hardware || hardware_device_node(name) != Some(false)
711}
712
713/// Whether the encoder `name`, run on the GPU when `on_hardware` is set, is
714/// listed only after an open probe ([`encoder_runs_here`]): a hardware
715/// encoder with no device node to stand for its device, NVENC, and an `_mf`
716/// or `_videotoolbox` one either way it runs. A `VideoToolbox` encoder opens
717/// only where the Mac has a hardware encoder, and a runtime may declare one
718/// software (`FFmpeg` 8.1's `h264_videotoolbox`, which the class corrects),
719/// so the name keeps it probed whatever its class. An NVIDIA device node
720/// says nothing about NVENC: an A100 or H100 has the node and no encoder.
721pub(crate) fn needs_open_probe(on_hardware: bool, name: &str) -> bool {
722    (on_hardware && (hardware_device_node(name).is_none() || name.ends_with("_nvenc")))
723        || name.ends_with("_mf")
724        || name.ends_with("_videotoolbox")
725}
726
727/// Whether `encoder` encodes a frame on this machine, for the encoders whose
728/// registration does not say.
729///
730/// Registration says an encoder was compiled in, not that this machine runs
731/// it. `MediaFoundation` registers every `_mf` encoder whether or not Windows
732/// has a transform that runs it: on one runner `hevc_mf` found the HEVC
733/// Video Extension, opened, and failed its first frame with `E_FAIL` in
734/// every rate mode, and `av1_mf` found no transform at all. A hardware
735/// encoder is the same claim about a device: `h264_videotoolbox` registers in
736/// a headless macOS VM with no encoder to open, and NVENC on a Windows
737/// machine without the driver. Advertised on registration alone, they would
738/// be preferred and exports could only fall through. So an `_mf` or
739/// `_videotoolbox` encoder either way it runs, and a hardware one with no
740/// device node to stand for its device ([`hardware_device_node`]), is
741/// listed only after it has encoded one 256x144 frame with the options an
742/// export sends, once per process for each way it runs (a machine with no
743/// hardware transform still runs the software one); a failure is logged once.
744/// Any other software encoder is taken at its registration, and any failure
745/// when an export opens a listed encoder falls through to the next candidate.
746///
747/// Probes run when an encoder is first asked about, not at startup: an
748/// export asks only about its own codec's encoders, and a GPU-resident one
749/// about none. Each NVENC probe makes a CUDA context, which took about 0.4 s
750/// on an RTX 4090.
751fn encoder_runs_here(candidate: &VideoEncoderCandidate, name: &str) -> bool {
752    probe_outcome(candidate, name, true).unwrap_or(false)
753}
754
755/// Whether `candidate` runs here: `Some(true)` for an encoder that needs no
756/// probe or passed one, `Some(false)` for one that failed it, and `None` for
757/// one not probed yet when `probe` is unset.
758fn probe_outcome(candidate: &VideoEncoderCandidate, name: &str, probe: bool) -> Option<bool> {
759    static RUNS: LazyLock<Mutex<HashMap<(String, bool), bool>>> = LazyLock::new(Mutex::default);
760    if !needs_open_probe(candidate.on_hardware, name) {
761        return Some(true);
762    }
763    let key = (name.to_owned(), candidate.on_hardware);
764    let mut runs = RUNS
765        .lock()
766        .unwrap_or_else(std::sync::PoisonError::into_inner);
767    if let Some(runs_here) = runs.get(&key) {
768        return Some(*runs_here);
769    }
770    if !probe {
771        return None;
772    }
773    let runs_here = match probe_software_encode(candidate, name, 256, 144, None).0 {
774        None => true,
775        Some(error) => {
776            alpha2_os::diagnostic!(
777                "alpha2 native: video encoder {name} {} is registered but did not encode a \
778                 frame on this machine, so it is not offered that way: {error}",
779                mode_label(candidate.on_hardware)
780            );
781            false
782        }
783    };
784    runs.insert(key, runs_here);
785    Some(runs_here)
786}
787
788/// The encoders [`codec_capabilities`] lists, found without opening any:
789/// what it would list from what is known so far, and the encoders whose open
790/// probe has not run yet, which it leaves out.
791///
792/// For a log line at startup, which must not pay for every probe
793/// ([`encoder_runs_here`]).
794#[must_use]
795pub fn video_encoders_without_probing() -> (Vec<NativeVideoEncoderCapability>, Vec<String>) {
796    let mut unprobed = Vec::new();
797    let mut listed = Vec::new();
798    for codec in [
799        NativeVideoCodec::H264,
800        NativeVideoCodec::Hevc,
801        NativeVideoCodec::Vp9,
802        NativeVideoCodec::Av1,
803        NativeVideoCodec::Mpeg4Part2,
804    ] {
805        for candidate in registered_video_encoders(codec) {
806            let name = candidate.name();
807            if !registration_is_compatible(&candidate, &name) {
808                continue;
809            }
810            match probe_outcome(&candidate, &name, false) {
811                Some(true) => {
812                    let (minimum_width, minimum_height) = encoder_minimum_dimensions(&name);
813                    listed.push(NativeVideoEncoderCapability {
814                        codec,
815                        class: encoder_class(&candidate.encoder),
816                        hardware_accelerated: candidate.on_hardware,
817                        preferred: !listed
818                            .iter()
819                            .any(|listed: &NativeVideoEncoderCapability| listed.codec == codec),
820                        encoder_name: name,
821                        minimum_width,
822                        minimum_height,
823                        library: None,
824                    });
825                }
826                Some(false) => {}
827                None => unprobed.push(format!("{name} {}", mode_label(candidate.on_hardware))),
828            }
829        }
830    }
831    (listed, unprobed)
832}
833
834/// How a log line names the way a candidate runs.
835pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
836    if on_hardware { "on GPU" } else { "on CPU" }
837}
838
839/// Whether the device a hardware encoder needs has a node here, for the
840/// encoder families Linux knows a node for; `None` for the rest.
841///
842/// A node is the evidence Linux takes for those families instead of an open
843/// probe ([`encoder_runs_here`]), except NVENC, whose node is only the first
844/// requirement: without one, nothing is probed, so a machine without an
845/// NVIDIA GPU pays nothing for the NVENC encoders the runtime registers. A
846/// node whose device still does not open costs the export one failed open,
847/// and the encoder it falls through to gets the thread split for its own
848/// kind (software or hardware).
849#[cfg(target_os = "linux")]
850fn hardware_device_node(name: &str) -> Option<bool> {
851    if name.ends_with("_nvenc") {
852        return Some(Path::new("/dev/nvidia0").exists() || Path::new("/dev/nvidiactl").exists());
853    }
854    if name.ends_with("_vaapi") || name.ends_with("_qsv") || name.ends_with("_amf") {
855        return Some(fs::read_dir("/dev/dri").is_ok_and(|entries| {
856            entries.flatten().any(|entry| {
857                entry
858                    .file_name()
859                    .to_str()
860                    .is_some_and(|name| name.starts_with("renderD") || name.starts_with("card"))
861            })
862        }));
863    }
864    if name.ends_with("_v4l2m2m") {
865        return Some((0..64).any(|index| Path::new(&format!("/dev/video{index}")).exists()));
866    }
867    None
868}
869
870/// Off Linux there is no device node to look at: whether the device is there
871/// is the open probe's to answer ([`encoder_runs_here`]).
872#[cfg(not(target_os = "linux"))]
873const fn hardware_device_node(_name: &str) -> Option<bool> {
874    None
875}
876
877/// Why an export of `codec` has no encoder, and what to do about it.
878///
879/// "h264 encoder is unavailable" reads like a bug in alpha2. What decides is
880/// the GPU-resident path, which says why it declined (`gpu_output_unavailable`,
881/// when the caller knows), the linked runtime, which says what it offers
882/// instead (`offered`, every codec's encoders), and for H.264 whether
883/// `OpenH264` is installed. The message names all three and the ways out.
884#[must_use]
885pub fn no_video_encoder_message(
886    codec: NativeVideoCodec,
887    offered: &[NativeVideoEncoderCapability],
888    gpu_output_unavailable: Option<&str>,
889) -> String {
890    let identity = crate::ffmpeg_runtime_identity();
891    let inventory = offered
892        .iter()
893        .filter(|capability| capability.library.is_none())
894        .map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
895        .collect::<Vec<_>>()
896        .join(", ");
897    let mut message = format!("no usable {} encoder: ", codec.as_str());
898    if let Some(reason) = gpu_output_unavailable {
899        let _ = write!(message, "GPU output path unavailable ({reason}); ");
900    }
901    let _ = write!(
902        message,
903        "the linked FFmpeg runtime ({}, pinned SDK {}) offers [{}]",
904        identity.version,
905        identity.pinned_sdk_release,
906        if inventory.is_empty() {
907            "none"
908        } else {
909            inventory.as_str()
910        }
911    );
912    if codec != NativeVideoCodec::H264 {
913        return message;
914    }
915    let status = crate::openh264_status();
916    if let Some(reason) = status.reason() {
917        let _ = write!(
918            message,
919            "; OpenH264 is {}: {reason}",
920            status.as_str().replace('-', " ")
921        );
922    }
923    let _ = write!(message, ". To export H.264 here: {}", h264_encoder_remedy());
924    message
925}
926
927/// What a user can do on a machine that has no H.264 encoder for CPU
928/// frames: install `OpenH264` (or turn it back on), bring an `FFmpeg` with
929/// `libx264`, or export another codec.
930#[must_use]
931pub fn h264_encoder_remedy() -> String {
932    let openh264 = match crate::openh264_status() {
933        crate::OpenH264Status::Disabled => {
934            "unset ALPHA2_OPENH264=0 to use the installed OpenH264 encoder".to_owned()
935        }
936        _ => format!(
937            "install the OpenH264 encoder ({})",
938            crate::OPENH264_INSTALL_COMMAND
939        ),
940    };
941    format!(
942        "{openh264}, use a thin runtime with an FFmpeg 8 that has libx264, or export VP9 (.webm) \
943         or AV1 instead"
944    )
945}
946
947/// [`no_video_encoder_message`] as an encode error, for a sink that does not
948/// know why the GPU path was not taken.
949fn unavailable_codec_error(codec: NativeVideoCodec) -> EncodeError {
950    EncodeError::Adapter(no_video_encoder_message(
951        codec,
952        &codec_capabilities().video,
953        None,
954    ))
955}
956
957/// Encoders to try for `codec`, most preferred first.
958///
959/// A named request is that encoder alone: a caller that pinned an encoder
960/// wants that encoder or an error, not a quiet substitution. A hybrid
961/// encoder is still tried each way it runs here.
962pub(crate) fn video_encoder_candidates(
963    codec: NativeVideoCodec,
964    requested_name: Option<&str>,
965) -> Result<Vec<VideoEncoderChoice>, EncodeError> {
966    if requested_name == Some(crate::OPENH264_ENCODER_NAME) {
967        if codec != NativeVideoCodec::H264 {
968            return Err(EncodeError::Adapter(format!(
969                "video encoder {} does not implement {}",
970                crate::OPENH264_ENCODER_NAME,
971                codec.as_str()
972            )));
973        }
974        let status = crate::openh264_status();
975        return match status.library() {
976            Some(library) => Ok(vec![VideoEncoderChoice::OpenH264(std::sync::Arc::clone(
977                library,
978            ))]),
979            None => Err(EncodeError::Adapter(format!(
980                "video encoder {} is unavailable: {}{}",
981                crate::OPENH264_ENCODER_NAME,
982                status.reason().unwrap_or_default(),
983                status
984                    .remedy()
985                    .map(|remedy| format!("; {remedy}"))
986                    .unwrap_or_default()
987            ))),
988        };
989    }
990    if let Some(name) = requested_name {
991        let encoder = select_video_encoder(codec, Some(name))?;
992        return Ok(registered_video_encoders(codec)
993            .into_iter()
994            .filter(|candidate| candidate.encoder.as_ptr() == encoder.as_ptr())
995            .filter(encoder_is_compatible)
996            .map(VideoEncoderChoice::Ffmpeg)
997            .collect());
998    }
999    let candidates = available_video_encoders(codec);
1000    if candidates.is_empty() {
1001        return Err(unavailable_codec_error(codec));
1002    }
1003    Ok(candidates)
1004}
1005
1006/// The one encoder an export of `codec` opens without a fallback: the pinned
1007/// `requested_name`, which must implement `codec` and be advertised for the
1008/// YUV420P sink, else the codec's preferred advertised encoder.
1009pub(crate) fn select_video_encoder(
1010    codec: NativeVideoCodec,
1011    requested_name: Option<&str>,
1012) -> Result<AVCodecRef<'static>, EncodeError> {
1013    if let Some(name) = requested_name {
1014        let name_c = CString::new(name)
1015            .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1016        let encoder = AVCodec::find_encoder_by_name(&name_c)
1017            .ok_or_else(|| EncodeError::Adapter(format!("video encoder {name} is unavailable")))?;
1018        if encoder.id != codec.codec_id() {
1019            return Err(EncodeError::Adapter(format!(
1020                "video encoder {name} does not implement {}",
1021                codec.as_str()
1022            )));
1023        }
1024        if !video_encoder_capabilities(codec)
1025            .iter()
1026            .any(|capability| capability.encoder_name == name)
1027        {
1028            return Err(EncodeError::Adapter(format!(
1029                "video encoder {name} is not compatible with the native encode path"
1030            )));
1031        }
1032        return Ok(encoder);
1033    }
1034
1035    let capability = video_encoder_capabilities(codec)
1036        .into_iter()
1037        .find(|capability| capability.library.is_none())
1038        .ok_or_else(|| unavailable_codec_error(codec))?;
1039    let name = CString::new(capability.encoder_name)
1040        .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1041    AVCodec::find_encoder_by_name(&name).ok_or_else(|| unavailable_codec_error(codec))
1042}
1043
1044/// What one sink needs to open its video encoder.
1045pub(crate) struct VideoEncoderOpenRequest<'a> {
1046    /// Output format the encoder is configured for.
1047    pub config: &'a VideoEncodeConfig,
1048    /// Concrete encoder the caller pinned, if any.
1049    pub requested_name: Option<&'a str>,
1050    /// Software worker budget, asked for only when a candidate that runs on
1051    /// the CPU is opened; a hardware candidate takes none.
1052    pub threads: SoftwareEncoderThreads<'a>,
1053    /// Whether the container wants the extradata in the stream header.
1054    pub global_header: bool,
1055    /// Coded size, already range-checked by the caller.
1056    pub width: i32,
1057    /// Coded size, already range-checked by the caller.
1058    pub height: i32,
1059}
1060
1061/// The worker count a software video encoder opens with, decided only when
1062/// one is opened.
1063///
1064/// An export divides its CPU between decode and the encoder, and which way it
1065/// divides depends on whether the encoder runs on the CPU at all. That is
1066/// known only once a candidate opens: the preferred one can be a hardware
1067/// encoder that fails `avcodec_open2` and falls through to a software one.
1068/// A count decided before the open would give that fallback the hardware
1069/// split's share of the encoder, which is none, and it would run on one
1070/// worker. The caller passes how to decide instead; it is asked at most once.
1071pub(crate) struct SoftwareEncoderThreads<'a> {
1072    decide: &'a dyn Fn() -> i64,
1073    decided: std::cell::OnceCell<i64>,
1074}
1075
1076impl<'a> SoftwareEncoderThreads<'a> {
1077    /// The count `decide` returns, asked for when a software candidate opens.
1078    pub(crate) fn new(decide: &'a dyn Fn() -> i64) -> Self {
1079        Self {
1080            decide,
1081            decided: std::cell::OnceCell::new(),
1082        }
1083    }
1084
1085    /// The decided count, at least one worker.
1086    pub(crate) fn get(&self) -> i64 {
1087        *self.decided.get_or_init(|| (self.decide)().max(1))
1088    }
1089}
1090
1091/// Opens the first candidate encoder that this machine can actually run.
1092///
1093/// `avcodec_open2` is where a registered encoder becomes a running one, and
1094/// the two are not the same claim: a Windows SKU or VM without the
1095/// `MediaFoundation` H.264 MFT registers `h264_mf` and cannot open it, and a
1096/// headless container registers `h264_videotoolbox` and cannot open it. Each
1097/// rejection is recorded with a typed reason and carried on the sink so the
1098/// run can report which encoder it ended up on.
1099///
1100/// # Errors
1101/// Returns the last candidate's failure, prefixed with every earlier one, when
1102/// no candidate opens.
1103pub(crate) fn open_video_encoder(
1104    request: &VideoEncoderOpenRequest<'_>,
1105) -> Result<
1106    (
1107        crate::video_backend::VideoEncoderBackend,
1108        NativeVideoEncoderSelection,
1109    ),
1110    EncodeError,
1111> {
1112    let candidates = video_encoder_candidates(request.config.codec, request.requested_name)?;
1113    open_first_video_encoder(request, candidates)
1114}
1115
1116/// An opened encoder, how it runs the rate control, and its CPU workers.
1117struct OpenedVideoEncoder {
1118    backend: crate::video_backend::VideoEncoderBackend,
1119    rate_control: crate::AppliedRateControl,
1120    quantizer_refused: Option<String>,
1121    software_threads: Option<i64>,
1122}
1123
1124/// Opens the first of `candidates` that runs, recording why each one before
1125/// it did not; when none does, the error names every candidate's reason.
1126pub(crate) fn open_first_video_encoder(
1127    request: &VideoEncoderOpenRequest<'_>,
1128    candidates: Vec<VideoEncoderChoice>,
1129) -> Result<
1130    (
1131        crate::video_backend::VideoEncoderBackend,
1132        NativeVideoEncoderSelection,
1133    ),
1134    EncodeError,
1135> {
1136    let mut rejected = Vec::new();
1137    let last = candidates.len().saturating_sub(1);
1138    for (index, candidate) in candidates.into_iter().enumerate() {
1139        let name = candidate.name();
1140        let class = candidate.class();
1141        let opened = match &candidate {
1142            VideoEncoderChoice::Ffmpeg(candidate) => {
1143                open_one_video_encoder(request, candidate, &name).map(
1144                    |(encoder, rate_control, quantizer_refused)| OpenedVideoEncoder {
1145                        backend: crate::video_backend::VideoEncoderBackend::Ffmpeg(encoder),
1146                        rate_control,
1147                        quantizer_refused,
1148                        software_threads: (!candidate.on_hardware).then(|| request.threads.get()),
1149                    },
1150                )
1151            }
1152            VideoEncoderChoice::OpenH264(library) => open_openh264(request, library),
1153        };
1154        match opened {
1155            Ok(opened) => {
1156                return Ok((
1157                    opened.backend,
1158                    NativeVideoEncoderSelection {
1159                        encoder_name: name,
1160                        class,
1161                        hardware_accelerated: candidate.on_hardware(),
1162                        rejected,
1163                        rate_control: opened.rate_control,
1164                        quantizer_refused: opened.quantizer_refused,
1165                        software_threads: opened.software_threads,
1166                    },
1167                ));
1168            }
1169            Err((reason, error)) => {
1170                let detail = error.to_string();
1171                if index == last {
1172                    let earlier = rejected
1173                        .iter()
1174                        .map(|fallback: &NativeVideoEncoderFallback| {
1175                            format!(
1176                                "{} {} ({}): {}",
1177                                fallback.encoder_name,
1178                                mode_label(fallback.hardware_accelerated),
1179                                fallback.reason.as_str(),
1180                                fallback.detail
1181                            )
1182                        })
1183                        .collect::<Vec<_>>();
1184                    let prefix = if earlier.is_empty() {
1185                        String::new()
1186                    } else {
1187                        format!("after {}; ", earlier.join("; "))
1188                    };
1189                    return Err(EncodeError::Adapter(format!(
1190                        "{prefix}{name} {} ({}): {detail}",
1191                        mode_label(candidate.on_hardware()),
1192                        reason.as_str()
1193                    )));
1194                }
1195                alpha2_os::diagnostic!(
1196                    "alpha2 native export: video encoder {name} {} ({}) rejected: {detail}",
1197                    mode_label(candidate.on_hardware()),
1198                    reason.as_str()
1199                );
1200                rejected.push(NativeVideoEncoderFallback {
1201                    encoder_name: name,
1202                    hardware_accelerated: candidate.on_hardware(),
1203                    reason,
1204                    detail,
1205                });
1206            }
1207        }
1208    }
1209    Err(unavailable_codec_error(request.config.codec))
1210}
1211
1212/// Opens `OpenH264` for `request`: on its constant QP when the rate control
1213/// has a quantizer, which `OpenH264` always honours, else on its bitrate.
1214///
1215/// Its thread count comes from the picture height
1216/// (`openh264::encoder::slice_count`), not from the CPU budget. The software
1217/// budget is still decided here, since the rest of the export divides the
1218/// CPU from that decision, but the encoder does not run with it.
1219fn open_openh264(
1220    request: &VideoEncoderOpenRequest<'_>,
1221    library: &crate::OpenH264Library,
1222) -> Result<OpenedVideoEncoder, (NativeEncoderRejection, EncodeError)> {
1223    let config = request.config;
1224    validate_encoder_dimensions(crate::OPENH264_ENCODER_NAME, config.width, config.height)
1225        .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1226    let open_failed = |message: String| {
1227        (
1228            NativeEncoderRejection::OpenFailed,
1229            EncodeError::Adapter(message),
1230        )
1231    };
1232    if !request.global_header {
1233        return Err(open_failed(
1234            "OpenH264 writes its parameter sets into the stream header, which this container \
1235             lacks"
1236                .to_owned(),
1237        ));
1238    }
1239    let (rate, rate_control) = crate::rate_control::openh264_rate(config.rate_control);
1240    let time_base = encoder_time_base(config.frame_rate, None)
1241        .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1242    let _ = request.threads.get();
1243    let encoder = crate::openh264::OpenH264Encoder::open(
1244        library,
1245        &crate::openh264::OpenH264Settings {
1246            width: config.width,
1247            height: config.height,
1248            frame_rate: config.frame_rate,
1249            color: config.color,
1250            sample_aspect_ratio: config.display.sample_aspect_ratio,
1251            rate,
1252            keyframe_interval: u32::try_from(keyframe_interval(config.frame_rate)).unwrap_or(1),
1253        },
1254        time_base,
1255    )
1256    .map_err(open_failed)?;
1257    let threads = i64::from(encoder.threads());
1258    Ok(OpenedVideoEncoder {
1259        backend: crate::video_backend::VideoEncoderBackend::OpenH264(Box::new(encoder)),
1260        rate_control,
1261        quantizer_refused: None,
1262        software_threads: Some(threads),
1263    })
1264}
1265
1266/// Opens `candidate`, on the rate control's quantizer when it has one and the
1267/// encoder accepts it, otherwise on its bitrate.
1268///
1269/// A refused quantizer is not a refused encoder: the bitrate is the rate
1270/// control's own fallback, so the export continues on it and the selection
1271/// says so rather than failing over to another candidate.
1272fn open_one_video_encoder(
1273    request: &VideoEncoderOpenRequest<'_>,
1274    candidate: &VideoEncoderCandidate,
1275    name: &str,
1276) -> Result<
1277    (AVCodecContext, crate::AppliedRateControl, Option<String>),
1278    (NativeEncoderRejection, EncodeError),
1279> {
1280    validate_encoder_dimensions(name, request.config.width, request.config.height)
1281        .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1282    let refused = match crate::rate_control::quantizer_request(name, request.config.rate_control) {
1283        None => None,
1284        Some(Err(reason)) => Some(reason),
1285        Some(Ok(quantized)) => {
1286            match open_video_encoder_context(request, candidate, name, Some(&quantized)) {
1287                Ok(encoder) => return Ok((encoder, crate::AppliedRateControl::Quantizer, None)),
1288                Err((_, error)) => Some(format!("{} refused: {error}", quantized.describe())),
1289            }
1290        }
1291    };
1292    if let Some(reason) = &refused {
1293        alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
1294    }
1295    open_video_encoder_context(request, candidate, name, None)
1296        .map(|encoder| (encoder, crate::AppliedRateControl::Bitrate, refused))
1297}
1298
1299fn open_video_encoder_context(
1300    request: &VideoEncoderOpenRequest<'_>,
1301    candidate: &VideoEncoderCandidate,
1302    name: &str,
1303    quantized: Option<&crate::rate_control::MechanismRequest>,
1304) -> Result<AVCodecContext, (NativeEncoderRejection, EncodeError)> {
1305    let mut encoder = AVCodecContext::new(&candidate.encoder);
1306    encoder.set_width(request.width);
1307    encoder.set_height(request.height);
1308    encoder.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
1309    let rate = request.config.frame_rate;
1310    let time_base = encoder_time_base(rate, max_time_base_denominator(candidate.encoder.id))
1311        .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1312    encoder.set_time_base(time_base);
1313    // Square pixels are what an undeclared ratio means; only another is written.
1314    let sar = request.config.display.sample_aspect_ratio;
1315    if sar != alpha2_media::SampleAspectRatio::default() {
1316        encoder.set_sample_aspect_ratio(rsmpeg::avutil::ra(
1317            i32::try_from(sar.num).unwrap_or(1),
1318            i32::try_from(sar.den).unwrap_or(1),
1319        ));
1320    }
1321    encoder.set_framerate(rsmpeg::avutil::ra(
1322        i32::try_from(rate.numerator()).unwrap_or(i32::MAX),
1323        i32::try_from(rate.denominator()).unwrap_or(1),
1324    ));
1325    encoder.set_bit_rate(quantized.map_or_else(
1326        || i64::try_from(request.config.rate_control.bitrate).unwrap_or(i64::MAX),
1327        |quantized| quantized.bit_rate,
1328    ));
1329    encoder.set_gop_size(keyframe_interval(request.config.frame_rate));
1330    encoder.set_max_b_frames(encoder_max_b_frames(name));
1331    if request.global_header {
1332        encoder.set_flags(
1333            encoder.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
1334        );
1335    }
1336    // A hardware candidate never asks, so an export whose GPU encoder opens never decides a
1337    // software split it would not use.
1338    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
1373/// Frames between forced keyframes: two seconds of output.
1374///
1375/// libavcodec's own `gop_size` default is 12, which at 24 fps is a keyframe
1376/// every half second. Nothing in alpha2 seeks or thumbnails its own output, so the
1377/// interval is only a rate question. Measured at a fixed rate on the 4K
1378/// showcase, 12 s of the colour clip, PSNR-Y: going from 12 to two seconds
1379/// (48) gained 0.53-0.64 dB on `h264_vulkan` and 0.67-0.77 dB on
1380/// `h264_nvenc`; going on to ten seconds (240) gained a further 0.11-0.12 dB
1381/// on `h264_vulkan`. Two seconds is where nearly all of that is and a player
1382/// can still start or seek within two seconds of any point.
1383pub(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
1390/// The largest time base denominator encoders of `codec` can write, `None`
1391/// for a codec without one. MPEG-4 Part 2 and H.263 store it as the 16-bit
1392/// `vop_time_increment_resolution`, so `FFmpeg`'s encoders refuse a time base
1393/// of 1001/120000 (119.88 fps).
1394pub(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
1401/// The time base an encoder of frames at `rate` runs in: the frame length
1402/// itself (1001/30000 for 29.97 fps), when its denominator is within `limit`.
1403/// Otherwise 1/D for the D within `limit` that rounds frame times the least
1404/// (the largest error over every frame), so a rate a time base of this
1405/// encoder cannot hold keeps its frame count and frame times, each rounded
1406/// by under one unit. 119.88 fps (120000/1001) is 1/64000: frame times are
1407/// off by at most 7/960000 s.
1408///
1409/// # Errors
1410/// For a rate whose terms do not fit an `FFmpeg` rational.
1411pub(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    // Frame k is at k*den/num s, k*den*D/num units of 1/D. Its fractional part is a multiple of
1427    // 1/q, q = num / gcd(num, den*D), so the largest rounding is floor(q/2)/q units,
1428    // floor(q/2)/(q*D) s.
1429    // A unit no longer than a frame keeps every frame its own time.
1430    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
1454/// `max_b_frames` for encoder `name`.
1455pub(crate) fn encoder_max_b_frames(name: &str) -> i32 {
1456    // VideoToolbox's default is no frame reordering. Forcing B-frames makes support
1457    // device-dependent and can yield invalid packet timestamps on otherwise valid hardware
1458    // sessions, so preserve that native default.
1459    if name.ends_with("_videotoolbox") {
1460        0
1461    } else {
1462        2
1463    }
1464}
1465
1466/// The smallest even width and height encoder `name` accepts.
1467pub(crate) fn encoder_minimum_dimensions(name: &str) -> (u32, u32) {
1468    // NVIDIA AV1 requires at least 145x128. Its H.264/HEVC encoders reject dimensions below 145x49.
1469    // alpha2 exposes even 4:2:0 dimensions.
1470    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/// One hardware configuration a video encoder declares.
1495#[derive(Clone, Debug, Eq, PartialEq)]
1496pub struct NativeEncoderHardwareConfig {
1497    /// Hardware pixel format this configuration carries frames in.
1498    pub pixel_format: String,
1499    /// `FFmpeg` hardware device type backing it.
1500    pub device_type: String,
1501    /// `AV_CODEC_HW_CONFIG_METHOD_*` bits.
1502    pub methods: i32,
1503    /// Whether the encoder takes its frames through an `AVHWFramesContext`.
1504    pub needs_hardware_frames: bool,
1505}
1506
1507/// What one registered video encoder declares, and what happened when alpha2
1508/// opened it on this machine.
1509///
1510/// Registration, advertisement and instantiation are three different claims,
1511/// and only the third is the one an export depends on. This probe makes all
1512/// three observable in one place, per platform, without an export.
1513#[derive(Clone, Debug, Eq, PartialEq)]
1514// Independent observations of one encoder, reported side by side, not a state.
1515#[allow(clippy::struct_excessive_bools)]
1516pub struct NativeVideoEncoderProbe {
1517    /// Logical codec this encoder implements.
1518    pub codec: NativeVideoCodec,
1519    /// `FFmpeg` short name.
1520    pub encoder_name: String,
1521    /// Class taken from the encoder's own capability bits.
1522    pub class: NativeEncoderClass,
1523    /// Whether this row runs the encoder on the GPU.
1524    pub hardware_accelerated: bool,
1525    /// Raw `AV_CODEC_CAP_*` bits, so an unexpected class can be traced.
1526    pub capability_bits: u32,
1527    /// Pixel formats the encoder declares, or empty when it declares none.
1528    pub pixel_formats: Vec<String>,
1529    /// Hardware configurations the encoder declares.
1530    pub hardware_configs: Vec<NativeEncoderHardwareConfig>,
1531    /// Whether `AV_PIX_FMT_YUV420P` is among the declared formats.
1532    pub accepts_yuv420p: bool,
1533    /// Whether every declared format is a hardware one, so a software frame
1534    /// can never be sent to it. This is what keeps `h264_vulkan` unadvertised.
1535    pub requires_hardware_frames: bool,
1536    /// Whether [`codec_capabilities`] advertises it for the YUV420P sink.
1537    pub advertised: bool,
1538    /// Smallest frame the encoder accepts, as alpha2 records it.
1539    pub minimum_size: (u32, u32),
1540    /// `None` when the encoder opened and produced at least one packet from a
1541    /// software YUV420P frame; otherwise why it did not.
1542    pub software_open_error: Option<String>,
1543    /// Packets drained from that attempt.
1544    pub software_packets: u32,
1545    /// Which rate-control mechanisms opened, and with what options.
1546    pub rate_control: Vec<NativeRateControlProbe>,
1547}
1548
1549/// Whether one encoder opened with one rate-control mechanism.
1550#[derive(Clone, Debug, Eq, PartialEq)]
1551pub struct NativeRateControlProbe {
1552    /// The mechanism asked for.
1553    pub mechanism: crate::RateControlMechanism,
1554    /// The options that carried it, or `None` where alpha2 knows no spelling for
1555    /// it on this encoder's family.
1556    pub options: Option<String>,
1557    /// Packets drained when it opened, consumed every option and encoded a
1558    /// frame; otherwise why not.
1559    pub outcome: Result<u32, String>,
1560    /// The device a hardware-frames encoder was probed on: a Vulkan device
1561    /// index for `*_vulkan`, and for `*_d3d12va` the DX12 adapter that
1562    /// answered (`DX12 adapter <index> (<name>)`).
1563    pub device: Option<String>,
1564}
1565
1566/// A quantizer every encoder of `codec` accepts, for a probe open: the
1567/// software VP9 and AV1 encoders (`libvpx-vp9`, `libsvtav1`) take `crf` on
1568/// 0-63, the hardware ones a QP on 0-255.
1569pub(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/// Opens every registered video encoder and reports what it actually does.
1584///
1585/// The name of an encoder says nothing about whether this machine can run it:
1586/// a Windows SKU without the `MediaFoundation` H.264 MFT registers `h264_mf`
1587/// and cannot open it, and `h264_vulkan` is registered on every Linux build
1588/// and cannot take a software frame at all. Each candidate is opened at
1589/// `width`x`height` and fed one YUV420P frame, so the report distinguishes
1590/// "registered", "advertised" and "ran". A hybrid encoder is a row for each
1591/// way it runs here.
1592#[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                    // D3D12VA takes 0-52 for every codec, AV1 included.
1652                    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/// Opens an encoder that only takes hardware frames, with `request`.
1723///
1724/// `h264_vulkan` is the shipped Linux encoder and cannot take a software
1725/// frame at all, so its rate-control surface is only observable through a
1726/// device. `FFmpeg` creates its own Vulkan device here, with whatever
1727/// video-encode queue the physical device offers, the same capability the
1728/// export's shared device is checked for.
1729///
1730/// The D3D12VA encoders are Windows' GPU-path encoders and take D3D12
1731/// frames only. Their rows open on a DX12 adapter's device, the first one
1732/// whose D3D12 Video Encode reports the encoder, and a mode that device does
1733/// not list is refused with its reason rather than opened.
1734///
1735/// Returns the packets and the device that answered: a Vulkan device's
1736/// index, or a DX12 adapter's index and name.
1737#[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
1758/// Opens `candidate` and feeds it one software YUV420P frame.
1759fn probe_software_encode(
1760    candidate: &VideoEncoderCandidate,
1761    name: &str,
1762    width: u32,
1763    height: u32,
1764    request: Option<&crate::rate_control::MechanismRequest>,
1765) -> (Option<String>, u32) {
1766    let Ok(coded_width) = i32::try_from(width) else {
1767        return (Some("width is out of range".to_owned()), 0);
1768    };
1769    let Ok(coded_height) = i32::try_from(height) else {
1770        return (Some("height is out of range".to_owned()), 0);
1771    };
1772    if let Err(error) = validate_encoder_dimensions(name, width, height) {
1773        return (Some(error.to_string()), 0);
1774    }
1775    let encoder = &candidate.encoder;
1776    let mut context = AVCodecContext::new(encoder);
1777    context.set_width(coded_width);
1778    context.set_height(coded_height);
1779    context.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
1780    let time_base = rsmpeg::avutil::ra(1, 30);
1781    context.set_time_base(time_base);
1782    context.set_framerate(rsmpeg::avutil::ra(30, 1));
1783    context.set_bit_rate(request.map_or(2_000_000, |request| request.bit_rate));
1784    context.set_gop_size(12);
1785    context.set_max_b_frames(encoder_max_b_frames(name));
1786    let options = video_encoder_options(
1787        ColorMetadata::EXPORT,
1788        encoder,
1789        candidate.on_hardware,
1790        1,
1791        width,
1792    );
1793    let options = match request {
1794        Some(request) => request.dictionary(options),
1795        None => options,
1796    };
1797    match alpha2_ffmpeg_bridge::open_codec_context(&mut context, options) {
1798        Err(error) => return (Some(error.to_string()), 0),
1799        Ok(unused) => {
1800            if let Err(error) = reject_unused_encode_options(unused) {
1801                return (Some(error.to_string()), 0);
1802            }
1803        }
1804    }
1805    let Some(image) =
1806        rsmpeg::avutil::AVImage::new(ffi::AV_PIX_FMT_YUV420P, coded_width, coded_height, 1)
1807    else {
1808        return (Some("cannot allocate a probe frame".to_owned()), 0);
1809    };
1810    let mut frame = rsmpeg::avutil::AVFrameWithImage::new(image);
1811    frame.set_pts(0);
1812    frame.set_time_base(time_base);
1813    if let Err(error) = context.send_frame(Some(&frame)) {
1814        return (Some(error.to_string()), 0);
1815    }
1816    if let Err(error) = context.send_frame(None) {
1817        return (Some(error.to_string()), 0);
1818    }
1819    let mut packets = 0;
1820    loop {
1821        match context.receive_packet() {
1822            Ok(_) => packets += 1,
1823            Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
1824            Err(error) => return (Some(error.to_string()), packets),
1825        }
1826    }
1827    if packets == 0 {
1828        return (
1829            Some("opened but produced no packet from a software frame".to_owned()),
1830            0,
1831        );
1832    }
1833    (None, packets)
1834}