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