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_ffmpeg_bridge::{EncoderContext, LogLine};
13use alpha2_media::{ColorMetadata, EncodeError, EncoderRefusal, OpenFailureKind};
14use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef};
15use rsmpeg::avutil::AVRational;
16use rsmpeg::error::RsmpegError;
17use rsmpeg::ffi;
18
19/// Configuration for transactional audio/video export.
20#[derive(Copy, Clone, Debug, PartialEq)]
21pub struct AvEncodeConfig {
22    /// Video encoder settings.
23    pub video: VideoEncodeConfig,
24    /// Project audio sample rate.
25    pub audio_sample_rate: u32,
26    /// Audio channel count.
27    pub audio_channels: u16,
28    /// Audio codec selected by the downstream host.
29    pub audio_codec: NativeAudioCodec,
30    /// Audio bitrate in bits per second.
31    pub audio_bit_rate: u32,
32}
33
34/// Video codecs implemented by the native encode sinks.
35#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
36#[non_exhaustive]
37pub enum NativeVideoCodec {
38    /// H.264/AVC, preferred for broad browser, Windows, and Apple playback.
39    H264,
40    /// H.265/HEVC.
41    Hevc,
42    /// VP9, when the linked `FFmpeg` build provides a VP9 encoder.
43    Vp9,
44    /// AV1, when the linked `FFmpeg` build provides an AV1 encoder.
45    Av1,
46    /// MPEG-4 Part 2, retained for compatibility with existing alpha2 hosts.
47    Mpeg4Part2,
48    /// Apple `ProRes`, through `prores_ks`: 422 HQ, or 4444 with alpha.
49    ProRes,
50}
51
52impl NativeVideoCodec {
53    /// Stable identifier suitable for an application protocol or settings file.
54    #[must_use]
55    pub const fn as_str(self) -> &'static str {
56        match self {
57            Self::H264 => "h264",
58            Self::Hevc => "hevc",
59            Self::Vp9 => "vp9",
60            Self::Av1 => "av1",
61            Self::Mpeg4Part2 => "mpeg4-part2",
62            Self::ProRes => "prores",
63        }
64    }
65
66    /// The pictures an encoder must take to be offered: 10-bit 4:2:2 for
67    /// `ProRes`, 8-bit 4:2:0 otherwise.
68    #[must_use]
69    pub const fn base_layout(self) -> alpha2_media::PixelLayout {
70        match self {
71            Self::ProRes => alpha2_media::PixelLayout {
72                chroma: alpha2_media::Chroma::Yuv422,
73                bit_depth: alpha2_media::BitDepth::Ten,
74                alpha: false,
75                planes: alpha2_media::PlaneLayout::Planar,
76            },
77            _ => alpha2_media::PixelLayout::YUV420P,
78        }
79    }
80
81    /// The engine's codec this encoder family implements.
82    #[must_use]
83    pub const fn model(self) -> alpha2_media::VideoCodec {
84        match self {
85            Self::H264 => alpha2_media::VideoCodec::H264,
86            Self::Hevc => alpha2_media::VideoCodec::Hevc,
87            Self::Vp9 => alpha2_media::VideoCodec::Vp9,
88            Self::Av1 => alpha2_media::VideoCodec::Av1,
89            Self::Mpeg4Part2 => alpha2_media::VideoCodec::Mpeg4Part2,
90            Self::ProRes => alpha2_media::VideoCodec::ProRes,
91        }
92    }
93
94    /// The native codec for the engine's `codec`.
95    #[must_use]
96    pub const fn from_model(codec: alpha2_media::VideoCodec) -> Self {
97        match codec {
98            alpha2_media::VideoCodec::H264 => Self::H264,
99            alpha2_media::VideoCodec::Hevc => Self::Hevc,
100            alpha2_media::VideoCodec::Vp9 => Self::Vp9,
101            alpha2_media::VideoCodec::Av1 => Self::Av1,
102            alpha2_media::VideoCodec::Mpeg4Part2 => Self::Mpeg4Part2,
103            alpha2_media::VideoCodec::ProRes => Self::ProRes,
104        }
105    }
106
107    pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
108        match self {
109            Self::H264 => ffi::AV_CODEC_ID_H264,
110            Self::Hevc => ffi::AV_CODEC_ID_HEVC,
111            Self::Vp9 => ffi::AV_CODEC_ID_VP9,
112            Self::Av1 => ffi::AV_CODEC_ID_AV1,
113            Self::Mpeg4Part2 => ffi::AV_CODEC_ID_MPEG4,
114            Self::ProRes => ffi::AV_CODEC_ID_PRORES,
115        }
116    }
117}
118
119/// Audio codecs implemented by the native encode sinks.
120#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
121#[non_exhaustive]
122pub enum NativeAudioCodec {
123    /// AAC-LC compatible audio, through `FFmpeg`'s `aac`.
124    Aac,
125    /// Opus, through `libopus`.
126    Opus,
127}
128
129impl NativeAudioCodec {
130    /// Every codec, in the order the engine lists them.
131    pub const ALL: [Self; 2] = [Self::Aac, Self::Opus];
132
133    /// Stable identifier suitable for an application protocol or settings file.
134    #[must_use]
135    pub const fn as_str(self) -> &'static str {
136        match self {
137            Self::Aac => "aac",
138            Self::Opus => "opus",
139        }
140    }
141
142    /// The engine's codec this encoder family implements.
143    #[must_use]
144    pub const fn model(self) -> alpha2_media::AudioCodec {
145        match self {
146            Self::Aac => alpha2_media::AudioCodec::Aac,
147            Self::Opus => alpha2_media::AudioCodec::Opus,
148        }
149    }
150
151    /// The native codec for the engine's `codec`.
152    #[must_use]
153    pub const fn from_model(codec: alpha2_media::AudioCodec) -> Self {
154        match codec {
155            alpha2_media::AudioCodec::Aac => Self::Aac,
156            alpha2_media::AudioCodec::Opus => Self::Opus,
157        }
158    }
159
160    /// The one encoder alpha2 opens: `aac`, or the reference `libopus` (`FFmpeg`'s own `opus` is
161    /// experimental and never a candidate).
162    pub(crate) const fn encoder_name(self) -> &'static std::ffi::CStr {
163        match self {
164            Self::Aac => c"aac",
165            Self::Opus => c"libopus",
166        }
167    }
168}
169
170/// The encoder this runtime opens for `codec`, or `None` when only an experimental one exists.
171pub(crate) fn audio_encoder(codec: NativeAudioCodec) -> Option<AVCodecRef<'static>> {
172    AVCodec::find_encoder_by_name(codec.encoder_name()).filter(|encoder| {
173        u32::try_from(encoder.capabilities).unwrap_or(0) & ffi::AV_CODEC_CAP_EXPERIMENTAL == 0
174    })
175}
176
177/// How a concrete encoder executes, as the linked runtime declares it.
178///
179/// Taken from `AV_CODEC_CAP_HARDWARE` / `AV_CODEC_CAP_HYBRID` rather than the name
180/// suffix, so unknown encoders classify correctly. A few misdeclared encoders are
181/// corrected by `declared_encoder_class`.
182#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
183#[non_exhaustive]
184pub enum NativeEncoderClass {
185    /// Runs on the CPU and draws on the software codec thread budget.
186    Software,
187    /// Runs on a hardware engine and ignores the thread budget.
188    Hardware,
189    /// Can do either. `MediaFoundation`'s `_mf` encoders enumerate a synchronous software transform
190    /// with `hw_encoding=0` and an asynchronous hardware one with `hw_encoding=1`.
191    Hybrid,
192}
193
194impl NativeEncoderClass {
195    /// Stable identifier for diagnostics and the export result.
196    #[must_use]
197    pub const fn as_str(self) -> &'static str {
198        match self {
199            Self::Software => "software",
200            Self::Hardware => "hardware",
201            Self::Hybrid => "hybrid",
202        }
203    }
204
205    /// The ways alpha2 runs an encoder of this class, most preferred first (`true` = GPU). A hybrid
206    /// encoder runs on the CPU unless `hybrid_hardware` is set, and then keeps the CPU as fallback.
207    #[must_use]
208    pub const fn modes(self, hybrid_hardware: bool) -> &'static [bool] {
209        match self {
210            Self::Hardware => &[true],
211            Self::Hybrid if hybrid_hardware => &[true, false],
212            Self::Software | Self::Hybrid => &[false],
213        }
214    }
215}
216
217/// One way to run a registered encoder. Only a hybrid encoder has two.
218pub(crate) struct VideoEncoderCandidate {
219    pub(crate) encoder: AVCodecRef<'static>,
220    pub(crate) on_hardware: bool,
221}
222
223impl VideoEncoderCandidate {
224    pub(crate) fn name(&self) -> String {
225        self.encoder.name().to_string_lossy().into_owned()
226    }
227
228    pub(crate) fn codec(&self) -> NativeVideoCodec {
229        [
230            NativeVideoCodec::H264,
231            NativeVideoCodec::Hevc,
232            NativeVideoCodec::Vp9,
233            NativeVideoCodec::Av1,
234            NativeVideoCodec::Mpeg4Part2,
235            NativeVideoCodec::ProRes,
236        ]
237        .into_iter()
238        .find(|codec| codec.codec_id() == self.encoder.id)
239        .unwrap_or(NativeVideoCodec::H264)
240    }
241}
242
243/// Whether hybrid encoders are configured to run on the GPU.
244///
245/// `_mf` encoders default to `hw_encoding=0` (software MFT). `ALPHA2_VIDEO_ENCODER_HYBRID=hardware`
246/// tries `hw_encoding=1` first and keeps the software transform behind it, because `FFmpeg` does
247/// not fall back between them itself.
248#[must_use]
249pub fn hybrid_encoders_use_hardware() -> bool {
250    std::env::var("ALPHA2_VIDEO_ENCODER_HYBRID")
251        .is_ok_and(|value| value.eq_ignore_ascii_case("hardware"))
252}
253
254/// Why one candidate encoder was not the one an export opened.
255#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
256#[non_exhaustive]
257pub enum NativeEncoderRejection {
258    /// The encoder refuses an output this small.
259    Dimensions,
260    /// `avcodec_open2` failed: no device, no licence, unsupported profile.
261    OpenFailed,
262    /// The encoder silently ignored an option alpha2 requires it to honour.
263    OptionsIgnored,
264    /// The encoder takes none of the pixel formats the output can be written in.
265    PixelFormat,
266}
267
268impl NativeEncoderRejection {
269    /// Stable identifier for diagnostics and the export result.
270    #[must_use]
271    pub const fn as_str(self) -> &'static str {
272        match self {
273            Self::Dimensions => "dimensions",
274            Self::OpenFailed => "open-failed",
275            Self::OptionsIgnored => "options-ignored",
276            Self::PixelFormat => "pixel-format",
277        }
278    }
279}
280
281/// One candidate encoder an export tried and moved past.
282#[derive(Clone, Debug, Eq, PartialEq)]
283pub struct NativeVideoEncoderFallback {
284    /// `FFmpeg` short name of the candidate.
285    pub encoder_name: String,
286    /// Whether the candidate was configured to encode on the GPU.
287    pub hardware_accelerated: bool,
288    /// Typed reason it was rejected.
289    pub reason: NativeEncoderRejection,
290    /// What the rejection is evidence of; a busy device can open later.
291    pub kind: OpenFailureKind,
292    /// The runtime's own message, kept verbatim.
293    pub detail: String,
294}
295
296impl NativeVideoEncoderFallback {
297    /// This rejection as a refusal.
298    #[must_use]
299    pub fn refusal(&self) -> EncoderRefusal {
300        EncoderRefusal {
301            encoder: self.encoder_name.clone(),
302            hardware_accelerated: self.hardware_accelerated,
303            kind: self.kind,
304            reason: self.detail.clone(),
305        }
306    }
307}
308
309/// The encoder an export actually opened, and the candidates it passed over.
310///
311/// A platform can register an encoder it cannot open, so every export reports what it opened.
312#[derive(Clone, Debug, Eq, PartialEq)]
313pub struct NativeVideoEncoderSelection {
314    /// `FFmpeg` short name of the encoder that opened.
315    pub encoder_name: String,
316    /// How that encoder executes.
317    pub class: NativeEncoderClass,
318    /// Whether it is configured to encode on the GPU.
319    pub hardware_accelerated: bool,
320    /// Candidates tried first, in preference order.
321    pub rejected: Vec<NativeVideoEncoderFallback>,
322    /// Whether the encoder runs the rate control's quantizer or its bitrate.
323    pub rate_control: crate::AppliedRateControl,
324    /// Why a quantizer that was asked for is not the one running.
325    pub quantizer_refused: Option<String>,
326    /// `FFmpeg` workers the encoder was opened with on the CPU; `None` on the GPU.
327    pub software_threads: Option<i64>,
328    /// The pictures it takes: the output's layout, or its `P010` form for an encoder that takes
329    /// 10-bit 4:2:0 only that way.
330    pub pixels: alpha2_media::PixelLayout,
331}
332
333/// One linked video encoder available to the native sinks.
334#[derive(Clone, Debug, Eq, PartialEq)]
335pub struct NativeVideoEncoderCapability {
336    /// Stable logical codec requested through [`VideoEncodeConfig`].
337    pub codec: NativeVideoCodec,
338    /// Concrete `FFmpeg` encoder implementation for this codec.
339    pub encoder_name: String,
340    /// How the linked runtime declares this encoder executes.
341    pub class: NativeEncoderClass,
342    /// Whether this entry runs the encoder on the GPU. A hybrid encoder has an entry per mode.
343    pub hardware_accelerated: bool,
344    /// Whether this is the default encoder for its logical codec.
345    pub preferred: bool,
346    /// Smallest even width accepted by this concrete encoder.
347    pub minimum_width: u32,
348    /// Smallest even height accepted by this concrete encoder.
349    pub minimum_height: u32,
350    /// The user-installed library behind a non-`FFmpeg` encoder; `None` for `FFmpeg`'s own.
351    pub library: Option<NativeEncoderLibrary>,
352}
353
354/// A run-time loaded encoder library ([`crate::OPENH264_ENCODER_NAME`]).
355#[derive(Clone, Debug, Eq, PartialEq)]
356pub struct NativeEncoderLibrary {
357    /// Who provided it ([`crate::OpenH264Provider::as_str`]).
358    pub provider: &'static str,
359    /// The version it reports.
360    pub version: String,
361    /// Where it was loaded from.
362    pub path: String,
363    /// The notice its licence requires be shown.
364    pub notice: &'static str,
365}
366
367impl NativeEncoderLibrary {
368    fn openh264(library: &crate::OpenH264Library) -> Self {
369        Self {
370            provider: library.provider().as_str(),
371            version: library.version().to_owned(),
372            path: library.path().to_owned(),
373            notice: crate::OPENH264_NOTICE,
374        }
375    }
376}
377
378/// One linked audio encoder available to the native sinks.
379#[derive(Clone, Debug, Eq, PartialEq)]
380pub struct NativeAudioEncoderCapability {
381    /// Stable logical codec requested through [`AvEncodeConfig`].
382    pub codec: NativeAudioCodec,
383    /// Concrete `FFmpeg` encoder selected for that codec on this host.
384    pub encoder_name: String,
385}
386
387/// Runtime codec inventory for the native sinks.
388///
389/// Container/codec pairing is engine policy ([`alpha2_media::Container`]); this says which
390/// codecs this runtime can encode.
391#[derive(Clone, Debug, Eq, PartialEq)]
392pub struct NativeCodecCapabilities {
393    /// Video encoders accepted by [`NativeVideoEncodeSink`](crate::NativeVideoEncodeSink) and
394    /// [`NativeAvEncodeSink`](crate::NativeAvEncodeSink).
395    pub video: Vec<NativeVideoEncoderCapability>,
396    /// Audio encoders accepted by [`NativeAvEncodeSink`](crate::NativeAvEncodeSink) and the
397    /// GPU-resident and packet-copy exports.
398    pub audio: Vec<NativeAudioEncoderCapability>,
399}
400
401/// Reports the encoders usable by the native sinks: the linked `FFmpeg`'s, and
402/// `OpenH264` when installed.
403///
404/// A hybrid encoder has one entry per mode. Order is usable hardware first, then software in
405/// `FFmpeg`'s order, then `OpenH264`; exactly one entry per codec is marked preferred.
406#[must_use]
407pub fn codec_capabilities() -> NativeCodecCapabilities {
408    let video = [
409        NativeVideoCodec::H264,
410        NativeVideoCodec::Hevc,
411        NativeVideoCodec::Vp9,
412        NativeVideoCodec::Av1,
413        NativeVideoCodec::Mpeg4Part2,
414        NativeVideoCodec::ProRes,
415    ]
416    .into_iter()
417    .flat_map(|codec| video_encoder_inventory(codec).usable)
418    .collect();
419    NativeCodecCapabilities {
420        video,
421        audio: audio_encoder_capabilities(),
422    }
423}
424
425/// Whether some encoder of `codec` takes `layout`'s pictures or their `P010` form.
426#[must_use]
427pub fn encodes_pixels(
428    capabilities: &NativeCodecCapabilities,
429    codec: NativeVideoCodec,
430    layout: alpha2_media::PixelLayout,
431) -> bool {
432    capabilities
433        .video
434        .iter()
435        .filter(|capability| capability.codec == codec)
436        .any(|capability| {
437            capability.encoder_name == crate::OPENH264_ENCODER_NAME && layout.is_yuv420p()
438                || std::ffi::CString::new(capability.encoder_name.as_str())
439                    .ok()
440                    .and_then(|name| AVCodec::find_encoder_by_name(&name))
441                    .is_some_and(|encoder| encoder_layout(&encoder, layout).is_some())
442        })
443}
444
445/// The audio encoders [`codec_capabilities`] lists, without probing video encoders.
446#[must_use]
447pub fn audio_encoder_capabilities() -> Vec<NativeAudioEncoderCapability> {
448    NativeAudioCodec::ALL
449        .into_iter()
450        .filter_map(|codec| {
451            let encoder = audio_encoder(codec)?;
452            Some(NativeAudioEncoderCapability {
453                codec,
454                encoder_name: encoder.name().to_string_lossy().into_owned(),
455            })
456        })
457        .collect()
458}
459
460/// The encoders of one codec this machine runs, and the ones its open probe refused.
461#[derive(Clone, Debug, Default, Eq, PartialEq)]
462pub struct EncoderInventory {
463    /// The encoders usable for the codec, as [`codec_capabilities`] lists them.
464    pub usable: Vec<NativeVideoEncoderCapability>,
465    /// Registered encoders whose open probe failed, and why. A busy one is asked again next time.
466    pub refused: Vec<EncoderRefusal>,
467}
468
469/// The usable encoders for `codec` alone, and the ones whose probe refused.
470///
471/// Only this codec's encoders are open-probed.
472#[must_use]
473pub fn video_encoder_inventory(codec: NativeVideoCodec) -> EncoderInventory {
474    let (choices, refused) = available_video_encoders(codec);
475    let usable = choices
476        .into_iter()
477        .enumerate()
478        .map(|(index, choice)| {
479            let encoder_name = choice.name();
480            let (minimum_width, minimum_height) = encoder_minimum_dimensions(&encoder_name);
481            NativeVideoEncoderCapability {
482                codec,
483                class: choice.class(),
484                hardware_accelerated: choice.on_hardware(),
485                preferred: index == 0,
486                encoder_name,
487                minimum_width,
488                minimum_height,
489                library: match &choice {
490                    VideoEncoderChoice::Ffmpeg(_) => None,
491                    VideoEncoderChoice::OpenH264(library) => {
492                        Some(NativeEncoderLibrary::openh264(library))
493                    }
494                },
495            }
496        })
497        .collect();
498    EncoderInventory { usable, refused }
499}
500
501/// One encoder an export can try: a registered `FFmpeg` one, or `OpenH264`.
502pub(crate) enum VideoEncoderChoice {
503    Ffmpeg(VideoEncoderCandidate),
504    OpenH264(std::sync::Arc<crate::OpenH264Library>),
505}
506
507impl VideoEncoderChoice {
508    pub(crate) fn name(&self) -> String {
509        match self {
510            Self::Ffmpeg(candidate) => candidate.name(),
511            Self::OpenH264(_) => crate::OPENH264_ENCODER_NAME.to_owned(),
512        }
513    }
514
515    pub(crate) const fn on_hardware(&self) -> bool {
516        match self {
517            Self::Ffmpeg(candidate) => candidate.on_hardware,
518            Self::OpenH264(_) => false,
519        }
520    }
521
522    pub(crate) fn class(&self) -> NativeEncoderClass {
523        match self {
524            Self::Ffmpeg(candidate) => encoder_class(&candidate.encoder),
525            Self::OpenH264(_) => NativeEncoderClass::Software,
526        }
527    }
528}
529
530/// Every encoder for `codec` this machine runs, most preferred first, plus the probe refusals.
531/// `OpenH264` comes last because `libx264` compresses better where present.
532pub(crate) fn available_video_encoders(
533    codec: NativeVideoCodec,
534) -> (Vec<VideoEncoderChoice>, Vec<EncoderRefusal>) {
535    let (mut choices, refused) = compatible_encoders(registered_video_encoders(codec));
536    if codec == NativeVideoCodec::H264
537        && let Some(library) = crate::openh264_status().library()
538    {
539        choices.push(VideoEncoderChoice::OpenH264(std::sync::Arc::clone(library)));
540    }
541    (choices, refused)
542}
543
544/// `candidates` split into those that run here and probe refusals; ones the registration
545/// rules out are in neither.
546fn compatible_encoders(
547    candidates: Vec<VideoEncoderCandidate>,
548) -> (Vec<VideoEncoderChoice>, Vec<EncoderRefusal>) {
549    let mut choices = Vec::new();
550    let mut refused = Vec::new();
551    for candidate in candidates {
552        let name = candidate.name();
553        if !registration_is_compatible(&candidate, &name) {
554            continue;
555        }
556        match encoder_runs_here(&candidate, &name) {
557            Ok(()) => choices.push(VideoEncoderChoice::Ffmpeg(candidate)),
558            Err(refusal) => refused.push(refusal),
559        }
560    }
561    (choices, refused)
562}
563
564/// One codec's hardware decode registration for this platform's backend.
565#[derive(Clone, Debug, Eq, PartialEq)]
566pub struct NativeHardwareDecodeCapability {
567    /// Logical codec.
568    pub codec: NativeVideoCodec,
569    /// Concrete `FFmpeg` decoder implementation, when one is linked.
570    pub decoder_name: Option<String>,
571    /// Whether that decoder advertises this platform's hardware configuration.
572    ///
573    /// Registration only; device, profile and surface support are decided when a stream opens.
574    pub hardware_config: bool,
575    /// The decoder a software session opens, or `None` when only hardware can decode the
576    /// codec (AV1 without libdav1d).
577    pub software_decoder: Option<String>,
578}
579
580/// Hardware decode registration reported by the linked `FFmpeg` build.
581#[derive(Clone, Debug, Eq, PartialEq)]
582pub struct NativeHardwareDecodeCapabilities {
583    /// Stable backend identifier, or `none` on a build without a GPU path.
584    pub backend: &'static str,
585    /// One entry per codec alpha2 can present from a decoder surface.
586    pub codecs: Vec<NativeHardwareDecodeCapability>,
587}
588
589/// Backend this build decodes with, independent of any device being present.
590#[must_use]
591pub const fn hardware_decode_backend() -> &'static str {
592    #[cfg(feature = "hwdecode")]
593    {
594        #[cfg(target_os = "linux")]
595        {
596            "vulkan"
597        }
598        #[cfg(target_os = "macos")]
599        {
600            "videotoolbox"
601        }
602        #[cfg(target_os = "windows")]
603        {
604            "d3d12va"
605        }
606        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
607        {
608            "none"
609        }
610    }
611    #[cfg(not(feature = "hwdecode"))]
612    {
613        "none"
614    }
615}
616
617/// Reports which codecs the linked `FFmpeg` registers for this backend.
618#[must_use]
619pub fn hardware_decode_capabilities() -> NativeHardwareDecodeCapabilities {
620    let codecs = [
621        NativeVideoCodec::H264,
622        NativeVideoCodec::Hevc,
623        NativeVideoCodec::Av1,
624        NativeVideoCodec::Vp9,
625    ]
626    .into_iter()
627    .map(|codec| {
628        let decoder = AVCodec::find_decoder(codec.codec_id());
629        NativeHardwareDecodeCapability {
630            codec,
631            decoder_name: decoder
632                .as_ref()
633                .map(|decoder| decoder.name().to_string_lossy().into_owned()),
634            hardware_config: crate::decode::registered_video_decoders(codec.codec_id())
635                .any(|decoder| decoder_has_hardware_config(&decoder)),
636            software_decoder: crate::decode::software_video_decoder(codec.codec_id())
637                .ok()
638                .map(|decoder| decoder.name().to_string_lossy().into_owned()),
639        }
640    })
641    .collect();
642    NativeHardwareDecodeCapabilities {
643        backend: hardware_decode_backend(),
644        codecs,
645    }
646}
647
648/// Whether `decoder` advertises this platform's hardware device configuration.
649#[allow(unused_variables)]
650fn decoder_has_hardware_config(decoder: &AVCodecRef<'_>) -> bool {
651    #[cfg(all(
652        any(target_os = "linux", target_os = "macos", target_os = "windows"),
653        feature = "hwdecode"
654    ))]
655    {
656        #[cfg(target_os = "linux")]
657        let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_VULKAN, ffi::AV_PIX_FMT_VULKAN);
658        #[cfg(target_os = "macos")]
659        let (device_type, pixel_format) = (
660            ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
661            ffi::AV_PIX_FMT_VIDEOTOOLBOX,
662        );
663        #[cfg(target_os = "windows")]
664        let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_D3D12VA, ffi::AV_PIX_FMT_D3D12);
665        (0..)
666            .map_while(|index| decoder.hw_config(index))
667            .any(|config| config.device_type == device_type && config.pix_fmt == pixel_format)
668    }
669    #[cfg(not(all(
670        any(target_os = "linux", target_os = "macos", target_os = "windows"),
671        feature = "hwdecode"
672    )))]
673    {
674        false
675    }
676}
677
678/// Encoders named in `ALPHA2_TEST_SKIP_ENCODERS` (comma separated), hidden in test builds as if
679/// unregistered. Valgrind runs use it for `libsvtav1`, which reads uninitialized memory.
680#[cfg(test)]
681pub(crate) fn test_skips_encoder(name: &std::ffi::CStr) -> bool {
682    static SKIPPED: LazyLock<Vec<String>> = LazyLock::new(|| {
683        std::env::var("ALPHA2_TEST_SKIP_ENCODERS")
684            .unwrap_or_default()
685            .split(',')
686            .map(str::trim)
687            .filter(|name| !name.is_empty())
688            .map(str::to_owned)
689            .collect()
690    });
691    let name = name.to_string_lossy();
692    SKIPPED.iter().any(|skipped| *skipped == name)
693}
694
695#[cfg(not(test))]
696const fn test_skips_encoder(_name: &std::ffi::CStr) -> bool {
697    false
698}
699
700/// Every way to run each encoder registered for `codec`, GPU ones first.
701pub(crate) fn registered_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderCandidate> {
702    let hybrid_hardware = hybrid_encoders_use_hardware();
703    let mut candidates = AVCodec::iterate()
704        .filter(|encoder| {
705            encoder.id == codec.codec_id()
706                // Only `prores_ks` takes the 4444 alpha and vendor options alpha2 sets.
707                && (codec != NativeVideoCodec::ProRes || encoder.name() == c"prores_ks")
708                && !test_skips_encoder(encoder.name())
709                && AVCodec::find_encoder_by_name(encoder.name())
710                    .is_some_and(|found| found.as_ptr() == encoder.as_ptr())
711        })
712        .flat_map(|encoder| {
713            encoder_class(&encoder)
714                .modes(hybrid_hardware)
715                .iter()
716                .filter_map(move |&on_hardware| {
717                    AVCodec::find_encoder_by_name(encoder.name()).map(|encoder| {
718                        VideoEncoderCandidate {
719                            encoder,
720                            on_hardware,
721                        }
722                    })
723                })
724        })
725        .collect::<Vec<_>>();
726    // Stable sort: FFmpeg's registration order within each tier, hardware first.
727    candidates.sort_by_key(|candidate| !candidate.on_hardware);
728    candidates
729}
730
731/// Classifies `encoder` from the capability bits it declares.
732pub(crate) fn encoder_class(encoder: &AVCodec) -> NativeEncoderClass {
733    let name = encoder.name().to_string_lossy();
734    declared_encoder_class(&name, u32::try_from(encoder.capabilities).unwrap_or(0))
735}
736
737/// Encoders the pinned runtime declares as software although they run only on hardware.
738///
739/// `FFmpeg` 8.x declares `h264_videotoolbox` with neither `AV_CODEC_CAP_HARDWARE` nor
740/// `AV_CODEC_CAP_HYBRID` yet opens it with `allow_sw=0`. An entry applies only while the
741/// runtime declares neither bit.
742const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
743
744/// The class from `capabilities`, except that a [`MISDECLARED_HARDWARE_ENCODERS`] entry declaring
745/// neither bit is `Hardware`.
746pub(crate) fn declared_encoder_class(name: &str, capabilities: u32) -> NativeEncoderClass {
747    if capabilities & ffi::AV_CODEC_CAP_HYBRID != 0 {
748        NativeEncoderClass::Hybrid
749    } else if capabilities & ffi::AV_CODEC_CAP_HARDWARE != 0
750        || MISDECLARED_HARDWARE_ENCODERS.contains(&name)
751    {
752        NativeEncoderClass::Hardware
753    } else {
754        NativeEncoderClass::Software
755    }
756}
757
758#[cfg(test)]
759pub(crate) fn encoder_is_compatible(candidate: &VideoEncoderCandidate) -> bool {
760    let name = candidate.name();
761    registration_is_compatible(candidate, &name) && encoder_runs_here(candidate, &name).is_ok()
762}
763
764/// What the registration alone rules out: an encoder that cannot take its codec's base pictures
765/// ([`NativeVideoCodec::base_layout`]), or a hardware one whose device has no node here.
766fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
767    // The native sink submits software frames, so encoders needing an `AVHWFramesContext` are out.
768    let codec = candidate.codec();
769    if encoder_layout(&candidate.encoder, codec.base_layout()).is_none() {
770        return false;
771    }
772    !candidate.on_hardware || hardware_device_node(name) != Some(false)
773}
774
775/// The pictures `encoder` takes for `layout`: `layout` itself, or its `P010` form for an encoder
776/// that takes 10-bit 4:2:0 only that way (NVENC); `None` when neither.
777pub(crate) fn encoder_layout(
778    encoder: &AVCodec,
779    layout: alpha2_media::PixelLayout,
780) -> Option<alpha2_media::PixelLayout> {
781    let Some(formats) = encoder.pix_fmts() else {
782        return Some(layout);
783    };
784    [layout, layout.as_p010()]
785        .into_iter()
786        .find(|candidate| formats.contains(&crate::convert::pixel_format(*candidate)))
787}
788
789/// Whether the encoder is listed only after an open probe ([`encoder_runs_here`]).
790///
791/// This covers hardware encoders without a device node, NVENC, and every `_mf` or `_videotoolbox`
792/// encoder. `VideoToolbox` is probed by name because a runtime may misdeclare its class, and an
793/// NVIDIA device node does not imply NVENC (compute-only GPUs have none).
794pub(crate) fn needs_open_probe(on_hardware: bool, name: &str) -> bool {
795    (on_hardware && (hardware_device_node(name).is_none() || name.ends_with("_nvenc")))
796        || name.ends_with("_mf")
797        || name.ends_with("_videotoolbox")
798}
799
800/// Whether `encoder` encodes a frame on this machine, for encoders whose
801/// registration does not prove it.
802///
803/// `MediaFoundation` registers every `_mf` encoder whether or not a working transform exists, and
804/// hardware encoders register without their device (`VideoToolbox` in a headless VM, NVENC without
805/// the driver). Such encoders are listed only after encoding one 256x144 frame with export options.
806/// Verdicts that cannot change (ran, or absent) are cached per process and mode, and so is a
807/// run of [`UNCLASSIFIED_FAILURES_KEPT`] unclassified failures. Busy failures are asked again.
808///
809/// Probes run lazily per codec, since each NVENC probe creates a CUDA context (about 0.4 s).
810fn encoder_runs_here(candidate: &VideoEncoderCandidate, name: &str) -> Result<(), EncoderRefusal> {
811    probe_outcome(candidate, name, true).unwrap_or(Ok(()))
812}
813
814/// A probe answer that cannot change while the process runs.
815#[derive(Clone, Debug, Eq, PartialEq)]
816pub(crate) enum ProbeVerdict {
817    /// The encoder encoded a frame.
818    Runs,
819    /// The encoder failed in a way asking again would repeat, with the failure kind and reason.
820    Refused(OpenFailureKind, String),
821}
822
823/// Unclassified probe failures in a row after which the last one is kept as the verdict.
824///
825/// Some transforms leak resources on every failed open, so a long-lived host must stop asking.
826pub(crate) const UNCLASSIFIED_FAILURES_KEPT: u8 = 3;
827
828/// What this process knows about one encoder and way it runs.
829#[derive(Debug, Default)]
830pub(crate) struct ProbeEntry {
831    verdict: Option<ProbeVerdict>,
832    unclassified_failures: u8,
833}
834
835/// The probe outcomes kept for this process, by encoder name and way it runs.
836pub(crate) type ProbeCache = Mutex<HashMap<(String, bool), ProbeEntry>>;
837
838/// `Some(Ok)` for an encoder that needs no probe or passed one, `Some(Err)` for one that failed,
839/// and `None` for one not probed yet when `probe` is unset.
840fn probe_outcome(
841    candidate: &VideoEncoderCandidate,
842    name: &str,
843    probe: bool,
844) -> Option<Result<(), EncoderRefusal>> {
845    static VERDICTS: LazyLock<ProbeCache> = LazyLock::new(Mutex::default);
846    if !needs_open_probe(candidate.on_hardware, name) {
847        return Some(Ok(()));
848    }
849    cached_probe(
850        &VERDICTS,
851        name,
852        candidate.on_hardware,
853        probe.then_some(|| probe_open(candidate, name)),
854    )
855}
856
857/// The cached verdict for `name` and mode, else `probe`'s answer (cached when it cannot change, or
858/// after [`UNCLASSIFIED_FAILURES_KEPT`] unclassified failures in a row); `None` when nothing is
859/// cached and there is no `probe`.
860pub(crate) fn cached_probe(
861    cache: &ProbeCache,
862    name: &str,
863    on_hardware: bool,
864    probe: Option<impl FnOnce() -> Result<(), (OpenFailureKind, String)>>,
865) -> Option<Result<(), EncoderRefusal>> {
866    let refusal = |kind, reason| EncoderRefusal {
867        encoder: name.to_owned(),
868        hardware_accelerated: on_hardware,
869        kind,
870        reason,
871    };
872    // Held across the probe, so two exports asking at once probe once.
873    let mut entries = cache
874        .lock()
875        .unwrap_or_else(std::sync::PoisonError::into_inner);
876    let entry = entries.entry((name.to_owned(), on_hardware)).or_default();
877    match &entry.verdict {
878        Some(ProbeVerdict::Runs) => return Some(Ok(())),
879        Some(ProbeVerdict::Refused(kind, reason)) => {
880            return Some(Err(refusal(*kind, reason.clone())));
881        }
882        None => {}
883    }
884    let outcome = probe?();
885    entry.unclassified_failures = match &outcome {
886        Err((OpenFailureKind::Unknown, _)) => entry.unclassified_failures.saturating_add(1),
887        _ => 0,
888    };
889    entry.verdict = match &outcome {
890        Ok(()) => Some(ProbeVerdict::Runs),
891        Err((kind @ (OpenFailureKind::Absent | OpenFailureKind::Unsuitable), reason)) => {
892            Some(ProbeVerdict::Refused(*kind, reason.clone()))
893        }
894        Err((OpenFailureKind::Unknown, reason))
895            if entry.unclassified_failures >= UNCLASSIFIED_FAILURES_KEPT =>
896        {
897            Some(ProbeVerdict::Refused(
898                OpenFailureKind::Unknown,
899                reason.clone(),
900            ))
901        }
902        Err(_) => None,
903    };
904    if let Err((kind, error)) = &outcome {
905        alpha2_os::diagnostic!(
906            "alpha2 native: video encoder {name} {} is registered but did not encode a frame on \
907             this machine, so it is not offered that way ({}{}): {error}",
908            mode_label(on_hardware),
909            kind.as_str(),
910            if entry.verdict.is_some() {
911                ""
912            } else {
913                "; asked again at the next export"
914            }
915        );
916    }
917    Some(outcome.map_err(|(kind, reason)| refusal(kind, reason)))
918}
919
920/// Encodes one probe frame and classifies a failure from what the encoder logged.
921fn probe_open(
922    candidate: &VideoEncoderCandidate,
923    name: &str,
924) -> Result<(), (OpenFailureKind, String)> {
925    let mark = alpha2_ffmpeg_bridge::log_mark();
926    match probe_software_encode(candidate, name, 256, 144, None).0 {
927        None => Ok(()),
928        Some(error) => {
929            let lines = alpha2_ffmpeg_bridge::log_since(mark, &[name]);
930            Err((
931                classify_open_failure(name, &error, &lines),
932                format!("{error}{}", alpha2_ffmpeg_bridge::log_detail(&lines)),
933            ))
934        }
935    }
936}
937
938/// What a failed open of `encoder` is evidence of, from its error and logged `lines`.
939///
940/// Only fixed-wording statuses are read: NVENC's parenthesized status, Vulkan's `VkResult`,
941/// `VideoToolbox`'s `OSStatus`, and `MediaFoundation`'s missing transform or `E_FAIL`. Anything
942/// else is [`OpenFailureKind::Unknown`], never busy and cached only after repeats, so a wording
943/// change costs re-probes rather than a wrong class.
944pub(crate) fn classify_open_failure(
945    encoder: &str,
946    error: &str,
947    lines: &[LogLine],
948) -> OpenFailureKind {
949    let said =
950        |words: &str| error.contains(words) || lines.iter().any(|line| line.text.contains(words));
951    if encoder.ends_with("_nvenc") {
952        // OUT_OF_MEMORY, LOCK_BUSY, ENCODER_BUSY and INCOMPATIBLE_CLIENT_KEY are how a
953        // consumer GPU reports its session limit (some drivers answer 21); 1-5 mean no or
954        // unsupported device. The status decides first, since `FFmpeg` logs "No capable
955        // devices found" after any failed device.
956        return match lines.iter().find_map(|line| nvenc_status(&line.text)) {
957            Some(10 | 13 | 18 | 21) => OpenFailureKind::Busy,
958            Some(1..=5) => OpenFailureKind::Absent,
959            None if said("No capable devices found") || said("Cannot load") => {
960                OpenFailureKind::Absent
961            }
962            _ => OpenFailureKind::Unknown,
963        };
964    }
965    if encoder.ends_with("_vulkan") {
966        let busy = lines.iter().any(|line| {
967            line.text.contains("Unable to create video session")
968                && (line.text.contains("VK_ERROR_OUT_OF_DEVICE_MEMORY")
969                    || line.text.contains("VK_ERROR_TOO_MANY_OBJECTS"))
970        });
971        return if busy {
972            OpenFailureKind::Busy
973        } else {
974            OpenFailureKind::Unknown
975        };
976    }
977    if encoder.ends_with("_videotoolbox") {
978        // kVTVideoEncoderNotAvailableNowErr and kVTCouldNotFindVideoEncoderErr.
979        if said("-12915") {
980            return OpenFailureKind::Busy;
981        }
982        if said("-12908") {
983            return OpenFailureKind::Absent;
984        }
985        return OpenFailureKind::Unknown;
986    }
987    // 80004005 is E_FAIL as `mfenc` logs a failed `ProcessInput`.
988    if encoder.ends_with("_mf")
989        && (said("could not find any MFT") || said("E_FAIL") || said("80004005"))
990    {
991        return OpenFailureKind::Absent;
992    }
993    OpenFailureKind::Unknown
994}
995
996/// The first parenthesized number in an `nvenc_print_error` line.
997fn nvenc_status(line: &str) -> Option<u32> {
998    line.match_indices('(').find_map(|(start, _)| {
999        let rest = &line[start + 1..];
1000        let end = rest.find(')')?;
1001        let digits = &rest[..end];
1002        (!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()))
1003            .then(|| digits.parse().ok())
1004            .flatten()
1005    })
1006}
1007
1008/// The encoders [`codec_capabilities`] would list from what is known so far, without running any
1009/// probe; unprobed encoders are left out. For a startup log line.
1010#[must_use]
1011pub fn video_encoders_without_probing() -> (Vec<NativeVideoEncoderCapability>, Vec<String>) {
1012    let mut unprobed = Vec::new();
1013    let mut listed = Vec::new();
1014    for codec in [
1015        NativeVideoCodec::H264,
1016        NativeVideoCodec::Hevc,
1017        NativeVideoCodec::Vp9,
1018        NativeVideoCodec::Av1,
1019        NativeVideoCodec::Mpeg4Part2,
1020        NativeVideoCodec::ProRes,
1021    ] {
1022        for candidate in registered_video_encoders(codec) {
1023            let name = candidate.name();
1024            if !registration_is_compatible(&candidate, &name) {
1025                continue;
1026            }
1027            match probe_outcome(&candidate, &name, false) {
1028                Some(Ok(())) => {
1029                    let (minimum_width, minimum_height) = encoder_minimum_dimensions(&name);
1030                    listed.push(NativeVideoEncoderCapability {
1031                        codec,
1032                        class: encoder_class(&candidate.encoder),
1033                        hardware_accelerated: candidate.on_hardware,
1034                        preferred: !listed
1035                            .iter()
1036                            .any(|listed: &NativeVideoEncoderCapability| listed.codec == codec),
1037                        encoder_name: name,
1038                        minimum_width,
1039                        minimum_height,
1040                        library: None,
1041                    });
1042                }
1043                Some(Err(_)) => {}
1044                None => unprobed.push(format!("{name} {}", mode_label(candidate.on_hardware))),
1045            }
1046        }
1047    }
1048    (listed, unprobed)
1049}
1050
1051pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
1052    if on_hardware { "on GPU" } else { "on CPU" }
1053}
1054
1055/// Whether a hardware encoder's device has a node here, for families Linux knows a node
1056/// for; `None` for the rest.
1057///
1058/// A node replaces the open probe for those families, except NVENC, where it only gates the probe
1059/// so machines without an NVIDIA GPU pay nothing.
1060#[cfg(target_os = "linux")]
1061fn hardware_device_node(name: &str) -> Option<bool> {
1062    if name.ends_with("_nvenc") {
1063        return Some(Path::new("/dev/nvidia0").exists() || Path::new("/dev/nvidiactl").exists());
1064    }
1065    if name.ends_with("_vaapi") || name.ends_with("_qsv") || name.ends_with("_amf") {
1066        return Some(fs::read_dir("/dev/dri").is_ok_and(|entries| {
1067            entries.flatten().any(|entry| {
1068                entry
1069                    .file_name()
1070                    .to_str()
1071                    .is_some_and(|name| name.starts_with("renderD") || name.starts_with("card"))
1072            })
1073        }));
1074    }
1075    if name.ends_with("_v4l2m2m") {
1076        return Some((0..64).any(|index| Path::new(&format!("/dev/video{index}")).exists()));
1077    }
1078    None
1079}
1080
1081/// Off Linux there is no device node to check; the open probe decides.
1082#[cfg(not(target_os = "linux"))]
1083const fn hardware_device_node(_name: &str) -> Option<bool> {
1084    None
1085}
1086
1087/// Why an export of `codec` has no encoder, and what to do about it.
1088///
1089/// Names why the GPU path declined (`gpu_output_unavailable`), what the runtime offers instead
1090/// (`offered`), and for H.264 whether `OpenH264` is installed.
1091#[must_use]
1092pub fn no_video_encoder_message(
1093    codec: NativeVideoCodec,
1094    offered: &[NativeVideoEncoderCapability],
1095    gpu_output_unavailable: Option<&str>,
1096) -> String {
1097    let identity = crate::ffmpeg_runtime_identity();
1098    let inventory = offered
1099        .iter()
1100        .filter(|capability| capability.library.is_none())
1101        .map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
1102        .collect::<Vec<_>>()
1103        .join(", ");
1104    let mut message = format!("no usable {} encoder: ", codec.as_str());
1105    if let Some(reason) = gpu_output_unavailable {
1106        let _ = write!(message, "GPU output path unavailable ({reason}); ");
1107    }
1108    let _ = write!(
1109        message,
1110        "the linked FFmpeg runtime ({}, pinned SDK {}) offers [{}]",
1111        identity.version,
1112        identity.pinned_sdk_release,
1113        if inventory.is_empty() {
1114            "none"
1115        } else {
1116            inventory.as_str()
1117        }
1118    );
1119    if codec != NativeVideoCodec::H264 {
1120        return message;
1121    }
1122    let status = crate::openh264_status();
1123    if let Some(reason) = status.reason() {
1124        let _ = write!(
1125            message,
1126            "; OpenH264 is {}: {reason}",
1127            status.as_str().replace('-', " ")
1128        );
1129    }
1130    let _ = write!(message, ". To export H.264 here: {}", h264_encoder_remedy());
1131    message
1132}
1133
1134/// Remedies for a machine without a CPU-frame H.264 encoder: install `OpenH264`, use an `FFmpeg`
1135/// with `libx264`, or export another codec.
1136#[must_use]
1137pub fn h264_encoder_remedy() -> String {
1138    let openh264 = match crate::openh264_status() {
1139        crate::OpenH264Status::Disabled => {
1140            "unset ALPHA2_OPENH264=0 to use the installed OpenH264 encoder".to_owned()
1141        }
1142        _ => format!(
1143            "install the OpenH264 encoder ({})",
1144            crate::OPENH264_INSTALL_COMMAND
1145        ),
1146    };
1147    format!(
1148        "{openh264}, build alpha2-host from source against an FFmpeg 8 that has libx264, or \
1149         export VP9 (.webm) or AV1 instead"
1150    )
1151}
1152
1153/// [`no_video_encoder_message`] as an encode error, with `codec`'s probe refusals so a busy encoder
1154/// is told from a missing one.
1155fn unavailable_codec_error(codec: NativeVideoCodec, refused: Vec<EncoderRefusal>) -> EncodeError {
1156    let message = no_video_encoder_message(codec, &codec_capabilities().video, None);
1157    if refused.is_empty() {
1158        return EncodeError::Adapter(message);
1159    }
1160    EncodeError::EncodersRefused {
1161        message: format!("{}; {message}", refusal_list(&refused)),
1162        refusals: refused,
1163    }
1164}
1165
1166/// `refusals` for a message: `"h264_nvenc on GPU (busy): <reason>; ..."`.
1167fn refusal_list(refusals: &[EncoderRefusal]) -> String {
1168    refusals
1169        .iter()
1170        .map(|refusal| {
1171            format!(
1172                "{} {} ({}): {}",
1173                refusal.encoder,
1174                mode_label(refusal.hardware_accelerated),
1175                refusal.kind.as_str(),
1176                refusal.reason
1177            )
1178        })
1179        .collect::<Vec<_>>()
1180        .join("; ")
1181}
1182
1183/// [`video_encoder_candidates_and_refusals`] without the refusals.
1184#[cfg(test)]
1185pub(crate) fn video_encoder_candidates(
1186    codec: NativeVideoCodec,
1187    requested_name: Option<&str>,
1188) -> Result<Vec<VideoEncoderChoice>, EncodeError> {
1189    video_encoder_candidates_and_refusals(codec, requested_name).map(|(candidates, _)| candidates)
1190}
1191
1192/// Encoders to try for `codec`, most preferred first, and the probe refusals left out.
1193///
1194/// A pinned encoder is tried alone (no silent substitution), each way it runs.
1195fn video_encoder_candidates_and_refusals(
1196    codec: NativeVideoCodec,
1197    requested_name: Option<&str>,
1198) -> Result<(Vec<VideoEncoderChoice>, Vec<EncoderRefusal>), EncodeError> {
1199    if requested_name == Some(crate::OPENH264_ENCODER_NAME) {
1200        if codec != NativeVideoCodec::H264 {
1201            return Err(EncodeError::Adapter(format!(
1202                "video encoder {} does not implement {}",
1203                crate::OPENH264_ENCODER_NAME,
1204                codec.as_str()
1205            )));
1206        }
1207        let status = crate::openh264_status();
1208        return match status.library() {
1209            Some(library) => Ok((
1210                vec![VideoEncoderChoice::OpenH264(std::sync::Arc::clone(library))],
1211                Vec::new(),
1212            )),
1213            None => Err(EncodeError::Adapter(format!(
1214                "video encoder {} is unavailable: {}{}",
1215                crate::OPENH264_ENCODER_NAME,
1216                status.reason().unwrap_or_default(),
1217                status
1218                    .remedy()
1219                    .map(|remedy| format!("; {remedy}"))
1220                    .unwrap_or_default()
1221            ))),
1222        };
1223    }
1224    if let Some(name) = requested_name {
1225        let encoder = select_video_encoder(codec, Some(name))?;
1226        let (candidates, refused) = compatible_encoders(
1227            registered_video_encoders(codec)
1228                .into_iter()
1229                .filter(|candidate| candidate.encoder.as_ptr() == encoder.as_ptr())
1230                .collect(),
1231        );
1232        if candidates.is_empty() && !refused.is_empty() {
1233            return Err(EncodeError::EncodersRefused {
1234                message: refusal_list(&refused),
1235                refusals: refused,
1236            });
1237        }
1238        return Ok((candidates, refused));
1239    }
1240    let (candidates, refused) = available_video_encoders(codec);
1241    if candidates.is_empty() {
1242        return Err(unavailable_codec_error(codec, refused));
1243    }
1244    Ok((candidates, refused))
1245}
1246
1247/// The one encoder an export opens without fallback: the pinned `requested_name` (which must
1248/// implement `codec` and be advertised), else the codec's preferred encoder.
1249pub(crate) fn select_video_encoder(
1250    codec: NativeVideoCodec,
1251    requested_name: Option<&str>,
1252) -> Result<AVCodecRef<'static>, EncodeError> {
1253    if let Some(name) = requested_name {
1254        let name_c = CString::new(name)
1255            .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1256        let encoder = AVCodec::find_encoder_by_name(&name_c)
1257            .ok_or_else(|| EncodeError::Adapter(format!("video encoder {name} is unavailable")))?;
1258        if encoder.id != codec.codec_id() {
1259            return Err(EncodeError::Adapter(format!(
1260                "video encoder {name} does not implement {}",
1261                codec.as_str()
1262            )));
1263        }
1264        let inventory = video_encoder_inventory(codec);
1265        if !inventory
1266            .usable
1267            .iter()
1268            .any(|capability| capability.encoder_name == name)
1269        {
1270            let refused = inventory
1271                .refused
1272                .into_iter()
1273                .filter(|refusal| refusal.encoder == name)
1274                .collect::<Vec<_>>();
1275            if !refused.is_empty() {
1276                return Err(EncodeError::EncodersRefused {
1277                    message: refusal_list(&refused),
1278                    refusals: refused,
1279                });
1280            }
1281            return Err(EncodeError::Adapter(format!(
1282                "video encoder {name} is not compatible with the native encode path"
1283            )));
1284        }
1285        return Ok(encoder);
1286    }
1287
1288    let inventory = video_encoder_inventory(codec);
1289    let Some(capability) = inventory
1290        .usable
1291        .into_iter()
1292        .find(|capability| capability.library.is_none())
1293    else {
1294        return Err(unavailable_codec_error(codec, inventory.refused));
1295    };
1296    let name = CString::new(capability.encoder_name)
1297        .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
1298    AVCodec::find_encoder_by_name(&name).ok_or_else(|| unavailable_codec_error(codec, Vec::new()))
1299}
1300
1301/// What one sink needs to open its video encoder.
1302pub(crate) struct VideoEncoderOpenRequest<'a> {
1303    /// Output format the encoder is configured for.
1304    pub config: &'a VideoEncodeConfig,
1305    /// Concrete encoder the caller pinned, if any.
1306    pub requested_name: Option<&'a str>,
1307    /// Software worker budget, asked for only when a CPU candidate opens.
1308    pub threads: SoftwareEncoderThreads<'a>,
1309    /// Whether the container wants the extradata in the stream header.
1310    pub global_header: bool,
1311    /// Coded width, already range-checked by the caller.
1312    pub width: i32,
1313    /// Coded height, already range-checked by the caller.
1314    pub height: i32,
1315}
1316
1317/// The worker count a software video encoder opens with, decided only when one opens.
1318///
1319/// The decode/encode CPU split depends on whether the encoder runs on the CPU, which is known only
1320/// after a candidate opens: a hardware encoder may fail and fall through to a software one, which
1321/// must not inherit the hardware split's zero share. Asked at most once.
1322pub(crate) struct SoftwareEncoderThreads<'a> {
1323    decide: &'a dyn Fn(Option<usize>) -> i64,
1324    decided: std::cell::OnceCell<i64>,
1325}
1326
1327impl<'a> SoftwareEncoderThreads<'a> {
1328    /// Defers the count to `decide`, which receives the encoder's own count when the picture
1329    /// fixes it ([`crate::CodecThreadRequest::encoder_fixed_threads`]).
1330    pub(crate) fn new(decide: &'a dyn Fn(Option<usize>) -> i64) -> Self {
1331        Self {
1332            decide,
1333            decided: std::cell::OnceCell::new(),
1334        }
1335    }
1336
1337    /// The decided count for an encoder that runs as many workers as it is given.
1338    pub(crate) fn get(&self) -> i64 {
1339        self.get_fixed(None)
1340    }
1341
1342    /// The decided count, for an encoder that runs `fixed` workers when it does.
1343    pub(crate) fn get_fixed(&self, fixed: Option<usize>) -> i64 {
1344        *self.decided.get_or_init(|| (self.decide)(fixed).max(1))
1345    }
1346}
1347
1348/// Opens the first candidate encoder this machine can actually run.
1349///
1350/// A registered encoder may still fail `avcodec_open2` (no MFT, no device). Each rejection is
1351/// recorded with a typed reason so the run reports which encoder it ended up on.
1352///
1353/// # Errors
1354/// Returns the last candidate's failure, prefixed with every earlier one, when none opens.
1355pub(crate) fn open_video_encoder(
1356    request: &VideoEncoderOpenRequest<'_>,
1357) -> Result<
1358    (
1359        crate::video_backend::VideoEncoderBackend,
1360        NativeVideoEncoderSelection,
1361    ),
1362    EncodeError,
1363> {
1364    let (candidates, refused) =
1365        video_encoder_candidates_and_refusals(request.config.codec, request.requested_name)?;
1366    open_first_video_encoder(request, candidates).map_err(|error| match error {
1367        // Probe refusals come first; a busy one among them decides the class.
1368        EncodeError::EncodersRefused {
1369            message,
1370            refusals: opened,
1371        } if !refused.is_empty() => EncodeError::EncodersRefused {
1372            message: format!("{}; {message}", refusal_list(&refused)),
1373            refusals: refused.into_iter().chain(opened).collect(),
1374        },
1375        error => error,
1376    })
1377}
1378
1379/// An opened encoder, how it runs the rate control, and its CPU workers.
1380struct OpenedVideoEncoder {
1381    backend: crate::video_backend::VideoEncoderBackend,
1382    rate_control: crate::AppliedRateControl,
1383    quantizer_refused: Option<String>,
1384    software_threads: Option<i64>,
1385    pixels: alpha2_media::PixelLayout,
1386}
1387
1388/// Opens the first of `candidates` that runs, recording why each earlier one did not.
1389pub(crate) fn open_first_video_encoder(
1390    request: &VideoEncoderOpenRequest<'_>,
1391    candidates: Vec<VideoEncoderChoice>,
1392) -> Result<
1393    (
1394        crate::video_backend::VideoEncoderBackend,
1395        NativeVideoEncoderSelection,
1396    ),
1397    EncodeError,
1398> {
1399    let mut rejected = Vec::new();
1400    let last = candidates.len().saturating_sub(1);
1401    for (index, candidate) in candidates.into_iter().enumerate() {
1402        let name = candidate.name();
1403        let class = candidate.class();
1404        let mark = alpha2_ffmpeg_bridge::log_mark();
1405        let opened = match &candidate {
1406            VideoEncoderChoice::Ffmpeg(candidate) => {
1407                open_one_video_encoder(request, candidate, &name).map(
1408                    |(encoder, rate_control, quantizer_refused, pixels)| OpenedVideoEncoder {
1409                        backend: crate::video_backend::VideoEncoderBackend::Ffmpeg(encoder),
1410                        rate_control,
1411                        quantizer_refused,
1412                        software_threads: (!candidate.on_hardware).then(|| request.threads.get()),
1413                        pixels,
1414                    },
1415                )
1416            }
1417            VideoEncoderChoice::OpenH264(library) => open_openh264(request, library),
1418        };
1419        match opened {
1420            Ok(opened) => {
1421                return Ok((
1422                    opened.backend,
1423                    NativeVideoEncoderSelection {
1424                        encoder_name: name,
1425                        class,
1426                        hardware_accelerated: candidate.on_hardware(),
1427                        rejected,
1428                        rate_control: opened.rate_control,
1429                        quantizer_refused: opened.quantizer_refused,
1430                        software_threads: opened.software_threads,
1431                        pixels: opened.pixels,
1432                    },
1433                ));
1434            }
1435            Err((reason, error)) => {
1436                let (kind, detail) = rejection_kind(&name, reason, &error, mark);
1437                if index == last {
1438                    let earlier = rejected
1439                        .iter()
1440                        .map(|fallback: &NativeVideoEncoderFallback| {
1441                            format!(
1442                                "{} {} ({}): {}",
1443                                fallback.encoder_name,
1444                                mode_label(fallback.hardware_accelerated),
1445                                fallback.reason.as_str(),
1446                                fallback.detail
1447                            )
1448                        })
1449                        .collect::<Vec<_>>();
1450                    let prefix = if earlier.is_empty() {
1451                        String::new()
1452                    } else {
1453                        format!("after {}; ", earlier.join("; "))
1454                    };
1455                    let message = format!(
1456                        "{prefix}{name} {} ({}): {detail}",
1457                        mode_label(candidate.on_hardware()),
1458                        reason.as_str()
1459                    );
1460                    let refusals = rejected
1461                        .iter()
1462                        .map(NativeVideoEncoderFallback::refusal)
1463                        .chain([EncoderRefusal {
1464                            encoder: name,
1465                            hardware_accelerated: candidate.on_hardware(),
1466                            kind,
1467                            reason: detail,
1468                        }])
1469                        .collect();
1470                    return Err(EncodeError::EncodersRefused { message, refusals });
1471                }
1472                alpha2_os::diagnostic!(
1473                    "alpha2 native export: video encoder {name} {} ({}) rejected: {detail}",
1474                    mode_label(candidate.on_hardware()),
1475                    reason.as_str()
1476                );
1477                rejected.push(NativeVideoEncoderFallback {
1478                    encoder_name: name,
1479                    hardware_accelerated: candidate.on_hardware(),
1480                    reason,
1481                    kind,
1482                    detail,
1483                });
1484            }
1485        }
1486    }
1487    Err(unavailable_codec_error(request.config.codec, Vec::new()))
1488}
1489
1490/// What a rejection of `name` is evidence of, and its detail. Open failures are classified from the
1491/// lines logged since `mark`.
1492fn rejection_kind(
1493    name: &str,
1494    reason: NativeEncoderRejection,
1495    error: &EncodeError,
1496    mark: alpha2_ffmpeg_bridge::LogMark,
1497) -> (OpenFailureKind, String) {
1498    match reason {
1499        NativeEncoderRejection::OpenFailed => {
1500            let lines = alpha2_ffmpeg_bridge::log_since(mark, &[name]);
1501            let detail = error.to_string();
1502            (
1503                classify_open_failure(name, &detail, &lines),
1504                format!("{detail}{}", alpha2_ffmpeg_bridge::log_detail(&lines)),
1505            )
1506        }
1507        NativeEncoderRejection::Dimensions
1508        | NativeEncoderRejection::OptionsIgnored
1509        | NativeEncoderRejection::PixelFormat => (OpenFailureKind::Unsuitable, error.to_string()),
1510    }
1511}
1512
1513/// Opens `OpenH264` on constant QP when the rate control has a quantizer, else on bitrate.
1514///
1515/// Its thread count comes from the picture height (`openh264::encoder::slice_count`). The software
1516/// budget is still decided here because the rest of the export splits the CPU from it.
1517fn open_openh264(
1518    request: &VideoEncoderOpenRequest<'_>,
1519    library: &crate::OpenH264Library,
1520) -> Result<OpenedVideoEncoder, (NativeEncoderRejection, EncodeError)> {
1521    let config = request.config;
1522    if !config.pixels.is_yuv420p() {
1523        return Err((
1524            NativeEncoderRejection::PixelFormat,
1525            EncodeError::Adapter(format!(
1526                "OpenH264 writes 8-bit 4:2:0 only, not {}",
1527                config.pixels.ffmpeg_name()
1528            )),
1529        ));
1530    }
1531    validate_encoder_dimensions(crate::OPENH264_ENCODER_NAME, config.width, config.height)
1532        .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1533    let open_failed = |message: String| {
1534        (
1535            NativeEncoderRejection::OpenFailed,
1536            EncodeError::Adapter(message),
1537        )
1538    };
1539    if !request.global_header {
1540        return Err(open_failed(
1541            "OpenH264 writes its parameter sets into the stream header, which this container \
1542             lacks"
1543                .to_owned(),
1544        ));
1545    }
1546    let (rate, rate_control) = crate::rate_control::openh264_rate(config.rate_control);
1547    let time_base = encoder_time_base(config.frame_rate, None)
1548        .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1549    // OpenH264 runs one thread per slice whatever it is offered; the split reserves that count.
1550    let _ = request
1551        .threads
1552        .get_fixed(Some(usize::from(crate::openh264::thread_count(
1553            crate::openh264::slice_count(config.height),
1554        ))));
1555    let encoder = crate::openh264::OpenH264Encoder::open(
1556        library,
1557        &crate::openh264::OpenH264Settings {
1558            width: config.width,
1559            height: config.height,
1560            frame_rate: config.frame_rate,
1561            color: config.color,
1562            sample_aspect_ratio: config.display.sample_aspect_ratio,
1563            rate,
1564            keyframe_interval: u32::try_from(keyframe_interval(config.frame_rate)).unwrap_or(1),
1565        },
1566        time_base,
1567    )
1568    .map_err(open_failed)?;
1569    let threads = i64::from(encoder.threads());
1570    Ok(OpenedVideoEncoder {
1571        backend: crate::video_backend::VideoEncoderBackend::OpenH264(Box::new(encoder)),
1572        rate_control,
1573        quantizer_refused: None,
1574        software_threads: Some(threads),
1575        pixels: alpha2_media::PixelLayout::YUV420P,
1576    })
1577}
1578
1579/// An opened context, its rate control, why a quantizer was refused, and the pictures it takes.
1580type OpenedContext = (
1581    EncoderContext,
1582    crate::AppliedRateControl,
1583    Option<String>,
1584    alpha2_media::PixelLayout,
1585);
1586
1587/// Opens `candidate` on the quantizer when the encoder accepts it, otherwise on bitrate.
1588///
1589/// A refused quantizer is not a refused encoder: the export continues on
1590/// the bitrate and reports it.
1591fn open_one_video_encoder(
1592    request: &VideoEncoderOpenRequest<'_>,
1593    candidate: &VideoEncoderCandidate,
1594    name: &str,
1595) -> Result<OpenedContext, (NativeEncoderRejection, EncodeError)> {
1596    let pixels = encoder_layout(&candidate.encoder, request.config.pixels).ok_or_else(|| {
1597        (
1598            NativeEncoderRejection::PixelFormat,
1599            EncodeError::Adapter(format!(
1600                "{name} does not take {} pictures",
1601                request.config.pixels.ffmpeg_name()
1602            )),
1603        )
1604    })?;
1605    validate_encoder_dimensions(name, request.config.width, request.config.height)
1606        .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
1607    let refused = match crate::rate_control::quantizer_request(name, request.config.rate_control) {
1608        None => None,
1609        Some(Err(reason)) => Some(reason),
1610        Some(Ok(quantized)) => {
1611            match open_video_encoder_context(request, candidate, name, Some(&quantized), pixels) {
1612                Ok(encoder) => {
1613                    let applied = crate::AppliedRateControl::of_quantizer_request(name);
1614                    return Ok((encoder, applied, None, pixels));
1615                }
1616                Err((_, error)) => Some(format!("{} refused: {error}", quantized.describe())),
1617            }
1618        }
1619    };
1620    if let Some(reason) = &refused {
1621        alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
1622    }
1623    open_video_encoder_context(request, candidate, name, None, pixels)
1624        .map(|encoder| (encoder, crate::AppliedRateControl::Bitrate, refused, pixels))
1625}
1626
1627fn open_video_encoder_context(
1628    request: &VideoEncoderOpenRequest<'_>,
1629    candidate: &VideoEncoderCandidate,
1630    name: &str,
1631    quantized: Option<&crate::rate_control::MechanismRequest>,
1632    pixels: alpha2_media::PixelLayout,
1633) -> Result<EncoderContext, (NativeEncoderRejection, EncodeError)> {
1634    let mut encoder = EncoderContext::from(AVCodecContext::new(&candidate.encoder));
1635    encoder.set_width(request.width);
1636    encoder.set_height(request.height);
1637    encoder.set_pix_fmt(crate::convert::pixel_format(pixels));
1638    let rate = request.config.frame_rate;
1639    let time_base = encoder_time_base(rate, max_time_base_denominator(candidate.encoder.id))
1640        .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
1641    encoder.set_time_base(time_base);
1642    // Square pixels are the default; only another ratio is written.
1643    let sar = request.config.display.sample_aspect_ratio;
1644    if sar != alpha2_media::SampleAspectRatio::default() {
1645        encoder.set_sample_aspect_ratio(rsmpeg::avutil::ra(
1646            i32::try_from(sar.num).unwrap_or(1),
1647            i32::try_from(sar.den).unwrap_or(1),
1648        ));
1649    }
1650    encoder.set_framerate(rsmpeg::avutil::ra(
1651        i32::try_from(rate.numerator()).unwrap_or(i32::MAX),
1652        i32::try_from(rate.denominator()).unwrap_or(1),
1653    ));
1654    encoder.set_bit_rate(quantized.map_or_else(
1655        || i64::try_from(request.config.rate_control.bitrate).unwrap_or(i64::MAX),
1656        |quantized| quantized.bit_rate,
1657    ));
1658    encoder.set_gop_size(keyframe_interval(request.config.frame_rate));
1659    encoder.set_max_b_frames(encoder_max_b_frames(name));
1660    if request.global_header {
1661        let flags = encoder.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0);
1662        encoder.set_flags(flags);
1663    }
1664    // Hardware candidates never decide the software split.
1665    let threads = if candidate.on_hardware {
1666        0
1667    } else {
1668        request.threads.get()
1669    };
1670    let mut base = video_encoder_options(
1671        request.config.color,
1672        &candidate.encoder,
1673        candidate.on_hardware,
1674        threads,
1675        request.config.width,
1676    );
1677    if candidate.codec() == NativeVideoCodec::ProRes {
1678        for (key, value) in crate::convert::prores_options(pixels) {
1679            base = crate::convert::set_encoder_option(base, key, &value);
1680        }
1681    }
1682    let options = match quantized {
1683        Some(quantized) => quantized.dictionary(base),
1684        None => crate::rate_control::rate_control_options(name, request.config.rate_control, base),
1685    };
1686    let unused =
1687        alpha2_ffmpeg_bridge::open_codec_context(&mut encoder, options).map_err(|error| {
1688            let hint = match quantized {
1689                Some(_) => "",
1690                None => crate::rate_control::constant_rate_hint(name, request.config.rate_control),
1691            };
1692            let error = match encode_adapter_error(error) {
1693                EncodeError::Adapter(message) if !hint.is_empty() => {
1694                    EncodeError::Adapter(format!("{message}{hint}"))
1695                }
1696                error => error,
1697            };
1698            (NativeEncoderRejection::OpenFailed, error)
1699        })?;
1700    reject_unused_encode_options(unused)
1701        .map_err(|error| (NativeEncoderRejection::OptionsIgnored, error))?;
1702    Ok(encoder)
1703}
1704
1705/// Frames between forced keyframes: two seconds of output.
1706///
1707/// libavcodec's default of 12 is half a second at 24 fps. Two seconds keeps nearly all of the
1708/// quality gain of longer intervals (about 0.5-0.8 dB PSNR-Y over 12 on hardware H.264) while a
1709/// player can still seek within two seconds.
1710pub(crate) fn keyframe_interval(frame_rate: alpha2_time::FrameRate) -> i32 {
1711    let frames = (2 * u64::from(frame_rate.numerator()))
1712        .div_ceil(u64::from(frame_rate.denominator()))
1713        .max(1);
1714    i32::try_from(frames).unwrap_or(i32::MAX)
1715}
1716
1717/// The largest time base denominator encoders of `codec` can write, if limited. MPEG-4 Part 2 and
1718/// H.263 store it in 16 bits, so 1001/120000 (119.88 fps) is refused.
1719pub(crate) const fn max_time_base_denominator(codec: ffi::AVCodecID) -> Option<u32> {
1720    match codec {
1721        ffi::AV_CODEC_ID_MPEG4 | ffi::AV_CODEC_ID_H263 | ffi::AV_CODEC_ID_H263P => Some(65_535),
1722        _ => None,
1723    }
1724}
1725
1726/// The time base for frames at `rate`: the frame length (1001/30000 for 29.97 fps) when its
1727/// denominator is within `limit`, else 1/D for the D within `limit` with the smallest worst-case
1728/// rounding of frame times. 119.88 fps becomes 1/64000, off by at most 7/960000 s.
1729///
1730/// # Errors
1731/// For a rate whose terms do not fit an `FFmpeg` rational.
1732pub(crate) fn encoder_time_base(
1733    rate: alpha2_time::FrameRate,
1734    limit: Option<u32>,
1735) -> Result<AVRational, EncodeError> {
1736    let out_of_range = || EncodeError::Adapter("frame rate is out of range".into());
1737    let (num, den) = (u64::from(rate.numerator()), u64::from(rate.denominator()));
1738    let exact = || {
1739        Ok(rsmpeg::avutil::ra(
1740            i32::try_from(den).map_err(|_| out_of_range())?,
1741            i32::try_from(num).map_err(|_| out_of_range())?,
1742        ))
1743    };
1744    let Some(limit) = limit.map(u64::from).filter(|limit| num > *limit) else {
1745        return exact();
1746    };
1747    // Frame k is at k*den/num s, k*den*D/num units of 1/D. Its fractional part is a multiple of
1748    // 1/q, q = num / gcd(num, den*D), so the largest rounding is floor(q/2)/q units. A unit no
1749    // longer than a frame keeps every frame its own time.
1750    let mut best: Option<(u64, u64, u64)> = None;
1751    for units in num.div_ceil(den).max(1)..=limit {
1752        let q = num / gcd_u64(num, den * units);
1753        let (error, scale) = (q / 2, q * units);
1754        if best.is_none_or(|(_, best_error, best_scale)| {
1755            u128::from(error) * u128::from(best_scale) < u128::from(best_error) * u128::from(scale)
1756        }) {
1757            best = Some((units, error, scale));
1758        }
1759    }
1760    let (units, _, _) = best.ok_or_else(out_of_range)?;
1761    Ok(rsmpeg::avutil::ra(
1762        1,
1763        i32::try_from(units).map_err(|_| out_of_range())?,
1764    ))
1765}
1766
1767const fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
1768    while b != 0 {
1769        (a, b) = (b, a % b);
1770    }
1771    a
1772}
1773
1774pub(crate) fn encoder_max_b_frames(name: &str) -> i32 {
1775    // Keep VideoToolbox's default of no reordering: forced B-frames are device-dependent and can
1776    // produce invalid packet timestamps.
1777    if name.ends_with("_videotoolbox") {
1778        0
1779    } else {
1780        2
1781    }
1782}
1783
1784/// The smallest even width and height encoder `name` accepts.
1785#[must_use]
1786pub fn encoder_minimum_dimensions(name: &str) -> (u32, u32) {
1787    // NVIDIA AV1 requires at least 145x128, its H.264/HEVC encoders 145x49; sizes stay even.
1788    if name == "av1_nvenc" {
1789        (146, 128)
1790    } else if name.ends_with("_nvenc") {
1791        (146, 50)
1792    } else {
1793        (2, 2)
1794    }
1795}
1796
1797pub(crate) fn validate_encoder_dimensions(
1798    encoder_name: &str,
1799    width: u32,
1800    height: u32,
1801) -> Result<(), EncodeError> {
1802    let (minimum_width, minimum_height) = encoder_minimum_dimensions(encoder_name);
1803    if width < minimum_width || height < minimum_height {
1804        Err(EncodeError::Adapter(format!(
1805            "encoder {encoder_name} requires at least {minimum_width}x{minimum_height}, received {width}x{height}"
1806        )))
1807    } else {
1808        Ok(())
1809    }
1810}
1811
1812/// One hardware configuration a video encoder declares.
1813#[derive(Clone, Debug, Eq, PartialEq)]
1814pub struct NativeEncoderHardwareConfig {
1815    /// Hardware pixel format this configuration carries frames in.
1816    pub pixel_format: String,
1817    /// `FFmpeg` hardware device type backing it.
1818    pub device_type: String,
1819    /// `AV_CODEC_HW_CONFIG_METHOD_*` bits.
1820    pub methods: i32,
1821    /// Whether the encoder takes its frames through an `AVHWFramesContext`.
1822    pub needs_hardware_frames: bool,
1823}
1824
1825/// What one registered video encoder declares, and what happened when alpha2 opened it.
1826///
1827/// Shows registration, advertisement and instantiation side by side, per platform.
1828#[derive(Clone, Debug, Eq, PartialEq)]
1829// Independent observations of one encoder, reported side by side, not a state.
1830#[allow(clippy::struct_excessive_bools)]
1831pub struct NativeVideoEncoderProbe {
1832    /// Logical codec this encoder implements.
1833    pub codec: NativeVideoCodec,
1834    /// `FFmpeg` short name.
1835    pub encoder_name: String,
1836    /// Class taken from the encoder's own capability bits.
1837    pub class: NativeEncoderClass,
1838    /// Whether this row runs the encoder on the GPU.
1839    pub hardware_accelerated: bool,
1840    /// Raw `AV_CODEC_CAP_*` bits, so an unexpected class can be traced.
1841    pub capability_bits: u32,
1842    /// Pixel formats the encoder declares, or empty when it declares none.
1843    pub pixel_formats: Vec<String>,
1844    /// Hardware configurations the encoder declares.
1845    pub hardware_configs: Vec<NativeEncoderHardwareConfig>,
1846    /// Whether `AV_PIX_FMT_YUV420P` is among the declared formats.
1847    pub accepts_yuv420p: bool,
1848    /// Whether every declared format is a hardware one (why `h264_vulkan` is not advertised).
1849    pub requires_hardware_frames: bool,
1850    /// Whether [`codec_capabilities`] advertises it for the YUV420P sink.
1851    pub advertised: bool,
1852    /// Smallest frame the encoder accepts, as alpha2 records it.
1853    pub minimum_size: (u32, u32),
1854    /// `None` when a software YUV420P frame produced a packet; otherwise why not.
1855    pub software_open_error: Option<String>,
1856    /// Packets drained from that attempt.
1857    pub software_packets: u32,
1858    /// Which rate-control mechanisms opened, and with what options.
1859    pub rate_control: Vec<NativeRateControlProbe>,
1860}
1861
1862/// Whether one encoder opened with one rate-control mechanism.
1863#[derive(Clone, Debug, Eq, PartialEq)]
1864pub struct NativeRateControlProbe {
1865    /// The mechanism asked for.
1866    pub mechanism: crate::RateControlMechanism,
1867    /// The options that carried it, or `None` where alpha2 knows no spelling for this family.
1868    pub options: Option<String>,
1869    /// Packets drained when it opened, used every option and encoded a frame; otherwise why not.
1870    pub outcome: Result<u32, String>,
1871    /// The device a hardware-frames encoder was probed on: a Vulkan device index, or for
1872    /// `*_d3d12va` the answering adapter (`DX12 adapter <index> (<name>)`).
1873    pub device: Option<String>,
1874}
1875
1876/// A quantizer every encoder of `codec` accepts: `crf` 0-63 for
1877/// `libvpx-vp9`/`libsvtav1`, QP 0-255 for hardware.
1878pub(crate) const fn probe_quantizer(codec: NativeVideoCodec, software: bool) -> u32 {
1879    match codec {
1880        NativeVideoCodec::H264 | NativeVideoCodec::Hevc => 26,
1881        NativeVideoCodec::Av1 | NativeVideoCodec::Vp9 => {
1882            if software {
1883                32
1884            } else {
1885                100
1886            }
1887        }
1888        NativeVideoCodec::Mpeg4Part2 => 4,
1889        // Never asked: `ProRes` has no quantizer mode alpha2 opens.
1890        NativeVideoCodec::ProRes => 0,
1891    }
1892}
1893
1894/// Opens every registered video encoder and reports what it actually does.
1895///
1896/// Each candidate is opened at `width`x`height` and fed one YUV420P frame, so the report tells
1897/// registered, advertised and ran apart. A hybrid encoder has a row per mode.
1898#[must_use]
1899#[allow(clippy::too_many_lines)]
1900pub fn probe_video_encoders(width: u32, height: u32) -> Vec<NativeVideoEncoderProbe> {
1901    let advertised = codec_capabilities();
1902    [
1903        NativeVideoCodec::H264,
1904        NativeVideoCodec::Hevc,
1905        NativeVideoCodec::Vp9,
1906        NativeVideoCodec::Av1,
1907        NativeVideoCodec::Mpeg4Part2,
1908        NativeVideoCodec::ProRes,
1909    ]
1910    .into_iter()
1911    .flat_map(|codec| {
1912        registered_video_encoders(codec)
1913            .into_iter()
1914            .map(move |candidate| (codec, candidate))
1915    })
1916    .map(|(codec, candidate)| {
1917        let encoder = &candidate.encoder;
1918        let encoder_name = candidate.name();
1919        let pixel_formats = encoder.pix_fmts().map_or_else(Vec::new, |formats| {
1920            formats
1921                .iter()
1922                .map(|format| {
1923                    alpha2_ffmpeg_bridge::pixel_format_name(*format)
1924                        .unwrap_or_else(|| "unknown".to_owned())
1925                })
1926                .collect()
1927        });
1928        let hardware_configs = (0..)
1929            .map_while(|index| encoder.hw_config(index))
1930            .map(|config| NativeEncoderHardwareConfig {
1931                pixel_format: alpha2_ffmpeg_bridge::pixel_format_name(config.pix_fmt)
1932                    .unwrap_or_else(|| "unknown".to_owned()),
1933                device_type: alpha2_ffmpeg_bridge::hardware_device_type_name(config.device_type)
1934                    .unwrap_or_else(|| "none".to_owned()),
1935                methods: config.methods,
1936                needs_hardware_frames: crate::decode::has_hw_config_method(
1937                    config.methods,
1938                    ffi::AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX,
1939                ),
1940            })
1941            .collect::<Vec<_>>();
1942        let accepts_yuv420p = encoder
1943            .pix_fmts()
1944            .is_none_or(|formats| formats.contains(&ffi::AV_PIX_FMT_YUV420P));
1945        let (software_open_error, software_packets) =
1946            probe_software_encode(&candidate, &encoder_name, width, height, None);
1947        let requires_hardware_frames = !accepts_yuv420p
1948            && hardware_configs
1949                .iter()
1950                .any(|config| config.needs_hardware_frames);
1951        let rate_control = crate::RateControlMechanism::ALL
1952            .into_iter()
1953            .map(|mechanism| {
1954                let request = crate::rate_control::mechanism_request(
1955                    &encoder_name,
1956                    mechanism,
1957                    2_000_000,
1958                    // D3D12VA takes 0-52 for every codec, AV1 included.
1959                    if encoder_name.ends_with("_d3d12va") {
1960                        26
1961                    } else {
1962                        probe_quantizer(
1963                            codec,
1964                            encoder_class(encoder) == NativeEncoderClass::Software,
1965                        )
1966                    },
1967                );
1968                let mut device = None;
1969                let outcome = match &request {
1970                    None => Err("no known option for this encoder family".to_owned()),
1971                    Some(request) if requires_hardware_frames => probe_hardware_frames_encode(
1972                        encoder,
1973                        &encoder_name,
1974                        (width, height),
1975                        mechanism,
1976                        request,
1977                    )
1978                    .map(|(packets, on)| {
1979                        device = Some(on);
1980                        packets
1981                    }),
1982                    Some(request) => {
1983                        match probe_software_encode(
1984                            &candidate,
1985                            &encoder_name,
1986                            width,
1987                            height,
1988                            Some(request),
1989                        ) {
1990                            (None, packets) => Ok(packets),
1991                            (Some(error), _) => Err(error),
1992                        }
1993                    }
1994                };
1995                NativeRateControlProbe {
1996                    mechanism,
1997                    options: request
1998                        .as_ref()
1999                        .map(crate::rate_control::MechanismRequest::describe),
2000                    outcome,
2001                    device,
2002                }
2003            })
2004            .collect();
2005        NativeVideoEncoderProbe {
2006            codec,
2007            class: encoder_class(encoder),
2008            hardware_accelerated: candidate.on_hardware,
2009            capability_bits: u32::try_from(encoder.capabilities).unwrap_or(0),
2010            advertised: advertised.video.iter().any(|capability| {
2011                capability.codec == codec
2012                    && capability.encoder_name == encoder_name
2013                    && capability.hardware_accelerated == candidate.on_hardware
2014            }),
2015            minimum_size: encoder_minimum_dimensions(&encoder_name),
2016            requires_hardware_frames,
2017            accepts_yuv420p,
2018            pixel_formats,
2019            hardware_configs,
2020            encoder_name,
2021            software_open_error,
2022            software_packets,
2023            rate_control,
2024        }
2025    })
2026    .collect()
2027}
2028
2029/// Opens an encoder that only takes hardware frames, with `request`.
2030///
2031/// For `*_vulkan`, `FFmpeg` creates its own device with whatever video-encode queue is offered.
2032/// D3D12VA rows open on the first DX12 adapter whose D3D12 Video Encode reports the encoder; a mode
2033/// the device does not list is refused with its reason.
2034///
2035/// Returns the packets and the answering device (Vulkan index, or DX12 adapter index and name).
2036#[allow(unused_variables)]
2037fn probe_hardware_frames_encode(
2038    encoder: &AVCodecRef<'static>,
2039    name: &str,
2040    (width, height): (u32, u32),
2041    mechanism: crate::RateControlMechanism,
2042    request: &crate::rate_control::MechanismRequest,
2043) -> Result<(u32, String), String> {
2044    #[cfg(all(target_os = "linux", feature = "hwencode"))]
2045    if name.ends_with("_vulkan") {
2046        return crate::hwencode::probe_vulkan_rate_control(encoder, width, height, request);
2047    }
2048    #[cfg(all(target_os = "windows", feature = "hwencode"))]
2049    if name.ends_with("_d3d12va") {
2050        return crate::hwencode::probe_d3d12va_rate_control(
2051            name, width, height, mechanism, request,
2052        );
2053    }
2054    Err("takes hardware frames this probe cannot supply".to_owned())
2055}
2056
2057/// Opens `candidate` and feeds it one software YUV420P frame.
2058fn probe_software_encode(
2059    candidate: &VideoEncoderCandidate,
2060    name: &str,
2061    width: u32,
2062    height: u32,
2063    request: Option<&crate::rate_control::MechanismRequest>,
2064) -> (Option<String>, u32) {
2065    let Ok(coded_width) = i32::try_from(width) else {
2066        return (Some("width is out of range".to_owned()), 0);
2067    };
2068    let Ok(coded_height) = i32::try_from(height) else {
2069        return (Some("height is out of range".to_owned()), 0);
2070    };
2071    if let Err(error) = validate_encoder_dimensions(name, width, height) {
2072        return (Some(error.to_string()), 0);
2073    }
2074    let encoder = &candidate.encoder;
2075    let base = candidate.codec().base_layout();
2076    let format =
2077        encoder_layout(encoder, base).map_or(ffi::AV_PIX_FMT_YUV420P, crate::convert::pixel_format);
2078    let mut context = EncoderContext::from(AVCodecContext::new(encoder));
2079    context.set_width(coded_width);
2080    context.set_height(coded_height);
2081    context.set_pix_fmt(format);
2082    let time_base = rsmpeg::avutil::ra(1, 30);
2083    context.set_time_base(time_base);
2084    context.set_framerate(rsmpeg::avutil::ra(30, 1));
2085    context.set_bit_rate(request.map_or(2_000_000, |request| request.bit_rate));
2086    context.set_gop_size(12);
2087    context.set_max_b_frames(encoder_max_b_frames(name));
2088    let options = video_encoder_options(
2089        ColorMetadata::EXPORT,
2090        encoder,
2091        candidate.on_hardware,
2092        1,
2093        width,
2094    );
2095    let options = match request {
2096        Some(request) => request.dictionary(options),
2097        None => options,
2098    };
2099    match alpha2_ffmpeg_bridge::open_codec_context(&mut context, options) {
2100        Err(error) => return (Some(error.to_string()), 0),
2101        Ok(unused) => {
2102            if let Err(error) = reject_unused_encode_options(unused) {
2103                return (Some(error.to_string()), 0);
2104            }
2105        }
2106    }
2107    let Some(image) = rsmpeg::avutil::AVImage::new(format, coded_width, coded_height, 1) else {
2108        return (Some("cannot allocate a probe frame".to_owned()), 0);
2109    };
2110    let mut frame = rsmpeg::avutil::AVFrameWithImage::new(image);
2111    frame.set_pts(0);
2112    frame.set_time_base(time_base);
2113    if let Err(error) = context.send_frame(Some(&frame)) {
2114        return (Some(error.to_string()), 0);
2115    }
2116    if let Err(error) = context.send_frame(None) {
2117        return (Some(error.to_string()), 0);
2118    }
2119    let mut packets = 0;
2120    loop {
2121        match context.receive_packet() {
2122            Ok(_) => packets += 1,
2123            Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
2124            Err(error) => return (Some(error.to_string()), packets),
2125        }
2126    }
2127    if packets == 0 {
2128        return (
2129            Some("opened but produced no packet from a software frame".to_owned()),
2130            0,
2131        );
2132    }
2133    (None, packets)
2134}