Skip to main content

oxideav_core/registry/
codec.rs

1//! In-process codec registry.
2//!
3//! Every codec crate declares itself with one [`CodecInfo`] value —
4//! capabilities, factory functions, the container tags it claims, and
5//! (optionally) a probe function used to disambiguate genuine tag
6//! collisions. The registry stores those registrations and exposes
7//! three orthogonal lookups:
8//!
9//! - **id-keyed** — `make_decoder(params)` / `make_encoder(params)` walk
10//!   the implementations registered under `params.codec_id`, filter by
11//!   capability restrictions, and try them in priority order with init-
12//!   time fallback.
13//! - **tag-keyed** — `resolve_tag(&ProbeContext)` walks every
14//!   registration whose `tags` contains `ctx.tag`, calls each probe
15//!   (treating `None` as "returns 1.0"), and returns the id with the
16//!   highest resulting confidence. Ties go to the lower
17//!   [`CodecInfo::resolution_priority`], then to the earlier
18//!   registration; `resolve_tag_candidates` exposes the ranked list.
19//! - **payload-magic-keyed** — `resolve_payload_magic(first_bytes)`
20//!   prefix-matches the claimed payload magic prefixes against a
21//!   stream's leading bytes; longest matching magic wins, then the
22//!   lower resolution priority, then registration order
23//!   (`resolve_payload_magic_candidates` exposes the ranked list). For
24//!   containers that identify a codec by the payload itself rather
25//!   than a tag (e.g. an Ogg logical stream's first packet, or raw
26//!   elementary streams).
27//! - **diagnostic** — `all_implementations`, `all_tag_registrations`,
28//!   `all_payload_magic_registrations`.
29//!
30//! The tag path explicitly DOES NOT short-circuit on "first claim with
31//! no probe" — every claimant is asked, so a lower-priority probed
32//! claim can out-rank a higher-priority unprobed one when the content
33//! is actually ambiguous (DIV3 XVID-with-real-MSMPEG4 payload etc.).
34
35use std::collections::HashMap;
36
37use crate::arena;
38use crate::{
39    CodecCapabilities, CodecId, CodecOptionsStruct, CodecParameters, CodecResolver, CodecTag,
40    Error, ExecutionContext, Frame, OptionField, Packet, PixelFormat, ProbeContext, ProbeFn,
41    Result,
42};
43
44// ───────────────────────── codec traits ─────────────────────────
45
46/// A packet-to-frame decoder.
47pub trait Decoder: Send {
48    /// Identifier of the codec this decoder handles.
49    fn codec_id(&self) -> &CodecId;
50
51    /// Feed one compressed packet. May or may not produce a frame immediately —
52    /// call `receive_frame` in a loop afterwards.
53    fn send_packet(&mut self, packet: &Packet) -> Result<()>;
54
55    /// Pull the next decoded frame, if any. Returns `Error::NeedMore` when the
56    /// decoder needs another packet.
57    fn receive_frame(&mut self) -> Result<Frame>;
58
59    /// Pull the next decoded frame as an arena-backed [`arena::sync::Frame`].
60    ///
61    /// Decoders that build their output through an
62    /// [`arena::sync::ArenaPool`] override this to return the pooled
63    /// [`arena::sync::Frame`] **directly**, with no per-plane memcpy
64    /// out — the caller gets true zero-copy plane access via
65    /// [`arena::sync::FrameInner::plane`].
66    ///
67    /// The default implementation delegates to [`Self::receive_frame`]
68    /// and copies the video planes into a freshly-leased one-shot
69    /// `arena::sync::ArenaPool`. This makes the method an additive
70    /// change for every existing [`Decoder`] impl: callers using the
71    /// new API still work, but pay one memcpy per plane.
72    ///
73    /// **Audio / subtitle frames:** the [`arena::sync::Frame`] body is
74    /// video-only (planes + [`arena::sync::FrameHeader`] with
75    /// width/height/pixel format). The default implementation returns
76    /// [`Error::Unsupported`] for non-video frames; an audio decoder
77    /// that wants to expose `receive_arena_frame()` must override it
78    /// with its own arena-backed audio-frame type once the framework
79    /// gains one. Until then, audio decoders should keep using
80    /// [`Self::receive_frame`].
81    fn receive_arena_frame(&mut self) -> Result<arena::sync::Frame> {
82        let frame = self.receive_frame()?;
83        match frame {
84            Frame::Video(v) => video_frame_to_arena_sync_frame(&v),
85            Frame::Audio(_) => Err(Error::unsupported(
86                "receive_arena_frame: audio frames not yet supported by default impl",
87            )),
88            Frame::Subtitle(_) => Err(Error::unsupported(
89                "receive_arena_frame: subtitle frames have no arena-backed representation",
90            )),
91            Frame::Vector(_) => Err(Error::unsupported(
92                "receive_arena_frame: vector frames have no arena-backed representation",
93            )),
94        }
95    }
96
97    /// Signal end-of-stream. After this, `receive_frame` will drain buffered
98    /// frames and eventually return `Error::Eof`.
99    fn flush(&mut self) -> Result<()>;
100
101    /// Discard all carry-over state so the decoder can resume from a new
102    /// bitstream position without producing stale output. Called by the
103    /// player after a container seek.
104    ///
105    /// Unlike [`flush`](Self::flush) (which signals end-of-stream and
106    /// drains buffered frames), `reset` is expected to:
107    /// * drop every buffered input packet and pending output frame;
108    /// * zero any per-stream filter / predictor / overlap memory so the
109    ///   next `send_packet` decodes as if it were the first;
110    /// * leave the codec id and stream parameters untouched.
111    ///
112    /// The default is a conservative "drain-then-forget": call
113    /// [`flush`](Self::flush) and ignore any remaining frames. Stateful
114    /// codecs (LPC predictors, backward-adaptive gain, IMDCT overlap,
115    /// reference pictures, …) should override this to wipe their
116    /// internal state explicitly — otherwise the first ~N output
117    /// samples after a seek will be glitchy until the state re-adapts.
118    fn reset(&mut self) -> Result<()> {
119        self.flush()?;
120        // Drain any remaining output frames so the next send_packet
121        // starts clean. NeedMore / Eof both mean "no more frames"; any
122        // other error is surfaced so the caller can see why.
123        loop {
124            match self.receive_frame() {
125                Ok(_) => {}
126                Err(Error::NeedMore) | Err(Error::Eof) => return Ok(()),
127                Err(e) => return Err(e),
128            }
129        }
130    }
131
132    /// Advisory: announce the runtime environment (today: a thread budget
133    /// for codec-internal parallelism). Called at most once, before the
134    /// first `send_packet`. Default no-op; codecs that want to run
135    /// slice-/GOP-/tile-parallel override this to capture the budget.
136    /// Ignoring the hint is always safe — callers must still work with
137    /// a decoder that runs serial.
138    fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
139
140    /// Pixel layout of the video frames this decoder emits, stated
141    /// before the first frame — from the codec's configuration record
142    /// in `extradata`, from a negotiated hardware session, or from the
143    /// decoder's fixed output policy. `Some` is a **fact about the
144    /// frames to come** and takes precedence over the container's
145    /// [`CodecParameters::pixel_format`](crate::CodecParameters::pixel_format)
146    /// (a stream label is a claim the container made; the decoder is
147    /// the one producing the planes). `None` — the default — means
148    /// "not known yet: trust the stream parameters", and a decoder
149    /// whose output depends on the first packet (an untagged file that
150    /// decides its layout per picture) answers `None` until it has
151    /// seen one and the real layout afterwards.
152    ///
153    /// Pipelines and gateways read it to label frames from codec-only
154    /// registrations and to pick the conversion into an encoder's
155    /// [`accepted_pixel_formats`](crate::CodecCapabilities::accepted_pixel_formats);
156    /// a frame whose plane shape disagrees with the stated layout is
157    /// the decoder's defect, not the consumer's. The answer may change
158    /// between frames only for a decoder that documents it.
159    ///
160    /// This is the narrow, older form of
161    /// [`output_params`](Self::output_params), whose default derives
162    /// from it. New decoders implement `output_params` (the whole
163    /// output description) and forward this method to it; consumers
164    /// should prefer `output_params`.
165    fn output_pixel_format(&self) -> Option<crate::PixelFormat> {
166        None
167    }
168
169    /// The decoder's resolved description of the frames it emits — the
170    /// **stream-level** home for what [`Frame`]s deliberately do not
171    /// repeat (a [`VideoFrame`](crate::VideoFrame) /
172    /// [`AudioFrame`](crate::AudioFrame) carries no width, height,
173    /// format or rate: the shape does not change 60 times a second, so
174    /// it is stated once, here, and re-stated only when it changes).
175    ///
176    /// Fields a decoder fills when it knows them:
177    ///
178    /// * **video** — [`width`](CodecParameters::width) /
179    ///   [`height`](CodecParameters::height) as the frames have them
180    ///   (the cropped / display size of the emitted planes, not the
181    ///   coded macroblock-aligned size),
182    ///   [`pixel_format`](CodecParameters::pixel_format),
183    ///   [`frame_rate`](CodecParameters::frame_rate) when the bitstream
184    ///   signals one, [`color_signal`](CodecParameters::color_signal)
185    ///   from the bitstream's own signalling, and
186    ///   [`layers`](CodecParameters::layers) for multi-layer output;
187    /// * **audio** — [`sample_rate`](CodecParameters::sample_rate),
188    ///   [`channels`](CodecParameters::channels),
189    ///   [`sample_format`](CodecParameters::sample_format),
190    ///   [`channel_layout`](CodecParameters::channel_layout).
191    ///
192    /// Fields the decoder does not know stay `None` / unspecified;
193    /// container-side fields (`extradata`, `bit_rate`, `tag`,
194    /// `language`, `blobs`, `options`, `limits`) are not the decoder's
195    /// to answer and are ignored by
196    /// [`CodecParameters::merge_output_params`], the helper that folds
197    /// this answer into a stream's parameters.
198    ///
199    /// `Some` is a **fact about the frames** and takes precedence over
200    /// the container's claim for every field it sets. `None` means "not
201    /// known yet: trust the stream parameters". A decoder answers as
202    /// soon as it has parsed what defines the output — its
203    /// configuration record in `extradata` at construction, or the
204    /// first sequence / frame header — ideally before the first frame
205    /// is returned, and at the latest once `receive_frame` has returned
206    /// the first frame. From then on the answer describes the frame
207    /// most recently returned by `receive_frame` (and the ones after
208    /// it, until [`output_params_generation`](Self::output_params_generation)
209    /// changes).
210    ///
211    /// **Default and the relation to [`output_pixel_format`](Self::output_pixel_format):**
212    /// the default implementation derives from `output_pixel_format()`
213    /// — `Some` of a video [`CodecParameters`] carrying only
214    /// `codec_id` and `pixel_format` when that answers `Some`, `None`
215    /// otherwise — so every decoder that implements the older narrow
216    /// query is described here for free. The direction is one-way:
217    /// `output_params` reads `output_pixel_format`, never the other
218    /// way round (no default recursion). A decoder that implements
219    /// `output_params` itself should also forward
220    /// `output_pixel_format` to it
221    /// (`self.output_params().and_then(|p| p.pixel_format)`) so callers
222    /// of the narrow query keep seeing the same answer. Consumers
223    /// should query `output_params` — it is the complete answer.
224    fn output_params(&self) -> Option<CodecParameters> {
225        self.output_pixel_format().map(|pf| {
226            let mut p = CodecParameters::video(self.codec_id().clone());
227            p.pixel_format = Some(pf);
228            p
229        })
230    }
231
232    /// A cheap change signal for [`output_params`](Self::output_params):
233    /// an integer that changes whenever the output description changes
234    /// **mid-stream** (a resolution switch at a keyframe, a new pixel
235    /// format at a new sequence header, a sample-rate change in a
236    /// chained stream). Consumers keep the last value they saw and
237    /// compare one integer after each successful `receive_frame`;
238    /// only when it differs do they re-read `output_params()`. Frames
239    /// carry nothing extra.
240    ///
241    /// The contract:
242    ///
243    /// * Consumers always read `output_params()` once after the first
244    ///   frame (or before it, if they need the shape up front) —
245    ///   resolving the description from `None` to `Some` before or at
246    ///   the first frame is **not** a change and needs no bump.
247    /// * After the first frame has been returned, a decoder whose
248    ///   output description changes increments the value (wrapping,
249    ///   so it only ever needs `!=` comparison) **at a frame
250    ///   boundary**: the bump and the new `output_params()` answer
251    ///   become visible together, when `receive_frame` returns the
252    ///   first frame with the new shape — not when the decoder parses
253    ///   the header announcing it, because frames of the old shape may
254    ///   still be queued in its reorder buffer. Once `receive_frame`
255    ///   has returned a frame, `output_params()` describes that frame.
256    /// * It never changes while the description stays the same, and a
257    ///   bump always comes with a changed description.
258    /// * [`reset`](Self::reset) after a seek may change it (the new
259    ///   position can be in a segment of a different shape) under the
260    ///   same rule: visible with the first frame returned afterwards.
261    ///
262    /// The default `0` — "never changes" — is correct for every decoder
263    /// whose output shape is fixed for the stream's lifetime.
264    /// [`OutputParamsWatch`] packages the consumer side in one call.
265    fn output_params_generation(&self) -> u64 {
266        0
267    }
268}
269
270/// Consumer-side helper for the [`Decoder::output_params`] /
271/// [`Decoder::output_params_generation`] contract: folds the decoder's
272/// output description into a stream's [`CodecParameters`] once, and
273/// again only when the decoder signals a mid-stream change — one
274/// integer compare per frame otherwise.
275///
276/// Call [`observe`](Self::observe) after **every** successful
277/// `receive_frame` (the first call always reads the description; later
278/// calls re-read only when the generation moved). Call
279/// [`prime`](Self::prime) before the first frame when the shape is
280/// needed up front (to size a renderer, pick a converter); it merges
281/// whatever the decoder already knows without consuming the
282/// first-frame read, since the description may still be refined up to
283/// the first frame.
284///
285/// ```
286/// # use oxideav_core::{CodecParameters, Decoder, OutputParamsWatch};
287/// fn pump(dec: &mut dyn Decoder, stream: &mut CodecParameters) -> oxideav_core::Result<()> {
288///     let mut watch = OutputParamsWatch::new();
289///     while let Ok(_frame) = dec.receive_frame() {
290///         if watch.observe(&*dec, stream) {
291///             // shape changed (or was first resolved): reconfigure
292///             // the converter / renderer from `stream` here.
293///         }
294///     }
295///     Ok(())
296/// }
297/// ```
298#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
299pub struct OutputParamsWatch {
300    seen: Option<u64>,
301}
302
303impl OutputParamsWatch {
304    /// A watch that has not read the decoder yet.
305    pub const fn new() -> Self {
306        Self { seen: None }
307    }
308
309    /// Call after a successful `receive_frame`. Re-reads
310    /// [`Decoder::output_params`] on the first call and whenever
311    /// [`Decoder::output_params_generation`] differs from the value of
312    /// the last read, merging it into `stream` through
313    /// [`CodecParameters::merge_output_params`]. Returns `true` when
314    /// that changed an output-describing field of `stream`.
315    ///
316    /// A decoder that still answers `None` leaves `stream` untouched
317    /// and is asked again on the next call.
318    pub fn observe<D: Decoder + ?Sized>(
319        &mut self,
320        decoder: &D,
321        stream: &mut CodecParameters,
322    ) -> bool {
323        let generation = decoder.output_params_generation();
324        if self.seen == Some(generation) {
325            return false;
326        }
327        match decoder.output_params() {
328            Some(p) => {
329                self.seen = Some(generation);
330                stream.merge_output_params(&p)
331            }
332            None => false,
333        }
334    }
335
336    /// Merge what the decoder already knows into `stream` without
337    /// recording a read — for use before the first frame. Returns
338    /// `true` when an output-describing field of `stream` changed.
339    pub fn prime<D: Decoder + ?Sized>(&self, decoder: &D, stream: &mut CodecParameters) -> bool {
340        decoder
341            .output_params()
342            .is_some_and(|p| stream.merge_output_params(&p))
343    }
344
345    /// Forget the last read so the next [`observe`](Self::observe)
346    /// re-reads unconditionally (e.g. after swapping the decoder).
347    pub fn reset(&mut self) {
348        self.seen = None;
349    }
350}
351
352/// A frame-to-packet encoder.
353pub trait Encoder: Send {
354    /// Identifier of the codec this encoder produces.
355    fn codec_id(&self) -> &CodecId;
356
357    /// Parameters describing this encoder's output stream (to feed into a muxer).
358    fn output_params(&self) -> &CodecParameters;
359
360    /// Feed one uncompressed frame. May or may not produce a packet
361    /// immediately — call `receive_packet` in a loop afterwards.
362    fn send_frame(&mut self, frame: &Frame) -> Result<()>;
363
364    /// Pull the next encoded packet, if any. Returns `Error::NeedMore`
365    /// when the encoder needs another frame (or a `flush`).
366    fn receive_packet(&mut self) -> Result<Packet>;
367
368    /// Signal end of input: drain internal lookahead so the remaining
369    /// packets become available via `receive_packet`.
370    fn flush(&mut self) -> Result<()>;
371
372    /// Advisory: announce the runtime environment. Same semantics as
373    /// [`Decoder::set_execution_context`].
374    fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
375}
376
377/// Default-impl helper for [`Decoder::receive_arena_frame`]: copy a
378/// heap-backed [`crate::VideoFrame`] into a freshly-leased
379/// [`arena::sync::Frame`].
380///
381/// Allocates a single-slot, single-arena `arena::sync::ArenaPool`
382/// sized to fit the planes verbatim. The pool is dropped at the end of
383/// this call; the returned `Frame` keeps its leased buffer alive via
384/// `Arc<FrameInner>` (the `Arena`'s `Weak` handle to the dropped pool
385/// just stops upgrading — the buffer drops normally when the last
386/// `Frame` clone goes away).
387///
388/// Width / height / pixel-format on the returned `FrameHeader` are
389/// derived from the plane shape: `width = plane[0].stride`,
390/// `height = plane[0].data.len() / stride`. Pixel format is left as
391/// [`PixelFormat::Yuv420P`] when there are 3 planes, else the first
392/// per-plane sensible default — this is a best-effort label for the
393/// generic conversion path; decoders that override
394/// `receive_arena_frame` themselves should set the correct pixel
395/// format.
396fn video_frame_to_arena_sync_frame(v: &crate::VideoFrame) -> Result<arena::sync::Frame> {
397    if v.planes.is_empty() {
398        return Err(Error::invalid(
399            "receive_arena_frame: video frame has no planes",
400        ));
401    }
402    let total_bytes: usize = v.planes.iter().map(|p| p.data.len()).sum();
403    if total_bytes == 0 {
404        return Err(Error::invalid(
405            "receive_arena_frame: video frame planes are empty",
406        ));
407    }
408    // One-shot pool sized exactly to the frame. The pool drops at end
409    // of scope; the leased Arena lives on inside the returned Frame
410    // (its Weak<ArenaPool> handle just won't upgrade in Drop, so the
411    // Box<[u8]> falls through to a normal heap free).
412    let pool = arena::sync::ArenaPool::with_alloc_count_cap(
413        1,
414        total_bytes,
415        // One alloc per plane, plus a generous safety margin.
416        (v.planes.len() as u32).saturating_add(4),
417    );
418    let arena = pool.lease()?;
419    let mut plane_offsets: Vec<(usize, usize)> = Vec::with_capacity(v.planes.len());
420    let mut cursor = 0usize;
421    for plane in &v.planes {
422        let dst = arena.alloc::<u8>(plane.data.len())?;
423        dst.copy_from_slice(&plane.data);
424        plane_offsets.push((cursor, plane.data.len()));
425        cursor += plane.data.len();
426    }
427    // Best-effort header: width = stride of plane 0, height inferred
428    // from plane 0's data length. Pixel format defaults to Yuv420P for
429    // the common 3-plane case, Gray8 for single-plane, otherwise
430    // Yuv444P. Decoders that care about exact pixel-format / width /
431    // height should override `receive_arena_frame` themselves so they
432    // can emit a correct `FrameHeader` straight from their arena
433    // build path.
434    let stride0 = v.planes[0].stride.max(1);
435    let width = stride0 as u32;
436    let height = (v.planes[0].data.len() / stride0) as u32;
437    // Count only image planes for the format guess — side-channel
438    // entries (palette, significant bits; copied verbatim above like
439    // any other plane) must not bump e.g. a single-plane palette frame
440    // out of the Gray8 label.
441    let pixel_format = match v.image_plane_count() {
442        1 => PixelFormat::Gray8,
443        3 => PixelFormat::Yuv420P,
444        _ => PixelFormat::Yuv444P,
445    };
446    let header = arena::sync::FrameHeader::new(width, height, pixel_format, v.pts);
447    arena::sync::FrameInner::new(arena, &plane_offsets, header)
448}
449
450/// Factory that builds a decoder for a given codec parameter set.
451pub type DecoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Decoder>>;
452
453/// Factory that builds an encoder for a given codec parameter set.
454pub type EncoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Encoder>>;
455
456// ───────────────────────── CodecInfo ─────────────────────────
457
458/// A single registration: capabilities, decoder/encoder factories,
459/// optional probe, and the container tags this codec claims.
460///
461/// Codec crates build one of these per codec id inside their
462/// `register(reg)` function and hand it to
463/// [`CodecRegistry::register`]. The struct is `#[non_exhaustive]` so
464/// additional fields can be added without breaking existing codec
465/// crates — construction is only possible through
466/// [`CodecInfo::new`] plus the builder methods below.
467#[non_exhaustive]
468pub struct CodecInfo {
469    /// Canonical codec identifier this entry registers.
470    pub id: CodecId,
471    /// Capability description (media kind, feature flags, priority).
472    pub capabilities: CodecCapabilities,
473    /// Factory producing a fresh decoder instance, if decode is supported.
474    pub decoder_factory: Option<DecoderFactory>,
475    /// Factory producing a fresh encoder instance, if encode is supported.
476    pub encoder_factory: Option<EncoderFactory>,
477    /// Probe function that returns a confidence in `0.0..=1.0` for a
478    /// given [`ProbeContext`]. `None` means "confidence 1.0 for every
479    /// claimed tag" — the correct default for codecs whose tag claims
480    /// are unambiguous.
481    pub probe: Option<ProbeFn>,
482    /// Tags this codec is willing to be looked up under. One codec may
483    /// claim many tags (an AAC decoder covers several WaveFormat ids,
484    /// a FourCC, an MP4 OTI, and a Matroska CodecID string at once).
485    pub tags: Vec<CodecTag>,
486    /// Payload magic prefixes this codec answers to (`\x01vorbis`,
487    /// `OpusHead`, …). Some carriage formats have no codec tag — the
488    /// codec is announced by a magic byte prefix on the payload itself
489    /// (an Ogg logical stream's first packet is the canonical case;
490    /// raw elementary streams are another). Such claims are
491    /// prefix-matched by
492    /// [`CodecRegistry::resolve_payload_magic_ref`] instead of living in
493    /// the exact-match [`CodecTag`] index. Attached with
494    /// [`Self::payload_magic`] / [`Self::payload_magics`]. Empty prefixes are
495    /// ignored at registration time (a zero-length prefix would match
496    /// every stream while carrying no evidence).
497    pub payload_magics: Vec<Vec<u8>>,
498    /// Schema of the encoder's recognised option keys
499    /// (`CodecParameters::options`). Attached with
500    /// [`Self::encoder_options`]. Used for validation / `oxideav list`
501    /// / pipeline JSON checks.
502    pub encoder_options_schema: Option<&'static [OptionField]>,
503    /// Schema of the decoder's recognised option keys.
504    pub decoder_options_schema: Option<&'static [OptionField]>,
505    /// HW backend identifier, e.g. `"nvidia"`, `"vaapi"`, `"vdpau"`,
506    /// `"vulkan-video"`, `"videotoolbox"`. Set by HW siblings on every
507    /// `CodecInfo` they register; SW codecs leave this `None`.
508    /// Consumers (e.g. the CLI's `info` command) use it to group
509    /// codec entries by backend and to dedupe probe calls — multiple
510    /// `CodecInfo` entries with the same `engine_id` typically share
511    /// an `engine_probe` function, and consumers should call the probe
512    /// at most once per `engine_id` per pass. Attached via
513    /// [`Self::with_engine_id`].
514    pub engine_id: Option<&'static str>,
515    /// Optional engine probe function. When `Some`, calling it returns
516    /// one [`crate::engine::HwDeviceInfo`] entry per device the backend
517    /// sees. Phase-2 HW siblings populate this on every `CodecInfo`
518    /// they register; Phase-3 consumers (CLI) call it on demand.
519    /// Attached via [`Self::with_engine_probe`].
520    pub engine_probe: Option<crate::engine::EngineProbeFn>,
521    /// Tie-break rank for this registration's **claims** (tags and
522    /// payload magics) when another codec id claims the same tag /
523    /// magic with equal evidence. Lower is preferred; defaults to
524    /// [`DEFAULT_PRIORITY`](crate::DEFAULT_PRIORITY). Deliberately
525    /// separate from [`CodecCapabilities::priority`]: that field ranks
526    /// *implementations of one codec id* against each other (HW before
527    /// SW), and a backend's implementation preference must not silently
528    /// out-rank a different codec's identity claim on an ambiguous tag.
529    /// Attached via [`Self::with_resolution_priority`].
530    pub resolution_priority: i32,
531}
532
533impl CodecInfo {
534    /// Start a new registration for `id` with empty capabilities, no
535    /// factories, no probe, and no tags. Chain the builder methods
536    /// below to fill it in, then hand the result to
537    /// [`CodecRegistry::register`].
538    pub fn new(id: CodecId) -> Self {
539        Self {
540            capabilities: CodecCapabilities::audio(id.as_str()),
541            id,
542            decoder_factory: None,
543            encoder_factory: None,
544            probe: None,
545            tags: Vec::new(),
546            payload_magics: Vec::new(),
547            encoder_options_schema: None,
548            decoder_options_schema: None,
549            engine_id: None,
550            engine_probe: None,
551            resolution_priority: crate::DEFAULT_PRIORITY,
552        }
553    }
554
555    /// Set the claim tie-break priority (see
556    /// [`Self::resolution_priority`]). Lower is preferred. Only consulted
557    /// when another codec id claims the same tag with equal probe
558    /// confidence, or the same-length payload magic; evidence always
559    /// out-ranks priority, and equal priorities fall through to
560    /// registration order (earlier wins).
561    pub fn with_resolution_priority(mut self, priority: i32) -> Self {
562        self.resolution_priority = priority;
563        self
564    }
565
566    /// Replace the capability description. The default built by
567    /// [`Self::new`] is a placeholder (audio-flavoured, no flags); every
568    /// real registration should call this.
569    pub fn capabilities(mut self, caps: CodecCapabilities) -> Self {
570        self.capabilities = caps;
571        self
572    }
573
574    /// Builder: attach the decoder factory.
575    pub fn decoder(mut self, factory: DecoderFactory) -> Self {
576        self.decoder_factory = Some(factory);
577        self
578    }
579
580    /// Builder: attach the encoder factory.
581    pub fn encoder(mut self, factory: EncoderFactory) -> Self {
582        self.encoder_factory = Some(factory);
583        self
584    }
585
586    /// Builder: attach a confidence probe (see [`CodecInfo::probe`]).
587    pub fn probe(mut self, probe: ProbeFn) -> Self {
588        self.probe = Some(probe);
589        self
590    }
591
592    /// Claim a single container tag for this codec. Equivalent to
593    /// `.tags([tag])` but avoids the array ceremony for single-tag
594    /// claims.
595    pub fn tag(mut self, tag: CodecTag) -> Self {
596        self.tags.push(tag);
597        self
598    }
599
600    /// Claim a set of container tags for this codec. Takes any
601    /// iterable (arrays, `Vec`, `Option`, …) so the common case of a
602    /// codec with 3-6 tags reads as one clean block.
603    pub fn tags(mut self, tags: impl IntoIterator<Item = CodecTag>) -> Self {
604        self.tags.extend(tags);
605        self
606    }
607
608    /// Claim one payload magic prefix for this codec (see
609    /// [`Self::payload_magics`]). Chain repeatedly for codecs that answer
610    /// to more than one magic:
611    ///
612    /// ```
613    /// # use oxideav_core::registry::CodecInfo;
614    /// # use oxideav_core::CodecId;
615    /// let info = CodecInfo::new(CodecId::new("vorbis")).payload_magic(b"\x01vorbis");
616    /// # let _ = info;
617    /// ```
618    pub fn payload_magic(mut self, magic: impl Into<Vec<u8>>) -> Self {
619        self.payload_magics.push(magic.into());
620        self
621    }
622
623    /// Claim a set of payload magic prefixes for this codec — the
624    /// iterable companion to [`Self::payload_magic`], mirroring the
625    /// [`Self::tag`] / [`Self::tags`] pair.
626    pub fn payload_magics<I>(mut self, magics: I) -> Self
627    where
628        I: IntoIterator,
629        I::Item: Into<Vec<u8>>,
630    {
631        self.payload_magics
632            .extend(magics.into_iter().map(Into::into));
633        self
634    }
635
636    /// Declare the options struct this codec's encoder factory expects.
637    /// Attaches `T::SCHEMA` so the registry can enumerate recognised
638    /// option keys (for `oxideav list`, pipeline JSON validation, etc.).
639    /// The factory itself still has to call
640    /// [`crate::parse_options::<T>()`] against
641    /// `CodecParameters::options` at init time.
642    pub fn encoder_options<T: CodecOptionsStruct>(mut self) -> Self {
643        self.encoder_options_schema = Some(T::SCHEMA);
644        self
645    }
646
647    /// Declare the options struct this codec's decoder factory expects.
648    /// See [`Self::encoder_options`] for the encoder counterpart.
649    pub fn decoder_options<T: CodecOptionsStruct>(mut self) -> Self {
650        self.decoder_options_schema = Some(T::SCHEMA);
651        self
652    }
653
654    /// Tag this codec as belonging to a HW backend identified by
655    /// `engine_id`. Should match the `engine_id` of every other
656    /// `CodecInfo` registered by the same backend, and the corresponding
657    /// `engine_id` field used by the CLI for grouping. SW codecs leave
658    /// this unset.
659    pub fn with_engine_id(mut self, engine_id: &'static str) -> Self {
660        self.engine_id = Some(engine_id);
661        self
662    }
663
664    /// Attach a probe function. Consumers call it to enumerate the
665    /// engines (devices) this backend can dispatch to. Probes are
666    /// expected to be idempotent and side-effect free; consumers may
667    /// call them more than once per process and should dedupe by
668    /// [`Self::engine_id`].
669    pub fn with_engine_probe(mut self, probe: crate::engine::EngineProbeFn) -> Self {
670        self.engine_probe = Some(probe);
671        self
672    }
673}
674
675/// Internal per-impl record held inside the registry's id map. Kept
676/// distinct from [`CodecInfo`] so the id map stays cheap to walk
677/// during `make_decoder` / `make_encoder` lookups.
678#[derive(Clone)]
679pub struct CodecImplementation {
680    /// Capability description copied from the originating [`CodecInfo`].
681    pub caps: CodecCapabilities,
682    /// Decoder factory, if this implementation can decode.
683    pub make_decoder: Option<DecoderFactory>,
684    /// Encoder factory, if this implementation can encode.
685    pub make_encoder: Option<EncoderFactory>,
686    /// Encoder options schema declared via
687    /// [`CodecInfo::encoder_options`]. `None` means the encoder accepts
688    /// no tuning knobs (any non-empty `CodecParameters::options` will
689    /// still be rejected by the factory if the encoder calls
690    /// `parse_options` — this is purely informational for discovery).
691    pub encoder_options_schema: Option<&'static [OptionField]>,
692    /// Decoder options schema declared via
693    /// [`CodecInfo::decoder_options`]; same semantics as the encoder
694    /// schema above.
695    pub decoder_options_schema: Option<&'static [OptionField]>,
696    /// HW backend identifier copied verbatim from the originating
697    /// [`CodecInfo::engine_id`]. `Some("nvidia"/"vaapi"/...)` on HW
698    /// backends; `None` on SW codecs. Consumers (CLI `info` command,
699    /// pipeline dispatcher, bench loop) read this to group entries by
700    /// backend without grepping `caps.implementation`.
701    pub engine_id: Option<&'static str>,
702    /// Engine probe function copied verbatim from the originating
703    /// [`CodecInfo::engine_probe`]. `Some(fn)` on HW backends with a
704    /// probe wired; `None` on SW codecs. Consumers call it on demand
705    /// to enumerate per-device info ([`crate::engine::HwDeviceInfo`]).
706    pub engine_probe: Option<crate::engine::EngineProbeFn>,
707}
708
709/// Registry mapping codec ids and container tags to their registered
710/// implementations; the lookup point behind `make_decoder` /
711/// `make_encoder` / `resolve_tag`.
712#[derive(Default)]
713pub struct CodecRegistry {
714    /// id → list of implementations. Each registered codec appends one
715    /// entry here. `make_decoder` / `make_encoder` walk this list in
716    /// preference order.
717    impls: HashMap<CodecId, Vec<CodecImplementation>>,
718    /// Codec ids in the order their first implementation was
719    /// registered — drives every id-level enumeration so listings are
720    /// stable across processes.
721    impl_order: Vec<CodecId>,
722    /// Append-only list of every registration — the `tag_index` stores
723    /// offsets into this vector.
724    registrations: Vec<RegistrationRecord>,
725    /// Tag → indices into `registrations`. Indices are stored in
726    /// registration order so tie-breaking in `resolve_tag` is
727    /// deterministic (first-registered wins).
728    tag_index: HashMap<CodecTag, Vec<usize>>,
729    /// Payload magic-prefix claims: `(magic, registration index)` in
730    /// registration order. Kept as a flat list rather than a map
731    /// because resolution is prefix matching (see
732    /// [`Self::resolve_payload_magic_ref`]), not exact-key lookup.
733    magic_index: Vec<(Vec<u8>, usize)>,
734}
735
736/// Internal registry record. Mirrors the subset of [`CodecInfo`]
737/// needed at resolve time.
738struct RegistrationRecord {
739    id: CodecId,
740    probe: Option<ProbeFn>,
741    /// Claim tie-break rank copied from [`CodecInfo::resolution_priority`].
742    priority: i32,
743    /// Tags claimed by this registration, in declaration order — the
744    /// source of [`CodecRegistry::all_tag_registrations`]'s ordering.
745    tags: Vec<CodecTag>,
746}
747
748/// One ranked entry from [`CodecRegistry::resolve_tag_candidates`].
749///
750/// Ordered by the registry's resolution rule — `confidence`
751/// descending, then `priority` ascending (lower is preferred), then
752/// `order` ascending (earlier registration wins) — so the first
753/// element is exactly what [`CodecRegistry::resolve_tag_ref`] returns.
754/// `#[non_exhaustive]`: read by field, never constructed by callers.
755#[non_exhaustive]
756#[derive(Clone, Copy, Debug, PartialEq)]
757pub struct TagCandidate<'a> {
758    /// Codec id of the claiming registration.
759    pub id: &'a CodecId,
760    /// Probe confidence for this context (`1.0` for unprobed claims;
761    /// never `<= 0.0` — such claims are not candidates).
762    pub confidence: crate::Confidence,
763    /// Claim tie-break priority (see [`CodecInfo::resolution_priority`]).
764    pub priority: i32,
765    /// 0-based registration sequence number (one per
766    /// [`CodecRegistry::register`] call) — the final tie-break. The
767    /// same id can appear more than once when it registered the claim
768    /// twice; the registry does not fold duplicates.
769    pub order: usize,
770}
771
772/// One ranked entry from [`CodecRegistry::resolve_payload_magic_candidates`].
773///
774/// Ordered by the registry's resolution rule — magic length
775/// descending (most specific prefix), then `priority` ascending, then
776/// `order` ascending — so the first element is exactly what
777/// [`CodecRegistry::resolve_payload_magic_ref`] returns.
778#[non_exhaustive]
779#[derive(Clone, Copy, Debug, PartialEq, Eq)]
780pub struct PayloadMagicCandidate<'a> {
781    /// Codec id of the claiming registration.
782    pub id: &'a CodecId,
783    /// The magic prefix that matched.
784    pub magic: &'a [u8],
785    /// Claim tie-break priority (see [`CodecInfo::resolution_priority`]).
786    pub priority: i32,
787    /// 0-based registration sequence number — the final tie-break.
788    pub order: usize,
789}
790
791impl CodecRegistry {
792    /// An empty registry (same as `Default`).
793    pub fn new() -> Self {
794        Self::default()
795    }
796
797    /// Register one codec. Expands into:
798    ///   * an entry in the id → implementations map (for
799    ///     `make_decoder` / `make_encoder`);
800    ///   * an entry in the tag index for every claimed tag (for
801    ///     `resolve_tag`).
802    ///
803    /// Calling `register` multiple times with the same id is allowed
804    /// and how multi-implementation codecs (software-plus-hardware
805    /// FLAC, for example) are expressed.
806    pub fn register(&mut self, info: CodecInfo) {
807        let CodecInfo {
808            id,
809            capabilities,
810            decoder_factory,
811            encoder_factory,
812            probe,
813            tags,
814            payload_magics,
815            encoder_options_schema,
816            decoder_options_schema,
817            // engine_id / engine_probe are metadata attached to a
818            // CodecInfo for backends that want consumers (CLI `info`,
819            // pipeline bench) to enumerate the underlying devices on
820            // demand. They're surfaced verbatim on the resulting
821            // CodecImplementation so consumers can read them without
822            // grepping `caps.implementation`. Tag-only CodecInfo entries
823            // (no factories) drop the values on the floor — there's no
824            // CodecImplementation built in that branch.
825            engine_id,
826            engine_probe,
827            resolution_priority,
828        } = info;
829
830        let caps = {
831            let mut c = capabilities;
832            if decoder_factory.is_some() {
833                c = c.with_decode();
834            }
835            if encoder_factory.is_some() {
836                c = c.with_encode();
837            }
838            c
839        };
840
841        // Only record an implementation entry when at least one factory
842        // is present. A "tag-only" CodecInfo — used to attach extra tag
843        // claims to a codec that was already registered with factories —
844        // shouldn't pollute the impl list.
845        if decoder_factory.is_some() || encoder_factory.is_some() {
846            let slot = self.impls.entry(id.clone()).or_default();
847            if slot.is_empty() {
848                self.impl_order.push(id.clone());
849            }
850            slot.push(CodecImplementation {
851                caps,
852                make_decoder: decoder_factory,
853                make_encoder: encoder_factory,
854                encoder_options_schema,
855                decoder_options_schema,
856                engine_id,
857                engine_probe,
858            });
859        }
860
861        let record_idx = self.registrations.len();
862        for tag in &tags {
863            self.tag_index
864                .entry(tag.clone())
865                .or_default()
866                .push(record_idx);
867        }
868        self.registrations.push(RegistrationRecord {
869            id: id.clone(),
870            probe,
871            priority: resolution_priority,
872            tags,
873        });
874        for magic in payload_magics {
875            // A zero-length prefix would match every stream while
876            // carrying no evidence — drop it here so resolution never
877            // has to special-case it.
878            if !magic.is_empty() {
879                self.magic_index.push((magic, record_idx));
880            }
881        }
882    }
883
884    /// Whether at least one registered implementation of `id` can decode.
885    pub fn has_decoder(&self, id: &CodecId) -> bool {
886        self.impls
887            .get(id)
888            .map(|v| v.iter().any(|i| i.make_decoder.is_some()))
889            .unwrap_or(false)
890    }
891
892    /// Whether at least one registered implementation of `id` can encode.
893    pub fn has_encoder(&self, id: &CodecId) -> bool {
894        self.impls
895            .get(id)
896            .map(|v| v.iter().any(|i| i.make_encoder.is_some()))
897            .unwrap_or(false)
898    }
899
900    /// First registered decoder factory for `params.codec_id`, invoked
901    /// with `params`. No priority walk, no preference filter, no
902    /// init-time fallback to a lower-priority impl. Errors if no
903    /// decoder is registered for the codec.
904    ///
905    /// Intended for single-impl scenarios — typically a codec crate's
906    /// own self-tests, where exactly one impl has been registered into
907    /// a freshly-constructed registry. Production callers selecting
908    /// among multiple candidates (e.g. h264_sw vs h264_videotoolbox)
909    /// should use `oxideav_pipeline::make_decoder_with` instead, which
910    /// applies `CodecPreferences` and walks priorities.
911    pub fn first_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
912        let imp = self
913            .implementations(&params.codec_id)
914            .iter()
915            .find(|i| i.make_decoder.is_some())
916            .ok_or_else(|| {
917                Error::CodecNotFound(format!("no decoder for codec {}", params.codec_id))
918            })?;
919        (imp.make_decoder.expect("checked above"))(params)
920    }
921
922    /// First registered encoder factory — see [`first_decoder`].
923    ///
924    /// [`first_decoder`]: Self::first_decoder
925    pub fn first_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
926        let imp = self
927            .implementations(&params.codec_id)
928            .iter()
929            .find(|i| i.make_encoder.is_some())
930            .ok_or_else(|| {
931                Error::CodecNotFound(format!("no encoder for codec {}", params.codec_id))
932            })?;
933        (imp.make_encoder.expect("checked above"))(params)
934    }
935
936    /// Look up a decoder by exact implementation name
937    /// (`"h264_sw"`, `"aac_audiotoolbox"`, ...). Errors if the impl
938    /// isn't registered or if it has no decoder factory.
939    pub fn decoder_by_impl(
940        &self,
941        impl_name: &str,
942        params: &CodecParameters,
943    ) -> Result<Box<dyn Decoder>> {
944        let imp = self
945            .implementations(&params.codec_id)
946            .iter()
947            .find(|i| i.caps.implementation == impl_name)
948            .ok_or_else(|| {
949                Error::CodecNotFound(format!(
950                    "no implementation `{impl_name}` for codec {}",
951                    params.codec_id
952                ))
953            })?;
954        let factory = imp
955            .make_decoder
956            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is encoder-only")))?;
957        factory(params)
958    }
959
960    /// Look up an encoder by exact implementation name — see
961    /// [`decoder_by_impl`].
962    ///
963    /// [`decoder_by_impl`]: Self::decoder_by_impl
964    pub fn encoder_by_impl(
965        &self,
966        impl_name: &str,
967        params: &CodecParameters,
968    ) -> Result<Box<dyn Encoder>> {
969        let imp = self
970            .implementations(&params.codec_id)
971            .iter()
972            .find(|i| i.caps.implementation == impl_name)
973            .ok_or_else(|| {
974                Error::CodecNotFound(format!(
975                    "no implementation `{impl_name}` for codec {}",
976                    params.codec_id
977                ))
978            })?;
979        let factory = imp
980            .make_encoder
981            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is decoder-only")))?;
982        factory(params)
983    }
984
985    /// Iterate codec ids that have at least one decoder implementation,
986    /// in the order each id's first implementation was registered.
987    pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
988        self.impl_order.iter().filter(|id| self.has_decoder(id))
989    }
990
991    /// Iterate codec ids that have at least one encoder implementation,
992    /// in the order each id's first implementation was registered.
993    pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
994        self.impl_order.iter().filter(|id| self.has_encoder(id))
995    }
996
997    /// All registered implementations of a given codec id.
998    pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
999        self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
1000    }
1001
1002    /// Lookup the encoder options schema for a registered codec. Walks
1003    /// implementations in registration order and returns the first
1004    /// schema found. `None` means either the codec isn't registered or
1005    /// no implementation declared an encoder schema.
1006    pub fn encoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
1007        self.impls
1008            .get(id)?
1009            .iter()
1010            .find_map(|i| i.encoder_options_schema)
1011    }
1012
1013    /// Lookup the decoder options schema — see
1014    /// [`encoder_options_schema`](Self::encoder_options_schema).
1015    pub fn decoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
1016        self.impls
1017            .get(id)?
1018            .iter()
1019            .find_map(|i| i.decoder_options_schema)
1020    }
1021
1022    /// Iterator over every (codec_id, impl) pair — useful for `oxideav list`
1023    /// to show capability flags per implementation. Ids come in the
1024    /// order their first implementation was registered; implementations
1025    /// of one id in their own registration order.
1026    pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
1027        self.impl_order
1028            .iter()
1029            .flat_map(move |id| self.implementations(id).iter().map(move |i| (id, i)))
1030    }
1031
1032    /// Iterator over every `(tag, codec_id)` pair currently registered —
1033    /// used by `oxideav tags` debug output and by tests that want to
1034    /// walk the tag surface. Registration order, then each
1035    /// registration's tag declaration order.
1036    pub fn all_tag_registrations(&self) -> impl Iterator<Item = (&CodecTag, &CodecId)> {
1037        self.registrations
1038            .iter()
1039            .flat_map(|rec| rec.tags.iter().map(move |tag| (tag, &rec.id)))
1040    }
1041
1042    /// Inherent form of tag resolution that returns a reference.
1043    /// The owned-value form used by container code lives behind the
1044    /// [`CodecResolver`] trait impl below.
1045    ///
1046    /// Walks every registration that claimed `ctx.tag`, calls its
1047    /// probe with `ctx`, and returns the id of the registration that
1048    /// scored highest. Probes that return `0.0` (or anything not
1049    /// strictly positive, `NaN` included) are discarded; ties on
1050    /// confidence are broken by [`CodecInfo::resolution_priority`]
1051    /// (lower wins), then by registration order (first wins).
1052    /// Registrations with no probe are treated as returning `1.0`.
1053    /// [`resolve_tag_candidates`](Self::resolve_tag_candidates) returns
1054    /// the whole ranked list this picks the head of.
1055    pub fn resolve_tag_ref(&self, ctx: &ProbeContext) -> Option<&CodecId> {
1056        self.resolve_tag_candidates(ctx).first().map(|c| c.id)
1057    }
1058
1059    /// Every registration that claimed `ctx.tag` and whose probe
1060    /// accepts the context, ranked by the resolution rule: confidence
1061    /// descending, then priority ascending, then registration order
1062    /// ascending. Empty when the tag is unclaimed or every claimant's
1063    /// probe refused. The observation hook for collision audits: a
1064    /// list with two heads at equal confidence *and* equal priority is
1065    /// a tie the registry broke by registration order alone.
1066    pub fn resolve_tag_candidates(&self, ctx: &ProbeContext) -> Vec<TagCandidate<'_>> {
1067        let Some(idxs) = self.tag_index.get(ctx.tag) else {
1068            return Vec::new();
1069        };
1070        let mut out: Vec<TagCandidate<'_>> = idxs
1071            .iter()
1072            .filter_map(|&i| {
1073                let rec = &self.registrations[i];
1074                let confidence = match rec.probe {
1075                    Some(f) => f(ctx),
1076                    None => 1.0,
1077                };
1078                // `> 0.0` is false for NaN, so a broken probe is
1079                // dropped rather than ranked unpredictably.
1080                (confidence > 0.0).then_some(TagCandidate {
1081                    id: &rec.id,
1082                    confidence,
1083                    priority: rec.priority,
1084                    order: i,
1085                })
1086            })
1087            .collect();
1088        // `idxs` is already in registration order and `order` is
1089        // unique, so the sort key is total and the result is fixed by
1090        // the registrations alone.
1091        out.sort_by(|a, b| {
1092            b.confidence
1093                .partial_cmp(&a.confidence)
1094                .unwrap_or(std::cmp::Ordering::Equal)
1095                .then(a.priority.cmp(&b.priority))
1096                .then(a.order.cmp(&b.order))
1097        });
1098        out
1099    }
1100
1101    /// Inherent form of payload-magic resolution that returns a
1102    /// reference. The owned-value form used by container code lives
1103    /// behind the [`CodecResolver`] trait impl below.
1104    ///
1105    /// Walks every registered payload magic prefix (declared via
1106    /// [`CodecInfo::payload_magic`] / [`CodecInfo::payload_magics`]) and
1107    /// returns the codec whose magic is a prefix of `first_bytes` —
1108    /// however much of the stream's leading payload the caller has
1109    /// (an Ogg demuxer passes the first packet of a logical stream; a
1110    /// raw-stream prober passes the file head). The **longest**
1111    /// matching magic wins (most specific claim); remaining ties are
1112    /// broken by [`CodecInfo::resolution_priority`] (lower wins), then
1113    /// by registration order (first wins). Unlike the tag path there
1114    /// is no probe step: a payload magic is itself the bitstream
1115    /// evidence a probe would look for, and specificity is expressed
1116    /// by prefix length instead of a confidence value.
1117    /// [`resolve_payload_magic_candidates`](Self::resolve_payload_magic_candidates)
1118    /// returns the whole ranked list this picks the head of.
1119    pub fn resolve_payload_magic_ref(&self, first_bytes: &[u8]) -> Option<&CodecId> {
1120        self.resolve_payload_magic_candidates(first_bytes)
1121            .first()
1122            .map(|c| c.id)
1123    }
1124
1125    /// Every registered payload magic that is a prefix of
1126    /// `first_bytes`, ranked by the resolution rule: magic length
1127    /// descending, then priority ascending, then registration order
1128    /// ascending. The payload-magic companion to
1129    /// [`resolve_tag_candidates`](Self::resolve_tag_candidates).
1130    pub fn resolve_payload_magic_candidates(
1131        &self,
1132        first_bytes: &[u8],
1133    ) -> Vec<PayloadMagicCandidate<'_>> {
1134        let mut out: Vec<PayloadMagicCandidate<'_>> = self
1135            .magic_index
1136            .iter()
1137            .filter(|(magic, _)| first_bytes.starts_with(magic))
1138            .map(|(magic, i)| {
1139                let rec = &self.registrations[*i];
1140                PayloadMagicCandidate {
1141                    id: &rec.id,
1142                    magic: magic.as_slice(),
1143                    priority: rec.priority,
1144                    order: *i,
1145                }
1146            })
1147            .collect();
1148        // One registration may claim several magics that all match
1149        // (`order` then repeats); the stable sort keeps those in their
1150        // declaration order, which is the only remaining distinction
1151        // and is itself fixed by the registration.
1152        out.sort_by(|a, b| {
1153            b.magic
1154                .len()
1155                .cmp(&a.magic.len())
1156                .then(a.priority.cmp(&b.priority))
1157                .then(a.order.cmp(&b.order))
1158        });
1159        out
1160    }
1161
1162    /// Iterator over every `(payload magic, codec_id)` pair currently
1163    /// registered, in registration order — the payload-magic companion
1164    /// to [`all_tag_registrations`](Self::all_tag_registrations).
1165    pub fn all_payload_magic_registrations(&self) -> impl Iterator<Item = (&[u8], &CodecId)> {
1166        self.magic_index
1167            .iter()
1168            .map(move |(magic, i)| (magic.as_slice(), &self.registrations[*i].id))
1169    }
1170}
1171
1172/// Implement the shared [`CodecResolver`] interface so container
1173/// demuxers can accept `&dyn CodecResolver` without depending on
1174/// this crate directly — the trait lives in oxideav-core.
1175impl CodecResolver for CodecRegistry {
1176    fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId> {
1177        self.resolve_tag_ref(ctx).cloned()
1178    }
1179
1180    fn resolve_payload_magic(&self, first_packet: &[u8]) -> Option<CodecId> {
1181        self.resolve_payload_magic_ref(first_packet).cloned()
1182    }
1183}
1184
1185#[cfg(test)]
1186mod tag_tests {
1187    use super::*;
1188    use crate::CodecCapabilities;
1189
1190    /// Probe: return 1.0 iff the peeked bytes look like MS-MPEG4 (no
1191    /// 0x000001 start code in the first few bytes).
1192    fn probe_msmpeg4(ctx: &ProbeContext) -> f32 {
1193        match ctx.packet {
1194            Some(d) if !d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
1195            Some(_) => 0.0,
1196            None => 0.5, // no data yet — weak evidence
1197        }
1198    }
1199
1200    /// Probe: return 1.0 iff the peeked bytes look like MPEG-4 Part 2
1201    /// (starts with a 0x000001 start code in the first few bytes).
1202    fn probe_mpeg4_part2(ctx: &ProbeContext) -> f32 {
1203        match ctx.packet {
1204            Some(d) if d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
1205            Some(_) => 0.0,
1206            None => 0.5,
1207        }
1208    }
1209
1210    fn info(id: &str) -> CodecInfo {
1211        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1212    }
1213
1214    #[test]
1215    fn resolve_single_claim_no_probe() {
1216        let mut reg = CodecRegistry::new();
1217        reg.register(info("flac").tag(CodecTag::fourcc(b"FLAC")));
1218        let t = CodecTag::fourcc(b"FLAC");
1219        assert_eq!(
1220            reg.resolve_tag_ref(&ProbeContext::new(&t))
1221                .map(|c| c.as_str()),
1222            Some("flac"),
1223        );
1224    }
1225
1226    #[test]
1227    fn resolve_missing_tag_returns_none() {
1228        let reg = CodecRegistry::new();
1229        let t = CodecTag::fourcc(b"????");
1230        assert!(reg.resolve_tag_ref(&ProbeContext::new(&t)).is_none());
1231    }
1232
1233    #[test]
1234    fn unprobed_claims_tie_first_registered_wins() {
1235        // Two unprobed claims on the same tag: deterministic order.
1236        let mut reg = CodecRegistry::new();
1237        reg.register(info("first").tag(CodecTag::fourcc(b"TEST")));
1238        reg.register(info("second").tag(CodecTag::fourcc(b"TEST")));
1239        let t = CodecTag::fourcc(b"TEST");
1240        assert_eq!(
1241            reg.resolve_tag_ref(&ProbeContext::new(&t))
1242                .map(|c| c.as_str()),
1243            Some("first"),
1244        );
1245    }
1246
1247    #[test]
1248    fn probe_picks_matching_bitstream() {
1249        // The core bug fix: every probe is asked and the highest
1250        // confidence wins regardless of registration order.
1251        let mut reg = CodecRegistry::new();
1252        reg.register(
1253            info("msmpeg4v3")
1254                .probe(probe_msmpeg4)
1255                .tag(CodecTag::fourcc(b"DIV3")),
1256        );
1257        reg.register(
1258            info("mpeg4video")
1259                .probe(probe_mpeg4_part2)
1260                .tag(CodecTag::fourcc(b"DIV3")),
1261        );
1262
1263        let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
1264        let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
1265        let tag = CodecTag::fourcc(b"DIV3");
1266
1267        let ctx_part2 = ProbeContext::new(&tag).packet(&mpeg4_part2);
1268        assert_eq!(
1269            reg.resolve_tag_ref(&ctx_part2).map(|c| c.as_str()),
1270            Some("mpeg4video"),
1271        );
1272        let ctx_ms = ProbeContext::new(&tag).packet(&ms_mpeg4);
1273        assert_eq!(
1274            reg.resolve_tag_ref(&ctx_ms).map(|c| c.as_str()),
1275            Some("msmpeg4v3"),
1276        );
1277    }
1278
1279    #[test]
1280    fn unprobed_claim_wins_against_low_confidence_probe() {
1281        // One codec claims a tag without a probe (→ confidence 1.0)
1282        // and another claims it with a probe returning 0.3. The
1283        // unprobed one wins — a codec that knows it owns the tag
1284        // outright should not lose to a speculative probe.
1285        let mut reg = CodecRegistry::new();
1286        reg.register(info("owner").tag(CodecTag::fourcc(b"OWN_")));
1287        reg.register(
1288            info("speculative")
1289                .probe(|_| 0.3)
1290                .tag(CodecTag::fourcc(b"OWN_")),
1291        );
1292        let t = CodecTag::fourcc(b"OWN_");
1293        assert_eq!(
1294            reg.resolve_tag_ref(&ProbeContext::new(&t))
1295                .map(|c| c.as_str()),
1296            Some("owner"),
1297        );
1298    }
1299
1300    #[test]
1301    fn probe_returning_zero_is_skipped() {
1302        let mut reg = CodecRegistry::new();
1303        reg.register(
1304            info("refuses")
1305                .probe(|_| 0.0)
1306                .tag(CodecTag::fourcc(b"MAYB")),
1307        );
1308        reg.register(info("fallback").tag(CodecTag::fourcc(b"MAYB")));
1309        let t = CodecTag::fourcc(b"MAYB");
1310        let ctx = ProbeContext::new(&t).packet(b"hello");
1311        assert_eq!(
1312            reg.resolve_tag_ref(&ctx).map(|c| c.as_str()),
1313            Some("fallback"),
1314        );
1315    }
1316
1317    #[test]
1318    fn fourcc_case_insensitive_lookup() {
1319        let mut reg = CodecRegistry::new();
1320        reg.register(info("vid").tag(CodecTag::fourcc(b"div3")));
1321        // Registered as "DIV3" (uppercase via ctor); lookup using
1322        // lowercase / mixed case also hits.
1323        let upper = CodecTag::fourcc(b"DIV3");
1324        let lower = CodecTag::fourcc(b"div3");
1325        let mixed = CodecTag::fourcc(b"DiV3");
1326        assert!(reg.resolve_tag_ref(&ProbeContext::new(&upper)).is_some());
1327        assert!(reg.resolve_tag_ref(&ProbeContext::new(&lower)).is_some());
1328        assert!(reg.resolve_tag_ref(&ProbeContext::new(&mixed)).is_some());
1329    }
1330
1331    #[test]
1332    fn wave_format_and_matroska_tags_work() {
1333        let mut reg = CodecRegistry::new();
1334        reg.register(info("mp3").tag(CodecTag::wave_format(0x0055)));
1335        reg.register(info("h264").tag(CodecTag::matroska("V_MPEG4/ISO/AVC")));
1336        let wf = CodecTag::wave_format(0x0055);
1337        let mk = CodecTag::matroska("V_MPEG4/ISO/AVC");
1338        assert_eq!(
1339            reg.resolve_tag_ref(&ProbeContext::new(&wf))
1340                .map(|c| c.as_str()),
1341            Some("mp3"),
1342        );
1343        assert_eq!(
1344            reg.resolve_tag_ref(&ProbeContext::new(&mk))
1345                .map(|c| c.as_str()),
1346            Some("h264"),
1347        );
1348    }
1349
1350    #[test]
1351    fn mp4_object_type_tag_works() {
1352        let mut reg = CodecRegistry::new();
1353        reg.register(info("aac").tag(CodecTag::mp4_object_type(0x40)));
1354        let t = CodecTag::mp4_object_type(0x40);
1355        assert_eq!(
1356            reg.resolve_tag_ref(&ProbeContext::new(&t))
1357                .map(|c| c.as_str()),
1358            Some("aac"),
1359        );
1360    }
1361
1362    #[test]
1363    fn multi_tag_claim_all_resolve() {
1364        let mut reg = CodecRegistry::new();
1365        reg.register(info("aac").tags([
1366            CodecTag::fourcc(b"MP4A"),
1367            CodecTag::wave_format(0x00FF),
1368            CodecTag::mp4_object_type(0x40),
1369            CodecTag::matroska("A_AAC"),
1370        ]));
1371        for t in [
1372            CodecTag::fourcc(b"MP4A"),
1373            CodecTag::wave_format(0x00FF),
1374            CodecTag::mp4_object_type(0x40),
1375            CodecTag::matroska("A_AAC"),
1376        ] {
1377            assert_eq!(
1378                reg.resolve_tag_ref(&ProbeContext::new(&t))
1379                    .map(|c| c.as_str()),
1380                Some("aac"),
1381                "tag {t:?} did not resolve",
1382            );
1383        }
1384    }
1385}
1386
1387#[cfg(test)]
1388mod payload_magic_tests {
1389    use super::*;
1390    use crate::CodecCapabilities;
1391
1392    fn info(id: &str) -> CodecInfo {
1393        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1394    }
1395
1396    /// Registry with the classic Ogg family registered, each under its
1397    /// real BOS magic.
1398    fn ogg_family_registry() -> CodecRegistry {
1399        let mut reg = CodecRegistry::new();
1400        reg.register(info("vorbis").payload_magic(b"\x01vorbis"));
1401        reg.register(info("opus").payload_magic(b"OpusHead"));
1402        reg.register(info("theora").payload_magic(b"\x80theora"));
1403        reg.register(info("flac").payload_magic(b"\x7fFLAC"));
1404        reg
1405    }
1406
1407    #[test]
1408    fn resolve_payload_magic_matches_first_packet_prefix() {
1409        let reg = ogg_family_registry();
1410        // A Vorbis identification header: magic + version + channels +
1411        // rate + ... — the resolver only needs the prefix to match.
1412        let vorbis_id_header = b"\x01vorbis\x00\x00\x00\x00\x02\x44\xac\x00\x00";
1413        assert_eq!(
1414            reg.resolve_payload_magic_ref(vorbis_id_header)
1415                .map(|c| c.as_str()),
1416            Some("vorbis"),
1417        );
1418        assert_eq!(
1419            reg.resolve_payload_magic_ref(b"OpusHead\x01\x02\x38\x01")
1420                .map(|c| c.as_str()),
1421            Some("opus"),
1422        );
1423        assert_eq!(
1424            reg.resolve_payload_magic_ref(b"\x80theora\x03\x02\x01")
1425                .map(|c| c.as_str()),
1426            Some("theora"),
1427        );
1428        assert_eq!(
1429            reg.resolve_payload_magic_ref(b"\x7fFLAC\x01\x00")
1430                .map(|c| c.as_str()),
1431            Some("flac"),
1432        );
1433    }
1434
1435    #[test]
1436    fn resolve_payload_magic_exact_length_packet_matches() {
1437        // A packet that is exactly the magic (nothing after it) still
1438        // resolves — starts_with is inclusive of equality.
1439        let reg = ogg_family_registry();
1440        assert_eq!(
1441            reg.resolve_payload_magic_ref(b"OpusHead")
1442                .map(|c| c.as_str()),
1443            Some("opus"),
1444        );
1445    }
1446
1447    #[test]
1448    fn resolve_payload_magic_unknown_or_short_packet_is_none() {
1449        let reg = ogg_family_registry();
1450        // Unknown magic.
1451        assert!(reg.resolve_payload_magic_ref(b"Speex   1.2.0").is_none());
1452        // Packet shorter than every registered magic.
1453        assert!(reg.resolve_payload_magic_ref(b"Opus").is_none());
1454        // Empty packet.
1455        assert!(reg.resolve_payload_magic_ref(b"").is_none());
1456    }
1457
1458    #[test]
1459    fn resolve_payload_magic_longest_prefix_wins_regardless_of_order() {
1460        // A shorter magic that is itself a prefix of a longer one must
1461        // lose to the more specific claim, whichever registered first.
1462        let mut reg = CodecRegistry::new();
1463        reg.register(info("generic").payload_magic(b"Opus"));
1464        reg.register(info("opus").payload_magic(b"OpusHead"));
1465        assert_eq!(
1466            reg.resolve_payload_magic_ref(b"OpusHead\x01")
1467                .map(|c| c.as_str()),
1468            Some("opus"),
1469        );
1470        // ...but the shorter claim still wins packets only it matches.
1471        assert_eq!(
1472            reg.resolve_payload_magic_ref(b"OpusTags")
1473                .map(|c| c.as_str()),
1474            Some("generic"),
1475        );
1476
1477        // Same result with the registration order flipped.
1478        let mut reg = CodecRegistry::new();
1479        reg.register(info("opus").payload_magic(b"OpusHead"));
1480        reg.register(info("generic").payload_magic(b"Opus"));
1481        assert_eq!(
1482            reg.resolve_payload_magic_ref(b"OpusHead\x01")
1483                .map(|c| c.as_str()),
1484            Some("opus"),
1485        );
1486    }
1487
1488    #[test]
1489    fn resolve_payload_magic_equal_length_tie_first_registered_wins() {
1490        let mut reg = CodecRegistry::new();
1491        reg.register(info("first").payload_magic(b"SameMagic"));
1492        reg.register(info("second").payload_magic(b"SameMagic"));
1493        assert_eq!(
1494            reg.resolve_payload_magic_ref(b"SameMagic\x00")
1495                .map(|c| c.as_str()),
1496            Some("first"),
1497        );
1498    }
1499
1500    #[test]
1501    fn empty_payload_magic_is_ignored_at_registration() {
1502        let mut reg = CodecRegistry::new();
1503        reg.register(info("greedy").payload_magic(b""));
1504        assert!(reg.resolve_payload_magic_ref(b"anything at all").is_none());
1505        assert_eq!(reg.all_payload_magic_registrations().count(), 0);
1506    }
1507
1508    #[test]
1509    fn payload_magics_plural_builder_and_diagnostics() {
1510        // One codec answering to several magics via the iterable
1511        // builder; the diagnostic iterator surfaces each claim in
1512        // registration order.
1513        let mut reg = CodecRegistry::new();
1514        reg.register(info("speex").payload_magics([b"Speex   ".to_vec(), b"speex-alt".to_vec()]));
1515        assert_eq!(
1516            reg.resolve_payload_magic_ref(b"Speex   1.2")
1517                .map(|c| c.as_str()),
1518            Some("speex"),
1519        );
1520        assert_eq!(
1521            reg.resolve_payload_magic_ref(b"speex-alt\x00")
1522                .map(|c| c.as_str()),
1523            Some("speex"),
1524        );
1525        let all: Vec<(&[u8], &str)> = reg
1526            .all_payload_magic_registrations()
1527            .map(|(m, id)| (m, id.as_str()))
1528            .collect();
1529        assert_eq!(
1530            all,
1531            vec![
1532                (b"Speex   ".as_slice(), "speex"),
1533                (b"speex-alt".as_slice(), "speex"),
1534            ],
1535        );
1536    }
1537
1538    #[test]
1539    fn magic_claims_compose_with_tag_claims_on_one_registration() {
1540        // A codec that lives in both Ogg and Matroska declares both
1541        // claim kinds on one CodecInfo; each resolution path finds it.
1542        let mut reg = CodecRegistry::new();
1543        reg.register(
1544            info("vorbis")
1545                .tag(CodecTag::matroska("A_VORBIS"))
1546                .payload_magic(b"\x01vorbis"),
1547        );
1548        let mk = CodecTag::matroska("A_VORBIS");
1549        assert_eq!(
1550            reg.resolve_tag_ref(&ProbeContext::new(&mk))
1551                .map(|c| c.as_str()),
1552            Some("vorbis"),
1553        );
1554        assert_eq!(
1555            reg.resolve_payload_magic_ref(b"\x01vorbis\x00")
1556                .map(|c| c.as_str()),
1557            Some("vorbis"),
1558        );
1559    }
1560
1561    #[test]
1562    fn resolver_trait_payload_magic_surface() {
1563        // The owned-value trait form mirrors the inherent form, and
1564        // the default implementation (NullCodecResolver) resolves
1565        // nothing.
1566        let reg = ogg_family_registry();
1567        let resolver: &dyn CodecResolver = &reg;
1568        assert_eq!(
1569            resolver
1570                .resolve_payload_magic(b"OpusHead\x01")
1571                .map(|c| c.0.clone()),
1572            Some("opus".to_owned()),
1573        );
1574        assert!(resolver.resolve_payload_magic(b"unknown").is_none());
1575
1576        let null = crate::NullCodecResolver;
1577        assert!(null.resolve_payload_magic(b"OpusHead\x01").is_none());
1578    }
1579
1580    /// The surface is container-agnostic: a raw elementary stream
1581    /// identified by a file-head magic resolves through the same path
1582    /// as the Ogg family — nothing about the mechanism is Ogg-shaped.
1583    #[test]
1584    fn payload_magic_serves_non_ogg_carriage() {
1585        let mut reg = CodecRegistry::new();
1586        reg.register(info("flac").payload_magic(b"fLaC"));
1587        reg.register(info("shorten").payload_magic(b"ajkg"));
1588
1589        assert_eq!(
1590            reg.resolve_payload_magic_ref(b"fLaC\x00\x00\x00\x22"),
1591            Some(&CodecId::new("flac"))
1592        );
1593        assert_eq!(
1594            reg.resolve_payload_magic_ref(b"ajkg\x02"),
1595            Some(&CodecId::new("shorten"))
1596        );
1597        assert_eq!(reg.resolve_payload_magic_ref(b"RIFF"), None);
1598    }
1599}
1600
1601/// Resolution-order tests. Every id / tag / magic here is invented:
1602/// the rule is format-agnostic and must be provable with synthetic
1603/// registrations only.
1604#[cfg(test)]
1605mod resolution_order_tests {
1606    use super::*;
1607    use crate::CodecCapabilities;
1608
1609    fn info(id: &str) -> CodecInfo {
1610        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1611    }
1612
1613    fn tag_ids<'a>(cands: &[TagCandidate<'a>]) -> Vec<&'a str> {
1614        cands.iter().map(|c| c.id.as_str()).collect()
1615    }
1616
1617    fn magic_ids<'a>(cands: &[PayloadMagicCandidate<'a>]) -> Vec<&'a str> {
1618        cands.iter().map(|c| c.id.as_str()).collect()
1619    }
1620
1621    #[test]
1622    fn codec_info_resolution_priority_defaults_and_builder() {
1623        assert_eq!(info("x").resolution_priority, crate::DEFAULT_PRIORITY);
1624        assert_eq!(info("x").with_resolution_priority(7).resolution_priority, 7);
1625    }
1626
1627    #[test]
1628    fn equal_tag_claims_resolve_by_registration_order_across_fresh_contexts() {
1629        const ORDER: [&str; 5] = ["zeta", "alpha", "omicron", "beta", "kappa"];
1630        let tag = CodecTag::fourcc(b"SYNT");
1631        for _ in 0..1000 {
1632            let mut ctx = crate::RuntimeContext::new();
1633            for id in ORDER {
1634                ctx.codecs.register(info(id).tag(tag.clone()));
1635            }
1636            let pc = ProbeContext::new(&tag);
1637            assert_eq!(
1638                ctx.codecs.resolve_tag_ref(&pc).map(|c| c.as_str()),
1639                Some("zeta")
1640            );
1641            let cands = ctx.codecs.resolve_tag_candidates(&pc);
1642            assert_eq!(tag_ids(&cands), ORDER.to_vec());
1643            assert_eq!(
1644                cands.iter().map(|c| c.order).collect::<Vec<_>>(),
1645                vec![0, 1, 2, 3, 4]
1646            );
1647        }
1648    }
1649
1650    #[test]
1651    fn equal_magic_claims_resolve_by_registration_order_across_fresh_contexts() {
1652        const ORDER: [&str; 4] = ["zeta", "alpha", "omicron", "beta"];
1653        for _ in 0..1000 {
1654            let mut ctx = crate::RuntimeContext::new();
1655            for id in ORDER {
1656                ctx.codecs.register(info(id).payload_magic(b"SYNTHMAGIC"));
1657            }
1658            assert_eq!(
1659                ctx.codecs
1660                    .resolve_payload_magic_ref(b"SYNTHMAGIC\x00")
1661                    .map(|c| c.as_str()),
1662                Some("zeta")
1663            );
1664            let cands = ctx
1665                .codecs
1666                .resolve_payload_magic_candidates(b"SYNTHMAGIC\x00");
1667            assert_eq!(magic_ids(&cands), ORDER.to_vec());
1668        }
1669    }
1670
1671    #[test]
1672    fn tag_priority_beats_registration_order() {
1673        let tag = CodecTag::wave_format(0x7777);
1674        let mut reg = CodecRegistry::new();
1675        reg.register(info("earlier").tag(tag.clone()));
1676        reg.register(
1677            info("later-preferred")
1678                .with_resolution_priority(10)
1679                .tag(tag.clone()),
1680        );
1681        let pc = ProbeContext::new(&tag);
1682        assert_eq!(
1683            reg.resolve_tag_ref(&pc).map(|c| c.as_str()),
1684            Some("later-preferred")
1685        );
1686        let cands = reg.resolve_tag_candidates(&pc);
1687        assert_eq!(tag_ids(&cands), vec!["later-preferred", "earlier"]);
1688        assert_eq!(cands[0].priority, 10);
1689        assert_eq!(cands[0].order, 1);
1690        assert_eq!(cands[1].priority, crate::DEFAULT_PRIORITY);
1691        assert_eq!(cands[1].order, 0);
1692        assert_eq!(cands[0].confidence, 1.0);
1693    }
1694
1695    #[test]
1696    fn tag_confidence_beats_priority() {
1697        // A probe that positively identifies the payload wins against
1698        // an unprobed-but-top-priority claim only when its confidence
1699        // is higher; at 1.0 vs 1.0 the priority decides.
1700        let tag = CodecTag::fourcc(b"AMBI");
1701        let mut reg = CodecRegistry::new();
1702        reg.register(
1703            info("prioritised")
1704                .with_resolution_priority(i32::MIN)
1705                .probe(|_| 0.6)
1706                .tag(tag.clone()),
1707        );
1708        reg.register(info("certain").tag(tag.clone()));
1709        let pc = ProbeContext::new(&tag);
1710        assert_eq!(
1711            reg.resolve_tag_ref(&pc).map(|c| c.as_str()),
1712            Some("certain")
1713        );
1714        let cands = reg.resolve_tag_candidates(&pc);
1715        assert_eq!(tag_ids(&cands), vec!["certain", "prioritised"]);
1716    }
1717
1718    #[test]
1719    fn tag_candidates_drop_refusing_and_non_finite_probes() {
1720        let tag = CodecTag::fourcc(b"NANP");
1721        let mut reg = CodecRegistry::new();
1722        reg.register(info("refuses").probe(|_| 0.0).tag(tag.clone()));
1723        reg.register(info("broken").probe(|_| f32::NAN).tag(tag.clone()));
1724        reg.register(info("negative").probe(|_| -1.0).tag(tag.clone()));
1725        reg.register(info("accepts").probe(|_| 0.2).tag(tag.clone()));
1726        let pc = ProbeContext::new(&tag);
1727        assert_eq!(tag_ids(&reg.resolve_tag_candidates(&pc)), vec!["accepts"]);
1728        assert_eq!(
1729            reg.resolve_tag_ref(&pc).map(|c| c.as_str()),
1730            Some("accepts")
1731        );
1732        // An unclaimed tag yields an empty list, not an error.
1733        let other = CodecTag::fourcc(b"NONE");
1734        assert!(
1735            reg.resolve_tag_candidates(&ProbeContext::new(&other))
1736                .is_empty()
1737        );
1738    }
1739
1740    #[test]
1741    fn duplicate_tag_claim_by_one_id_is_visible_not_folded() {
1742        // The registry contract allows registering one id several
1743        // times (multi-implementation codecs). A tag claimed twice by
1744        // the same id resolves to that id either way; the candidate
1745        // list shows both claims so an audit can flag the redundancy.
1746        let tag = CodecTag::fourcc(b"TWIC");
1747        let mut reg = CodecRegistry::new();
1748        reg.register(info("dup").tag(tag.clone()));
1749        reg.register(info("other").tag(tag.clone()));
1750        reg.register(info("dup").tag(tag.clone()));
1751        let pc = ProbeContext::new(&tag);
1752        let cands = reg.resolve_tag_candidates(&pc);
1753        assert_eq!(tag_ids(&cands), vec!["dup", "other", "dup"]);
1754        assert_eq!(
1755            cands.iter().map(|c| c.order).collect::<Vec<_>>(),
1756            vec![0, 1, 2]
1757        );
1758        assert_eq!(reg.resolve_tag_ref(&pc).map(|c| c.as_str()), Some("dup"));
1759    }
1760
1761    #[test]
1762    fn magic_priority_beats_registration_order_but_not_length() {
1763        let mut reg = CodecRegistry::new();
1764        reg.register(info("earlier").payload_magic(b"MAGIC"));
1765        reg.register(
1766            info("later-preferred")
1767                .with_resolution_priority(1)
1768                .payload_magic(b"MAGIC"),
1769        );
1770        reg.register(info("longer").payload_magic(b"MAGICAL"));
1771        // Equal-length tie: priority decides.
1772        let cands = reg.resolve_payload_magic_candidates(b"MAGIC\x00");
1773        assert_eq!(magic_ids(&cands), vec!["later-preferred", "earlier"]);
1774        assert_eq!(cands[0].order, 1);
1775        assert_eq!(cands[0].magic, b"MAGIC");
1776        // A longer matching magic beats both regardless of priority.
1777        let cands = reg.resolve_payload_magic_candidates(b"MAGICAL\x00");
1778        assert_eq!(
1779            magic_ids(&cands),
1780            vec!["longer", "later-preferred", "earlier"]
1781        );
1782        assert_eq!(
1783            reg.resolve_payload_magic_ref(b"MAGICAL\x00")
1784                .map(|c| c.as_str()),
1785            Some("longer")
1786        );
1787        // Nothing matches: empty list, None.
1788        assert!(reg.resolve_payload_magic_candidates(b"other").is_empty());
1789        assert!(reg.resolve_payload_magic_ref(b"other").is_none());
1790    }
1791
1792    fn dec(_p: &crate::CodecParameters) -> crate::Result<Box<dyn Decoder>> {
1793        Err(Error::unsupported("dummy"))
1794    }
1795    fn enc(_p: &crate::CodecParameters) -> crate::Result<Box<dyn Encoder>> {
1796        Err(Error::unsupported("dummy"))
1797    }
1798
1799    #[test]
1800    fn enumerations_follow_registration_order() {
1801        for _ in 0..200 {
1802            let mut reg = CodecRegistry::new();
1803            reg.register(
1804                info("zeta")
1805                    .decoder(dec)
1806                    .tags([CodecTag::fourcc(b"ZET1"), CodecTag::fourcc(b"ZET2")]),
1807            );
1808            reg.register(info("alpha").encoder(enc).tag(CodecTag::fourcc(b"ALPH")));
1809            reg.register(info("omicron").decoder(dec).encoder(enc));
1810            // Second impl of an existing id does not move it.
1811            reg.register(info("zeta").encoder(enc).tag(CodecTag::fourcc(b"ZET3")));
1812            // Tag-only registration: no impl, but its tags are listed.
1813            reg.register(info("beta").tag(CodecTag::fourcc(b"BETA")));
1814
1815            let ids: Vec<&str> = reg
1816                .all_implementations()
1817                .map(|(id, _)| id.as_str())
1818                .collect();
1819            assert_eq!(ids, vec!["zeta", "zeta", "alpha", "omicron"]);
1820            let dec_ids: Vec<&str> = reg.decoder_ids().map(|i| i.as_str()).collect();
1821            assert_eq!(dec_ids, vec!["zeta", "omicron"]);
1822            let enc_ids: Vec<&str> = reg.encoder_ids().map(|i| i.as_str()).collect();
1823            assert_eq!(enc_ids, vec!["zeta", "alpha", "omicron"]);
1824            let tags: Vec<(String, &str)> = reg
1825                .all_tag_registrations()
1826                .map(|(t, id)| (format!("{t:?}"), id.as_str()))
1827                .collect();
1828            let want: Vec<(String, &str)> = [
1829                (CodecTag::fourcc(b"ZET1"), "zeta"),
1830                (CodecTag::fourcc(b"ZET2"), "zeta"),
1831                (CodecTag::fourcc(b"ALPH"), "alpha"),
1832                (CodecTag::fourcc(b"ZET3"), "zeta"),
1833                (CodecTag::fourcc(b"BETA"), "beta"),
1834            ]
1835            .into_iter()
1836            .map(|(t, id)| (format!("{t:?}"), id))
1837            .collect();
1838            assert_eq!(tags, want);
1839        }
1840    }
1841
1842    #[test]
1843    fn resolution_priority_is_independent_of_capabilities_priority() {
1844        // A registration that ranks itself first as an *implementation*
1845        // (HW-style capabilities priority) does not thereby win a tag
1846        // tie against a different codec id registered earlier.
1847        let tag = CodecTag::fourcc(b"SHRD");
1848        let mut reg = CodecRegistry::new();
1849        reg.register(info("sw-id").tag(tag.clone()));
1850        reg.register(
1851            CodecInfo::new(CodecId::new("hw-id"))
1852                .capabilities(CodecCapabilities::video("hw-id").with_priority(10))
1853                .tag(tag.clone()),
1854        );
1855        let pc = ProbeContext::new(&tag);
1856        assert_eq!(reg.resolve_tag_ref(&pc).map(|c| c.as_str()), Some("sw-id"));
1857        let cands = reg.resolve_tag_candidates(&pc);
1858        assert!(cands.iter().all(|c| c.priority == crate::DEFAULT_PRIORITY));
1859    }
1860}
1861
1862#[cfg(test)]
1863mod engine_tests {
1864    use super::*;
1865    use crate::engine::HwDeviceInfo;
1866
1867    #[test]
1868    fn codec_info_engine_id_and_probe_default_to_none() {
1869        let ci = CodecInfo::new(CodecId::new("h264"));
1870        assert!(ci.engine_id.is_none());
1871        assert!(ci.engine_probe.is_none());
1872    }
1873
1874    #[test]
1875    fn codec_info_engine_builder_methods_set_fields() {
1876        fn dummy_probe() -> Vec<HwDeviceInfo> {
1877            vec![]
1878        }
1879        let ci = CodecInfo::new(CodecId::new("h264"))
1880            .with_engine_id("nvidia")
1881            .with_engine_probe(dummy_probe);
1882        assert_eq!(ci.engine_id, Some("nvidia"));
1883        assert!(ci.engine_probe.is_some());
1884        let probe = ci.engine_probe.unwrap();
1885        let result = probe();
1886        assert!(result.is_empty());
1887    }
1888
1889    #[test]
1890    fn registering_codec_with_engine_metadata_does_not_panic() {
1891        // The new fields are passthrough metadata — register() should
1892        // accept them without affecting existing id/tag bookkeeping.
1893        fn dummy_probe() -> Vec<HwDeviceInfo> {
1894            vec![]
1895        }
1896        let mut reg = CodecRegistry::new();
1897        reg.register(
1898            CodecInfo::new(CodecId::new("h264"))
1899                .capabilities(CodecCapabilities::audio("h264_nvdec"))
1900                .tag(CodecTag::fourcc(b"H264"))
1901                .with_engine_id("nvidia")
1902                .with_engine_probe(dummy_probe),
1903        );
1904        let t = CodecTag::fourcc(b"H264");
1905        assert_eq!(
1906            reg.resolve_tag_ref(&ProbeContext::new(&t))
1907                .map(|c| c.as_str()),
1908            Some("h264"),
1909        );
1910    }
1911
1912    /// No-op decoder factory so the registration produces a real
1913    /// CodecImplementation (the registry skips tag-only entries —
1914    /// without a factory there'd be nothing in `implementations()`
1915    /// to assert against).
1916    fn dummy_decoder_factory(
1917        _params: &crate::CodecParameters,
1918    ) -> crate::Result<Box<dyn super::Decoder>> {
1919        Err(crate::Error::unsupported("dummy decoder"))
1920    }
1921
1922    #[test]
1923    fn engine_metadata_propagates_through_register() {
1924        fn dummy_probe() -> Vec<HwDeviceInfo> {
1925            vec![]
1926        }
1927        let mut reg = CodecRegistry::default();
1928        reg.register(
1929            CodecInfo::new(CodecId::new("h264"))
1930                .capabilities(CodecCapabilities::video("h264_test"))
1931                .decoder(dummy_decoder_factory)
1932                .with_engine_id("test-backend")
1933                .with_engine_probe(dummy_probe),
1934        );
1935        let impls = reg.implementations(&CodecId::new("h264"));
1936        assert_eq!(impls.len(), 1);
1937        assert_eq!(impls[0].engine_id, Some("test-backend"));
1938        assert!(impls[0].engine_probe.is_some());
1939    }
1940
1941    #[test]
1942    fn engine_metadata_absent_for_sw_codecs() {
1943        // SW codecs don't call the engine builders — both fields
1944        // should land as None on the resulting CodecImplementation.
1945        let mut reg = CodecRegistry::default();
1946        reg.register(
1947            CodecInfo::new(CodecId::new("flac"))
1948                .capabilities(CodecCapabilities::audio("flac_sw"))
1949                .decoder(dummy_decoder_factory),
1950        );
1951        let impls = reg.implementations(&CodecId::new("flac"));
1952        assert_eq!(impls.len(), 1);
1953        assert!(impls[0].engine_id.is_none());
1954        assert!(impls[0].engine_probe.is_none());
1955    }
1956}
1957
1958#[cfg(test)]
1959mod output_params_tests {
1960    use super::*;
1961    use crate::{
1962        ChannelLayout, ColorRange, ColorSignal, MediaType, Rational, SampleFormat, VideoFrame,
1963    };
1964
1965    /// Shared plumbing for the mocks: the codec id they report.
1966    struct Base {
1967        id: CodecId,
1968    }
1969
1970    impl Base {
1971        fn new(id: &str) -> Self {
1972            Self {
1973                id: CodecId::new(id),
1974            }
1975        }
1976    }
1977
1978    /// Implements nothing of the output-description surface.
1979    struct Neither(Base);
1980
1981    impl Decoder for Neither {
1982        fn codec_id(&self) -> &CodecId {
1983            &self.0.id
1984        }
1985        fn send_packet(&mut self, _: &Packet) -> Result<()> {
1986            Ok(())
1987        }
1988        fn receive_frame(&mut self) -> Result<Frame> {
1989            Err(Error::NeedMore)
1990        }
1991        fn flush(&mut self) -> Result<()> {
1992            Ok(())
1993        }
1994    }
1995
1996    /// Implements only the older narrow query.
1997    struct PixOnly(Base);
1998
1999    impl Decoder for PixOnly {
2000        fn codec_id(&self) -> &CodecId {
2001            &self.0.id
2002        }
2003        fn send_packet(&mut self, _: &Packet) -> Result<()> {
2004            Ok(())
2005        }
2006        fn receive_frame(&mut self) -> Result<Frame> {
2007            Err(Error::NeedMore)
2008        }
2009        fn flush(&mut self) -> Result<()> {
2010            Ok(())
2011        }
2012        fn output_pixel_format(&self) -> Option<PixelFormat> {
2013            Some(PixelFormat::Yuv420P10Le)
2014        }
2015    }
2016
2017    /// A decoder that changes resolution mid-stream: every packet whose
2018    /// first byte is non-zero is a "keyframe" announcing that byte as
2019    /// the new height; the shape becomes visible with the first frame
2020    /// decoded from it, following the trait contract.
2021    struct Switching {
2022        base: Base,
2023        queued: std::collections::VecDeque<u32>,
2024        current: Option<u32>,
2025        generation: u64,
2026    }
2027
2028    impl Switching {
2029        fn new() -> Self {
2030            Self {
2031                base: Base::new("mock-switch"),
2032                queued: Default::default(),
2033                current: None,
2034                generation: 0,
2035            }
2036        }
2037    }
2038
2039    impl Decoder for Switching {
2040        fn codec_id(&self) -> &CodecId {
2041            &self.base.id
2042        }
2043        fn send_packet(&mut self, packet: &Packet) -> Result<()> {
2044            let h = packet.data.first().copied().unwrap_or(0) as u32;
2045            let h = if h == 0 {
2046                *self.queued.back().or(self.current.as_ref()).unwrap_or(&16)
2047            } else {
2048                h
2049            };
2050            self.queued.push_back(h);
2051            Ok(())
2052        }
2053        fn receive_frame(&mut self) -> Result<Frame> {
2054            let h = self.queued.pop_front().ok_or(Error::NeedMore)?;
2055            if let Some(cur) = self.current
2056                && cur != h
2057            {
2058                self.generation = self.generation.wrapping_add(1);
2059            }
2060            self.current = Some(h);
2061            Ok(Frame::Video(VideoFrame {
2062                pts: None,
2063                planes: Vec::new(),
2064            }))
2065        }
2066        fn flush(&mut self) -> Result<()> {
2067            Ok(())
2068        }
2069        fn output_params(&self) -> Option<CodecParameters> {
2070            let h = self.current?;
2071            let mut p = CodecParameters::video(self.base.id.clone());
2072            p.width = Some(32);
2073            p.height = Some(h);
2074            p.pixel_format = Some(PixelFormat::Yuv420P);
2075            Some(p)
2076        }
2077        fn output_pixel_format(&self) -> Option<PixelFormat> {
2078            self.output_params().and_then(|p| p.pixel_format)
2079        }
2080        fn output_params_generation(&self) -> u64 {
2081            self.generation
2082        }
2083    }
2084
2085    fn pkt(b: u8) -> Packet {
2086        Packet::new(0, crate::TimeBase::new(1, 1), vec![b])
2087    }
2088
2089    #[test]
2090    fn default_without_any_answer_is_none_and_generation_zero() {
2091        let d = Neither(Base::new("mock"));
2092        assert!(d.output_params().is_none());
2093        assert_eq!(d.output_params_generation(), 0);
2094    }
2095
2096    #[test]
2097    fn default_derives_from_output_pixel_format() {
2098        let d = PixOnly(Base::new("mock-pix"));
2099        let p = d.output_params().expect("derived from output_pixel_format");
2100        assert_eq!(p.codec_id, CodecId::new("mock-pix"));
2101        assert_eq!(p.media_type, MediaType::Video);
2102        assert_eq!(p.pixel_format, Some(PixelFormat::Yuv420P10Le));
2103        assert_eq!(p.width, None);
2104        assert_eq!(p.height, None);
2105        assert_eq!(d.output_params_generation(), 0);
2106    }
2107
2108    #[test]
2109    fn merge_decoder_wins_for_output_fields_container_keeps_the_rest() {
2110        let mut stream = CodecParameters::video(CodecId::new("h264"));
2111        stream.width = Some(1920);
2112        stream.height = Some(1088);
2113        stream.frame_rate = Some(Rational::new(25, 1));
2114        stream.extradata = vec![1, 2, 3];
2115        stream.bit_rate = Some(5_000_000);
2116        stream.language = Some("en".into());
2117        stream.color_signal = ColorSignal::from_code_points(1, 1, 1, false);
2118
2119        let mut out = CodecParameters::video(CodecId::new("h264"));
2120        out.height = Some(1080);
2121        out.pixel_format = Some(PixelFormat::Yuv420P);
2122        out.color_signal = ColorSignal::default().with_range(ColorRange::Full);
2123
2124        assert!(stream.merge_output_params(&out));
2125        assert_eq!(stream.width, Some(1920), "decoder None keeps container");
2126        assert_eq!(stream.height, Some(1080), "decoder Some wins");
2127        assert_eq!(stream.pixel_format, Some(PixelFormat::Yuv420P));
2128        assert_eq!(stream.frame_rate, Some(Rational::new(25, 1)));
2129        assert_eq!(stream.extradata, vec![1, 2, 3]);
2130        assert_eq!(stream.bit_rate, Some(5_000_000));
2131        assert_eq!(stream.language.as_deref(), Some("en"));
2132        assert_eq!(stream.color_signal.range, ColorRange::Full);
2133        assert_eq!(
2134            stream.color_signal.primaries,
2135            ColorSignal::from_code_points(1, 1, 1, false).primaries,
2136            "unspecified decoder parts keep the container's"
2137        );
2138        assert!(!stream.merge_output_params(&out), "second merge is a no-op");
2139    }
2140
2141    #[test]
2142    fn merge_audio_keeps_count_and_layout_consistent() {
2143        let mut stream = CodecParameters::audio(CodecId::new("mp3"))
2144            .channels(2)
2145            .channel_layout(ChannelLayout::Stereo);
2146        stream.sample_rate = Some(44_100);
2147        let mut out = CodecParameters::audio(CodecId::new("mp3")).channels(1);
2148        out.sample_format = Some(SampleFormat::S16);
2149        out.sample_rate = Some(48_000);
2150        assert!(stream.merge_output_params(&out));
2151        assert_eq!(stream.channels, Some(1));
2152        assert_eq!(stream.channel_layout, None, "stale stereo layout dropped");
2153        assert_eq!(stream.resolved_layout(), Some(ChannelLayout::Mono));
2154        assert_eq!(stream.sample_rate, Some(48_000));
2155        assert_eq!(stream.sample_format, Some(SampleFormat::S16));
2156
2157        // Layout-only answer re-derives a disagreeing count.
2158        let mut stream = CodecParameters::audio(CodecId::new("aac")).channels(2);
2159        let out = CodecParameters::audio(CodecId::new("aac")).channel_layout(ChannelLayout::Mono);
2160        assert!(stream.merge_output_params(&out));
2161        assert_eq!(stream.channels, Some(1));
2162        assert_eq!(stream.channel_layout, Some(ChannelLayout::Mono));
2163    }
2164
2165    #[test]
2166    fn merge_of_empty_answer_changes_nothing() {
2167        let mut stream = CodecParameters::video(CodecId::new("vp9"));
2168        stream.width = Some(640);
2169        let out = CodecParameters::video(CodecId::new("vp9"));
2170        assert!(!stream.merge_output_params(&out));
2171        assert_eq!(stream.width, Some(640));
2172    }
2173
2174    #[test]
2175    fn generation_bumps_with_the_first_frame_of_the_new_shape() {
2176        let mut d = Switching::new();
2177        assert!(d.output_params().is_none(), "nothing parsed yet");
2178        d.send_packet(&pkt(16)).unwrap();
2179        d.send_packet(&pkt(0)).unwrap();
2180        // New shape announced while old-shape frames are still queued.
2181        d.send_packet(&pkt(24)).unwrap();
2182
2183        d.receive_frame().unwrap();
2184        assert_eq!(d.output_params().unwrap().height, Some(16));
2185        assert_eq!(d.output_params_generation(), 0, "first resolve is no bump");
2186        d.receive_frame().unwrap();
2187        assert_eq!(d.output_params_generation(), 0, "same shape, no bump");
2188        assert_eq!(d.output_params().unwrap().height, Some(16));
2189        d.receive_frame().unwrap();
2190        assert_eq!(d.output_params_generation(), 1);
2191        assert_eq!(d.output_params().unwrap().height, Some(24));
2192        assert_eq!(d.output_pixel_format(), Some(PixelFormat::Yuv420P));
2193    }
2194
2195    #[test]
2196    fn watch_merges_once_and_again_only_on_change() {
2197        let mut d = Switching::new();
2198        let mut stream = CodecParameters::video(CodecId::new("mock-switch"));
2199        stream.bit_rate = Some(1234);
2200        let mut watch = OutputParamsWatch::new();
2201
2202        // Nothing known yet: prime and observe leave the stream alone.
2203        assert!(!watch.prime(&d, &mut stream));
2204        assert!(!watch.observe(&d, &mut stream));
2205
2206        for b in [16, 0, 0, 24, 0] {
2207            d.send_packet(&pkt(b)).unwrap();
2208        }
2209        let mut changes = Vec::new();
2210        let dyn_dec: &mut dyn Decoder = &mut d;
2211        while dyn_dec.receive_frame().is_ok() {
2212            changes.push(watch.observe(&*dyn_dec, &mut stream));
2213        }
2214        assert_eq!(changes, vec![true, false, false, true, false]);
2215        assert_eq!(stream.height, Some(24));
2216        assert_eq!(stream.width, Some(32));
2217        assert_eq!(stream.bit_rate, Some(1234));
2218
2219        // reset forces a re-read; same answer → no field change.
2220        watch.reset();
2221        assert!(!watch.observe(&d, &mut stream));
2222        assert_eq!(watch, {
2223            let mut w = OutputParamsWatch::new();
2224            w.observe(&d, &mut stream.clone());
2225            w
2226        });
2227    }
2228
2229    #[test]
2230    fn watch_picks_up_the_derived_default() {
2231        let d = PixOnly(Base::new("mock-pix"));
2232        let mut stream = CodecParameters::video(CodecId::new("mock-pix"));
2233        stream.width = Some(64);
2234        let mut watch = OutputParamsWatch::new();
2235        assert!(watch.observe(&d, &mut stream));
2236        assert_eq!(stream.pixel_format, Some(PixelFormat::Yuv420P10Le));
2237        assert_eq!(stream.width, Some(64));
2238        assert!(!watch.observe(&d, &mut stream));
2239    }
2240}