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//! - **diagnostic** — `all_implementations`, `all_tag_registrations`.
18//!
19//! The tag path explicitly DOES NOT short-circuit on "first claim with
20//! no probe" — every claimant is asked, so a lower-priority probed
21//! claim can out-rank a higher-priority unprobed one when the content
22//! is actually ambiguous (DIV3 XVID-with-real-MSMPEG4 payload etc.).
23
24use std::collections::HashMap;
25
26use crate::arena;
27use crate::{
28    CodecCapabilities, CodecId, CodecOptionsStruct, CodecParameters, CodecResolver, CodecTag,
29    Error, ExecutionContext, Frame, OptionField, Packet, PixelFormat, ProbeContext, ProbeFn,
30    Result,
31};
32
33// ───────────────────────── codec traits ─────────────────────────
34
35/// A packet-to-frame decoder.
36pub trait Decoder: Send {
37    fn codec_id(&self) -> &CodecId;
38
39    /// Feed one compressed packet. May or may not produce a frame immediately —
40    /// call `receive_frame` in a loop afterwards.
41    fn send_packet(&mut self, packet: &Packet) -> Result<()>;
42
43    /// Pull the next decoded frame, if any. Returns `Error::NeedMore` when the
44    /// decoder needs another packet.
45    fn receive_frame(&mut self) -> Result<Frame>;
46
47    /// Pull the next decoded frame as an arena-backed [`arena::sync::Frame`].
48    ///
49    /// Decoders that build their output through an
50    /// [`arena::sync::ArenaPool`] override this to return the pooled
51    /// [`arena::sync::Frame`] **directly**, with no per-plane memcpy
52    /// out — the caller gets true zero-copy plane access via
53    /// [`arena::sync::FrameInner::plane`].
54    ///
55    /// The default implementation delegates to [`Self::receive_frame`]
56    /// and copies the video planes into a freshly-leased one-shot
57    /// `arena::sync::ArenaPool`. This makes the method an additive
58    /// change for every existing [`Decoder`] impl: callers using the
59    /// new API still work, but pay one memcpy per plane.
60    ///
61    /// **Audio / subtitle frames:** the [`arena::sync::Frame`] body is
62    /// video-only (planes + [`arena::sync::FrameHeader`] with
63    /// width/height/pixel format). The default implementation returns
64    /// [`Error::Unsupported`] for non-video frames; an audio decoder
65    /// that wants to expose `receive_arena_frame()` must override it
66    /// with its own arena-backed audio-frame type once the framework
67    /// gains one. Until then, audio decoders should keep using
68    /// [`Self::receive_frame`].
69    fn receive_arena_frame(&mut self) -> Result<arena::sync::Frame> {
70        let frame = self.receive_frame()?;
71        match frame {
72            Frame::Video(v) => video_frame_to_arena_sync_frame(&v),
73            Frame::Audio(_) => Err(Error::unsupported(
74                "receive_arena_frame: audio frames not yet supported by default impl",
75            )),
76            Frame::Subtitle(_) => Err(Error::unsupported(
77                "receive_arena_frame: subtitle frames have no arena-backed representation",
78            )),
79            Frame::Vector(_) => Err(Error::unsupported(
80                "receive_arena_frame: vector frames have no arena-backed representation",
81            )),
82        }
83    }
84
85    /// Signal end-of-stream. After this, `receive_frame` will drain buffered
86    /// frames and eventually return `Error::Eof`.
87    fn flush(&mut self) -> Result<()>;
88
89    /// Discard all carry-over state so the decoder can resume from a new
90    /// bitstream position without producing stale output. Called by the
91    /// player after a container seek.
92    ///
93    /// Unlike [`flush`](Self::flush) (which signals end-of-stream and
94    /// drains buffered frames), `reset` is expected to:
95    /// * drop every buffered input packet and pending output frame;
96    /// * zero any per-stream filter / predictor / overlap memory so the
97    ///   next `send_packet` decodes as if it were the first;
98    /// * leave the codec id and stream parameters untouched.
99    ///
100    /// The default is a conservative "drain-then-forget": call
101    /// [`flush`](Self::flush) and ignore any remaining frames. Stateful
102    /// codecs (LPC predictors, backward-adaptive gain, IMDCT overlap,
103    /// reference pictures, …) should override this to wipe their
104    /// internal state explicitly — otherwise the first ~N output
105    /// samples after a seek will be glitchy until the state re-adapts.
106    fn reset(&mut self) -> Result<()> {
107        self.flush()?;
108        // Drain any remaining output frames so the next send_packet
109        // starts clean. NeedMore / Eof both mean "no more frames"; any
110        // other error is surfaced so the caller can see why.
111        loop {
112            match self.receive_frame() {
113                Ok(_) => {}
114                Err(Error::NeedMore) | Err(Error::Eof) => return Ok(()),
115                Err(e) => return Err(e),
116            }
117        }
118    }
119
120    /// Advisory: announce the runtime environment (today: a thread budget
121    /// for codec-internal parallelism). Called at most once, before the
122    /// first `send_packet`. Default no-op; codecs that want to run
123    /// slice-/GOP-/tile-parallel override this to capture the budget.
124    /// Ignoring the hint is always safe — callers must still work with
125    /// a decoder that runs serial.
126    fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
127}
128
129/// A frame-to-packet encoder.
130pub trait Encoder: Send {
131    fn codec_id(&self) -> &CodecId;
132
133    /// Parameters describing this encoder's output stream (to feed into a muxer).
134    fn output_params(&self) -> &CodecParameters;
135
136    fn send_frame(&mut self, frame: &Frame) -> Result<()>;
137
138    fn receive_packet(&mut self) -> Result<Packet>;
139
140    fn flush(&mut self) -> Result<()>;
141
142    /// Advisory: announce the runtime environment. Same semantics as
143    /// [`Decoder::set_execution_context`].
144    fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
145}
146
147/// Default-impl helper for [`Decoder::receive_arena_frame`]: copy a
148/// heap-backed [`crate::VideoFrame`] into a freshly-leased
149/// [`arena::sync::Frame`].
150///
151/// Allocates a single-slot, single-arena `arena::sync::ArenaPool`
152/// sized to fit the planes verbatim. The pool is dropped at the end of
153/// this call; the returned `Frame` keeps its leased buffer alive via
154/// `Arc<FrameInner>` (the `Arena`'s `Weak` handle to the dropped pool
155/// just stops upgrading — the buffer drops normally when the last
156/// `Frame` clone goes away).
157///
158/// Width / height / pixel-format on the returned `FrameHeader` are
159/// derived from the plane shape: `width = plane[0].stride`,
160/// `height = plane[0].data.len() / stride`. Pixel format is left as
161/// [`PixelFormat::Yuv420P`] when there are 3 planes, else the first
162/// per-plane sensible default — this is a best-effort label for the
163/// generic conversion path; decoders that override
164/// `receive_arena_frame` themselves should set the correct pixel
165/// format.
166fn video_frame_to_arena_sync_frame(v: &crate::VideoFrame) -> Result<arena::sync::Frame> {
167    if v.planes.is_empty() {
168        return Err(Error::invalid(
169            "receive_arena_frame: video frame has no planes",
170        ));
171    }
172    let total_bytes: usize = v.planes.iter().map(|p| p.data.len()).sum();
173    if total_bytes == 0 {
174        return Err(Error::invalid(
175            "receive_arena_frame: video frame planes are empty",
176        ));
177    }
178    // One-shot pool sized exactly to the frame. The pool drops at end
179    // of scope; the leased Arena lives on inside the returned Frame
180    // (its Weak<ArenaPool> handle just won't upgrade in Drop, so the
181    // Box<[u8]> falls through to a normal heap free).
182    let pool = arena::sync::ArenaPool::with_alloc_count_cap(
183        1,
184        total_bytes,
185        // One alloc per plane, plus a generous safety margin.
186        (v.planes.len() as u32).saturating_add(4),
187    );
188    let arena = pool.lease()?;
189    let mut plane_offsets: Vec<(usize, usize)> = Vec::with_capacity(v.planes.len());
190    let mut cursor = 0usize;
191    for plane in &v.planes {
192        let dst = arena.alloc::<u8>(plane.data.len())?;
193        dst.copy_from_slice(&plane.data);
194        plane_offsets.push((cursor, plane.data.len()));
195        cursor += plane.data.len();
196    }
197    // Best-effort header: width = stride of plane 0, height inferred
198    // from plane 0's data length. Pixel format defaults to Yuv420P for
199    // the common 3-plane case, Gray8 for single-plane, otherwise
200    // Yuv444P. Decoders that care about exact pixel-format / width /
201    // height should override `receive_arena_frame` themselves so they
202    // can emit a correct `FrameHeader` straight from their arena
203    // build path.
204    let stride0 = v.planes[0].stride.max(1);
205    let width = stride0 as u32;
206    let height = (v.planes[0].data.len() / stride0) as u32;
207    let pixel_format = match v.planes.len() {
208        1 => PixelFormat::Gray8,
209        3 => PixelFormat::Yuv420P,
210        _ => PixelFormat::Yuv444P,
211    };
212    let header = arena::sync::FrameHeader::new(width, height, pixel_format, v.pts);
213    arena::sync::FrameInner::new(arena, &plane_offsets, header)
214}
215
216/// Factory that builds a decoder for a given codec parameter set.
217pub type DecoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Decoder>>;
218
219/// Factory that builds an encoder for a given codec parameter set.
220pub type EncoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Encoder>>;
221
222// ───────────────────────── CodecInfo ─────────────────────────
223
224/// A single registration: capabilities, decoder/encoder factories,
225/// optional probe, and the container tags this codec claims.
226///
227/// Codec crates build one of these per codec id inside their
228/// `register(reg)` function and hand it to
229/// [`CodecRegistry::register`]. The struct is `#[non_exhaustive]` so
230/// additional fields can be added without breaking existing codec
231/// crates — construction is only possible through
232/// [`CodecInfo::new`] plus the builder methods below.
233#[non_exhaustive]
234pub struct CodecInfo {
235    pub id: CodecId,
236    pub capabilities: CodecCapabilities,
237    pub decoder_factory: Option<DecoderFactory>,
238    pub encoder_factory: Option<EncoderFactory>,
239    /// Probe function that returns a confidence in `0.0..=1.0` for a
240    /// given [`ProbeContext`]. `None` means "confidence 1.0 for every
241    /// claimed tag" — the correct default for codecs whose tag claims
242    /// are unambiguous.
243    pub probe: Option<ProbeFn>,
244    /// Tags this codec is willing to be looked up under. One codec may
245    /// claim many tags (an AAC decoder covers several WaveFormat ids,
246    /// a FourCC, an MP4 OTI, and a Matroska CodecID string at once).
247    pub tags: Vec<CodecTag>,
248    /// Schema of the encoder's recognised option keys
249    /// (`CodecParameters::options`). Attached with
250    /// [`Self::encoder_options`]. Used for validation / `oxideav list`
251    /// / pipeline JSON checks.
252    pub encoder_options_schema: Option<&'static [OptionField]>,
253    /// Schema of the decoder's recognised option keys.
254    pub decoder_options_schema: Option<&'static [OptionField]>,
255}
256
257impl CodecInfo {
258    /// Start a new registration for `id` with empty capabilities, no
259    /// factories, no probe, and no tags. Chain the builder methods
260    /// below to fill it in, then hand the result to
261    /// [`CodecRegistry::register`].
262    pub fn new(id: CodecId) -> Self {
263        Self {
264            capabilities: CodecCapabilities::audio(id.as_str()),
265            id,
266            decoder_factory: None,
267            encoder_factory: None,
268            probe: None,
269            tags: Vec::new(),
270            encoder_options_schema: None,
271            decoder_options_schema: None,
272        }
273    }
274
275    /// Replace the capability description. The default built by
276    /// [`Self::new`] is a placeholder (audio-flavoured, no flags); every
277    /// real registration should call this.
278    pub fn capabilities(mut self, caps: CodecCapabilities) -> Self {
279        self.capabilities = caps;
280        self
281    }
282
283    pub fn decoder(mut self, factory: DecoderFactory) -> Self {
284        self.decoder_factory = Some(factory);
285        self
286    }
287
288    pub fn encoder(mut self, factory: EncoderFactory) -> Self {
289        self.encoder_factory = Some(factory);
290        self
291    }
292
293    pub fn probe(mut self, probe: ProbeFn) -> Self {
294        self.probe = Some(probe);
295        self
296    }
297
298    /// Claim a single container tag for this codec. Equivalent to
299    /// `.tags([tag])` but avoids the array ceremony for single-tag
300    /// claims.
301    pub fn tag(mut self, tag: CodecTag) -> Self {
302        self.tags.push(tag);
303        self
304    }
305
306    /// Claim a set of container tags for this codec. Takes any
307    /// iterable (arrays, `Vec`, `Option`, …) so the common case of a
308    /// codec with 3-6 tags reads as one clean block.
309    pub fn tags(mut self, tags: impl IntoIterator<Item = CodecTag>) -> Self {
310        self.tags.extend(tags);
311        self
312    }
313
314    /// Declare the options struct this codec's encoder factory expects.
315    /// Attaches `T::SCHEMA` so the registry can enumerate recognised
316    /// option keys (for `oxideav list`, pipeline JSON validation, etc.).
317    /// The factory itself still has to call
318    /// [`crate::parse_options::<T>()`] against
319    /// `CodecParameters::options` at init time.
320    pub fn encoder_options<T: CodecOptionsStruct>(mut self) -> Self {
321        self.encoder_options_schema = Some(T::SCHEMA);
322        self
323    }
324
325    /// Declare the options struct this codec's decoder factory expects.
326    /// See [`Self::encoder_options`] for the encoder counterpart.
327    pub fn decoder_options<T: CodecOptionsStruct>(mut self) -> Self {
328        self.decoder_options_schema = Some(T::SCHEMA);
329        self
330    }
331}
332
333/// Internal per-impl record held inside the registry's id map. Kept
334/// distinct from [`CodecInfo`] so the id map stays cheap to walk
335/// during `make_decoder` / `make_encoder` lookups.
336#[derive(Clone)]
337pub struct CodecImplementation {
338    pub caps: CodecCapabilities,
339    pub make_decoder: Option<DecoderFactory>,
340    pub make_encoder: Option<EncoderFactory>,
341    /// Encoder options schema declared via
342    /// [`CodecInfo::encoder_options`]. `None` means the encoder accepts
343    /// no tuning knobs (any non-empty `CodecParameters::options` will
344    /// still be rejected by the factory if the encoder calls
345    /// `parse_options` — this is purely informational for discovery).
346    pub encoder_options_schema: Option<&'static [OptionField]>,
347    pub decoder_options_schema: Option<&'static [OptionField]>,
348}
349
350#[derive(Default)]
351pub struct CodecRegistry {
352    /// id → list of implementations. Each registered codec appends one
353    /// entry here. `make_decoder` / `make_encoder` walk this list in
354    /// preference order.
355    impls: HashMap<CodecId, Vec<CodecImplementation>>,
356    /// Append-only list of every registration — the `tag_index` stores
357    /// offsets into this vector.
358    registrations: Vec<RegistrationRecord>,
359    /// Tag → indices into `registrations`. Indices are stored in
360    /// registration order so tie-breaking in `resolve_tag` is
361    /// deterministic (first-registered wins).
362    tag_index: HashMap<CodecTag, Vec<usize>>,
363}
364
365/// Internal registry record. Mirrors the subset of [`CodecInfo`]
366/// needed at resolve time.
367struct RegistrationRecord {
368    id: CodecId,
369    probe: Option<ProbeFn>,
370}
371
372impl CodecRegistry {
373    pub fn new() -> Self {
374        Self::default()
375    }
376
377    /// Register one codec. Expands into:
378    ///   * an entry in the id → implementations map (for
379    ///     `make_decoder` / `make_encoder`);
380    ///   * an entry in the tag index for every claimed tag (for
381    ///     `resolve_tag`).
382    ///
383    /// Calling `register` multiple times with the same id is allowed
384    /// and how multi-implementation codecs (software-plus-hardware
385    /// FLAC, for example) are expressed.
386    pub fn register(&mut self, info: CodecInfo) {
387        let CodecInfo {
388            id,
389            capabilities,
390            decoder_factory,
391            encoder_factory,
392            probe,
393            tags,
394            encoder_options_schema,
395            decoder_options_schema,
396        } = info;
397
398        let caps = {
399            let mut c = capabilities;
400            if decoder_factory.is_some() {
401                c = c.with_decode();
402            }
403            if encoder_factory.is_some() {
404                c = c.with_encode();
405            }
406            c
407        };
408
409        // Only record an implementation entry when at least one factory
410        // is present. A "tag-only" CodecInfo — used to attach extra tag
411        // claims to a codec that was already registered with factories —
412        // shouldn't pollute the impl list.
413        if decoder_factory.is_some() || encoder_factory.is_some() {
414            self.impls
415                .entry(id.clone())
416                .or_default()
417                .push(CodecImplementation {
418                    caps,
419                    make_decoder: decoder_factory,
420                    make_encoder: encoder_factory,
421                    encoder_options_schema,
422                    decoder_options_schema,
423                });
424        }
425
426        let record_idx = self.registrations.len();
427        self.registrations.push(RegistrationRecord {
428            id: id.clone(),
429            probe,
430        });
431        for tag in tags {
432            self.tag_index.entry(tag).or_default().push(record_idx);
433        }
434    }
435
436    pub fn has_decoder(&self, id: &CodecId) -> bool {
437        self.impls
438            .get(id)
439            .map(|v| v.iter().any(|i| i.make_decoder.is_some()))
440            .unwrap_or(false)
441    }
442
443    pub fn has_encoder(&self, id: &CodecId) -> bool {
444        self.impls
445            .get(id)
446            .map(|v| v.iter().any(|i| i.make_encoder.is_some()))
447            .unwrap_or(false)
448    }
449
450    /// First registered decoder factory for `params.codec_id`, invoked
451    /// with `params`. No priority walk, no preference filter, no
452    /// init-time fallback to a lower-priority impl. Errors if no
453    /// decoder is registered for the codec.
454    ///
455    /// Intended for single-impl scenarios — typically a codec crate's
456    /// own self-tests, where exactly one impl has been registered into
457    /// a freshly-constructed registry. Production callers selecting
458    /// among multiple candidates (e.g. h264_sw vs h264_videotoolbox)
459    /// should use `oxideav_pipeline::make_decoder_with` instead, which
460    /// applies `CodecPreferences` and walks priorities.
461    pub fn first_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
462        let imp = self
463            .implementations(&params.codec_id)
464            .iter()
465            .find(|i| i.make_decoder.is_some())
466            .ok_or_else(|| {
467                Error::CodecNotFound(format!("no decoder for codec {}", params.codec_id))
468            })?;
469        (imp.make_decoder.expect("checked above"))(params)
470    }
471
472    /// First registered encoder factory — see [`first_decoder`].
473    ///
474    /// [`first_decoder`]: Self::first_decoder
475    pub fn first_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
476        let imp = self
477            .implementations(&params.codec_id)
478            .iter()
479            .find(|i| i.make_encoder.is_some())
480            .ok_or_else(|| {
481                Error::CodecNotFound(format!("no encoder for codec {}", params.codec_id))
482            })?;
483        (imp.make_encoder.expect("checked above"))(params)
484    }
485
486    /// Look up a decoder by exact implementation name
487    /// (`"h264_sw"`, `"aac_audiotoolbox"`, ...). Errors if the impl
488    /// isn't registered or if it has no decoder factory.
489    pub fn decoder_by_impl(
490        &self,
491        impl_name: &str,
492        params: &CodecParameters,
493    ) -> Result<Box<dyn Decoder>> {
494        let imp = self
495            .implementations(&params.codec_id)
496            .iter()
497            .find(|i| i.caps.implementation == impl_name)
498            .ok_or_else(|| {
499                Error::CodecNotFound(format!(
500                    "no implementation `{impl_name}` for codec {}",
501                    params.codec_id
502                ))
503            })?;
504        let factory = imp
505            .make_decoder
506            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is encoder-only")))?;
507        factory(params)
508    }
509
510    /// Look up an encoder by exact implementation name — see
511    /// [`decoder_by_impl`].
512    ///
513    /// [`decoder_by_impl`]: Self::decoder_by_impl
514    pub fn encoder_by_impl(
515        &self,
516        impl_name: &str,
517        params: &CodecParameters,
518    ) -> Result<Box<dyn Encoder>> {
519        let imp = self
520            .implementations(&params.codec_id)
521            .iter()
522            .find(|i| i.caps.implementation == impl_name)
523            .ok_or_else(|| {
524                Error::CodecNotFound(format!(
525                    "no implementation `{impl_name}` for codec {}",
526                    params.codec_id
527                ))
528            })?;
529        let factory = imp
530            .make_encoder
531            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is decoder-only")))?;
532        factory(params)
533    }
534
535    /// Iterate codec ids that have at least one decoder implementation.
536    pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
537        self.impls
538            .iter()
539            .filter(|(_, v)| v.iter().any(|i| i.make_decoder.is_some()))
540            .map(|(id, _)| id)
541    }
542
543    pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
544        self.impls
545            .iter()
546            .filter(|(_, v)| v.iter().any(|i| i.make_encoder.is_some()))
547            .map(|(id, _)| id)
548    }
549
550    /// All registered implementations of a given codec id.
551    pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
552        self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
553    }
554
555    /// Lookup the encoder options schema for a registered codec. Walks
556    /// implementations in registration order and returns the first
557    /// schema found. `None` means either the codec isn't registered or
558    /// no implementation declared an encoder schema.
559    pub fn encoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
560        self.impls
561            .get(id)?
562            .iter()
563            .find_map(|i| i.encoder_options_schema)
564    }
565
566    /// Lookup the decoder options schema — see
567    /// [`encoder_options_schema`](Self::encoder_options_schema).
568    pub fn decoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
569        self.impls
570            .get(id)?
571            .iter()
572            .find_map(|i| i.decoder_options_schema)
573    }
574
575    /// Iterator over every (codec_id, impl) pair — useful for `oxideav list`
576    /// to show capability flags per implementation.
577    pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
578        self.impls
579            .iter()
580            .flat_map(|(id, v)| v.iter().map(move |i| (id, i)))
581    }
582
583    /// Iterator over every `(tag, codec_id)` pair currently registered —
584    /// used by `oxideav tags` debug output and by tests that want to
585    /// walk the tag surface.
586    pub fn all_tag_registrations(&self) -> impl Iterator<Item = (&CodecTag, &CodecId)> {
587        self.tag_index.iter().flat_map(move |(tag, idxs)| {
588            idxs.iter().map(move |&i| (tag, &self.registrations[i].id))
589        })
590    }
591
592    /// Inherent form of tag resolution that returns a reference.
593    /// The owned-value form used by container code lives behind the
594    /// [`CodecResolver`] trait impl below.
595    ///
596    /// Walks every registration that claimed `ctx.tag`, calls its
597    /// probe with `ctx`, and returns the id of the registration that
598    /// scored highest. Probes that return `0.0` are discarded; ties
599    /// on confidence are broken by registration order (first wins).
600    /// Registrations with no probe are treated as returning `1.0`.
601    pub fn resolve_tag_ref(&self, ctx: &ProbeContext) -> Option<&CodecId> {
602        let idxs = self.tag_index.get(ctx.tag)?;
603        let mut best: Option<(f32, usize)> = None;
604        for &i in idxs {
605            let rec = &self.registrations[i];
606            let conf = match rec.probe {
607                Some(f) => f(ctx),
608                None => 1.0,
609            };
610            if conf <= 0.0 {
611                continue;
612            }
613            best = match best {
614                None => Some((conf, i)),
615                Some((bc, _)) if conf > bc => Some((conf, i)),
616                other => other,
617            };
618        }
619        best.map(|(_, i)| &self.registrations[i].id)
620    }
621}
622
623/// Implement the shared [`CodecResolver`] interface so container
624/// demuxers can accept `&dyn CodecResolver` without depending on
625/// this crate directly — the trait lives in oxideav-core.
626impl CodecResolver for CodecRegistry {
627    fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId> {
628        self.resolve_tag_ref(ctx).cloned()
629    }
630}
631
632#[cfg(test)]
633mod tag_tests {
634    use super::*;
635    use crate::CodecCapabilities;
636
637    /// Probe: return 1.0 iff the peeked bytes look like MS-MPEG4 (no
638    /// 0x000001 start code in the first few bytes).
639    fn probe_msmpeg4(ctx: &ProbeContext) -> f32 {
640        match ctx.packet {
641            Some(d) if !d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
642            Some(_) => 0.0,
643            None => 0.5, // no data yet — weak evidence
644        }
645    }
646
647    /// Probe: return 1.0 iff the peeked bytes look like MPEG-4 Part 2
648    /// (starts with a 0x000001 start code in the first few bytes).
649    fn probe_mpeg4_part2(ctx: &ProbeContext) -> f32 {
650        match ctx.packet {
651            Some(d) if d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
652            Some(_) => 0.0,
653            None => 0.5,
654        }
655    }
656
657    fn info(id: &str) -> CodecInfo {
658        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
659    }
660
661    #[test]
662    fn resolve_single_claim_no_probe() {
663        let mut reg = CodecRegistry::new();
664        reg.register(info("flac").tag(CodecTag::fourcc(b"FLAC")));
665        let t = CodecTag::fourcc(b"FLAC");
666        assert_eq!(
667            reg.resolve_tag_ref(&ProbeContext::new(&t))
668                .map(|c| c.as_str()),
669            Some("flac"),
670        );
671    }
672
673    #[test]
674    fn resolve_missing_tag_returns_none() {
675        let reg = CodecRegistry::new();
676        let t = CodecTag::fourcc(b"????");
677        assert!(reg.resolve_tag_ref(&ProbeContext::new(&t)).is_none());
678    }
679
680    #[test]
681    fn unprobed_claims_tie_first_registered_wins() {
682        // Two unprobed claims on the same tag: deterministic order.
683        let mut reg = CodecRegistry::new();
684        reg.register(info("first").tag(CodecTag::fourcc(b"TEST")));
685        reg.register(info("second").tag(CodecTag::fourcc(b"TEST")));
686        let t = CodecTag::fourcc(b"TEST");
687        assert_eq!(
688            reg.resolve_tag_ref(&ProbeContext::new(&t))
689                .map(|c| c.as_str()),
690            Some("first"),
691        );
692    }
693
694    #[test]
695    fn probe_picks_matching_bitstream() {
696        // The core bug fix: every probe is asked and the highest
697        // confidence wins regardless of registration order.
698        let mut reg = CodecRegistry::new();
699        reg.register(
700            info("msmpeg4v3")
701                .probe(probe_msmpeg4)
702                .tag(CodecTag::fourcc(b"DIV3")),
703        );
704        reg.register(
705            info("mpeg4video")
706                .probe(probe_mpeg4_part2)
707                .tag(CodecTag::fourcc(b"DIV3")),
708        );
709
710        let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
711        let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
712        let tag = CodecTag::fourcc(b"DIV3");
713
714        let ctx_part2 = ProbeContext::new(&tag).packet(&mpeg4_part2);
715        assert_eq!(
716            reg.resolve_tag_ref(&ctx_part2).map(|c| c.as_str()),
717            Some("mpeg4video"),
718        );
719        let ctx_ms = ProbeContext::new(&tag).packet(&ms_mpeg4);
720        assert_eq!(
721            reg.resolve_tag_ref(&ctx_ms).map(|c| c.as_str()),
722            Some("msmpeg4v3"),
723        );
724    }
725
726    #[test]
727    fn unprobed_claim_wins_against_low_confidence_probe() {
728        // One codec claims a tag without a probe (→ confidence 1.0)
729        // and another claims it with a probe returning 0.3. The
730        // unprobed one wins — a codec that knows it owns the tag
731        // outright should not lose to a speculative probe.
732        let mut reg = CodecRegistry::new();
733        reg.register(info("owner").tag(CodecTag::fourcc(b"OWN_")));
734        reg.register(
735            info("speculative")
736                .probe(|_| 0.3)
737                .tag(CodecTag::fourcc(b"OWN_")),
738        );
739        let t = CodecTag::fourcc(b"OWN_");
740        assert_eq!(
741            reg.resolve_tag_ref(&ProbeContext::new(&t))
742                .map(|c| c.as_str()),
743            Some("owner"),
744        );
745    }
746
747    #[test]
748    fn probe_returning_zero_is_skipped() {
749        let mut reg = CodecRegistry::new();
750        reg.register(
751            info("refuses")
752                .probe(|_| 0.0)
753                .tag(CodecTag::fourcc(b"MAYB")),
754        );
755        reg.register(info("fallback").tag(CodecTag::fourcc(b"MAYB")));
756        let t = CodecTag::fourcc(b"MAYB");
757        let ctx = ProbeContext::new(&t).packet(b"hello");
758        assert_eq!(
759            reg.resolve_tag_ref(&ctx).map(|c| c.as_str()),
760            Some("fallback"),
761        );
762    }
763
764    #[test]
765    fn fourcc_case_insensitive_lookup() {
766        let mut reg = CodecRegistry::new();
767        reg.register(info("vid").tag(CodecTag::fourcc(b"div3")));
768        // Registered as "DIV3" (uppercase via ctor); lookup using
769        // lowercase / mixed case also hits.
770        let upper = CodecTag::fourcc(b"DIV3");
771        let lower = CodecTag::fourcc(b"div3");
772        let mixed = CodecTag::fourcc(b"DiV3");
773        assert!(reg.resolve_tag_ref(&ProbeContext::new(&upper)).is_some());
774        assert!(reg.resolve_tag_ref(&ProbeContext::new(&lower)).is_some());
775        assert!(reg.resolve_tag_ref(&ProbeContext::new(&mixed)).is_some());
776    }
777
778    #[test]
779    fn wave_format_and_matroska_tags_work() {
780        let mut reg = CodecRegistry::new();
781        reg.register(info("mp3").tag(CodecTag::wave_format(0x0055)));
782        reg.register(info("h264").tag(CodecTag::matroska("V_MPEG4/ISO/AVC")));
783        let wf = CodecTag::wave_format(0x0055);
784        let mk = CodecTag::matroska("V_MPEG4/ISO/AVC");
785        assert_eq!(
786            reg.resolve_tag_ref(&ProbeContext::new(&wf))
787                .map(|c| c.as_str()),
788            Some("mp3"),
789        );
790        assert_eq!(
791            reg.resolve_tag_ref(&ProbeContext::new(&mk))
792                .map(|c| c.as_str()),
793            Some("h264"),
794        );
795    }
796
797    #[test]
798    fn mp4_object_type_tag_works() {
799        let mut reg = CodecRegistry::new();
800        reg.register(info("aac").tag(CodecTag::mp4_object_type(0x40)));
801        let t = CodecTag::mp4_object_type(0x40);
802        assert_eq!(
803            reg.resolve_tag_ref(&ProbeContext::new(&t))
804                .map(|c| c.as_str()),
805            Some("aac"),
806        );
807    }
808
809    #[test]
810    fn multi_tag_claim_all_resolve() {
811        let mut reg = CodecRegistry::new();
812        reg.register(info("aac").tags([
813            CodecTag::fourcc(b"MP4A"),
814            CodecTag::wave_format(0x00FF),
815            CodecTag::mp4_object_type(0x40),
816            CodecTag::matroska("A_AAC"),
817        ]));
818        for t in [
819            CodecTag::fourcc(b"MP4A"),
820            CodecTag::wave_format(0x00FF),
821            CodecTag::mp4_object_type(0x40),
822            CodecTag::matroska("A_AAC"),
823        ] {
824            assert_eq!(
825                reg.resolve_tag_ref(&ProbeContext::new(&t))
826                    .map(|c| c.as_str()),
827                Some("aac"),
828                "tag {t:?} did not resolve",
829            );
830        }
831    }
832}