Skip to main content

oximedia_codec/
codec_registry.rs

1//! Codec registry — register and look up codecs by name, FOURCC, or [`CodecId`].
2//!
3//! The registry stores one [`CodecDescriptor`] per entry and supports queries by:
4//! - String name (e.g. `"av1"`, `"vp9"`)
5//! - Four-character code (`[u8; 4]`, e.g. `*b"AV01"`)
6//! - [`CodecId`] enum variant
7//! - Capability flags (encode / decode / lossless)
8//! - Profile enumeration
9//!
10//! # Example
11//!
12//! ```rust
13//! use oximedia_codec::codec_registry::{CodecRegistry, CodecDescriptor, CodecDirection};
14//! use oximedia_core::CodecId;
15//!
16//! let mut registry = CodecRegistry::default_registry();
17//! let desc = registry.lookup_by_id(CodecId::Av1).expect("AV1 should be registered");
18//! assert!(desc.can_encode);
19//! assert!(desc.can_decode);
20//! ```
21
22use std::collections::HashMap;
23
24use oximedia_core::CodecId;
25
26/// A four-character code (`FOURCC`) identifying a codec in container metadata.
27pub type Fourcc = [u8; 4];
28
29/// Direction a codec entry supports.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CodecDirection {
32    /// Encoding (raw → compressed) only.
33    EncodeOnly,
34    /// Decoding (compressed → raw) only.
35    DecodeOnly,
36    /// Both encoding and decoding.
37    Both,
38}
39
40/// A single named codec profile.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct CodecProfile {
43    /// Profile name (e.g. `"Main"`, `"High"`, `"Profile 0"`).
44    pub name: String,
45    /// Numeric profile identifier (codec-specific).
46    pub id: u32,
47    /// Optional human-readable description.
48    pub description: Option<String>,
49}
50
51impl CodecProfile {
52    /// Create a new profile entry.
53    pub fn new(name: impl Into<String>, id: u32) -> Self {
54        Self {
55            name: name.into(),
56            id,
57            description: None,
58        }
59    }
60
61    /// Builder: set a description.
62    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
63        self.description = Some(desc.into());
64        self
65    }
66}
67
68/// Complete description of a codec as stored in the registry.
69#[derive(Debug, Clone)]
70pub struct CodecDescriptor {
71    /// Canonical [`CodecId`] variant.
72    pub codec_id: CodecId,
73    /// Short lowercase name used for string-based lookup (e.g. `"av1"`).
74    pub name: String,
75    /// Optional human-readable long name (e.g. `"AOMedia Video 1"`).
76    pub long_name: Option<String>,
77    /// All `FOURCC` codes associated with this codec.
78    pub fourccs: Vec<Fourcc>,
79    /// Whether this codec entry supports encoding.
80    pub can_encode: bool,
81    /// Whether this codec entry supports decoding.
82    pub can_decode: bool,
83    /// Whether lossless mode is available.
84    pub is_lossless: bool,
85    /// Named profiles supported by this codec.
86    pub profiles: Vec<CodecProfile>,
87    /// Maximum bit depth (e.g. 8, 10, 12).
88    pub max_bit_depth: u8,
89}
90
91impl CodecDescriptor {
92    /// Create a minimal descriptor from a [`CodecId`].
93    pub fn new(codec_id: CodecId) -> Self {
94        Self {
95            name: codec_id.name().to_string(),
96            codec_id,
97            long_name: None,
98            fourccs: Vec::new(),
99            can_encode: false,
100            can_decode: false,
101            is_lossless: codec_id.is_lossless(),
102            profiles: Vec::new(),
103            max_bit_depth: 8,
104        }
105    }
106
107    /// Builder: set the long name.
108    pub fn with_long_name(mut self, s: impl Into<String>) -> Self {
109        self.long_name = Some(s.into());
110        self
111    }
112
113    /// Builder: add a FOURCC.
114    pub fn with_fourcc(mut self, fourcc: Fourcc) -> Self {
115        self.fourccs.push(fourcc);
116        self
117    }
118
119    /// Builder: set encode/decode direction.
120    pub fn with_direction(mut self, dir: CodecDirection) -> Self {
121        match dir {
122            CodecDirection::EncodeOnly => {
123                self.can_encode = true;
124                self.can_decode = false;
125            }
126            CodecDirection::DecodeOnly => {
127                self.can_encode = false;
128                self.can_decode = true;
129            }
130            CodecDirection::Both => {
131                self.can_encode = true;
132                self.can_decode = true;
133            }
134        }
135        self
136    }
137
138    /// Builder: override lossless flag (normally derived from [`CodecId::is_lossless`]).
139    pub fn with_lossless(mut self, lossless: bool) -> Self {
140        self.is_lossless = lossless;
141        self
142    }
143
144    /// Builder: add a profile entry.
145    pub fn with_profile(mut self, profile: CodecProfile) -> Self {
146        self.profiles.push(profile);
147        self
148    }
149
150    /// Builder: set maximum bit depth.
151    pub fn with_max_bit_depth(mut self, depth: u8) -> Self {
152        self.max_bit_depth = depth;
153        self
154    }
155
156    /// Returns `true` if this descriptor supports a given FOURCC.
157    pub fn has_fourcc(&self, fourcc: &Fourcc) -> bool {
158        self.fourccs.contains(fourcc)
159    }
160
161    /// Look up a profile by name (case-insensitive).
162    pub fn profile_by_name(&self, name: &str) -> Option<&CodecProfile> {
163        let lower = name.to_lowercase();
164        self.profiles
165            .iter()
166            .find(|p| p.name.to_lowercase() == lower)
167    }
168
169    /// Look up a profile by numeric id.
170    pub fn profile_by_id(&self, id: u32) -> Option<&CodecProfile> {
171        self.profiles.iter().find(|p| p.id == id)
172    }
173}
174
175/// Central registry of codec descriptors.
176///
177/// Stores at most one descriptor per [`CodecId`]. Secondary indices on name and
178/// FOURCC are rebuilt lazily on mutation.
179#[derive(Debug, Default)]
180pub struct CodecRegistry {
181    /// Primary store — keyed by [`CodecId`].
182    entries: HashMap<CodecId, CodecDescriptor>,
183    /// Secondary index: short name → [`CodecId`].
184    name_index: HashMap<String, CodecId>,
185    /// Secondary index: FOURCC bytes → [`CodecId`].
186    fourcc_index: HashMap<Fourcc, CodecId>,
187}
188
189impl CodecRegistry {
190    /// Create an empty registry.
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Register or replace a codec descriptor.
196    ///
197    /// Updates all secondary indices automatically.
198    pub fn register(&mut self, desc: CodecDescriptor) {
199        let id = desc.codec_id;
200
201        // Remove stale index entries if we're replacing an existing entry.
202        if let Some(old) = self.entries.get(&id) {
203            self.name_index.remove(&old.name);
204            for fc in &old.fourccs {
205                self.fourcc_index.remove(fc);
206            }
207        }
208
209        // Build new index entries.
210        self.name_index.insert(desc.name.clone(), id);
211        for fc in &desc.fourccs {
212            self.fourcc_index.insert(*fc, id);
213        }
214
215        self.entries.insert(id, desc);
216    }
217
218    /// Remove a codec by [`CodecId`].
219    ///
220    /// Returns the removed descriptor, or `None` if it was not registered.
221    pub fn remove(&mut self, id: CodecId) -> Option<CodecDescriptor> {
222        let desc = self.entries.remove(&id)?;
223        self.name_index.remove(&desc.name);
224        for fc in &desc.fourccs {
225            self.fourcc_index.remove(fc);
226        }
227        Some(desc)
228    }
229
230    /// Look up a descriptor by [`CodecId`].
231    pub fn lookup_by_id(&self, id: CodecId) -> Option<&CodecDescriptor> {
232        self.entries.get(&id)
233    }
234
235    /// Look up a descriptor by short name (case-insensitive).
236    pub fn lookup_by_name(&self, name: &str) -> Option<&CodecDescriptor> {
237        let key = name.to_lowercase();
238        let id = self.name_index.get(&key)?;
239        self.entries.get(id)
240    }
241
242    /// Look up a descriptor by FOURCC bytes.
243    pub fn lookup_by_fourcc(&self, fourcc: &Fourcc) -> Option<&CodecDescriptor> {
244        let id = self.fourcc_index.get(fourcc)?;
245        self.entries.get(id)
246    }
247
248    /// Return all descriptors that can encode.
249    pub fn encoders(&self) -> Vec<&CodecDescriptor> {
250        self.entries.values().filter(|d| d.can_encode).collect()
251    }
252
253    /// Return all descriptors that can decode.
254    pub fn decoders(&self) -> Vec<&CodecDescriptor> {
255        self.entries.values().filter(|d| d.can_decode).collect()
256    }
257
258    /// Return all lossless codec descriptors.
259    pub fn lossless_codecs(&self) -> Vec<&CodecDescriptor> {
260        self.entries.values().filter(|d| d.is_lossless).collect()
261    }
262
263    /// Return the total number of registered codecs.
264    pub fn len(&self) -> usize {
265        self.entries.len()
266    }
267
268    /// Return `true` if no codecs are registered.
269    pub fn is_empty(&self) -> bool {
270        self.entries.is_empty()
271    }
272
273    /// Return all registered [`CodecId`]s.
274    pub fn codec_ids(&self) -> Vec<CodecId> {
275        self.entries.keys().copied().collect()
276    }
277
278    /// Build a default registry pre-populated with all OxiMedia-supported codecs.
279    pub fn default_registry() -> Self {
280        let mut reg = Self::new();
281
282        // ── Video ────────────────────────────────────────────────────────────
283        reg.register(
284            CodecDescriptor::new(CodecId::Av1)
285                .with_long_name("AOMedia Video 1")
286                .with_fourcc(*b"AV01")
287                .with_fourcc(*b"av01")
288                .with_direction(CodecDirection::Both)
289                .with_max_bit_depth(12)
290                .with_profile(CodecProfile::new("Main", 0).with_description("8/10-bit 4:2:0"))
291                .with_profile(CodecProfile::new("High", 1).with_description("8/10-bit 4:4:4"))
292                .with_profile(
293                    CodecProfile::new("Professional", 2)
294                        .with_description("8/10/12-bit 4:0:0/4:2:2/4:4:4"),
295                ),
296        );
297
298        reg.register(
299            CodecDescriptor::new(CodecId::Vp9)
300                .with_long_name("Google VP9")
301                .with_fourcc(*b"VP90")
302                .with_fourcc(*b"vp09")
303                .with_direction(CodecDirection::Both)
304                .with_max_bit_depth(12)
305                .with_profile(CodecProfile::new("Profile 0", 0).with_description("8-bit 4:2:0"))
306                .with_profile(
307                    CodecProfile::new("Profile 1", 1).with_description("8-bit 4:2:2/4:4:4"),
308                )
309                .with_profile(CodecProfile::new("Profile 2", 2).with_description("10/12-bit 4:2:0"))
310                .with_profile(
311                    CodecProfile::new("Profile 3", 3).with_description("10/12-bit 4:2:2/4:4:4"),
312                ),
313        );
314
315        reg.register(
316            CodecDescriptor::new(CodecId::Vp8)
317                .with_long_name("Google VP8")
318                .with_fourcc(*b"VP80")
319                .with_fourcc(*b"vp08")
320                .with_direction(CodecDirection::Both)
321                .with_max_bit_depth(8)
322                .with_profile(CodecProfile::new("Baseline", 0)),
323        );
324
325        reg.register(
326            CodecDescriptor::new(CodecId::Theora)
327                .with_long_name("Xiph.org Theora")
328                .with_fourcc(*b"theo")
329                .with_direction(CodecDirection::Both)
330                .with_max_bit_depth(8)
331                .with_profile(CodecProfile::new("VP3 Compatible", 0)),
332        );
333
334        reg.register(
335            CodecDescriptor::new(CodecId::Ffv1)
336                .with_long_name("FFV1 Lossless Video Codec")
337                .with_fourcc(*b"FFV1")
338                .with_direction(CodecDirection::Both)
339                .with_lossless(true)
340                .with_max_bit_depth(16)
341                .with_profile(CodecProfile::new("Version 0", 0))
342                .with_profile(CodecProfile::new("Version 1", 1))
343                .with_profile(CodecProfile::new("Version 3", 3).with_description("Multithreaded")),
344        );
345
346        reg.register(
347            CodecDescriptor::new(CodecId::Png)
348                .with_long_name("PNG Lossless Image")
349                .with_fourcc(*b"png ")
350                .with_direction(CodecDirection::Both)
351                .with_lossless(true)
352                .with_max_bit_depth(16),
353        );
354
355        // ── Audio ────────────────────────────────────────────────────────────
356        reg.register(
357            CodecDescriptor::new(CodecId::Opus)
358                .with_long_name("Opus Interactive Audio Codec")
359                .with_fourcc(*b"Opus")
360                .with_direction(CodecDirection::Both)
361                .with_max_bit_depth(16)
362                .with_profile(CodecProfile::new("SILK", 0).with_description("Speech optimised"))
363                .with_profile(CodecProfile::new("CELT", 1).with_description("Music/wideband"))
364                .with_profile(
365                    CodecProfile::new("Hybrid", 2).with_description("SILK+CELT combined"),
366                ),
367        );
368
369        reg.register(
370            CodecDescriptor::new(CodecId::Vorbis)
371                .with_long_name("Xiph.org Vorbis")
372                .with_fourcc(*b"vorb")
373                .with_direction(CodecDirection::Both)
374                .with_max_bit_depth(16),
375        );
376
377        reg.register(
378            CodecDescriptor::new(CodecId::Flac)
379                .with_long_name("Free Lossless Audio Codec")
380                .with_fourcc(*b"fLaC")
381                .with_direction(CodecDirection::Both)
382                .with_lossless(true)
383                .with_max_bit_depth(32),
384        );
385
386        reg.register(
387            CodecDescriptor::new(CodecId::Pcm)
388                .with_long_name("Raw PCM Audio")
389                .with_fourcc(*b"pcm ")
390                .with_direction(CodecDirection::Both)
391                .with_lossless(true)
392                .with_max_bit_depth(32),
393        );
394
395        reg
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    fn make_registry() -> CodecRegistry {
404        CodecRegistry::default_registry()
405    }
406
407    #[test]
408    fn test_lookup_by_id_av1() {
409        let reg = make_registry();
410        let desc = reg.lookup_by_id(CodecId::Av1).expect("AV1 registered");
411        assert_eq!(desc.name, "av1");
412        assert!(desc.can_encode);
413        assert!(desc.can_decode);
414    }
415
416    #[test]
417    fn test_lookup_by_name_case_insensitive() {
418        let reg = make_registry();
419        assert!(reg.lookup_by_name("AV1").is_some());
420        assert!(reg.lookup_by_name("av1").is_some());
421        assert!(reg.lookup_by_name("Vp9").is_some());
422    }
423
424    #[test]
425    fn test_lookup_by_fourcc() {
426        let reg = make_registry();
427        let desc = reg
428            .lookup_by_fourcc(b"AV01")
429            .expect("AV01 FOURCC registered");
430        assert_eq!(desc.codec_id, CodecId::Av1);
431    }
432
433    #[test]
434    fn test_lookup_missing_codec() {
435        let reg = make_registry();
436        assert!(reg.lookup_by_id(CodecId::H263).is_none());
437        assert!(reg.lookup_by_name("nonexistent").is_none());
438    }
439
440    #[test]
441    fn test_encoders_decoders() {
442        let reg = make_registry();
443        let encoders = reg.encoders();
444        let decoders = reg.decoders();
445        assert!(!encoders.is_empty(), "should have at least one encoder");
446        assert!(!decoders.is_empty(), "should have at least one decoder");
447        // All default entries have both directions
448        assert_eq!(encoders.len(), decoders.len());
449    }
450
451    #[test]
452    fn test_lossless_codecs() {
453        let reg = make_registry();
454        let lossless = reg.lossless_codecs();
455        let ids: Vec<_> = lossless.iter().map(|d| d.codec_id).collect();
456        assert!(ids.contains(&CodecId::Flac));
457        assert!(ids.contains(&CodecId::Pcm));
458        assert!(ids.contains(&CodecId::Ffv1));
459    }
460
461    #[test]
462    fn test_register_and_remove() {
463        let mut reg = CodecRegistry::new();
464        reg.register(CodecDescriptor::new(CodecId::Av1).with_direction(CodecDirection::DecodeOnly));
465        assert_eq!(reg.len(), 1);
466        let removed = reg.remove(CodecId::Av1).expect("should remove");
467        assert_eq!(removed.codec_id, CodecId::Av1);
468        assert!(reg.is_empty());
469        // Secondary indices should be cleaned up
470        assert!(reg.lookup_by_name("av1").is_none());
471    }
472
473    #[test]
474    fn test_replace_existing_entry() {
475        let mut reg = CodecRegistry::new();
476        reg.register(
477            CodecDescriptor::new(CodecId::Vp9)
478                .with_direction(CodecDirection::DecodeOnly)
479                .with_fourcc(*b"VP90"),
480        );
481        // Re-register with encode enabled
482        reg.register(
483            CodecDescriptor::new(CodecId::Vp9)
484                .with_direction(CodecDirection::Both)
485                .with_fourcc(*b"VP90"),
486        );
487        let desc = reg.lookup_by_id(CodecId::Vp9).expect("should be present");
488        assert!(desc.can_encode);
489        assert_eq!(reg.len(), 1, "replacement should not duplicate");
490    }
491
492    #[test]
493    fn test_profile_lookup() {
494        let reg = make_registry();
495        let desc = reg.lookup_by_id(CodecId::Av1).expect("AV1 registered");
496        let main = desc.profile_by_name("main").expect("Main profile exists");
497        assert_eq!(main.id, 0);
498        let prof2 = desc.profile_by_id(2).expect("Profile 2 exists");
499        assert_eq!(prof2.name, "Professional");
500    }
501
502    #[test]
503    fn test_codec_ids_all_present() {
504        let reg = make_registry();
505        let ids = reg.codec_ids();
506        assert!(ids.contains(&CodecId::Av1));
507        assert!(ids.contains(&CodecId::Opus));
508        assert!(ids.contains(&CodecId::Flac));
509    }
510
511    #[test]
512    fn test_has_fourcc() {
513        let desc = CodecDescriptor::new(CodecId::Vp8).with_fourcc(*b"VP80");
514        assert!(desc.has_fourcc(b"VP80"));
515        assert!(!desc.has_fourcc(b"VP90"));
516    }
517}