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