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