Skip to main content

oxideav_codec/
registry.rs

1//! In-process codec registry — supports multiple implementations per codec
2//! id, ranked by capability + priority + user preferences with init-time
3//! fallback.
4
5use std::collections::HashMap;
6
7use oxideav_core::{
8    CodecCapabilities, CodecId, CodecParameters, CodecPreferences, CodecResolver, CodecTag, Error,
9    Result,
10};
11
12use crate::{Decoder, DecoderFactory, Encoder, EncoderFactory};
13
14/// A bitstream-probe function that inspects the first bytes of a packet
15/// and returns true if this codec can decode it. Lets the registry
16/// disambiguate the many container tags that get mislabelled in the
17/// wild (most famous: AVI FourCC `DIV3` routing to MPEG-4 Part 2
18/// instead of MS-MPEG4v3).
19pub type CodecProbe = fn(&[u8]) -> bool;
20
21/// One codec's claim on a container tag. Stored inside the registry;
22/// callers don't usually construct this directly, see
23/// [`CodecRegistry::claim_tag`].
24#[derive(Clone, Copy)]
25pub struct TagClaim {
26    /// Higher = preferred when multiple codecs claim the same tag.
27    pub priority: u8,
28    /// Optional bitstream probe. Returns true to accept this claim,
29    /// false to skip and try the next one in priority order.
30    pub probe: Option<CodecProbe>,
31}
32
33/// One registered implementation: capability description + factories.
34/// Either / both factories may be present depending on whether the impl
35/// can decode, encode, or both.
36#[derive(Clone)]
37pub struct CodecImplementation {
38    pub caps: CodecCapabilities,
39    pub make_decoder: Option<DecoderFactory>,
40    pub make_encoder: Option<EncoderFactory>,
41}
42
43#[derive(Default)]
44pub struct CodecRegistry {
45    impls: HashMap<CodecId, Vec<CodecImplementation>>,
46    /// Tag-to-codec-id map. Containers call [`Self::resolve_tag`] to
47    /// turn their in-stream codec tag (FourCC / WAVEFORMATEX /
48    /// Matroska id / …) into a [`CodecId`] without hard-coding the
49    /// mapping themselves. Claims are kept sorted by priority
50    /// descending so resolution is cheap.
51    tag_claims: HashMap<CodecTag, Vec<(CodecId, TagClaim)>>,
52}
53
54impl CodecRegistry {
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Register a codec implementation. The same codec id may be registered
60    /// multiple times — for example a software FLAC decoder and (later) a
61    /// hardware one would both register under id `"flac"`.
62    pub fn register(&mut self, id: CodecId, implementation: CodecImplementation) {
63        self.impls.entry(id).or_default().push(implementation);
64    }
65
66    /// Convenience: register a decoder-only implementation built from a
67    /// caps + factory pair.
68    pub fn register_decoder_impl(
69        &mut self,
70        id: CodecId,
71        caps: CodecCapabilities,
72        factory: DecoderFactory,
73    ) {
74        self.register(
75            id,
76            CodecImplementation {
77                caps: caps.with_decode(),
78                make_decoder: Some(factory),
79                make_encoder: None,
80            },
81        );
82    }
83
84    /// Convenience: register an encoder-only implementation.
85    pub fn register_encoder_impl(
86        &mut self,
87        id: CodecId,
88        caps: CodecCapabilities,
89        factory: EncoderFactory,
90    ) {
91        self.register(
92            id,
93            CodecImplementation {
94                caps: caps.with_encode(),
95                make_decoder: None,
96                make_encoder: Some(factory),
97            },
98        );
99    }
100
101    /// Convenience: register a single implementation that handles both
102    /// decode and encode for a codec id.
103    pub fn register_both(
104        &mut self,
105        id: CodecId,
106        caps: CodecCapabilities,
107        decode: DecoderFactory,
108        encode: EncoderFactory,
109    ) {
110        self.register(
111            id,
112            CodecImplementation {
113                caps: caps.with_decode().with_encode(),
114                make_decoder: Some(decode),
115                make_encoder: Some(encode),
116            },
117        );
118    }
119
120    /// Backwards-compat shim: register a decoder-only impl with default
121    /// software capabilities. Prefer `register_decoder_impl` for new code.
122    pub fn register_decoder(&mut self, id: CodecId, factory: DecoderFactory) {
123        let caps = CodecCapabilities::audio(id.as_str()).with_decode();
124        self.register_decoder_impl(id, caps, factory);
125    }
126
127    /// Backwards-compat shim: register an encoder-only impl with default
128    /// software capabilities.
129    pub fn register_encoder(&mut self, id: CodecId, factory: EncoderFactory) {
130        let caps = CodecCapabilities::audio(id.as_str()).with_encode();
131        self.register_encoder_impl(id, caps, factory);
132    }
133
134    pub fn has_decoder(&self, id: &CodecId) -> bool {
135        self.impls
136            .get(id)
137            .map(|v| v.iter().any(|i| i.make_decoder.is_some()))
138            .unwrap_or(false)
139    }
140
141    pub fn has_encoder(&self, id: &CodecId) -> bool {
142        self.impls
143            .get(id)
144            .map(|v| v.iter().any(|i| i.make_encoder.is_some()))
145            .unwrap_or(false)
146    }
147
148    /// Build a decoder for `params`. Walks all implementations matching the
149    /// codec id in increasing priority order, skipping any excluded by the
150    /// caller's preferences. Init-time fallback: if a higher-priority impl's
151    /// constructor returns an error, the next candidate is tried.
152    pub fn make_decoder_with(
153        &self,
154        params: &CodecParameters,
155        prefs: &CodecPreferences,
156    ) -> Result<Box<dyn Decoder>> {
157        let candidates = self
158            .impls
159            .get(&params.codec_id)
160            .ok_or_else(|| Error::CodecNotFound(params.codec_id.to_string()))?;
161        let mut ranked: Vec<&CodecImplementation> = candidates
162            .iter()
163            .filter(|i| i.make_decoder.is_some() && !prefs.excludes(&i.caps))
164            .filter(|i| caps_fit_params(&i.caps, params, false))
165            .collect();
166        ranked.sort_by_key(|i| prefs.effective_priority(&i.caps));
167        let mut last_err: Option<Error> = None;
168        for imp in ranked {
169            match (imp.make_decoder.unwrap())(params) {
170                Ok(d) => return Ok(d),
171                Err(e) => last_err = Some(e),
172            }
173        }
174        Err(last_err.unwrap_or_else(|| {
175            Error::CodecNotFound(format!(
176                "no decoder for {} accepts the requested parameters",
177                params.codec_id
178            ))
179        }))
180    }
181
182    /// Build an encoder, with the same priority + fallback semantics.
183    pub fn make_encoder_with(
184        &self,
185        params: &CodecParameters,
186        prefs: &CodecPreferences,
187    ) -> Result<Box<dyn Encoder>> {
188        let candidates = self
189            .impls
190            .get(&params.codec_id)
191            .ok_or_else(|| Error::CodecNotFound(params.codec_id.to_string()))?;
192        let mut ranked: Vec<&CodecImplementation> = candidates
193            .iter()
194            .filter(|i| i.make_encoder.is_some() && !prefs.excludes(&i.caps))
195            .filter(|i| caps_fit_params(&i.caps, params, true))
196            .collect();
197        ranked.sort_by_key(|i| prefs.effective_priority(&i.caps));
198        let mut last_err: Option<Error> = None;
199        for imp in ranked {
200            match (imp.make_encoder.unwrap())(params) {
201                Ok(e) => return Ok(e),
202                Err(e) => last_err = Some(e),
203            }
204        }
205        Err(last_err.unwrap_or_else(|| {
206            Error::CodecNotFound(format!(
207                "no encoder for {} accepts the requested parameters",
208                params.codec_id
209            ))
210        }))
211    }
212
213    /// Default-preference shorthand for `make_decoder_with`.
214    pub fn make_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
215        self.make_decoder_with(params, &CodecPreferences::default())
216    }
217
218    /// Default-preference shorthand for `make_encoder_with`.
219    pub fn make_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
220        self.make_encoder_with(params, &CodecPreferences::default())
221    }
222
223    /// Iterate codec ids that have at least one decoder implementation.
224    pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
225        self.impls
226            .iter()
227            .filter(|(_, v)| v.iter().any(|i| i.make_decoder.is_some()))
228            .map(|(id, _)| id)
229    }
230
231    pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
232        self.impls
233            .iter()
234            .filter(|(_, v)| v.iter().any(|i| i.make_encoder.is_some()))
235            .map(|(id, _)| id)
236    }
237
238    /// All registered implementations of a given codec id.
239    pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
240        self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
241    }
242
243    /// Iterator over every (codec_id, impl) pair — useful for `oxideav list`
244    /// to show capability flags per implementation.
245    pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
246        self.impls
247            .iter()
248            .flat_map(|(id, v)| v.iter().map(move |i| (id, i)))
249    }
250
251    /// Attach a codec id to a container tag so demuxers can look up
252    /// the right decoder without each container maintaining its own
253    /// hand-written FourCC / WAVEFORMATEX / Matroska table.
254    ///
255    /// A single tag may be claimed by multiple codecs — the typical
256    /// reason is mislabelling in the wild: e.g. AVI FourCC `DIV3`
257    /// legally means MS-MPEG4v3 but in practice is applied to real
258    /// MPEG-4 Part 2 streams often enough that both `oxideav-msmpeg4`
259    /// and `oxideav-mpeg4video` want to claim it, each with a probe
260    /// that accepts the bitstream it actually understands.
261    ///
262    /// Claims are stored sorted by `priority` descending;
263    /// [`Self::resolve_tag`] walks them in order and returns the
264    /// first whose probe accepts (or the first with no probe).
265    pub fn claim_tag(
266        &mut self,
267        id: CodecId,
268        tag: CodecTag,
269        priority: u8,
270        probe: Option<CodecProbe>,
271    ) {
272        let entry = self.tag_claims.entry(tag).or_default();
273        entry.push((id, TagClaim { priority, probe }));
274        // Stable sort — later registrations with equal priority appear
275        // after earlier ones, which matches "probe-backed claims come
276        // first, catch-all fallbacks last" when priorities are equal.
277        entry.sort_by_key(|(_, claim)| std::cmp::Reverse(claim.priority));
278    }
279
280    /// Resolve a container tag to a codec id. Walks the priority-
281    /// ordered claim list and returns the first matching one:
282    ///
283    /// * Claim has a probe + `probe_data` is `Some(d)` → accept iff
284    ///   `probe(d)` returns true; otherwise skip and try the next.
285    /// * Claim has a probe + `probe_data` is `None` → accept
286    ///   (probing without bytes is impossible; fall back to priority).
287    /// * Claim has no probe → accept (catch-all).
288    ///
289    /// Returns `None` if the tag has no claimants.
290    ///
291    /// # Example
292    ///
293    /// ```
294    /// use oxideav_codec::CodecRegistry;
295    /// use oxideav_core::{CodecId, CodecTag};
296    ///
297    /// let mut reg = CodecRegistry::new();
298    /// reg.claim_tag(CodecId::new("flac"), CodecTag::fourcc(b"FLAC"), 10, None);
299    ///
300    /// let resolved = reg.resolve_tag(&CodecTag::fourcc(b"FLAC"), None);
301    /// assert_eq!(resolved.map(|c| c.as_str()), Some("flac"));
302    /// ```
303    pub fn resolve_tag(&self, tag: &CodecTag, probe_data: Option<&[u8]>) -> Option<&CodecId> {
304        let claims = self.tag_claims.get(tag)?;
305        for (id, claim) in claims {
306            match (claim.probe, probe_data) {
307                (None, _) => return Some(id),
308                (Some(_), None) => return Some(id),
309                (Some(p), Some(d)) => {
310                    if p(d) {
311                        return Some(id);
312                    }
313                }
314            }
315        }
316        None
317    }
318
319    /// Return the full list of claims for a tag in priority order —
320    /// useful for diagnostics (`oxideav tags` / error messages when
321    /// no claim accepts the probed bytes).
322    pub fn claims_for_tag(&self, tag: &CodecTag) -> &[(CodecId, TagClaim)] {
323        self.tag_claims
324            .get(tag)
325            .map(|v| v.as_slice())
326            .unwrap_or(&[])
327    }
328
329    /// Iterator over every tag claim currently registered — used by
330    /// `oxideav tags` debug output and by integration tests that want
331    /// to verify the full tag surface.
332    pub fn all_tag_claims(&self) -> impl Iterator<Item = (&CodecTag, &CodecId, &TagClaim)> {
333        self.tag_claims
334            .iter()
335            .flat_map(|(tag, claims)| claims.iter().map(move |(id, c)| (tag, id, c)))
336    }
337}
338
339/// Implement the shared [`CodecResolver`] interface so container
340/// demuxers can accept `&dyn CodecResolver` without depending on
341/// this crate directly — the trait lives in oxideav-core.
342impl CodecResolver for CodecRegistry {
343    fn resolve_tag(&self, tag: &CodecTag, probe_data: Option<&[u8]>) -> Option<CodecId> {
344        // Delegate to the inherent method and clone the result so the
345        // trait returns an owned CodecId (the inherent method returns
346        // &CodecId tied to the registry's lifetime).
347        CodecRegistry::resolve_tag(self, tag, probe_data).cloned()
348    }
349}
350
351/// Check whether an implementation's restrictions are compatible with the
352/// requested codec parameters. `for_encode` swaps the rare cases where a
353/// restriction only applies one way.
354fn caps_fit_params(caps: &CodecCapabilities, p: &CodecParameters, for_encode: bool) -> bool {
355    let _ = for_encode; // reserved for future use (e.g. encode-only bitrate caps)
356    if let (Some(max), Some(w)) = (caps.max_width, p.width) {
357        if w > max {
358            return false;
359        }
360    }
361    if let (Some(max), Some(h)) = (caps.max_height, p.height) {
362        if h > max {
363            return false;
364        }
365    }
366    if let (Some(max), Some(br)) = (caps.max_bitrate, p.bit_rate) {
367        if br > max {
368            return false;
369        }
370    }
371    if let (Some(max), Some(sr)) = (caps.max_sample_rate, p.sample_rate) {
372        if sr > max {
373            return false;
374        }
375    }
376    if let (Some(max), Some(ch)) = (caps.max_channels, p.channels) {
377        if ch > max {
378            return false;
379        }
380    }
381    true
382}
383
384#[cfg(test)]
385mod tag_tests {
386    use super::*;
387    use oxideav_core::CodecCapabilities;
388
389    fn looks_like_msmpeg4(data: &[u8]) -> bool {
390        // For tests: MS-MPEG4 if no 0x000001 start code in first 8 bytes.
391        !data.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01])
392    }
393
394    fn looks_like_mpeg4_part2(data: &[u8]) -> bool {
395        data.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01])
396    }
397
398    #[test]
399    fn resolve_single_claim_no_probe() {
400        let mut reg = CodecRegistry::new();
401        reg.claim_tag(CodecId::new("flac"), CodecTag::fourcc(b"FLAC"), 10, None);
402        assert_eq!(
403            reg.resolve_tag(&CodecTag::fourcc(b"FLAC"), None)
404                .map(|c| c.as_str()),
405            Some("flac"),
406        );
407    }
408
409    #[test]
410    fn resolve_missing_tag_returns_none() {
411        let reg = CodecRegistry::new();
412        assert!(reg.resolve_tag(&CodecTag::fourcc(b"????"), None).is_none());
413    }
414
415    #[test]
416    fn priority_highest_wins() {
417        let mut reg = CodecRegistry::new();
418        reg.claim_tag(CodecId::new("low"), CodecTag::fourcc(b"TEST"), 1, None);
419        reg.claim_tag(CodecId::new("high"), CodecTag::fourcc(b"TEST"), 10, None);
420        reg.claim_tag(CodecId::new("mid"), CodecTag::fourcc(b"TEST"), 5, None);
421        assert_eq!(
422            reg.resolve_tag(&CodecTag::fourcc(b"TEST"), None)
423                .map(|c| c.as_str()),
424            Some("high"),
425        );
426    }
427
428    #[test]
429    fn probe_chooses_matching_bitstream() {
430        // DIV3: msmpeg4v3 claims with "looks like MS" probe, mpeg4video
431        // claims with "looks like Part 2" probe. A packet beginning with
432        // 0x000001B0 (MPEG-4 Part 2 VOS) must route to mpeg4video even
433        // though msmpeg4v3 has the higher priority.
434        let mut reg = CodecRegistry::new();
435        reg.claim_tag(
436            CodecId::new("msmpeg4v3"),
437            CodecTag::fourcc(b"DIV3"),
438            10,
439            Some(looks_like_msmpeg4),
440        );
441        reg.claim_tag(
442            CodecId::new("mpeg4video"),
443            CodecTag::fourcc(b"DIV3"),
444            5,
445            Some(looks_like_mpeg4_part2),
446        );
447
448        let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
449        let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
450
451        assert_eq!(
452            reg.resolve_tag(&CodecTag::fourcc(b"DIV3"), Some(&mpeg4_part2))
453                .map(|c| c.as_str()),
454            Some("mpeg4video"),
455        );
456        assert_eq!(
457            reg.resolve_tag(&CodecTag::fourcc(b"DIV3"), Some(&ms_mpeg4))
458                .map(|c| c.as_str()),
459            Some("msmpeg4v3"),
460        );
461    }
462
463    #[test]
464    fn probed_claims_without_probe_data_fall_back_to_priority() {
465        let mut reg = CodecRegistry::new();
466        reg.claim_tag(
467            CodecId::new("msmpeg4v3"),
468            CodecTag::fourcc(b"DIV3"),
469            10,
470            Some(looks_like_msmpeg4),
471        );
472        reg.claim_tag(
473            CodecId::new("mpeg4video"),
474            CodecTag::fourcc(b"DIV3"),
475            5,
476            Some(looks_like_mpeg4_part2),
477        );
478        // No probe_data → highest-priority wins.
479        assert_eq!(
480            reg.resolve_tag(&CodecTag::fourcc(b"DIV3"), None)
481                .map(|c| c.as_str()),
482            Some("msmpeg4v3"),
483        );
484    }
485
486    #[test]
487    fn fallback_no_probe_catches_everything() {
488        let mut reg = CodecRegistry::new();
489        reg.claim_tag(
490            CodecId::new("picky"),
491            CodecTag::fourcc(b"MAYB"),
492            10,
493            Some(|_| false), // never accepts
494        );
495        reg.claim_tag(CodecId::new("fallback"), CodecTag::fourcc(b"MAYB"), 1, None);
496        assert_eq!(
497            reg.resolve_tag(&CodecTag::fourcc(b"MAYB"), Some(b"hello"))
498                .map(|c| c.as_str()),
499            Some("fallback"),
500        );
501    }
502
503    #[test]
504    fn claims_for_tag_returns_ordered_list() {
505        let mut reg = CodecRegistry::new();
506        reg.claim_tag(CodecId::new("a"), CodecTag::fourcc(b"XYZ1"), 1, None);
507        reg.claim_tag(CodecId::new("b"), CodecTag::fourcc(b"XYZ1"), 9, None);
508        reg.claim_tag(CodecId::new("c"), CodecTag::fourcc(b"XYZ1"), 5, None);
509        let claims: Vec<_> = reg
510            .claims_for_tag(&CodecTag::fourcc(b"XYZ1"))
511            .iter()
512            .map(|(id, c)| (id.as_str().to_string(), c.priority))
513            .collect();
514        assert_eq!(
515            claims,
516            vec![
517                ("b".to_string(), 9),
518                ("c".to_string(), 5),
519                ("a".to_string(), 1),
520            ],
521        );
522    }
523
524    #[test]
525    fn fourcc_case_insensitive_lookup() {
526        let mut reg = CodecRegistry::new();
527        reg.claim_tag(CodecId::new("vid"), CodecTag::fourcc(b"div3"), 10, None);
528        // Registered as "DIV3" (uppercase via ctor); lookup using lowercase
529        // also hits thanks to fourcc()-normalisation on lookup side.
530        assert!(reg.resolve_tag(&CodecTag::fourcc(b"DIV3"), None).is_some());
531        assert!(reg.resolve_tag(&CodecTag::fourcc(b"div3"), None).is_some());
532        assert!(reg.resolve_tag(&CodecTag::fourcc(b"DiV3"), None).is_some());
533    }
534
535    #[test]
536    fn wave_format_and_matroska_tags_work() {
537        let mut reg = CodecRegistry::new();
538        reg.claim_tag(CodecId::new("mp3"), CodecTag::wave_format(0x0055), 10, None);
539        reg.claim_tag(
540            CodecId::new("h264"),
541            CodecTag::matroska("V_MPEG4/ISO/AVC"),
542            10,
543            None,
544        );
545        assert_eq!(
546            reg.resolve_tag(&CodecTag::wave_format(0x0055), None)
547                .map(|c| c.as_str()),
548            Some("mp3"),
549        );
550        assert_eq!(
551            reg.resolve_tag(&CodecTag::matroska("V_MPEG4/ISO/AVC"), None)
552                .map(|c| c.as_str()),
553            Some("h264"),
554        );
555    }
556
557    #[test]
558    fn mp4_object_type_tag_works() {
559        let mut reg = CodecRegistry::new();
560        // 0x40 = MPEG-4 AAC per ISO/IEC 14496-1.
561        reg.claim_tag(
562            CodecId::new("aac"),
563            CodecTag::mp4_object_type(0x40),
564            10,
565            None,
566        );
567        assert_eq!(
568            reg.resolve_tag(&CodecTag::mp4_object_type(0x40), None)
569                .map(|c| c.as_str()),
570            Some("aac"),
571        );
572    }
573
574    #[test]
575    fn suppress_unused_caps() {
576        let _ = CodecCapabilities::audio("dummy");
577    }
578}