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