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, when it
141    /// knows it before the first frame (typically from the codec's
142    /// configuration record in `extradata`) and the container did not
143    /// declare it in [`CodecParameters::pixel_format`](crate::CodecParameters).
144    /// Pipelines use it to convert into an encoder's accepted layouts.
145    /// Default `None` ("unknown — trust the stream parameters").
146    fn output_pixel_format(&self) -> Option<crate::PixelFormat> {
147        None
148    }
149}
150
151/// A frame-to-packet encoder.
152pub trait Encoder: Send {
153    /// Identifier of the codec this encoder produces.
154    fn codec_id(&self) -> &CodecId;
155
156    /// Parameters describing this encoder's output stream (to feed into a muxer).
157    fn output_params(&self) -> &CodecParameters;
158
159    /// Feed one uncompressed frame. May or may not produce a packet
160    /// immediately — call `receive_packet` in a loop afterwards.
161    fn send_frame(&mut self, frame: &Frame) -> Result<()>;
162
163    /// Pull the next encoded packet, if any. Returns `Error::NeedMore`
164    /// when the encoder needs another frame (or a `flush`).
165    fn receive_packet(&mut self) -> Result<Packet>;
166
167    /// Signal end of input: drain internal lookahead so the remaining
168    /// packets become available via `receive_packet`.
169    fn flush(&mut self) -> Result<()>;
170
171    /// Advisory: announce the runtime environment. Same semantics as
172    /// [`Decoder::set_execution_context`].
173    fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
174}
175
176/// Default-impl helper for [`Decoder::receive_arena_frame`]: copy a
177/// heap-backed [`crate::VideoFrame`] into a freshly-leased
178/// [`arena::sync::Frame`].
179///
180/// Allocates a single-slot, single-arena `arena::sync::ArenaPool`
181/// sized to fit the planes verbatim. The pool is dropped at the end of
182/// this call; the returned `Frame` keeps its leased buffer alive via
183/// `Arc<FrameInner>` (the `Arena`'s `Weak` handle to the dropped pool
184/// just stops upgrading — the buffer drops normally when the last
185/// `Frame` clone goes away).
186///
187/// Width / height / pixel-format on the returned `FrameHeader` are
188/// derived from the plane shape: `width = plane[0].stride`,
189/// `height = plane[0].data.len() / stride`. Pixel format is left as
190/// [`PixelFormat::Yuv420P`] when there are 3 planes, else the first
191/// per-plane sensible default — this is a best-effort label for the
192/// generic conversion path; decoders that override
193/// `receive_arena_frame` themselves should set the correct pixel
194/// format.
195fn video_frame_to_arena_sync_frame(v: &crate::VideoFrame) -> Result<arena::sync::Frame> {
196    if v.planes.is_empty() {
197        return Err(Error::invalid(
198            "receive_arena_frame: video frame has no planes",
199        ));
200    }
201    let total_bytes: usize = v.planes.iter().map(|p| p.data.len()).sum();
202    if total_bytes == 0 {
203        return Err(Error::invalid(
204            "receive_arena_frame: video frame planes are empty",
205        ));
206    }
207    // One-shot pool sized exactly to the frame. The pool drops at end
208    // of scope; the leased Arena lives on inside the returned Frame
209    // (its Weak<ArenaPool> handle just won't upgrade in Drop, so the
210    // Box<[u8]> falls through to a normal heap free).
211    let pool = arena::sync::ArenaPool::with_alloc_count_cap(
212        1,
213        total_bytes,
214        // One alloc per plane, plus a generous safety margin.
215        (v.planes.len() as u32).saturating_add(4),
216    );
217    let arena = pool.lease()?;
218    let mut plane_offsets: Vec<(usize, usize)> = Vec::with_capacity(v.planes.len());
219    let mut cursor = 0usize;
220    for plane in &v.planes {
221        let dst = arena.alloc::<u8>(plane.data.len())?;
222        dst.copy_from_slice(&plane.data);
223        plane_offsets.push((cursor, plane.data.len()));
224        cursor += plane.data.len();
225    }
226    // Best-effort header: width = stride of plane 0, height inferred
227    // from plane 0's data length. Pixel format defaults to Yuv420P for
228    // the common 3-plane case, Gray8 for single-plane, otherwise
229    // Yuv444P. Decoders that care about exact pixel-format / width /
230    // height should override `receive_arena_frame` themselves so they
231    // can emit a correct `FrameHeader` straight from their arena
232    // build path.
233    let stride0 = v.planes[0].stride.max(1);
234    let width = stride0 as u32;
235    let height = (v.planes[0].data.len() / stride0) as u32;
236    // Count only image planes for the format guess — side-channel
237    // entries (palette, significant bits; copied verbatim above like
238    // any other plane) must not bump e.g. a single-plane palette frame
239    // out of the Gray8 label.
240    let pixel_format = match v.image_plane_count() {
241        1 => PixelFormat::Gray8,
242        3 => PixelFormat::Yuv420P,
243        _ => PixelFormat::Yuv444P,
244    };
245    let header = arena::sync::FrameHeader::new(width, height, pixel_format, v.pts);
246    arena::sync::FrameInner::new(arena, &plane_offsets, header)
247}
248
249/// Factory that builds a decoder for a given codec parameter set.
250pub type DecoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Decoder>>;
251
252/// Factory that builds an encoder for a given codec parameter set.
253pub type EncoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Encoder>>;
254
255// ───────────────────────── CodecInfo ─────────────────────────
256
257/// A single registration: capabilities, decoder/encoder factories,
258/// optional probe, and the container tags this codec claims.
259///
260/// Codec crates build one of these per codec id inside their
261/// `register(reg)` function and hand it to
262/// [`CodecRegistry::register`]. The struct is `#[non_exhaustive]` so
263/// additional fields can be added without breaking existing codec
264/// crates — construction is only possible through
265/// [`CodecInfo::new`] plus the builder methods below.
266#[non_exhaustive]
267pub struct CodecInfo {
268    /// Canonical codec identifier this entry registers.
269    pub id: CodecId,
270    /// Capability description (media kind, feature flags, priority).
271    pub capabilities: CodecCapabilities,
272    /// Factory producing a fresh decoder instance, if decode is supported.
273    pub decoder_factory: Option<DecoderFactory>,
274    /// Factory producing a fresh encoder instance, if encode is supported.
275    pub encoder_factory: Option<EncoderFactory>,
276    /// Probe function that returns a confidence in `0.0..=1.0` for a
277    /// given [`ProbeContext`]. `None` means "confidence 1.0 for every
278    /// claimed tag" — the correct default for codecs whose tag claims
279    /// are unambiguous.
280    pub probe: Option<ProbeFn>,
281    /// Tags this codec is willing to be looked up under. One codec may
282    /// claim many tags (an AAC decoder covers several WaveFormat ids,
283    /// a FourCC, an MP4 OTI, and a Matroska CodecID string at once).
284    pub tags: Vec<CodecTag>,
285    /// Payload magic prefixes this codec answers to (`\x01vorbis`,
286    /// `OpusHead`, …). Some carriage formats have no codec tag — the
287    /// codec is announced by a magic byte prefix on the payload itself
288    /// (an Ogg logical stream's first packet is the canonical case;
289    /// raw elementary streams are another). Such claims are
290    /// prefix-matched by
291    /// [`CodecRegistry::resolve_payload_magic_ref`] instead of living in
292    /// the exact-match [`CodecTag`] index. Attached with
293    /// [`Self::payload_magic`] / [`Self::payload_magics`]. Empty prefixes are
294    /// ignored at registration time (a zero-length prefix would match
295    /// every stream while carrying no evidence).
296    pub payload_magics: Vec<Vec<u8>>,
297    /// Schema of the encoder's recognised option keys
298    /// (`CodecParameters::options`). Attached with
299    /// [`Self::encoder_options`]. Used for validation / `oxideav list`
300    /// / pipeline JSON checks.
301    pub encoder_options_schema: Option<&'static [OptionField]>,
302    /// Schema of the decoder's recognised option keys.
303    pub decoder_options_schema: Option<&'static [OptionField]>,
304    /// HW backend identifier, e.g. `"nvidia"`, `"vaapi"`, `"vdpau"`,
305    /// `"vulkan-video"`, `"videotoolbox"`. Set by HW siblings on every
306    /// `CodecInfo` they register; SW codecs leave this `None`.
307    /// Consumers (e.g. the CLI's `info` command) use it to group
308    /// codec entries by backend and to dedupe probe calls — multiple
309    /// `CodecInfo` entries with the same `engine_id` typically share
310    /// an `engine_probe` function, and consumers should call the probe
311    /// at most once per `engine_id` per pass. Attached via
312    /// [`Self::with_engine_id`].
313    pub engine_id: Option<&'static str>,
314    /// Optional engine probe function. When `Some`, calling it returns
315    /// one [`crate::engine::HwDeviceInfo`] entry per device the backend
316    /// sees. Phase-2 HW siblings populate this on every `CodecInfo`
317    /// they register; Phase-3 consumers (CLI) call it on demand.
318    /// Attached via [`Self::with_engine_probe`].
319    pub engine_probe: Option<crate::engine::EngineProbeFn>,
320    /// Tie-break rank for this registration's **claims** (tags and
321    /// payload magics) when another codec id claims the same tag /
322    /// magic with equal evidence. Lower is preferred; defaults to
323    /// [`DEFAULT_PRIORITY`](crate::DEFAULT_PRIORITY). Deliberately
324    /// separate from [`CodecCapabilities::priority`]: that field ranks
325    /// *implementations of one codec id* against each other (HW before
326    /// SW), and a backend's implementation preference must not silently
327    /// out-rank a different codec's identity claim on an ambiguous tag.
328    /// Attached via [`Self::with_resolution_priority`].
329    pub resolution_priority: i32,
330}
331
332impl CodecInfo {
333    /// Start a new registration for `id` with empty capabilities, no
334    /// factories, no probe, and no tags. Chain the builder methods
335    /// below to fill it in, then hand the result to
336    /// [`CodecRegistry::register`].
337    pub fn new(id: CodecId) -> Self {
338        Self {
339            capabilities: CodecCapabilities::audio(id.as_str()),
340            id,
341            decoder_factory: None,
342            encoder_factory: None,
343            probe: None,
344            tags: Vec::new(),
345            payload_magics: Vec::new(),
346            encoder_options_schema: None,
347            decoder_options_schema: None,
348            engine_id: None,
349            engine_probe: None,
350            resolution_priority: crate::DEFAULT_PRIORITY,
351        }
352    }
353
354    /// Set the claim tie-break priority (see
355    /// [`Self::resolution_priority`]). Lower is preferred. Only consulted
356    /// when another codec id claims the same tag with equal probe
357    /// confidence, or the same-length payload magic; evidence always
358    /// out-ranks priority, and equal priorities fall through to
359    /// registration order (earlier wins).
360    pub fn with_resolution_priority(mut self, priority: i32) -> Self {
361        self.resolution_priority = priority;
362        self
363    }
364
365    /// Replace the capability description. The default built by
366    /// [`Self::new`] is a placeholder (audio-flavoured, no flags); every
367    /// real registration should call this.
368    pub fn capabilities(mut self, caps: CodecCapabilities) -> Self {
369        self.capabilities = caps;
370        self
371    }
372
373    /// Builder: attach the decoder factory.
374    pub fn decoder(mut self, factory: DecoderFactory) -> Self {
375        self.decoder_factory = Some(factory);
376        self
377    }
378
379    /// Builder: attach the encoder factory.
380    pub fn encoder(mut self, factory: EncoderFactory) -> Self {
381        self.encoder_factory = Some(factory);
382        self
383    }
384
385    /// Builder: attach a confidence probe (see [`CodecInfo::probe`]).
386    pub fn probe(mut self, probe: ProbeFn) -> Self {
387        self.probe = Some(probe);
388        self
389    }
390
391    /// Claim a single container tag for this codec. Equivalent to
392    /// `.tags([tag])` but avoids the array ceremony for single-tag
393    /// claims.
394    pub fn tag(mut self, tag: CodecTag) -> Self {
395        self.tags.push(tag);
396        self
397    }
398
399    /// Claim a set of container tags for this codec. Takes any
400    /// iterable (arrays, `Vec`, `Option`, …) so the common case of a
401    /// codec with 3-6 tags reads as one clean block.
402    pub fn tags(mut self, tags: impl IntoIterator<Item = CodecTag>) -> Self {
403        self.tags.extend(tags);
404        self
405    }
406
407    /// Claim one payload magic prefix for this codec (see
408    /// [`Self::payload_magics`]). Chain repeatedly for codecs that answer
409    /// to more than one magic:
410    ///
411    /// ```
412    /// # use oxideav_core::registry::CodecInfo;
413    /// # use oxideav_core::CodecId;
414    /// let info = CodecInfo::new(CodecId::new("vorbis")).payload_magic(b"\x01vorbis");
415    /// # let _ = info;
416    /// ```
417    pub fn payload_magic(mut self, magic: impl Into<Vec<u8>>) -> Self {
418        self.payload_magics.push(magic.into());
419        self
420    }
421
422    /// Claim a set of payload magic prefixes for this codec — the
423    /// iterable companion to [`Self::payload_magic`], mirroring the
424    /// [`Self::tag`] / [`Self::tags`] pair.
425    pub fn payload_magics<I>(mut self, magics: I) -> Self
426    where
427        I: IntoIterator,
428        I::Item: Into<Vec<u8>>,
429    {
430        self.payload_magics
431            .extend(magics.into_iter().map(Into::into));
432        self
433    }
434
435    /// Declare the options struct this codec's encoder factory expects.
436    /// Attaches `T::SCHEMA` so the registry can enumerate recognised
437    /// option keys (for `oxideav list`, pipeline JSON validation, etc.).
438    /// The factory itself still has to call
439    /// [`crate::parse_options::<T>()`] against
440    /// `CodecParameters::options` at init time.
441    pub fn encoder_options<T: CodecOptionsStruct>(mut self) -> Self {
442        self.encoder_options_schema = Some(T::SCHEMA);
443        self
444    }
445
446    /// Declare the options struct this codec's decoder factory expects.
447    /// See [`Self::encoder_options`] for the encoder counterpart.
448    pub fn decoder_options<T: CodecOptionsStruct>(mut self) -> Self {
449        self.decoder_options_schema = Some(T::SCHEMA);
450        self
451    }
452
453    /// Tag this codec as belonging to a HW backend identified by
454    /// `engine_id`. Should match the `engine_id` of every other
455    /// `CodecInfo` registered by the same backend, and the corresponding
456    /// `engine_id` field used by the CLI for grouping. SW codecs leave
457    /// this unset.
458    pub fn with_engine_id(mut self, engine_id: &'static str) -> Self {
459        self.engine_id = Some(engine_id);
460        self
461    }
462
463    /// Attach a probe function. Consumers call it to enumerate the
464    /// engines (devices) this backend can dispatch to. Probes are
465    /// expected to be idempotent and side-effect free; consumers may
466    /// call them more than once per process and should dedupe by
467    /// [`Self::engine_id`].
468    pub fn with_engine_probe(mut self, probe: crate::engine::EngineProbeFn) -> Self {
469        self.engine_probe = Some(probe);
470        self
471    }
472}
473
474/// Internal per-impl record held inside the registry's id map. Kept
475/// distinct from [`CodecInfo`] so the id map stays cheap to walk
476/// during `make_decoder` / `make_encoder` lookups.
477#[derive(Clone)]
478pub struct CodecImplementation {
479    /// Capability description copied from the originating [`CodecInfo`].
480    pub caps: CodecCapabilities,
481    /// Decoder factory, if this implementation can decode.
482    pub make_decoder: Option<DecoderFactory>,
483    /// Encoder factory, if this implementation can encode.
484    pub make_encoder: Option<EncoderFactory>,
485    /// Encoder options schema declared via
486    /// [`CodecInfo::encoder_options`]. `None` means the encoder accepts
487    /// no tuning knobs (any non-empty `CodecParameters::options` will
488    /// still be rejected by the factory if the encoder calls
489    /// `parse_options` — this is purely informational for discovery).
490    pub encoder_options_schema: Option<&'static [OptionField]>,
491    /// Decoder options schema declared via
492    /// [`CodecInfo::decoder_options`]; same semantics as the encoder
493    /// schema above.
494    pub decoder_options_schema: Option<&'static [OptionField]>,
495    /// HW backend identifier copied verbatim from the originating
496    /// [`CodecInfo::engine_id`]. `Some("nvidia"/"vaapi"/...)` on HW
497    /// backends; `None` on SW codecs. Consumers (CLI `info` command,
498    /// pipeline dispatcher, bench loop) read this to group entries by
499    /// backend without grepping `caps.implementation`.
500    pub engine_id: Option<&'static str>,
501    /// Engine probe function copied verbatim from the originating
502    /// [`CodecInfo::engine_probe`]. `Some(fn)` on HW backends with a
503    /// probe wired; `None` on SW codecs. Consumers call it on demand
504    /// to enumerate per-device info ([`crate::engine::HwDeviceInfo`]).
505    pub engine_probe: Option<crate::engine::EngineProbeFn>,
506}
507
508/// Registry mapping codec ids and container tags to their registered
509/// implementations; the lookup point behind `make_decoder` /
510/// `make_encoder` / `resolve_tag`.
511#[derive(Default)]
512pub struct CodecRegistry {
513    /// id → list of implementations. Each registered codec appends one
514    /// entry here. `make_decoder` / `make_encoder` walk this list in
515    /// preference order.
516    impls: HashMap<CodecId, Vec<CodecImplementation>>,
517    /// Codec ids in the order their first implementation was
518    /// registered — drives every id-level enumeration so listings are
519    /// stable across processes.
520    impl_order: Vec<CodecId>,
521    /// Append-only list of every registration — the `tag_index` stores
522    /// offsets into this vector.
523    registrations: Vec<RegistrationRecord>,
524    /// Tag → indices into `registrations`. Indices are stored in
525    /// registration order so tie-breaking in `resolve_tag` is
526    /// deterministic (first-registered wins).
527    tag_index: HashMap<CodecTag, Vec<usize>>,
528    /// Payload magic-prefix claims: `(magic, registration index)` in
529    /// registration order. Kept as a flat list rather than a map
530    /// because resolution is prefix matching (see
531    /// [`Self::resolve_payload_magic_ref`]), not exact-key lookup.
532    magic_index: Vec<(Vec<u8>, usize)>,
533}
534
535/// Internal registry record. Mirrors the subset of [`CodecInfo`]
536/// needed at resolve time.
537struct RegistrationRecord {
538    id: CodecId,
539    probe: Option<ProbeFn>,
540    /// Claim tie-break rank copied from [`CodecInfo::resolution_priority`].
541    priority: i32,
542    /// Tags claimed by this registration, in declaration order — the
543    /// source of [`CodecRegistry::all_tag_registrations`]'s ordering.
544    tags: Vec<CodecTag>,
545}
546
547/// One ranked entry from [`CodecRegistry::resolve_tag_candidates`].
548///
549/// Ordered by the registry's resolution rule — `confidence`
550/// descending, then `priority` ascending (lower is preferred), then
551/// `order` ascending (earlier registration wins) — so the first
552/// element is exactly what [`CodecRegistry::resolve_tag_ref`] returns.
553/// `#[non_exhaustive]`: read by field, never constructed by callers.
554#[non_exhaustive]
555#[derive(Clone, Copy, Debug, PartialEq)]
556pub struct TagCandidate<'a> {
557    /// Codec id of the claiming registration.
558    pub id: &'a CodecId,
559    /// Probe confidence for this context (`1.0` for unprobed claims;
560    /// never `<= 0.0` — such claims are not candidates).
561    pub confidence: crate::Confidence,
562    /// Claim tie-break priority (see [`CodecInfo::resolution_priority`]).
563    pub priority: i32,
564    /// 0-based registration sequence number (one per
565    /// [`CodecRegistry::register`] call) — the final tie-break. The
566    /// same id can appear more than once when it registered the claim
567    /// twice; the registry does not fold duplicates.
568    pub order: usize,
569}
570
571/// One ranked entry from [`CodecRegistry::resolve_payload_magic_candidates`].
572///
573/// Ordered by the registry's resolution rule — magic length
574/// descending (most specific prefix), then `priority` ascending, then
575/// `order` ascending — so the first element is exactly what
576/// [`CodecRegistry::resolve_payload_magic_ref`] returns.
577#[non_exhaustive]
578#[derive(Clone, Copy, Debug, PartialEq, Eq)]
579pub struct PayloadMagicCandidate<'a> {
580    /// Codec id of the claiming registration.
581    pub id: &'a CodecId,
582    /// The magic prefix that matched.
583    pub magic: &'a [u8],
584    /// Claim tie-break priority (see [`CodecInfo::resolution_priority`]).
585    pub priority: i32,
586    /// 0-based registration sequence number — the final tie-break.
587    pub order: usize,
588}
589
590impl CodecRegistry {
591    /// An empty registry (same as `Default`).
592    pub fn new() -> Self {
593        Self::default()
594    }
595
596    /// Register one codec. Expands into:
597    ///   * an entry in the id → implementations map (for
598    ///     `make_decoder` / `make_encoder`);
599    ///   * an entry in the tag index for every claimed tag (for
600    ///     `resolve_tag`).
601    ///
602    /// Calling `register` multiple times with the same id is allowed
603    /// and how multi-implementation codecs (software-plus-hardware
604    /// FLAC, for example) are expressed.
605    pub fn register(&mut self, info: CodecInfo) {
606        let CodecInfo {
607            id,
608            capabilities,
609            decoder_factory,
610            encoder_factory,
611            probe,
612            tags,
613            payload_magics,
614            encoder_options_schema,
615            decoder_options_schema,
616            // engine_id / engine_probe are metadata attached to a
617            // CodecInfo for backends that want consumers (CLI `info`,
618            // pipeline bench) to enumerate the underlying devices on
619            // demand. They're surfaced verbatim on the resulting
620            // CodecImplementation so consumers can read them without
621            // grepping `caps.implementation`. Tag-only CodecInfo entries
622            // (no factories) drop the values on the floor — there's no
623            // CodecImplementation built in that branch.
624            engine_id,
625            engine_probe,
626            resolution_priority,
627        } = info;
628
629        let caps = {
630            let mut c = capabilities;
631            if decoder_factory.is_some() {
632                c = c.with_decode();
633            }
634            if encoder_factory.is_some() {
635                c = c.with_encode();
636            }
637            c
638        };
639
640        // Only record an implementation entry when at least one factory
641        // is present. A "tag-only" CodecInfo — used to attach extra tag
642        // claims to a codec that was already registered with factories —
643        // shouldn't pollute the impl list.
644        if decoder_factory.is_some() || encoder_factory.is_some() {
645            let slot = self.impls.entry(id.clone()).or_default();
646            if slot.is_empty() {
647                self.impl_order.push(id.clone());
648            }
649            slot.push(CodecImplementation {
650                caps,
651                make_decoder: decoder_factory,
652                make_encoder: encoder_factory,
653                encoder_options_schema,
654                decoder_options_schema,
655                engine_id,
656                engine_probe,
657            });
658        }
659
660        let record_idx = self.registrations.len();
661        for tag in &tags {
662            self.tag_index
663                .entry(tag.clone())
664                .or_default()
665                .push(record_idx);
666        }
667        self.registrations.push(RegistrationRecord {
668            id: id.clone(),
669            probe,
670            priority: resolution_priority,
671            tags,
672        });
673        for magic in payload_magics {
674            // A zero-length prefix would match every stream while
675            // carrying no evidence — drop it here so resolution never
676            // has to special-case it.
677            if !magic.is_empty() {
678                self.magic_index.push((magic, record_idx));
679            }
680        }
681    }
682
683    /// Whether at least one registered implementation of `id` can decode.
684    pub fn has_decoder(&self, id: &CodecId) -> bool {
685        self.impls
686            .get(id)
687            .map(|v| v.iter().any(|i| i.make_decoder.is_some()))
688            .unwrap_or(false)
689    }
690
691    /// Whether at least one registered implementation of `id` can encode.
692    pub fn has_encoder(&self, id: &CodecId) -> bool {
693        self.impls
694            .get(id)
695            .map(|v| v.iter().any(|i| i.make_encoder.is_some()))
696            .unwrap_or(false)
697    }
698
699    /// First registered decoder factory for `params.codec_id`, invoked
700    /// with `params`. No priority walk, no preference filter, no
701    /// init-time fallback to a lower-priority impl. Errors if no
702    /// decoder is registered for the codec.
703    ///
704    /// Intended for single-impl scenarios — typically a codec crate's
705    /// own self-tests, where exactly one impl has been registered into
706    /// a freshly-constructed registry. Production callers selecting
707    /// among multiple candidates (e.g. h264_sw vs h264_videotoolbox)
708    /// should use `oxideav_pipeline::make_decoder_with` instead, which
709    /// applies `CodecPreferences` and walks priorities.
710    pub fn first_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
711        let imp = self
712            .implementations(&params.codec_id)
713            .iter()
714            .find(|i| i.make_decoder.is_some())
715            .ok_or_else(|| {
716                Error::CodecNotFound(format!("no decoder for codec {}", params.codec_id))
717            })?;
718        (imp.make_decoder.expect("checked above"))(params)
719    }
720
721    /// First registered encoder factory — see [`first_decoder`].
722    ///
723    /// [`first_decoder`]: Self::first_decoder
724    pub fn first_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
725        let imp = self
726            .implementations(&params.codec_id)
727            .iter()
728            .find(|i| i.make_encoder.is_some())
729            .ok_or_else(|| {
730                Error::CodecNotFound(format!("no encoder for codec {}", params.codec_id))
731            })?;
732        (imp.make_encoder.expect("checked above"))(params)
733    }
734
735    /// Look up a decoder by exact implementation name
736    /// (`"h264_sw"`, `"aac_audiotoolbox"`, ...). Errors if the impl
737    /// isn't registered or if it has no decoder factory.
738    pub fn decoder_by_impl(
739        &self,
740        impl_name: &str,
741        params: &CodecParameters,
742    ) -> Result<Box<dyn Decoder>> {
743        let imp = self
744            .implementations(&params.codec_id)
745            .iter()
746            .find(|i| i.caps.implementation == impl_name)
747            .ok_or_else(|| {
748                Error::CodecNotFound(format!(
749                    "no implementation `{impl_name}` for codec {}",
750                    params.codec_id
751                ))
752            })?;
753        let factory = imp
754            .make_decoder
755            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is encoder-only")))?;
756        factory(params)
757    }
758
759    /// Look up an encoder by exact implementation name — see
760    /// [`decoder_by_impl`].
761    ///
762    /// [`decoder_by_impl`]: Self::decoder_by_impl
763    pub fn encoder_by_impl(
764        &self,
765        impl_name: &str,
766        params: &CodecParameters,
767    ) -> Result<Box<dyn Encoder>> {
768        let imp = self
769            .implementations(&params.codec_id)
770            .iter()
771            .find(|i| i.caps.implementation == impl_name)
772            .ok_or_else(|| {
773                Error::CodecNotFound(format!(
774                    "no implementation `{impl_name}` for codec {}",
775                    params.codec_id
776                ))
777            })?;
778        let factory = imp
779            .make_encoder
780            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is decoder-only")))?;
781        factory(params)
782    }
783
784    /// Iterate codec ids that have at least one decoder implementation,
785    /// in the order each id's first implementation was registered.
786    pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
787        self.impl_order.iter().filter(|id| self.has_decoder(id))
788    }
789
790    /// Iterate codec ids that have at least one encoder implementation,
791    /// in the order each id's first implementation was registered.
792    pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
793        self.impl_order.iter().filter(|id| self.has_encoder(id))
794    }
795
796    /// All registered implementations of a given codec id.
797    pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
798        self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
799    }
800
801    /// Lookup the encoder options schema for a registered codec. Walks
802    /// implementations in registration order and returns the first
803    /// schema found. `None` means either the codec isn't registered or
804    /// no implementation declared an encoder schema.
805    pub fn encoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
806        self.impls
807            .get(id)?
808            .iter()
809            .find_map(|i| i.encoder_options_schema)
810    }
811
812    /// Lookup the decoder options schema — see
813    /// [`encoder_options_schema`](Self::encoder_options_schema).
814    pub fn decoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
815        self.impls
816            .get(id)?
817            .iter()
818            .find_map(|i| i.decoder_options_schema)
819    }
820
821    /// Iterator over every (codec_id, impl) pair — useful for `oxideav list`
822    /// to show capability flags per implementation. Ids come in the
823    /// order their first implementation was registered; implementations
824    /// of one id in their own registration order.
825    pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
826        self.impl_order
827            .iter()
828            .flat_map(move |id| self.implementations(id).iter().map(move |i| (id, i)))
829    }
830
831    /// Iterator over every `(tag, codec_id)` pair currently registered —
832    /// used by `oxideav tags` debug output and by tests that want to
833    /// walk the tag surface. Registration order, then each
834    /// registration's tag declaration order.
835    pub fn all_tag_registrations(&self) -> impl Iterator<Item = (&CodecTag, &CodecId)> {
836        self.registrations
837            .iter()
838            .flat_map(|rec| rec.tags.iter().map(move |tag| (tag, &rec.id)))
839    }
840
841    /// Inherent form of tag resolution that returns a reference.
842    /// The owned-value form used by container code lives behind the
843    /// [`CodecResolver`] trait impl below.
844    ///
845    /// Walks every registration that claimed `ctx.tag`, calls its
846    /// probe with `ctx`, and returns the id of the registration that
847    /// scored highest. Probes that return `0.0` (or anything not
848    /// strictly positive, `NaN` included) are discarded; ties on
849    /// confidence are broken by [`CodecInfo::resolution_priority`]
850    /// (lower wins), then by registration order (first wins).
851    /// Registrations with no probe are treated as returning `1.0`.
852    /// [`resolve_tag_candidates`](Self::resolve_tag_candidates) returns
853    /// the whole ranked list this picks the head of.
854    pub fn resolve_tag_ref(&self, ctx: &ProbeContext) -> Option<&CodecId> {
855        self.resolve_tag_candidates(ctx).first().map(|c| c.id)
856    }
857
858    /// Every registration that claimed `ctx.tag` and whose probe
859    /// accepts the context, ranked by the resolution rule: confidence
860    /// descending, then priority ascending, then registration order
861    /// ascending. Empty when the tag is unclaimed or every claimant's
862    /// probe refused. The observation hook for collision audits: a
863    /// list with two heads at equal confidence *and* equal priority is
864    /// a tie the registry broke by registration order alone.
865    pub fn resolve_tag_candidates(&self, ctx: &ProbeContext) -> Vec<TagCandidate<'_>> {
866        let Some(idxs) = self.tag_index.get(ctx.tag) else {
867            return Vec::new();
868        };
869        let mut out: Vec<TagCandidate<'_>> = idxs
870            .iter()
871            .filter_map(|&i| {
872                let rec = &self.registrations[i];
873                let confidence = match rec.probe {
874                    Some(f) => f(ctx),
875                    None => 1.0,
876                };
877                // `> 0.0` is false for NaN, so a broken probe is
878                // dropped rather than ranked unpredictably.
879                (confidence > 0.0).then_some(TagCandidate {
880                    id: &rec.id,
881                    confidence,
882                    priority: rec.priority,
883                    order: i,
884                })
885            })
886            .collect();
887        // `idxs` is already in registration order and `order` is
888        // unique, so the sort key is total and the result is fixed by
889        // the registrations alone.
890        out.sort_by(|a, b| {
891            b.confidence
892                .partial_cmp(&a.confidence)
893                .unwrap_or(std::cmp::Ordering::Equal)
894                .then(a.priority.cmp(&b.priority))
895                .then(a.order.cmp(&b.order))
896        });
897        out
898    }
899
900    /// Inherent form of payload-magic resolution that returns a
901    /// reference. The owned-value form used by container code lives
902    /// behind the [`CodecResolver`] trait impl below.
903    ///
904    /// Walks every registered payload magic prefix (declared via
905    /// [`CodecInfo::payload_magic`] / [`CodecInfo::payload_magics`]) and
906    /// returns the codec whose magic is a prefix of `first_bytes` —
907    /// however much of the stream's leading payload the caller has
908    /// (an Ogg demuxer passes the first packet of a logical stream; a
909    /// raw-stream prober passes the file head). The **longest**
910    /// matching magic wins (most specific claim); remaining ties are
911    /// broken by [`CodecInfo::resolution_priority`] (lower wins), then
912    /// by registration order (first wins). Unlike the tag path there
913    /// is no probe step: a payload magic is itself the bitstream
914    /// evidence a probe would look for, and specificity is expressed
915    /// by prefix length instead of a confidence value.
916    /// [`resolve_payload_magic_candidates`](Self::resolve_payload_magic_candidates)
917    /// returns the whole ranked list this picks the head of.
918    pub fn resolve_payload_magic_ref(&self, first_bytes: &[u8]) -> Option<&CodecId> {
919        self.resolve_payload_magic_candidates(first_bytes)
920            .first()
921            .map(|c| c.id)
922    }
923
924    /// Every registered payload magic that is a prefix of
925    /// `first_bytes`, ranked by the resolution rule: magic length
926    /// descending, then priority ascending, then registration order
927    /// ascending. The payload-magic companion to
928    /// [`resolve_tag_candidates`](Self::resolve_tag_candidates).
929    pub fn resolve_payload_magic_candidates(
930        &self,
931        first_bytes: &[u8],
932    ) -> Vec<PayloadMagicCandidate<'_>> {
933        let mut out: Vec<PayloadMagicCandidate<'_>> = self
934            .magic_index
935            .iter()
936            .filter(|(magic, _)| first_bytes.starts_with(magic))
937            .map(|(magic, i)| {
938                let rec = &self.registrations[*i];
939                PayloadMagicCandidate {
940                    id: &rec.id,
941                    magic: magic.as_slice(),
942                    priority: rec.priority,
943                    order: *i,
944                }
945            })
946            .collect();
947        // One registration may claim several magics that all match
948        // (`order` then repeats); the stable sort keeps those in their
949        // declaration order, which is the only remaining distinction
950        // and is itself fixed by the registration.
951        out.sort_by(|a, b| {
952            b.magic
953                .len()
954                .cmp(&a.magic.len())
955                .then(a.priority.cmp(&b.priority))
956                .then(a.order.cmp(&b.order))
957        });
958        out
959    }
960
961    /// Iterator over every `(payload magic, codec_id)` pair currently
962    /// registered, in registration order — the payload-magic companion
963    /// to [`all_tag_registrations`](Self::all_tag_registrations).
964    pub fn all_payload_magic_registrations(&self) -> impl Iterator<Item = (&[u8], &CodecId)> {
965        self.magic_index
966            .iter()
967            .map(move |(magic, i)| (magic.as_slice(), &self.registrations[*i].id))
968    }
969}
970
971/// Implement the shared [`CodecResolver`] interface so container
972/// demuxers can accept `&dyn CodecResolver` without depending on
973/// this crate directly — the trait lives in oxideav-core.
974impl CodecResolver for CodecRegistry {
975    fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId> {
976        self.resolve_tag_ref(ctx).cloned()
977    }
978
979    fn resolve_payload_magic(&self, first_packet: &[u8]) -> Option<CodecId> {
980        self.resolve_payload_magic_ref(first_packet).cloned()
981    }
982}
983
984#[cfg(test)]
985mod tag_tests {
986    use super::*;
987    use crate::CodecCapabilities;
988
989    /// Probe: return 1.0 iff the peeked bytes look like MS-MPEG4 (no
990    /// 0x000001 start code in the first few bytes).
991    fn probe_msmpeg4(ctx: &ProbeContext) -> f32 {
992        match ctx.packet {
993            Some(d) if !d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
994            Some(_) => 0.0,
995            None => 0.5, // no data yet — weak evidence
996        }
997    }
998
999    /// Probe: return 1.0 iff the peeked bytes look like MPEG-4 Part 2
1000    /// (starts with a 0x000001 start code in the first few bytes).
1001    fn probe_mpeg4_part2(ctx: &ProbeContext) -> f32 {
1002        match ctx.packet {
1003            Some(d) if d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
1004            Some(_) => 0.0,
1005            None => 0.5,
1006        }
1007    }
1008
1009    fn info(id: &str) -> CodecInfo {
1010        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1011    }
1012
1013    #[test]
1014    fn resolve_single_claim_no_probe() {
1015        let mut reg = CodecRegistry::new();
1016        reg.register(info("flac").tag(CodecTag::fourcc(b"FLAC")));
1017        let t = CodecTag::fourcc(b"FLAC");
1018        assert_eq!(
1019            reg.resolve_tag_ref(&ProbeContext::new(&t))
1020                .map(|c| c.as_str()),
1021            Some("flac"),
1022        );
1023    }
1024
1025    #[test]
1026    fn resolve_missing_tag_returns_none() {
1027        let reg = CodecRegistry::new();
1028        let t = CodecTag::fourcc(b"????");
1029        assert!(reg.resolve_tag_ref(&ProbeContext::new(&t)).is_none());
1030    }
1031
1032    #[test]
1033    fn unprobed_claims_tie_first_registered_wins() {
1034        // Two unprobed claims on the same tag: deterministic order.
1035        let mut reg = CodecRegistry::new();
1036        reg.register(info("first").tag(CodecTag::fourcc(b"TEST")));
1037        reg.register(info("second").tag(CodecTag::fourcc(b"TEST")));
1038        let t = CodecTag::fourcc(b"TEST");
1039        assert_eq!(
1040            reg.resolve_tag_ref(&ProbeContext::new(&t))
1041                .map(|c| c.as_str()),
1042            Some("first"),
1043        );
1044    }
1045
1046    #[test]
1047    fn probe_picks_matching_bitstream() {
1048        // The core bug fix: every probe is asked and the highest
1049        // confidence wins regardless of registration order.
1050        let mut reg = CodecRegistry::new();
1051        reg.register(
1052            info("msmpeg4v3")
1053                .probe(probe_msmpeg4)
1054                .tag(CodecTag::fourcc(b"DIV3")),
1055        );
1056        reg.register(
1057            info("mpeg4video")
1058                .probe(probe_mpeg4_part2)
1059                .tag(CodecTag::fourcc(b"DIV3")),
1060        );
1061
1062        let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
1063        let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
1064        let tag = CodecTag::fourcc(b"DIV3");
1065
1066        let ctx_part2 = ProbeContext::new(&tag).packet(&mpeg4_part2);
1067        assert_eq!(
1068            reg.resolve_tag_ref(&ctx_part2).map(|c| c.as_str()),
1069            Some("mpeg4video"),
1070        );
1071        let ctx_ms = ProbeContext::new(&tag).packet(&ms_mpeg4);
1072        assert_eq!(
1073            reg.resolve_tag_ref(&ctx_ms).map(|c| c.as_str()),
1074            Some("msmpeg4v3"),
1075        );
1076    }
1077
1078    #[test]
1079    fn unprobed_claim_wins_against_low_confidence_probe() {
1080        // One codec claims a tag without a probe (→ confidence 1.0)
1081        // and another claims it with a probe returning 0.3. The
1082        // unprobed one wins — a codec that knows it owns the tag
1083        // outright should not lose to a speculative probe.
1084        let mut reg = CodecRegistry::new();
1085        reg.register(info("owner").tag(CodecTag::fourcc(b"OWN_")));
1086        reg.register(
1087            info("speculative")
1088                .probe(|_| 0.3)
1089                .tag(CodecTag::fourcc(b"OWN_")),
1090        );
1091        let t = CodecTag::fourcc(b"OWN_");
1092        assert_eq!(
1093            reg.resolve_tag_ref(&ProbeContext::new(&t))
1094                .map(|c| c.as_str()),
1095            Some("owner"),
1096        );
1097    }
1098
1099    #[test]
1100    fn probe_returning_zero_is_skipped() {
1101        let mut reg = CodecRegistry::new();
1102        reg.register(
1103            info("refuses")
1104                .probe(|_| 0.0)
1105                .tag(CodecTag::fourcc(b"MAYB")),
1106        );
1107        reg.register(info("fallback").tag(CodecTag::fourcc(b"MAYB")));
1108        let t = CodecTag::fourcc(b"MAYB");
1109        let ctx = ProbeContext::new(&t).packet(b"hello");
1110        assert_eq!(
1111            reg.resolve_tag_ref(&ctx).map(|c| c.as_str()),
1112            Some("fallback"),
1113        );
1114    }
1115
1116    #[test]
1117    fn fourcc_case_insensitive_lookup() {
1118        let mut reg = CodecRegistry::new();
1119        reg.register(info("vid").tag(CodecTag::fourcc(b"div3")));
1120        // Registered as "DIV3" (uppercase via ctor); lookup using
1121        // lowercase / mixed case also hits.
1122        let upper = CodecTag::fourcc(b"DIV3");
1123        let lower = CodecTag::fourcc(b"div3");
1124        let mixed = CodecTag::fourcc(b"DiV3");
1125        assert!(reg.resolve_tag_ref(&ProbeContext::new(&upper)).is_some());
1126        assert!(reg.resolve_tag_ref(&ProbeContext::new(&lower)).is_some());
1127        assert!(reg.resolve_tag_ref(&ProbeContext::new(&mixed)).is_some());
1128    }
1129
1130    #[test]
1131    fn wave_format_and_matroska_tags_work() {
1132        let mut reg = CodecRegistry::new();
1133        reg.register(info("mp3").tag(CodecTag::wave_format(0x0055)));
1134        reg.register(info("h264").tag(CodecTag::matroska("V_MPEG4/ISO/AVC")));
1135        let wf = CodecTag::wave_format(0x0055);
1136        let mk = CodecTag::matroska("V_MPEG4/ISO/AVC");
1137        assert_eq!(
1138            reg.resolve_tag_ref(&ProbeContext::new(&wf))
1139                .map(|c| c.as_str()),
1140            Some("mp3"),
1141        );
1142        assert_eq!(
1143            reg.resolve_tag_ref(&ProbeContext::new(&mk))
1144                .map(|c| c.as_str()),
1145            Some("h264"),
1146        );
1147    }
1148
1149    #[test]
1150    fn mp4_object_type_tag_works() {
1151        let mut reg = CodecRegistry::new();
1152        reg.register(info("aac").tag(CodecTag::mp4_object_type(0x40)));
1153        let t = CodecTag::mp4_object_type(0x40);
1154        assert_eq!(
1155            reg.resolve_tag_ref(&ProbeContext::new(&t))
1156                .map(|c| c.as_str()),
1157            Some("aac"),
1158        );
1159    }
1160
1161    #[test]
1162    fn multi_tag_claim_all_resolve() {
1163        let mut reg = CodecRegistry::new();
1164        reg.register(info("aac").tags([
1165            CodecTag::fourcc(b"MP4A"),
1166            CodecTag::wave_format(0x00FF),
1167            CodecTag::mp4_object_type(0x40),
1168            CodecTag::matroska("A_AAC"),
1169        ]));
1170        for t in [
1171            CodecTag::fourcc(b"MP4A"),
1172            CodecTag::wave_format(0x00FF),
1173            CodecTag::mp4_object_type(0x40),
1174            CodecTag::matroska("A_AAC"),
1175        ] {
1176            assert_eq!(
1177                reg.resolve_tag_ref(&ProbeContext::new(&t))
1178                    .map(|c| c.as_str()),
1179                Some("aac"),
1180                "tag {t:?} did not resolve",
1181            );
1182        }
1183    }
1184}
1185
1186#[cfg(test)]
1187mod payload_magic_tests {
1188    use super::*;
1189    use crate::CodecCapabilities;
1190
1191    fn info(id: &str) -> CodecInfo {
1192        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1193    }
1194
1195    /// Registry with the classic Ogg family registered, each under its
1196    /// real BOS magic.
1197    fn ogg_family_registry() -> CodecRegistry {
1198        let mut reg = CodecRegistry::new();
1199        reg.register(info("vorbis").payload_magic(b"\x01vorbis"));
1200        reg.register(info("opus").payload_magic(b"OpusHead"));
1201        reg.register(info("theora").payload_magic(b"\x80theora"));
1202        reg.register(info("flac").payload_magic(b"\x7fFLAC"));
1203        reg
1204    }
1205
1206    #[test]
1207    fn resolve_payload_magic_matches_first_packet_prefix() {
1208        let reg = ogg_family_registry();
1209        // A Vorbis identification header: magic + version + channels +
1210        // rate + ... — the resolver only needs the prefix to match.
1211        let vorbis_id_header = b"\x01vorbis\x00\x00\x00\x00\x02\x44\xac\x00\x00";
1212        assert_eq!(
1213            reg.resolve_payload_magic_ref(vorbis_id_header)
1214                .map(|c| c.as_str()),
1215            Some("vorbis"),
1216        );
1217        assert_eq!(
1218            reg.resolve_payload_magic_ref(b"OpusHead\x01\x02\x38\x01")
1219                .map(|c| c.as_str()),
1220            Some("opus"),
1221        );
1222        assert_eq!(
1223            reg.resolve_payload_magic_ref(b"\x80theora\x03\x02\x01")
1224                .map(|c| c.as_str()),
1225            Some("theora"),
1226        );
1227        assert_eq!(
1228            reg.resolve_payload_magic_ref(b"\x7fFLAC\x01\x00")
1229                .map(|c| c.as_str()),
1230            Some("flac"),
1231        );
1232    }
1233
1234    #[test]
1235    fn resolve_payload_magic_exact_length_packet_matches() {
1236        // A packet that is exactly the magic (nothing after it) still
1237        // resolves — starts_with is inclusive of equality.
1238        let reg = ogg_family_registry();
1239        assert_eq!(
1240            reg.resolve_payload_magic_ref(b"OpusHead")
1241                .map(|c| c.as_str()),
1242            Some("opus"),
1243        );
1244    }
1245
1246    #[test]
1247    fn resolve_payload_magic_unknown_or_short_packet_is_none() {
1248        let reg = ogg_family_registry();
1249        // Unknown magic.
1250        assert!(reg.resolve_payload_magic_ref(b"Speex   1.2.0").is_none());
1251        // Packet shorter than every registered magic.
1252        assert!(reg.resolve_payload_magic_ref(b"Opus").is_none());
1253        // Empty packet.
1254        assert!(reg.resolve_payload_magic_ref(b"").is_none());
1255    }
1256
1257    #[test]
1258    fn resolve_payload_magic_longest_prefix_wins_regardless_of_order() {
1259        // A shorter magic that is itself a prefix of a longer one must
1260        // lose to the more specific claim, whichever registered first.
1261        let mut reg = CodecRegistry::new();
1262        reg.register(info("generic").payload_magic(b"Opus"));
1263        reg.register(info("opus").payload_magic(b"OpusHead"));
1264        assert_eq!(
1265            reg.resolve_payload_magic_ref(b"OpusHead\x01")
1266                .map(|c| c.as_str()),
1267            Some("opus"),
1268        );
1269        // ...but the shorter claim still wins packets only it matches.
1270        assert_eq!(
1271            reg.resolve_payload_magic_ref(b"OpusTags")
1272                .map(|c| c.as_str()),
1273            Some("generic"),
1274        );
1275
1276        // Same result with the registration order flipped.
1277        let mut reg = CodecRegistry::new();
1278        reg.register(info("opus").payload_magic(b"OpusHead"));
1279        reg.register(info("generic").payload_magic(b"Opus"));
1280        assert_eq!(
1281            reg.resolve_payload_magic_ref(b"OpusHead\x01")
1282                .map(|c| c.as_str()),
1283            Some("opus"),
1284        );
1285    }
1286
1287    #[test]
1288    fn resolve_payload_magic_equal_length_tie_first_registered_wins() {
1289        let mut reg = CodecRegistry::new();
1290        reg.register(info("first").payload_magic(b"SameMagic"));
1291        reg.register(info("second").payload_magic(b"SameMagic"));
1292        assert_eq!(
1293            reg.resolve_payload_magic_ref(b"SameMagic\x00")
1294                .map(|c| c.as_str()),
1295            Some("first"),
1296        );
1297    }
1298
1299    #[test]
1300    fn empty_payload_magic_is_ignored_at_registration() {
1301        let mut reg = CodecRegistry::new();
1302        reg.register(info("greedy").payload_magic(b""));
1303        assert!(reg.resolve_payload_magic_ref(b"anything at all").is_none());
1304        assert_eq!(reg.all_payload_magic_registrations().count(), 0);
1305    }
1306
1307    #[test]
1308    fn payload_magics_plural_builder_and_diagnostics() {
1309        // One codec answering to several magics via the iterable
1310        // builder; the diagnostic iterator surfaces each claim in
1311        // registration order.
1312        let mut reg = CodecRegistry::new();
1313        reg.register(info("speex").payload_magics([b"Speex   ".to_vec(), b"speex-alt".to_vec()]));
1314        assert_eq!(
1315            reg.resolve_payload_magic_ref(b"Speex   1.2")
1316                .map(|c| c.as_str()),
1317            Some("speex"),
1318        );
1319        assert_eq!(
1320            reg.resolve_payload_magic_ref(b"speex-alt\x00")
1321                .map(|c| c.as_str()),
1322            Some("speex"),
1323        );
1324        let all: Vec<(&[u8], &str)> = reg
1325            .all_payload_magic_registrations()
1326            .map(|(m, id)| (m, id.as_str()))
1327            .collect();
1328        assert_eq!(
1329            all,
1330            vec![
1331                (b"Speex   ".as_slice(), "speex"),
1332                (b"speex-alt".as_slice(), "speex"),
1333            ],
1334        );
1335    }
1336
1337    #[test]
1338    fn magic_claims_compose_with_tag_claims_on_one_registration() {
1339        // A codec that lives in both Ogg and Matroska declares both
1340        // claim kinds on one CodecInfo; each resolution path finds it.
1341        let mut reg = CodecRegistry::new();
1342        reg.register(
1343            info("vorbis")
1344                .tag(CodecTag::matroska("A_VORBIS"))
1345                .payload_magic(b"\x01vorbis"),
1346        );
1347        let mk = CodecTag::matroska("A_VORBIS");
1348        assert_eq!(
1349            reg.resolve_tag_ref(&ProbeContext::new(&mk))
1350                .map(|c| c.as_str()),
1351            Some("vorbis"),
1352        );
1353        assert_eq!(
1354            reg.resolve_payload_magic_ref(b"\x01vorbis\x00")
1355                .map(|c| c.as_str()),
1356            Some("vorbis"),
1357        );
1358    }
1359
1360    #[test]
1361    fn resolver_trait_payload_magic_surface() {
1362        // The owned-value trait form mirrors the inherent form, and
1363        // the default implementation (NullCodecResolver) resolves
1364        // nothing.
1365        let reg = ogg_family_registry();
1366        let resolver: &dyn CodecResolver = &reg;
1367        assert_eq!(
1368            resolver
1369                .resolve_payload_magic(b"OpusHead\x01")
1370                .map(|c| c.0.clone()),
1371            Some("opus".to_owned()),
1372        );
1373        assert!(resolver.resolve_payload_magic(b"unknown").is_none());
1374
1375        let null = crate::NullCodecResolver;
1376        assert!(null.resolve_payload_magic(b"OpusHead\x01").is_none());
1377    }
1378
1379    /// The surface is container-agnostic: a raw elementary stream
1380    /// identified by a file-head magic resolves through the same path
1381    /// as the Ogg family — nothing about the mechanism is Ogg-shaped.
1382    #[test]
1383    fn payload_magic_serves_non_ogg_carriage() {
1384        let mut reg = CodecRegistry::new();
1385        reg.register(info("flac").payload_magic(b"fLaC"));
1386        reg.register(info("shorten").payload_magic(b"ajkg"));
1387
1388        assert_eq!(
1389            reg.resolve_payload_magic_ref(b"fLaC\x00\x00\x00\x22"),
1390            Some(&CodecId::new("flac"))
1391        );
1392        assert_eq!(
1393            reg.resolve_payload_magic_ref(b"ajkg\x02"),
1394            Some(&CodecId::new("shorten"))
1395        );
1396        assert_eq!(reg.resolve_payload_magic_ref(b"RIFF"), None);
1397    }
1398}
1399
1400/// Resolution-order tests. Every id / tag / magic here is invented:
1401/// the rule is format-agnostic and must be provable with synthetic
1402/// registrations only.
1403#[cfg(test)]
1404mod resolution_order_tests {
1405    use super::*;
1406    use crate::CodecCapabilities;
1407
1408    fn info(id: &str) -> CodecInfo {
1409        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1410    }
1411
1412    fn tag_ids<'a>(cands: &[TagCandidate<'a>]) -> Vec<&'a str> {
1413        cands.iter().map(|c| c.id.as_str()).collect()
1414    }
1415
1416    fn magic_ids<'a>(cands: &[PayloadMagicCandidate<'a>]) -> Vec<&'a str> {
1417        cands.iter().map(|c| c.id.as_str()).collect()
1418    }
1419
1420    #[test]
1421    fn codec_info_resolution_priority_defaults_and_builder() {
1422        assert_eq!(info("x").resolution_priority, crate::DEFAULT_PRIORITY);
1423        assert_eq!(info("x").with_resolution_priority(7).resolution_priority, 7);
1424    }
1425
1426    #[test]
1427    fn equal_tag_claims_resolve_by_registration_order_across_fresh_contexts() {
1428        const ORDER: [&str; 5] = ["zeta", "alpha", "omicron", "beta", "kappa"];
1429        let tag = CodecTag::fourcc(b"SYNT");
1430        for _ in 0..1000 {
1431            let mut ctx = crate::RuntimeContext::new();
1432            for id in ORDER {
1433                ctx.codecs.register(info(id).tag(tag.clone()));
1434            }
1435            let pc = ProbeContext::new(&tag);
1436            assert_eq!(
1437                ctx.codecs.resolve_tag_ref(&pc).map(|c| c.as_str()),
1438                Some("zeta")
1439            );
1440            let cands = ctx.codecs.resolve_tag_candidates(&pc);
1441            assert_eq!(tag_ids(&cands), ORDER.to_vec());
1442            assert_eq!(
1443                cands.iter().map(|c| c.order).collect::<Vec<_>>(),
1444                vec![0, 1, 2, 3, 4]
1445            );
1446        }
1447    }
1448
1449    #[test]
1450    fn equal_magic_claims_resolve_by_registration_order_across_fresh_contexts() {
1451        const ORDER: [&str; 4] = ["zeta", "alpha", "omicron", "beta"];
1452        for _ in 0..1000 {
1453            let mut ctx = crate::RuntimeContext::new();
1454            for id in ORDER {
1455                ctx.codecs.register(info(id).payload_magic(b"SYNTHMAGIC"));
1456            }
1457            assert_eq!(
1458                ctx.codecs
1459                    .resolve_payload_magic_ref(b"SYNTHMAGIC\x00")
1460                    .map(|c| c.as_str()),
1461                Some("zeta")
1462            );
1463            let cands = ctx
1464                .codecs
1465                .resolve_payload_magic_candidates(b"SYNTHMAGIC\x00");
1466            assert_eq!(magic_ids(&cands), ORDER.to_vec());
1467        }
1468    }
1469
1470    #[test]
1471    fn tag_priority_beats_registration_order() {
1472        let tag = CodecTag::wave_format(0x7777);
1473        let mut reg = CodecRegistry::new();
1474        reg.register(info("earlier").tag(tag.clone()));
1475        reg.register(
1476            info("later-preferred")
1477                .with_resolution_priority(10)
1478                .tag(tag.clone()),
1479        );
1480        let pc = ProbeContext::new(&tag);
1481        assert_eq!(
1482            reg.resolve_tag_ref(&pc).map(|c| c.as_str()),
1483            Some("later-preferred")
1484        );
1485        let cands = reg.resolve_tag_candidates(&pc);
1486        assert_eq!(tag_ids(&cands), vec!["later-preferred", "earlier"]);
1487        assert_eq!(cands[0].priority, 10);
1488        assert_eq!(cands[0].order, 1);
1489        assert_eq!(cands[1].priority, crate::DEFAULT_PRIORITY);
1490        assert_eq!(cands[1].order, 0);
1491        assert_eq!(cands[0].confidence, 1.0);
1492    }
1493
1494    #[test]
1495    fn tag_confidence_beats_priority() {
1496        // A probe that positively identifies the payload wins against
1497        // an unprobed-but-top-priority claim only when its confidence
1498        // is higher; at 1.0 vs 1.0 the priority decides.
1499        let tag = CodecTag::fourcc(b"AMBI");
1500        let mut reg = CodecRegistry::new();
1501        reg.register(
1502            info("prioritised")
1503                .with_resolution_priority(i32::MIN)
1504                .probe(|_| 0.6)
1505                .tag(tag.clone()),
1506        );
1507        reg.register(info("certain").tag(tag.clone()));
1508        let pc = ProbeContext::new(&tag);
1509        assert_eq!(
1510            reg.resolve_tag_ref(&pc).map(|c| c.as_str()),
1511            Some("certain")
1512        );
1513        let cands = reg.resolve_tag_candidates(&pc);
1514        assert_eq!(tag_ids(&cands), vec!["certain", "prioritised"]);
1515    }
1516
1517    #[test]
1518    fn tag_candidates_drop_refusing_and_non_finite_probes() {
1519        let tag = CodecTag::fourcc(b"NANP");
1520        let mut reg = CodecRegistry::new();
1521        reg.register(info("refuses").probe(|_| 0.0).tag(tag.clone()));
1522        reg.register(info("broken").probe(|_| f32::NAN).tag(tag.clone()));
1523        reg.register(info("negative").probe(|_| -1.0).tag(tag.clone()));
1524        reg.register(info("accepts").probe(|_| 0.2).tag(tag.clone()));
1525        let pc = ProbeContext::new(&tag);
1526        assert_eq!(tag_ids(&reg.resolve_tag_candidates(&pc)), vec!["accepts"]);
1527        assert_eq!(
1528            reg.resolve_tag_ref(&pc).map(|c| c.as_str()),
1529            Some("accepts")
1530        );
1531        // An unclaimed tag yields an empty list, not an error.
1532        let other = CodecTag::fourcc(b"NONE");
1533        assert!(reg
1534            .resolve_tag_candidates(&ProbeContext::new(&other))
1535            .is_empty());
1536    }
1537
1538    #[test]
1539    fn duplicate_tag_claim_by_one_id_is_visible_not_folded() {
1540        // The registry contract allows registering one id several
1541        // times (multi-implementation codecs). A tag claimed twice by
1542        // the same id resolves to that id either way; the candidate
1543        // list shows both claims so an audit can flag the redundancy.
1544        let tag = CodecTag::fourcc(b"TWIC");
1545        let mut reg = CodecRegistry::new();
1546        reg.register(info("dup").tag(tag.clone()));
1547        reg.register(info("other").tag(tag.clone()));
1548        reg.register(info("dup").tag(tag.clone()));
1549        let pc = ProbeContext::new(&tag);
1550        let cands = reg.resolve_tag_candidates(&pc);
1551        assert_eq!(tag_ids(&cands), vec!["dup", "other", "dup"]);
1552        assert_eq!(
1553            cands.iter().map(|c| c.order).collect::<Vec<_>>(),
1554            vec![0, 1, 2]
1555        );
1556        assert_eq!(reg.resolve_tag_ref(&pc).map(|c| c.as_str()), Some("dup"));
1557    }
1558
1559    #[test]
1560    fn magic_priority_beats_registration_order_but_not_length() {
1561        let mut reg = CodecRegistry::new();
1562        reg.register(info("earlier").payload_magic(b"MAGIC"));
1563        reg.register(
1564            info("later-preferred")
1565                .with_resolution_priority(1)
1566                .payload_magic(b"MAGIC"),
1567        );
1568        reg.register(info("longer").payload_magic(b"MAGICAL"));
1569        // Equal-length tie: priority decides.
1570        let cands = reg.resolve_payload_magic_candidates(b"MAGIC\x00");
1571        assert_eq!(magic_ids(&cands), vec!["later-preferred", "earlier"]);
1572        assert_eq!(cands[0].order, 1);
1573        assert_eq!(cands[0].magic, b"MAGIC");
1574        // A longer matching magic beats both regardless of priority.
1575        let cands = reg.resolve_payload_magic_candidates(b"MAGICAL\x00");
1576        assert_eq!(
1577            magic_ids(&cands),
1578            vec!["longer", "later-preferred", "earlier"]
1579        );
1580        assert_eq!(
1581            reg.resolve_payload_magic_ref(b"MAGICAL\x00")
1582                .map(|c| c.as_str()),
1583            Some("longer")
1584        );
1585        // Nothing matches: empty list, None.
1586        assert!(reg.resolve_payload_magic_candidates(b"other").is_empty());
1587        assert!(reg.resolve_payload_magic_ref(b"other").is_none());
1588    }
1589
1590    fn dec(_p: &crate::CodecParameters) -> crate::Result<Box<dyn Decoder>> {
1591        Err(Error::unsupported("dummy"))
1592    }
1593    fn enc(_p: &crate::CodecParameters) -> crate::Result<Box<dyn Encoder>> {
1594        Err(Error::unsupported("dummy"))
1595    }
1596
1597    #[test]
1598    fn enumerations_follow_registration_order() {
1599        for _ in 0..200 {
1600            let mut reg = CodecRegistry::new();
1601            reg.register(
1602                info("zeta")
1603                    .decoder(dec)
1604                    .tags([CodecTag::fourcc(b"ZET1"), CodecTag::fourcc(b"ZET2")]),
1605            );
1606            reg.register(info("alpha").encoder(enc).tag(CodecTag::fourcc(b"ALPH")));
1607            reg.register(info("omicron").decoder(dec).encoder(enc));
1608            // Second impl of an existing id does not move it.
1609            reg.register(info("zeta").encoder(enc).tag(CodecTag::fourcc(b"ZET3")));
1610            // Tag-only registration: no impl, but its tags are listed.
1611            reg.register(info("beta").tag(CodecTag::fourcc(b"BETA")));
1612
1613            let ids: Vec<&str> = reg
1614                .all_implementations()
1615                .map(|(id, _)| id.as_str())
1616                .collect();
1617            assert_eq!(ids, vec!["zeta", "zeta", "alpha", "omicron"]);
1618            let dec_ids: Vec<&str> = reg.decoder_ids().map(|i| i.as_str()).collect();
1619            assert_eq!(dec_ids, vec!["zeta", "omicron"]);
1620            let enc_ids: Vec<&str> = reg.encoder_ids().map(|i| i.as_str()).collect();
1621            assert_eq!(enc_ids, vec!["zeta", "alpha", "omicron"]);
1622            let tags: Vec<(String, &str)> = reg
1623                .all_tag_registrations()
1624                .map(|(t, id)| (format!("{t:?}"), id.as_str()))
1625                .collect();
1626            let want: Vec<(String, &str)> = [
1627                (CodecTag::fourcc(b"ZET1"), "zeta"),
1628                (CodecTag::fourcc(b"ZET2"), "zeta"),
1629                (CodecTag::fourcc(b"ALPH"), "alpha"),
1630                (CodecTag::fourcc(b"ZET3"), "zeta"),
1631                (CodecTag::fourcc(b"BETA"), "beta"),
1632            ]
1633            .into_iter()
1634            .map(|(t, id)| (format!("{t:?}"), id))
1635            .collect();
1636            assert_eq!(tags, want);
1637        }
1638    }
1639
1640    #[test]
1641    fn resolution_priority_is_independent_of_capabilities_priority() {
1642        // A registration that ranks itself first as an *implementation*
1643        // (HW-style capabilities priority) does not thereby win a tag
1644        // tie against a different codec id registered earlier.
1645        let tag = CodecTag::fourcc(b"SHRD");
1646        let mut reg = CodecRegistry::new();
1647        reg.register(info("sw-id").tag(tag.clone()));
1648        reg.register(
1649            CodecInfo::new(CodecId::new("hw-id"))
1650                .capabilities(CodecCapabilities::video("hw-id").with_priority(10))
1651                .tag(tag.clone()),
1652        );
1653        let pc = ProbeContext::new(&tag);
1654        assert_eq!(reg.resolve_tag_ref(&pc).map(|c| c.as_str()), Some("sw-id"));
1655        let cands = reg.resolve_tag_candidates(&pc);
1656        assert!(cands.iter().all(|c| c.priority == crate::DEFAULT_PRIORITY));
1657    }
1658}
1659
1660#[cfg(test)]
1661mod engine_tests {
1662    use super::*;
1663    use crate::engine::HwDeviceInfo;
1664
1665    #[test]
1666    fn codec_info_engine_id_and_probe_default_to_none() {
1667        let ci = CodecInfo::new(CodecId::new("h264"));
1668        assert!(ci.engine_id.is_none());
1669        assert!(ci.engine_probe.is_none());
1670    }
1671
1672    #[test]
1673    fn codec_info_engine_builder_methods_set_fields() {
1674        fn dummy_probe() -> Vec<HwDeviceInfo> {
1675            vec![]
1676        }
1677        let ci = CodecInfo::new(CodecId::new("h264"))
1678            .with_engine_id("nvidia")
1679            .with_engine_probe(dummy_probe);
1680        assert_eq!(ci.engine_id, Some("nvidia"));
1681        assert!(ci.engine_probe.is_some());
1682        let probe = ci.engine_probe.unwrap();
1683        let result = probe();
1684        assert!(result.is_empty());
1685    }
1686
1687    #[test]
1688    fn registering_codec_with_engine_metadata_does_not_panic() {
1689        // The new fields are passthrough metadata — register() should
1690        // accept them without affecting existing id/tag bookkeeping.
1691        fn dummy_probe() -> Vec<HwDeviceInfo> {
1692            vec![]
1693        }
1694        let mut reg = CodecRegistry::new();
1695        reg.register(
1696            CodecInfo::new(CodecId::new("h264"))
1697                .capabilities(CodecCapabilities::audio("h264_nvdec"))
1698                .tag(CodecTag::fourcc(b"H264"))
1699                .with_engine_id("nvidia")
1700                .with_engine_probe(dummy_probe),
1701        );
1702        let t = CodecTag::fourcc(b"H264");
1703        assert_eq!(
1704            reg.resolve_tag_ref(&ProbeContext::new(&t))
1705                .map(|c| c.as_str()),
1706            Some("h264"),
1707        );
1708    }
1709
1710    /// No-op decoder factory so the registration produces a real
1711    /// CodecImplementation (the registry skips tag-only entries —
1712    /// without a factory there'd be nothing in `implementations()`
1713    /// to assert against).
1714    fn dummy_decoder_factory(
1715        _params: &crate::CodecParameters,
1716    ) -> crate::Result<Box<dyn super::Decoder>> {
1717        Err(crate::Error::unsupported("dummy decoder"))
1718    }
1719
1720    #[test]
1721    fn engine_metadata_propagates_through_register() {
1722        fn dummy_probe() -> Vec<HwDeviceInfo> {
1723            vec![]
1724        }
1725        let mut reg = CodecRegistry::default();
1726        reg.register(
1727            CodecInfo::new(CodecId::new("h264"))
1728                .capabilities(CodecCapabilities::video("h264_test"))
1729                .decoder(dummy_decoder_factory)
1730                .with_engine_id("test-backend")
1731                .with_engine_probe(dummy_probe),
1732        );
1733        let impls = reg.implementations(&CodecId::new("h264"));
1734        assert_eq!(impls.len(), 1);
1735        assert_eq!(impls[0].engine_id, Some("test-backend"));
1736        assert!(impls[0].engine_probe.is_some());
1737    }
1738
1739    #[test]
1740    fn engine_metadata_absent_for_sw_codecs() {
1741        // SW codecs don't call the engine builders — both fields
1742        // should land as None on the resulting CodecImplementation.
1743        let mut reg = CodecRegistry::default();
1744        reg.register(
1745            CodecInfo::new(CodecId::new("flac"))
1746                .capabilities(CodecCapabilities::audio("flac_sw"))
1747                .decoder(dummy_decoder_factory),
1748        );
1749        let impls = reg.implementations(&CodecId::new("flac"));
1750        assert_eq!(impls.len(), 1);
1751        assert!(impls[0].engine_id.is_none());
1752        assert!(impls[0].engine_probe.is_none());
1753    }
1754}