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. First-registered wins on ties.
17//! - **payload-magic-keyed** — `resolve_payload_magic(first_bytes)`
18//!   prefix-matches the claimed payload magic prefixes against a
19//!   stream's leading bytes; longest matching magic wins, then
20//!   registration order. For containers that identify a codec by the
21//!   payload itself rather than a tag (e.g. an Ogg logical stream's
22//!   first packet, or raw elementary streams).
23//! - **diagnostic** — `all_implementations`, `all_tag_registrations`,
24//!   `all_payload_magic_registrations`.
25//!
26//! The tag path explicitly DOES NOT short-circuit on "first claim with
27//! no probe" — every claimant is asked, so a lower-priority probed
28//! claim can out-rank a higher-priority unprobed one when the content
29//! is actually ambiguous (DIV3 XVID-with-real-MSMPEG4 payload etc.).
30
31use std::collections::HashMap;
32
33use crate::arena;
34use crate::{
35    CodecCapabilities, CodecId, CodecOptionsStruct, CodecParameters, CodecResolver, CodecTag,
36    Error, ExecutionContext, Frame, OptionField, Packet, PixelFormat, ProbeContext, ProbeFn,
37    Result,
38};
39
40// ───────────────────────── codec traits ─────────────────────────
41
42/// A packet-to-frame decoder.
43pub trait Decoder: Send {
44    /// Identifier of the codec this decoder handles.
45    fn codec_id(&self) -> &CodecId;
46
47    /// Feed one compressed packet. May or may not produce a frame immediately —
48    /// call `receive_frame` in a loop afterwards.
49    fn send_packet(&mut self, packet: &Packet) -> Result<()>;
50
51    /// Pull the next decoded frame, if any. Returns `Error::NeedMore` when the
52    /// decoder needs another packet.
53    fn receive_frame(&mut self) -> Result<Frame>;
54
55    /// Pull the next decoded frame as an arena-backed [`arena::sync::Frame`].
56    ///
57    /// Decoders that build their output through an
58    /// [`arena::sync::ArenaPool`] override this to return the pooled
59    /// [`arena::sync::Frame`] **directly**, with no per-plane memcpy
60    /// out — the caller gets true zero-copy plane access via
61    /// [`arena::sync::FrameInner::plane`].
62    ///
63    /// The default implementation delegates to [`Self::receive_frame`]
64    /// and copies the video planes into a freshly-leased one-shot
65    /// `arena::sync::ArenaPool`. This makes the method an additive
66    /// change for every existing [`Decoder`] impl: callers using the
67    /// new API still work, but pay one memcpy per plane.
68    ///
69    /// **Audio / subtitle frames:** the [`arena::sync::Frame`] body is
70    /// video-only (planes + [`arena::sync::FrameHeader`] with
71    /// width/height/pixel format). The default implementation returns
72    /// [`Error::Unsupported`] for non-video frames; an audio decoder
73    /// that wants to expose `receive_arena_frame()` must override it
74    /// with its own arena-backed audio-frame type once the framework
75    /// gains one. Until then, audio decoders should keep using
76    /// [`Self::receive_frame`].
77    fn receive_arena_frame(&mut self) -> Result<arena::sync::Frame> {
78        let frame = self.receive_frame()?;
79        match frame {
80            Frame::Video(v) => video_frame_to_arena_sync_frame(&v),
81            Frame::Audio(_) => Err(Error::unsupported(
82                "receive_arena_frame: audio frames not yet supported by default impl",
83            )),
84            Frame::Subtitle(_) => Err(Error::unsupported(
85                "receive_arena_frame: subtitle frames have no arena-backed representation",
86            )),
87            Frame::Vector(_) => Err(Error::unsupported(
88                "receive_arena_frame: vector frames have no arena-backed representation",
89            )),
90        }
91    }
92
93    /// Signal end-of-stream. After this, `receive_frame` will drain buffered
94    /// frames and eventually return `Error::Eof`.
95    fn flush(&mut self) -> Result<()>;
96
97    /// Discard all carry-over state so the decoder can resume from a new
98    /// bitstream position without producing stale output. Called by the
99    /// player after a container seek.
100    ///
101    /// Unlike [`flush`](Self::flush) (which signals end-of-stream and
102    /// drains buffered frames), `reset` is expected to:
103    /// * drop every buffered input packet and pending output frame;
104    /// * zero any per-stream filter / predictor / overlap memory so the
105    ///   next `send_packet` decodes as if it were the first;
106    /// * leave the codec id and stream parameters untouched.
107    ///
108    /// The default is a conservative "drain-then-forget": call
109    /// [`flush`](Self::flush) and ignore any remaining frames. Stateful
110    /// codecs (LPC predictors, backward-adaptive gain, IMDCT overlap,
111    /// reference pictures, …) should override this to wipe their
112    /// internal state explicitly — otherwise the first ~N output
113    /// samples after a seek will be glitchy until the state re-adapts.
114    fn reset(&mut self) -> Result<()> {
115        self.flush()?;
116        // Drain any remaining output frames so the next send_packet
117        // starts clean. NeedMore / Eof both mean "no more frames"; any
118        // other error is surfaced so the caller can see why.
119        loop {
120            match self.receive_frame() {
121                Ok(_) => {}
122                Err(Error::NeedMore) | Err(Error::Eof) => return Ok(()),
123                Err(e) => return Err(e),
124            }
125        }
126    }
127
128    /// Advisory: announce the runtime environment (today: a thread budget
129    /// for codec-internal parallelism). Called at most once, before the
130    /// first `send_packet`. Default no-op; codecs that want to run
131    /// slice-/GOP-/tile-parallel override this to capture the budget.
132    /// Ignoring the hint is always safe — callers must still work with
133    /// a decoder that runs serial.
134    fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
135}
136
137/// A frame-to-packet encoder.
138pub trait Encoder: Send {
139    /// Identifier of the codec this encoder produces.
140    fn codec_id(&self) -> &CodecId;
141
142    /// Parameters describing this encoder's output stream (to feed into a muxer).
143    fn output_params(&self) -> &CodecParameters;
144
145    /// Feed one uncompressed frame. May or may not produce a packet
146    /// immediately — call `receive_packet` in a loop afterwards.
147    fn send_frame(&mut self, frame: &Frame) -> Result<()>;
148
149    /// Pull the next encoded packet, if any. Returns `Error::NeedMore`
150    /// when the encoder needs another frame (or a `flush`).
151    fn receive_packet(&mut self) -> Result<Packet>;
152
153    /// Signal end of input: drain internal lookahead so the remaining
154    /// packets become available via `receive_packet`.
155    fn flush(&mut self) -> Result<()>;
156
157    /// Advisory: announce the runtime environment. Same semantics as
158    /// [`Decoder::set_execution_context`].
159    fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
160}
161
162/// Default-impl helper for [`Decoder::receive_arena_frame`]: copy a
163/// heap-backed [`crate::VideoFrame`] into a freshly-leased
164/// [`arena::sync::Frame`].
165///
166/// Allocates a single-slot, single-arena `arena::sync::ArenaPool`
167/// sized to fit the planes verbatim. The pool is dropped at the end of
168/// this call; the returned `Frame` keeps its leased buffer alive via
169/// `Arc<FrameInner>` (the `Arena`'s `Weak` handle to the dropped pool
170/// just stops upgrading — the buffer drops normally when the last
171/// `Frame` clone goes away).
172///
173/// Width / height / pixel-format on the returned `FrameHeader` are
174/// derived from the plane shape: `width = plane[0].stride`,
175/// `height = plane[0].data.len() / stride`. Pixel format is left as
176/// [`PixelFormat::Yuv420P`] when there are 3 planes, else the first
177/// per-plane sensible default — this is a best-effort label for the
178/// generic conversion path; decoders that override
179/// `receive_arena_frame` themselves should set the correct pixel
180/// format.
181fn video_frame_to_arena_sync_frame(v: &crate::VideoFrame) -> Result<arena::sync::Frame> {
182    if v.planes.is_empty() {
183        return Err(Error::invalid(
184            "receive_arena_frame: video frame has no planes",
185        ));
186    }
187    let total_bytes: usize = v.planes.iter().map(|p| p.data.len()).sum();
188    if total_bytes == 0 {
189        return Err(Error::invalid(
190            "receive_arena_frame: video frame planes are empty",
191        ));
192    }
193    // One-shot pool sized exactly to the frame. The pool drops at end
194    // of scope; the leased Arena lives on inside the returned Frame
195    // (its Weak<ArenaPool> handle just won't upgrade in Drop, so the
196    // Box<[u8]> falls through to a normal heap free).
197    let pool = arena::sync::ArenaPool::with_alloc_count_cap(
198        1,
199        total_bytes,
200        // One alloc per plane, plus a generous safety margin.
201        (v.planes.len() as u32).saturating_add(4),
202    );
203    let arena = pool.lease()?;
204    let mut plane_offsets: Vec<(usize, usize)> = Vec::with_capacity(v.planes.len());
205    let mut cursor = 0usize;
206    for plane in &v.planes {
207        let dst = arena.alloc::<u8>(plane.data.len())?;
208        dst.copy_from_slice(&plane.data);
209        plane_offsets.push((cursor, plane.data.len()));
210        cursor += plane.data.len();
211    }
212    // Best-effort header: width = stride of plane 0, height inferred
213    // from plane 0's data length. Pixel format defaults to Yuv420P for
214    // the common 3-plane case, Gray8 for single-plane, otherwise
215    // Yuv444P. Decoders that care about exact pixel-format / width /
216    // height should override `receive_arena_frame` themselves so they
217    // can emit a correct `FrameHeader` straight from their arena
218    // build path.
219    let stride0 = v.planes[0].stride.max(1);
220    let width = stride0 as u32;
221    let height = (v.planes[0].data.len() / stride0) as u32;
222    // Count only image planes for the format guess — side-channel
223    // entries (palette, significant bits; copied verbatim above like
224    // any other plane) must not bump e.g. a single-plane palette frame
225    // out of the Gray8 label.
226    let pixel_format = match v.image_plane_count() {
227        1 => PixelFormat::Gray8,
228        3 => PixelFormat::Yuv420P,
229        _ => PixelFormat::Yuv444P,
230    };
231    let header = arena::sync::FrameHeader::new(width, height, pixel_format, v.pts);
232    arena::sync::FrameInner::new(arena, &plane_offsets, header)
233}
234
235/// Factory that builds a decoder for a given codec parameter set.
236pub type DecoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Decoder>>;
237
238/// Factory that builds an encoder for a given codec parameter set.
239pub type EncoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Encoder>>;
240
241// ───────────────────────── CodecInfo ─────────────────────────
242
243/// A single registration: capabilities, decoder/encoder factories,
244/// optional probe, and the container tags this codec claims.
245///
246/// Codec crates build one of these per codec id inside their
247/// `register(reg)` function and hand it to
248/// [`CodecRegistry::register`]. The struct is `#[non_exhaustive]` so
249/// additional fields can be added without breaking existing codec
250/// crates — construction is only possible through
251/// [`CodecInfo::new`] plus the builder methods below.
252#[non_exhaustive]
253pub struct CodecInfo {
254    /// Canonical codec identifier this entry registers.
255    pub id: CodecId,
256    /// Capability description (media kind, feature flags, priority).
257    pub capabilities: CodecCapabilities,
258    /// Factory producing a fresh decoder instance, if decode is supported.
259    pub decoder_factory: Option<DecoderFactory>,
260    /// Factory producing a fresh encoder instance, if encode is supported.
261    pub encoder_factory: Option<EncoderFactory>,
262    /// Probe function that returns a confidence in `0.0..=1.0` for a
263    /// given [`ProbeContext`]. `None` means "confidence 1.0 for every
264    /// claimed tag" — the correct default for codecs whose tag claims
265    /// are unambiguous.
266    pub probe: Option<ProbeFn>,
267    /// Tags this codec is willing to be looked up under. One codec may
268    /// claim many tags (an AAC decoder covers several WaveFormat ids,
269    /// a FourCC, an MP4 OTI, and a Matroska CodecID string at once).
270    pub tags: Vec<CodecTag>,
271    /// Payload magic prefixes this codec answers to (`\x01vorbis`,
272    /// `OpusHead`, …). Some carriage formats have no codec tag — the
273    /// codec is announced by a magic byte prefix on the payload itself
274    /// (an Ogg logical stream's first packet is the canonical case;
275    /// raw elementary streams are another). Such claims are
276    /// prefix-matched by
277    /// [`CodecRegistry::resolve_payload_magic_ref`] instead of living in
278    /// the exact-match [`CodecTag`] index. Attached with
279    /// [`Self::payload_magic`] / [`Self::payload_magics`]. Empty prefixes are
280    /// ignored at registration time (a zero-length prefix would match
281    /// every stream while carrying no evidence).
282    pub payload_magics: Vec<Vec<u8>>,
283    /// Schema of the encoder's recognised option keys
284    /// (`CodecParameters::options`). Attached with
285    /// [`Self::encoder_options`]. Used for validation / `oxideav list`
286    /// / pipeline JSON checks.
287    pub encoder_options_schema: Option<&'static [OptionField]>,
288    /// Schema of the decoder's recognised option keys.
289    pub decoder_options_schema: Option<&'static [OptionField]>,
290    /// HW backend identifier, e.g. `"nvidia"`, `"vaapi"`, `"vdpau"`,
291    /// `"vulkan-video"`, `"videotoolbox"`. Set by HW siblings on every
292    /// `CodecInfo` they register; SW codecs leave this `None`.
293    /// Consumers (e.g. the CLI's `info` command) use it to group
294    /// codec entries by backend and to dedupe probe calls — multiple
295    /// `CodecInfo` entries with the same `engine_id` typically share
296    /// an `engine_probe` function, and consumers should call the probe
297    /// at most once per `engine_id` per pass. Attached via
298    /// [`Self::with_engine_id`].
299    pub engine_id: Option<&'static str>,
300    /// Optional engine probe function. When `Some`, calling it returns
301    /// one [`crate::engine::HwDeviceInfo`] entry per device the backend
302    /// sees. Phase-2 HW siblings populate this on every `CodecInfo`
303    /// they register; Phase-3 consumers (CLI) call it on demand.
304    /// Attached via [`Self::with_engine_probe`].
305    pub engine_probe: Option<crate::engine::EngineProbeFn>,
306}
307
308impl CodecInfo {
309    /// Start a new registration for `id` with empty capabilities, no
310    /// factories, no probe, and no tags. Chain the builder methods
311    /// below to fill it in, then hand the result to
312    /// [`CodecRegistry::register`].
313    pub fn new(id: CodecId) -> Self {
314        Self {
315            capabilities: CodecCapabilities::audio(id.as_str()),
316            id,
317            decoder_factory: None,
318            encoder_factory: None,
319            probe: None,
320            tags: Vec::new(),
321            payload_magics: Vec::new(),
322            encoder_options_schema: None,
323            decoder_options_schema: None,
324            engine_id: None,
325            engine_probe: None,
326        }
327    }
328
329    /// Replace the capability description. The default built by
330    /// [`Self::new`] is a placeholder (audio-flavoured, no flags); every
331    /// real registration should call this.
332    pub fn capabilities(mut self, caps: CodecCapabilities) -> Self {
333        self.capabilities = caps;
334        self
335    }
336
337    /// Builder: attach the decoder factory.
338    pub fn decoder(mut self, factory: DecoderFactory) -> Self {
339        self.decoder_factory = Some(factory);
340        self
341    }
342
343    /// Builder: attach the encoder factory.
344    pub fn encoder(mut self, factory: EncoderFactory) -> Self {
345        self.encoder_factory = Some(factory);
346        self
347    }
348
349    /// Builder: attach a confidence probe (see [`CodecInfo::probe`]).
350    pub fn probe(mut self, probe: ProbeFn) -> Self {
351        self.probe = Some(probe);
352        self
353    }
354
355    /// Claim a single container tag for this codec. Equivalent to
356    /// `.tags([tag])` but avoids the array ceremony for single-tag
357    /// claims.
358    pub fn tag(mut self, tag: CodecTag) -> Self {
359        self.tags.push(tag);
360        self
361    }
362
363    /// Claim a set of container tags for this codec. Takes any
364    /// iterable (arrays, `Vec`, `Option`, …) so the common case of a
365    /// codec with 3-6 tags reads as one clean block.
366    pub fn tags(mut self, tags: impl IntoIterator<Item = CodecTag>) -> Self {
367        self.tags.extend(tags);
368        self
369    }
370
371    /// Claim one payload magic prefix for this codec (see
372    /// [`Self::payload_magics`]). Chain repeatedly for codecs that answer
373    /// to more than one magic:
374    ///
375    /// ```
376    /// # use oxideav_core::registry::CodecInfo;
377    /// # use oxideav_core::CodecId;
378    /// let info = CodecInfo::new(CodecId::new("vorbis")).payload_magic(b"\x01vorbis");
379    /// # let _ = info;
380    /// ```
381    pub fn payload_magic(mut self, magic: impl Into<Vec<u8>>) -> Self {
382        self.payload_magics.push(magic.into());
383        self
384    }
385
386    /// Claim a set of payload magic prefixes for this codec — the
387    /// iterable companion to [`Self::payload_magic`], mirroring the
388    /// [`Self::tag`] / [`Self::tags`] pair.
389    pub fn payload_magics<I>(mut self, magics: I) -> Self
390    where
391        I: IntoIterator,
392        I::Item: Into<Vec<u8>>,
393    {
394        self.payload_magics
395            .extend(magics.into_iter().map(Into::into));
396        self
397    }
398
399    /// Declare the options struct this codec's encoder factory expects.
400    /// Attaches `T::SCHEMA` so the registry can enumerate recognised
401    /// option keys (for `oxideav list`, pipeline JSON validation, etc.).
402    /// The factory itself still has to call
403    /// [`crate::parse_options::<T>()`] against
404    /// `CodecParameters::options` at init time.
405    pub fn encoder_options<T: CodecOptionsStruct>(mut self) -> Self {
406        self.encoder_options_schema = Some(T::SCHEMA);
407        self
408    }
409
410    /// Declare the options struct this codec's decoder factory expects.
411    /// See [`Self::encoder_options`] for the encoder counterpart.
412    pub fn decoder_options<T: CodecOptionsStruct>(mut self) -> Self {
413        self.decoder_options_schema = Some(T::SCHEMA);
414        self
415    }
416
417    /// Tag this codec as belonging to a HW backend identified by
418    /// `engine_id`. Should match the `engine_id` of every other
419    /// `CodecInfo` registered by the same backend, and the corresponding
420    /// `engine_id` field used by the CLI for grouping. SW codecs leave
421    /// this unset.
422    pub fn with_engine_id(mut self, engine_id: &'static str) -> Self {
423        self.engine_id = Some(engine_id);
424        self
425    }
426
427    /// Attach a probe function. Consumers call it to enumerate the
428    /// engines (devices) this backend can dispatch to. Probes are
429    /// expected to be idempotent and side-effect free; consumers may
430    /// call them more than once per process and should dedupe by
431    /// [`Self::engine_id`].
432    pub fn with_engine_probe(mut self, probe: crate::engine::EngineProbeFn) -> Self {
433        self.engine_probe = Some(probe);
434        self
435    }
436}
437
438/// Internal per-impl record held inside the registry's id map. Kept
439/// distinct from [`CodecInfo`] so the id map stays cheap to walk
440/// during `make_decoder` / `make_encoder` lookups.
441#[derive(Clone)]
442pub struct CodecImplementation {
443    /// Capability description copied from the originating [`CodecInfo`].
444    pub caps: CodecCapabilities,
445    /// Decoder factory, if this implementation can decode.
446    pub make_decoder: Option<DecoderFactory>,
447    /// Encoder factory, if this implementation can encode.
448    pub make_encoder: Option<EncoderFactory>,
449    /// Encoder options schema declared via
450    /// [`CodecInfo::encoder_options`]. `None` means the encoder accepts
451    /// no tuning knobs (any non-empty `CodecParameters::options` will
452    /// still be rejected by the factory if the encoder calls
453    /// `parse_options` — this is purely informational for discovery).
454    pub encoder_options_schema: Option<&'static [OptionField]>,
455    /// Decoder options schema declared via
456    /// [`CodecInfo::decoder_options`]; same semantics as the encoder
457    /// schema above.
458    pub decoder_options_schema: Option<&'static [OptionField]>,
459    /// HW backend identifier copied verbatim from the originating
460    /// [`CodecInfo::engine_id`]. `Some("nvidia"/"vaapi"/...)` on HW
461    /// backends; `None` on SW codecs. Consumers (CLI `info` command,
462    /// pipeline dispatcher, bench loop) read this to group entries by
463    /// backend without grepping `caps.implementation`.
464    pub engine_id: Option<&'static str>,
465    /// Engine probe function copied verbatim from the originating
466    /// [`CodecInfo::engine_probe`]. `Some(fn)` on HW backends with a
467    /// probe wired; `None` on SW codecs. Consumers call it on demand
468    /// to enumerate per-device info ([`crate::engine::HwDeviceInfo`]).
469    pub engine_probe: Option<crate::engine::EngineProbeFn>,
470}
471
472/// Registry mapping codec ids and container tags to their registered
473/// implementations; the lookup point behind `make_decoder` /
474/// `make_encoder` / `resolve_tag`.
475#[derive(Default)]
476pub struct CodecRegistry {
477    /// id → list of implementations. Each registered codec appends one
478    /// entry here. `make_decoder` / `make_encoder` walk this list in
479    /// preference order.
480    impls: HashMap<CodecId, Vec<CodecImplementation>>,
481    /// Append-only list of every registration — the `tag_index` stores
482    /// offsets into this vector.
483    registrations: Vec<RegistrationRecord>,
484    /// Tag → indices into `registrations`. Indices are stored in
485    /// registration order so tie-breaking in `resolve_tag` is
486    /// deterministic (first-registered wins).
487    tag_index: HashMap<CodecTag, Vec<usize>>,
488    /// Payload magic-prefix claims: `(magic, registration index)` in
489    /// registration order. Kept as a flat list rather than a map
490    /// because resolution is prefix matching (see
491    /// [`Self::resolve_payload_magic_ref`]), not exact-key lookup.
492    magic_index: Vec<(Vec<u8>, usize)>,
493}
494
495/// Internal registry record. Mirrors the subset of [`CodecInfo`]
496/// needed at resolve time.
497struct RegistrationRecord {
498    id: CodecId,
499    probe: Option<ProbeFn>,
500}
501
502impl CodecRegistry {
503    /// An empty registry (same as `Default`).
504    pub fn new() -> Self {
505        Self::default()
506    }
507
508    /// Register one codec. Expands into:
509    ///   * an entry in the id → implementations map (for
510    ///     `make_decoder` / `make_encoder`);
511    ///   * an entry in the tag index for every claimed tag (for
512    ///     `resolve_tag`).
513    ///
514    /// Calling `register` multiple times with the same id is allowed
515    /// and how multi-implementation codecs (software-plus-hardware
516    /// FLAC, for example) are expressed.
517    pub fn register(&mut self, info: CodecInfo) {
518        let CodecInfo {
519            id,
520            capabilities,
521            decoder_factory,
522            encoder_factory,
523            probe,
524            tags,
525            payload_magics,
526            encoder_options_schema,
527            decoder_options_schema,
528            // engine_id / engine_probe are metadata attached to a
529            // CodecInfo for backends that want consumers (CLI `info`,
530            // pipeline bench) to enumerate the underlying devices on
531            // demand. They're surfaced verbatim on the resulting
532            // CodecImplementation so consumers can read them without
533            // grepping `caps.implementation`. Tag-only CodecInfo entries
534            // (no factories) drop the values on the floor — there's no
535            // CodecImplementation built in that branch.
536            engine_id,
537            engine_probe,
538        } = info;
539
540        let caps = {
541            let mut c = capabilities;
542            if decoder_factory.is_some() {
543                c = c.with_decode();
544            }
545            if encoder_factory.is_some() {
546                c = c.with_encode();
547            }
548            c
549        };
550
551        // Only record an implementation entry when at least one factory
552        // is present. A "tag-only" CodecInfo — used to attach extra tag
553        // claims to a codec that was already registered with factories —
554        // shouldn't pollute the impl list.
555        if decoder_factory.is_some() || encoder_factory.is_some() {
556            self.impls
557                .entry(id.clone())
558                .or_default()
559                .push(CodecImplementation {
560                    caps,
561                    make_decoder: decoder_factory,
562                    make_encoder: encoder_factory,
563                    encoder_options_schema,
564                    decoder_options_schema,
565                    engine_id,
566                    engine_probe,
567                });
568        }
569
570        let record_idx = self.registrations.len();
571        self.registrations.push(RegistrationRecord {
572            id: id.clone(),
573            probe,
574        });
575        for tag in tags {
576            self.tag_index.entry(tag).or_default().push(record_idx);
577        }
578        for magic in payload_magics {
579            // A zero-length prefix would match every stream while
580            // carrying no evidence — drop it here so resolution never
581            // has to special-case it.
582            if !magic.is_empty() {
583                self.magic_index.push((magic, record_idx));
584            }
585        }
586    }
587
588    /// Whether at least one registered implementation of `id` can decode.
589    pub fn has_decoder(&self, id: &CodecId) -> bool {
590        self.impls
591            .get(id)
592            .map(|v| v.iter().any(|i| i.make_decoder.is_some()))
593            .unwrap_or(false)
594    }
595
596    /// Whether at least one registered implementation of `id` can encode.
597    pub fn has_encoder(&self, id: &CodecId) -> bool {
598        self.impls
599            .get(id)
600            .map(|v| v.iter().any(|i| i.make_encoder.is_some()))
601            .unwrap_or(false)
602    }
603
604    /// First registered decoder factory for `params.codec_id`, invoked
605    /// with `params`. No priority walk, no preference filter, no
606    /// init-time fallback to a lower-priority impl. Errors if no
607    /// decoder is registered for the codec.
608    ///
609    /// Intended for single-impl scenarios — typically a codec crate's
610    /// own self-tests, where exactly one impl has been registered into
611    /// a freshly-constructed registry. Production callers selecting
612    /// among multiple candidates (e.g. h264_sw vs h264_videotoolbox)
613    /// should use `oxideav_pipeline::make_decoder_with` instead, which
614    /// applies `CodecPreferences` and walks priorities.
615    pub fn first_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
616        let imp = self
617            .implementations(&params.codec_id)
618            .iter()
619            .find(|i| i.make_decoder.is_some())
620            .ok_or_else(|| {
621                Error::CodecNotFound(format!("no decoder for codec {}", params.codec_id))
622            })?;
623        (imp.make_decoder.expect("checked above"))(params)
624    }
625
626    /// First registered encoder factory — see [`first_decoder`].
627    ///
628    /// [`first_decoder`]: Self::first_decoder
629    pub fn first_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
630        let imp = self
631            .implementations(&params.codec_id)
632            .iter()
633            .find(|i| i.make_encoder.is_some())
634            .ok_or_else(|| {
635                Error::CodecNotFound(format!("no encoder for codec {}", params.codec_id))
636            })?;
637        (imp.make_encoder.expect("checked above"))(params)
638    }
639
640    /// Look up a decoder by exact implementation name
641    /// (`"h264_sw"`, `"aac_audiotoolbox"`, ...). Errors if the impl
642    /// isn't registered or if it has no decoder factory.
643    pub fn decoder_by_impl(
644        &self,
645        impl_name: &str,
646        params: &CodecParameters,
647    ) -> Result<Box<dyn Decoder>> {
648        let imp = self
649            .implementations(&params.codec_id)
650            .iter()
651            .find(|i| i.caps.implementation == impl_name)
652            .ok_or_else(|| {
653                Error::CodecNotFound(format!(
654                    "no implementation `{impl_name}` for codec {}",
655                    params.codec_id
656                ))
657            })?;
658        let factory = imp
659            .make_decoder
660            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is encoder-only")))?;
661        factory(params)
662    }
663
664    /// Look up an encoder by exact implementation name — see
665    /// [`decoder_by_impl`].
666    ///
667    /// [`decoder_by_impl`]: Self::decoder_by_impl
668    pub fn encoder_by_impl(
669        &self,
670        impl_name: &str,
671        params: &CodecParameters,
672    ) -> Result<Box<dyn Encoder>> {
673        let imp = self
674            .implementations(&params.codec_id)
675            .iter()
676            .find(|i| i.caps.implementation == impl_name)
677            .ok_or_else(|| {
678                Error::CodecNotFound(format!(
679                    "no implementation `{impl_name}` for codec {}",
680                    params.codec_id
681                ))
682            })?;
683        let factory = imp
684            .make_encoder
685            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is decoder-only")))?;
686        factory(params)
687    }
688
689    /// Iterate codec ids that have at least one decoder implementation.
690    pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
691        self.impls
692            .iter()
693            .filter(|(_, v)| v.iter().any(|i| i.make_decoder.is_some()))
694            .map(|(id, _)| id)
695    }
696
697    /// Iterate codec ids that have at least one encoder implementation.
698    pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
699        self.impls
700            .iter()
701            .filter(|(_, v)| v.iter().any(|i| i.make_encoder.is_some()))
702            .map(|(id, _)| id)
703    }
704
705    /// All registered implementations of a given codec id.
706    pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
707        self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
708    }
709
710    /// Lookup the encoder options schema for a registered codec. Walks
711    /// implementations in registration order and returns the first
712    /// schema found. `None` means either the codec isn't registered or
713    /// no implementation declared an encoder schema.
714    pub fn encoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
715        self.impls
716            .get(id)?
717            .iter()
718            .find_map(|i| i.encoder_options_schema)
719    }
720
721    /// Lookup the decoder options schema — see
722    /// [`encoder_options_schema`](Self::encoder_options_schema).
723    pub fn decoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
724        self.impls
725            .get(id)?
726            .iter()
727            .find_map(|i| i.decoder_options_schema)
728    }
729
730    /// Iterator over every (codec_id, impl) pair — useful for `oxideav list`
731    /// to show capability flags per implementation.
732    pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
733        self.impls
734            .iter()
735            .flat_map(|(id, v)| v.iter().map(move |i| (id, i)))
736    }
737
738    /// Iterator over every `(tag, codec_id)` pair currently registered —
739    /// used by `oxideav tags` debug output and by tests that want to
740    /// walk the tag surface.
741    pub fn all_tag_registrations(&self) -> impl Iterator<Item = (&CodecTag, &CodecId)> {
742        self.tag_index.iter().flat_map(move |(tag, idxs)| {
743            idxs.iter().map(move |&i| (tag, &self.registrations[i].id))
744        })
745    }
746
747    /// Inherent form of tag resolution that returns a reference.
748    /// The owned-value form used by container code lives behind the
749    /// [`CodecResolver`] trait impl below.
750    ///
751    /// Walks every registration that claimed `ctx.tag`, calls its
752    /// probe with `ctx`, and returns the id of the registration that
753    /// scored highest. Probes that return `0.0` are discarded; ties
754    /// on confidence are broken by registration order (first wins).
755    /// Registrations with no probe are treated as returning `1.0`.
756    pub fn resolve_tag_ref(&self, ctx: &ProbeContext) -> Option<&CodecId> {
757        let idxs = self.tag_index.get(ctx.tag)?;
758        let mut best: Option<(f32, usize)> = None;
759        for &i in idxs {
760            let rec = &self.registrations[i];
761            let conf = match rec.probe {
762                Some(f) => f(ctx),
763                None => 1.0,
764            };
765            if conf <= 0.0 {
766                continue;
767            }
768            best = match best {
769                None => Some((conf, i)),
770                Some((bc, _)) if conf > bc => Some((conf, i)),
771                other => other,
772            };
773        }
774        best.map(|(_, i)| &self.registrations[i].id)
775    }
776
777    /// Inherent form of payload-magic resolution that returns a
778    /// reference. The owned-value form used by container code lives
779    /// behind the [`CodecResolver`] trait impl below.
780    ///
781    /// Walks every registered payload magic prefix (declared via
782    /// [`CodecInfo::payload_magic`] / [`CodecInfo::payload_magics`]) and
783    /// returns the codec whose magic is a prefix of `first_bytes` —
784    /// however much of the stream's leading payload the caller has
785    /// (an Ogg demuxer passes the first packet of a logical stream; a
786    /// raw-stream prober passes the file head). The **longest**
787    /// matching magic wins (most specific claim); remaining ties are
788    /// broken by registration order (first wins). Unlike the tag path
789    /// there is no probe step: a payload magic is itself the bitstream
790    /// evidence a probe would look for, and specificity is expressed
791    /// by prefix length instead of a confidence value.
792    pub fn resolve_payload_magic_ref(&self, first_bytes: &[u8]) -> Option<&CodecId> {
793        let mut best: Option<(usize, usize)> = None; // (magic_len, reg idx)
794        for (magic, idx) in &self.magic_index {
795            if !first_bytes.starts_with(magic) {
796                continue;
797            }
798            // Strict `>` keeps the earlier registration on equal
799            // lengths — `magic_index` is in registration order.
800            best = match best {
801                None => Some((magic.len(), *idx)),
802                Some((len, _)) if magic.len() > len => Some((magic.len(), *idx)),
803                other => other,
804            };
805        }
806        best.map(|(_, i)| &self.registrations[i].id)
807    }
808
809    /// Iterator over every `(payload magic, codec_id)` pair currently
810    /// registered, in registration order — the payload-magic companion
811    /// to [`all_tag_registrations`](Self::all_tag_registrations).
812    pub fn all_payload_magic_registrations(&self) -> impl Iterator<Item = (&[u8], &CodecId)> {
813        self.magic_index
814            .iter()
815            .map(move |(magic, i)| (magic.as_slice(), &self.registrations[*i].id))
816    }
817}
818
819/// Implement the shared [`CodecResolver`] interface so container
820/// demuxers can accept `&dyn CodecResolver` without depending on
821/// this crate directly — the trait lives in oxideav-core.
822impl CodecResolver for CodecRegistry {
823    fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId> {
824        self.resolve_tag_ref(ctx).cloned()
825    }
826
827    fn resolve_payload_magic(&self, first_packet: &[u8]) -> Option<CodecId> {
828        self.resolve_payload_magic_ref(first_packet).cloned()
829    }
830}
831
832#[cfg(test)]
833mod tag_tests {
834    use super::*;
835    use crate::CodecCapabilities;
836
837    /// Probe: return 1.0 iff the peeked bytes look like MS-MPEG4 (no
838    /// 0x000001 start code in the first few bytes).
839    fn probe_msmpeg4(ctx: &ProbeContext) -> f32 {
840        match ctx.packet {
841            Some(d) if !d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
842            Some(_) => 0.0,
843            None => 0.5, // no data yet — weak evidence
844        }
845    }
846
847    /// Probe: return 1.0 iff the peeked bytes look like MPEG-4 Part 2
848    /// (starts with a 0x000001 start code in the first few bytes).
849    fn probe_mpeg4_part2(ctx: &ProbeContext) -> f32 {
850        match ctx.packet {
851            Some(d) if d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
852            Some(_) => 0.0,
853            None => 0.5,
854        }
855    }
856
857    fn info(id: &str) -> CodecInfo {
858        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
859    }
860
861    #[test]
862    fn resolve_single_claim_no_probe() {
863        let mut reg = CodecRegistry::new();
864        reg.register(info("flac").tag(CodecTag::fourcc(b"FLAC")));
865        let t = CodecTag::fourcc(b"FLAC");
866        assert_eq!(
867            reg.resolve_tag_ref(&ProbeContext::new(&t))
868                .map(|c| c.as_str()),
869            Some("flac"),
870        );
871    }
872
873    #[test]
874    fn resolve_missing_tag_returns_none() {
875        let reg = CodecRegistry::new();
876        let t = CodecTag::fourcc(b"????");
877        assert!(reg.resolve_tag_ref(&ProbeContext::new(&t)).is_none());
878    }
879
880    #[test]
881    fn unprobed_claims_tie_first_registered_wins() {
882        // Two unprobed claims on the same tag: deterministic order.
883        let mut reg = CodecRegistry::new();
884        reg.register(info("first").tag(CodecTag::fourcc(b"TEST")));
885        reg.register(info("second").tag(CodecTag::fourcc(b"TEST")));
886        let t = CodecTag::fourcc(b"TEST");
887        assert_eq!(
888            reg.resolve_tag_ref(&ProbeContext::new(&t))
889                .map(|c| c.as_str()),
890            Some("first"),
891        );
892    }
893
894    #[test]
895    fn probe_picks_matching_bitstream() {
896        // The core bug fix: every probe is asked and the highest
897        // confidence wins regardless of registration order.
898        let mut reg = CodecRegistry::new();
899        reg.register(
900            info("msmpeg4v3")
901                .probe(probe_msmpeg4)
902                .tag(CodecTag::fourcc(b"DIV3")),
903        );
904        reg.register(
905            info("mpeg4video")
906                .probe(probe_mpeg4_part2)
907                .tag(CodecTag::fourcc(b"DIV3")),
908        );
909
910        let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
911        let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
912        let tag = CodecTag::fourcc(b"DIV3");
913
914        let ctx_part2 = ProbeContext::new(&tag).packet(&mpeg4_part2);
915        assert_eq!(
916            reg.resolve_tag_ref(&ctx_part2).map(|c| c.as_str()),
917            Some("mpeg4video"),
918        );
919        let ctx_ms = ProbeContext::new(&tag).packet(&ms_mpeg4);
920        assert_eq!(
921            reg.resolve_tag_ref(&ctx_ms).map(|c| c.as_str()),
922            Some("msmpeg4v3"),
923        );
924    }
925
926    #[test]
927    fn unprobed_claim_wins_against_low_confidence_probe() {
928        // One codec claims a tag without a probe (→ confidence 1.0)
929        // and another claims it with a probe returning 0.3. The
930        // unprobed one wins — a codec that knows it owns the tag
931        // outright should not lose to a speculative probe.
932        let mut reg = CodecRegistry::new();
933        reg.register(info("owner").tag(CodecTag::fourcc(b"OWN_")));
934        reg.register(
935            info("speculative")
936                .probe(|_| 0.3)
937                .tag(CodecTag::fourcc(b"OWN_")),
938        );
939        let t = CodecTag::fourcc(b"OWN_");
940        assert_eq!(
941            reg.resolve_tag_ref(&ProbeContext::new(&t))
942                .map(|c| c.as_str()),
943            Some("owner"),
944        );
945    }
946
947    #[test]
948    fn probe_returning_zero_is_skipped() {
949        let mut reg = CodecRegistry::new();
950        reg.register(
951            info("refuses")
952                .probe(|_| 0.0)
953                .tag(CodecTag::fourcc(b"MAYB")),
954        );
955        reg.register(info("fallback").tag(CodecTag::fourcc(b"MAYB")));
956        let t = CodecTag::fourcc(b"MAYB");
957        let ctx = ProbeContext::new(&t).packet(b"hello");
958        assert_eq!(
959            reg.resolve_tag_ref(&ctx).map(|c| c.as_str()),
960            Some("fallback"),
961        );
962    }
963
964    #[test]
965    fn fourcc_case_insensitive_lookup() {
966        let mut reg = CodecRegistry::new();
967        reg.register(info("vid").tag(CodecTag::fourcc(b"div3")));
968        // Registered as "DIV3" (uppercase via ctor); lookup using
969        // lowercase / mixed case also hits.
970        let upper = CodecTag::fourcc(b"DIV3");
971        let lower = CodecTag::fourcc(b"div3");
972        let mixed = CodecTag::fourcc(b"DiV3");
973        assert!(reg.resolve_tag_ref(&ProbeContext::new(&upper)).is_some());
974        assert!(reg.resolve_tag_ref(&ProbeContext::new(&lower)).is_some());
975        assert!(reg.resolve_tag_ref(&ProbeContext::new(&mixed)).is_some());
976    }
977
978    #[test]
979    fn wave_format_and_matroska_tags_work() {
980        let mut reg = CodecRegistry::new();
981        reg.register(info("mp3").tag(CodecTag::wave_format(0x0055)));
982        reg.register(info("h264").tag(CodecTag::matroska("V_MPEG4/ISO/AVC")));
983        let wf = CodecTag::wave_format(0x0055);
984        let mk = CodecTag::matroska("V_MPEG4/ISO/AVC");
985        assert_eq!(
986            reg.resolve_tag_ref(&ProbeContext::new(&wf))
987                .map(|c| c.as_str()),
988            Some("mp3"),
989        );
990        assert_eq!(
991            reg.resolve_tag_ref(&ProbeContext::new(&mk))
992                .map(|c| c.as_str()),
993            Some("h264"),
994        );
995    }
996
997    #[test]
998    fn mp4_object_type_tag_works() {
999        let mut reg = CodecRegistry::new();
1000        reg.register(info("aac").tag(CodecTag::mp4_object_type(0x40)));
1001        let t = CodecTag::mp4_object_type(0x40);
1002        assert_eq!(
1003            reg.resolve_tag_ref(&ProbeContext::new(&t))
1004                .map(|c| c.as_str()),
1005            Some("aac"),
1006        );
1007    }
1008
1009    #[test]
1010    fn multi_tag_claim_all_resolve() {
1011        let mut reg = CodecRegistry::new();
1012        reg.register(info("aac").tags([
1013            CodecTag::fourcc(b"MP4A"),
1014            CodecTag::wave_format(0x00FF),
1015            CodecTag::mp4_object_type(0x40),
1016            CodecTag::matroska("A_AAC"),
1017        ]));
1018        for t in [
1019            CodecTag::fourcc(b"MP4A"),
1020            CodecTag::wave_format(0x00FF),
1021            CodecTag::mp4_object_type(0x40),
1022            CodecTag::matroska("A_AAC"),
1023        ] {
1024            assert_eq!(
1025                reg.resolve_tag_ref(&ProbeContext::new(&t))
1026                    .map(|c| c.as_str()),
1027                Some("aac"),
1028                "tag {t:?} did not resolve",
1029            );
1030        }
1031    }
1032}
1033
1034#[cfg(test)]
1035mod payload_magic_tests {
1036    use super::*;
1037    use crate::CodecCapabilities;
1038
1039    fn info(id: &str) -> CodecInfo {
1040        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1041    }
1042
1043    /// Registry with the classic Ogg family registered, each under its
1044    /// real BOS magic.
1045    fn ogg_family_registry() -> CodecRegistry {
1046        let mut reg = CodecRegistry::new();
1047        reg.register(info("vorbis").payload_magic(b"\x01vorbis"));
1048        reg.register(info("opus").payload_magic(b"OpusHead"));
1049        reg.register(info("theora").payload_magic(b"\x80theora"));
1050        reg.register(info("flac").payload_magic(b"\x7fFLAC"));
1051        reg
1052    }
1053
1054    #[test]
1055    fn resolve_payload_magic_matches_first_packet_prefix() {
1056        let reg = ogg_family_registry();
1057        // A Vorbis identification header: magic + version + channels +
1058        // rate + ... — the resolver only needs the prefix to match.
1059        let vorbis_id_header = b"\x01vorbis\x00\x00\x00\x00\x02\x44\xac\x00\x00";
1060        assert_eq!(
1061            reg.resolve_payload_magic_ref(vorbis_id_header)
1062                .map(|c| c.as_str()),
1063            Some("vorbis"),
1064        );
1065        assert_eq!(
1066            reg.resolve_payload_magic_ref(b"OpusHead\x01\x02\x38\x01")
1067                .map(|c| c.as_str()),
1068            Some("opus"),
1069        );
1070        assert_eq!(
1071            reg.resolve_payload_magic_ref(b"\x80theora\x03\x02\x01")
1072                .map(|c| c.as_str()),
1073            Some("theora"),
1074        );
1075        assert_eq!(
1076            reg.resolve_payload_magic_ref(b"\x7fFLAC\x01\x00")
1077                .map(|c| c.as_str()),
1078            Some("flac"),
1079        );
1080    }
1081
1082    #[test]
1083    fn resolve_payload_magic_exact_length_packet_matches() {
1084        // A packet that is exactly the magic (nothing after it) still
1085        // resolves — starts_with is inclusive of equality.
1086        let reg = ogg_family_registry();
1087        assert_eq!(
1088            reg.resolve_payload_magic_ref(b"OpusHead")
1089                .map(|c| c.as_str()),
1090            Some("opus"),
1091        );
1092    }
1093
1094    #[test]
1095    fn resolve_payload_magic_unknown_or_short_packet_is_none() {
1096        let reg = ogg_family_registry();
1097        // Unknown magic.
1098        assert!(reg.resolve_payload_magic_ref(b"Speex   1.2.0").is_none());
1099        // Packet shorter than every registered magic.
1100        assert!(reg.resolve_payload_magic_ref(b"Opus").is_none());
1101        // Empty packet.
1102        assert!(reg.resolve_payload_magic_ref(b"").is_none());
1103    }
1104
1105    #[test]
1106    fn resolve_payload_magic_longest_prefix_wins_regardless_of_order() {
1107        // A shorter magic that is itself a prefix of a longer one must
1108        // lose to the more specific claim, whichever registered first.
1109        let mut reg = CodecRegistry::new();
1110        reg.register(info("generic").payload_magic(b"Opus"));
1111        reg.register(info("opus").payload_magic(b"OpusHead"));
1112        assert_eq!(
1113            reg.resolve_payload_magic_ref(b"OpusHead\x01")
1114                .map(|c| c.as_str()),
1115            Some("opus"),
1116        );
1117        // ...but the shorter claim still wins packets only it matches.
1118        assert_eq!(
1119            reg.resolve_payload_magic_ref(b"OpusTags")
1120                .map(|c| c.as_str()),
1121            Some("generic"),
1122        );
1123
1124        // Same result with the registration order flipped.
1125        let mut reg = CodecRegistry::new();
1126        reg.register(info("opus").payload_magic(b"OpusHead"));
1127        reg.register(info("generic").payload_magic(b"Opus"));
1128        assert_eq!(
1129            reg.resolve_payload_magic_ref(b"OpusHead\x01")
1130                .map(|c| c.as_str()),
1131            Some("opus"),
1132        );
1133    }
1134
1135    #[test]
1136    fn resolve_payload_magic_equal_length_tie_first_registered_wins() {
1137        let mut reg = CodecRegistry::new();
1138        reg.register(info("first").payload_magic(b"SameMagic"));
1139        reg.register(info("second").payload_magic(b"SameMagic"));
1140        assert_eq!(
1141            reg.resolve_payload_magic_ref(b"SameMagic\x00")
1142                .map(|c| c.as_str()),
1143            Some("first"),
1144        );
1145    }
1146
1147    #[test]
1148    fn empty_payload_magic_is_ignored_at_registration() {
1149        let mut reg = CodecRegistry::new();
1150        reg.register(info("greedy").payload_magic(b""));
1151        assert!(reg.resolve_payload_magic_ref(b"anything at all").is_none());
1152        assert_eq!(reg.all_payload_magic_registrations().count(), 0);
1153    }
1154
1155    #[test]
1156    fn payload_magics_plural_builder_and_diagnostics() {
1157        // One codec answering to several magics via the iterable
1158        // builder; the diagnostic iterator surfaces each claim in
1159        // registration order.
1160        let mut reg = CodecRegistry::new();
1161        reg.register(info("speex").payload_magics([b"Speex   ".to_vec(), b"speex-alt".to_vec()]));
1162        assert_eq!(
1163            reg.resolve_payload_magic_ref(b"Speex   1.2")
1164                .map(|c| c.as_str()),
1165            Some("speex"),
1166        );
1167        assert_eq!(
1168            reg.resolve_payload_magic_ref(b"speex-alt\x00")
1169                .map(|c| c.as_str()),
1170            Some("speex"),
1171        );
1172        let all: Vec<(&[u8], &str)> = reg
1173            .all_payload_magic_registrations()
1174            .map(|(m, id)| (m, id.as_str()))
1175            .collect();
1176        assert_eq!(
1177            all,
1178            vec![
1179                (b"Speex   ".as_slice(), "speex"),
1180                (b"speex-alt".as_slice(), "speex"),
1181            ],
1182        );
1183    }
1184
1185    #[test]
1186    fn magic_claims_compose_with_tag_claims_on_one_registration() {
1187        // A codec that lives in both Ogg and Matroska declares both
1188        // claim kinds on one CodecInfo; each resolution path finds it.
1189        let mut reg = CodecRegistry::new();
1190        reg.register(
1191            info("vorbis")
1192                .tag(CodecTag::matroska("A_VORBIS"))
1193                .payload_magic(b"\x01vorbis"),
1194        );
1195        let mk = CodecTag::matroska("A_VORBIS");
1196        assert_eq!(
1197            reg.resolve_tag_ref(&ProbeContext::new(&mk))
1198                .map(|c| c.as_str()),
1199            Some("vorbis"),
1200        );
1201        assert_eq!(
1202            reg.resolve_payload_magic_ref(b"\x01vorbis\x00")
1203                .map(|c| c.as_str()),
1204            Some("vorbis"),
1205        );
1206    }
1207
1208    #[test]
1209    fn resolver_trait_payload_magic_surface() {
1210        // The owned-value trait form mirrors the inherent form, and
1211        // the default implementation (NullCodecResolver) resolves
1212        // nothing.
1213        let reg = ogg_family_registry();
1214        let resolver: &dyn CodecResolver = &reg;
1215        assert_eq!(
1216            resolver
1217                .resolve_payload_magic(b"OpusHead\x01")
1218                .map(|c| c.0.clone()),
1219            Some("opus".to_owned()),
1220        );
1221        assert!(resolver.resolve_payload_magic(b"unknown").is_none());
1222
1223        let null = crate::NullCodecResolver;
1224        assert!(null.resolve_payload_magic(b"OpusHead\x01").is_none());
1225    }
1226
1227    /// The surface is container-agnostic: a raw elementary stream
1228    /// identified by a file-head magic resolves through the same path
1229    /// as the Ogg family — nothing about the mechanism is Ogg-shaped.
1230    #[test]
1231    fn payload_magic_serves_non_ogg_carriage() {
1232        let mut reg = CodecRegistry::new();
1233        reg.register(info("flac").payload_magic(b"fLaC"));
1234        reg.register(info("shorten").payload_magic(b"ajkg"));
1235
1236        assert_eq!(
1237            reg.resolve_payload_magic_ref(b"fLaC\x00\x00\x00\x22"),
1238            Some(&CodecId::new("flac"))
1239        );
1240        assert_eq!(
1241            reg.resolve_payload_magic_ref(b"ajkg\x02"),
1242            Some(&CodecId::new("shorten"))
1243        );
1244        assert_eq!(reg.resolve_payload_magic_ref(b"RIFF"), None);
1245    }
1246}
1247
1248#[cfg(test)]
1249mod engine_tests {
1250    use super::*;
1251    use crate::engine::HwDeviceInfo;
1252
1253    #[test]
1254    fn codec_info_engine_id_and_probe_default_to_none() {
1255        let ci = CodecInfo::new(CodecId::new("h264"));
1256        assert!(ci.engine_id.is_none());
1257        assert!(ci.engine_probe.is_none());
1258    }
1259
1260    #[test]
1261    fn codec_info_engine_builder_methods_set_fields() {
1262        fn dummy_probe() -> Vec<HwDeviceInfo> {
1263            vec![]
1264        }
1265        let ci = CodecInfo::new(CodecId::new("h264"))
1266            .with_engine_id("nvidia")
1267            .with_engine_probe(dummy_probe);
1268        assert_eq!(ci.engine_id, Some("nvidia"));
1269        assert!(ci.engine_probe.is_some());
1270        let probe = ci.engine_probe.unwrap();
1271        let result = probe();
1272        assert!(result.is_empty());
1273    }
1274
1275    #[test]
1276    fn registering_codec_with_engine_metadata_does_not_panic() {
1277        // The new fields are passthrough metadata — register() should
1278        // accept them without affecting existing id/tag bookkeeping.
1279        fn dummy_probe() -> Vec<HwDeviceInfo> {
1280            vec![]
1281        }
1282        let mut reg = CodecRegistry::new();
1283        reg.register(
1284            CodecInfo::new(CodecId::new("h264"))
1285                .capabilities(CodecCapabilities::audio("h264_nvdec"))
1286                .tag(CodecTag::fourcc(b"H264"))
1287                .with_engine_id("nvidia")
1288                .with_engine_probe(dummy_probe),
1289        );
1290        let t = CodecTag::fourcc(b"H264");
1291        assert_eq!(
1292            reg.resolve_tag_ref(&ProbeContext::new(&t))
1293                .map(|c| c.as_str()),
1294            Some("h264"),
1295        );
1296    }
1297
1298    /// No-op decoder factory so the registration produces a real
1299    /// CodecImplementation (the registry skips tag-only entries —
1300    /// without a factory there'd be nothing in `implementations()`
1301    /// to assert against).
1302    fn dummy_decoder_factory(
1303        _params: &crate::CodecParameters,
1304    ) -> crate::Result<Box<dyn super::Decoder>> {
1305        Err(crate::Error::unsupported("dummy decoder"))
1306    }
1307
1308    #[test]
1309    fn engine_metadata_propagates_through_register() {
1310        fn dummy_probe() -> Vec<HwDeviceInfo> {
1311            vec![]
1312        }
1313        let mut reg = CodecRegistry::default();
1314        reg.register(
1315            CodecInfo::new(CodecId::new("h264"))
1316                .capabilities(CodecCapabilities::video("h264_test"))
1317                .decoder(dummy_decoder_factory)
1318                .with_engine_id("test-backend")
1319                .with_engine_probe(dummy_probe),
1320        );
1321        let impls = reg.implementations(&CodecId::new("h264"));
1322        assert_eq!(impls.len(), 1);
1323        assert_eq!(impls[0].engine_id, Some("test-backend"));
1324        assert!(impls[0].engine_probe.is_some());
1325    }
1326
1327    #[test]
1328    fn engine_metadata_absent_for_sw_codecs() {
1329        // SW codecs don't call the engine builders — both fields
1330        // should land as None on the resulting CodecImplementation.
1331        let mut reg = CodecRegistry::default();
1332        reg.register(
1333            CodecInfo::new(CodecId::new("flac"))
1334                .capabilities(CodecCapabilities::audio("flac_sw"))
1335                .decoder(dummy_decoder_factory),
1336        );
1337        let impls = reg.implementations(&CodecId::new("flac"));
1338        assert_eq!(impls.len(), 1);
1339        assert!(impls[0].engine_id.is_none());
1340        assert!(impls[0].engine_probe.is_none());
1341    }
1342}