Skip to main content

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