1use std::collections::HashMap;
23
24use oximedia_core::CodecId;
25
26pub type Fourcc = [u8; 4];
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CodecDirection {
32 EncodeOnly,
34 DecodeOnly,
36 Both,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct CodecProfile {
43 pub name: String,
45 pub id: u32,
47 pub description: Option<String>,
49}
50
51impl CodecProfile {
52 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 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
63 self.description = Some(desc.into());
64 self
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct CodecDescriptor {
71 pub codec_id: CodecId,
73 pub name: String,
75 pub long_name: Option<String>,
77 pub fourccs: Vec<Fourcc>,
79 pub can_encode: bool,
81 pub can_decode: bool,
83 pub is_lossless: bool,
85 pub profiles: Vec<CodecProfile>,
87 pub max_bit_depth: u8,
89}
90
91impl CodecDescriptor {
92 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 pub fn with_long_name(mut self, s: impl Into<String>) -> Self {
109 self.long_name = Some(s.into());
110 self
111 }
112
113 pub fn with_fourcc(mut self, fourcc: Fourcc) -> Self {
115 self.fourccs.push(fourcc);
116 self
117 }
118
119 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 pub fn with_lossless(mut self, lossless: bool) -> Self {
140 self.is_lossless = lossless;
141 self
142 }
143
144 pub fn with_profile(mut self, profile: CodecProfile) -> Self {
146 self.profiles.push(profile);
147 self
148 }
149
150 pub fn with_max_bit_depth(mut self, depth: u8) -> Self {
152 self.max_bit_depth = depth;
153 self
154 }
155
156 pub fn has_fourcc(&self, fourcc: &Fourcc) -> bool {
158 self.fourccs.contains(fourcc)
159 }
160
161 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 pub fn profile_by_id(&self, id: u32) -> Option<&CodecProfile> {
171 self.profiles.iter().find(|p| p.id == id)
172 }
173}
174
175#[derive(Debug, Default)]
180pub struct CodecRegistry {
181 entries: HashMap<CodecId, CodecDescriptor>,
183 name_index: HashMap<String, CodecId>,
185 fourcc_index: HashMap<Fourcc, CodecId>,
187}
188
189impl CodecRegistry {
190 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub fn register(&mut self, desc: CodecDescriptor) {
199 let id = desc.codec_id;
200
201 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 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 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 pub fn lookup_by_id(&self, id: CodecId) -> Option<&CodecDescriptor> {
232 self.entries.get(&id)
233 }
234
235 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 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 pub fn encoders(&self) -> Vec<&CodecDescriptor> {
250 self.entries.values().filter(|d| d.can_encode).collect()
251 }
252
253 pub fn decoders(&self) -> Vec<&CodecDescriptor> {
255 self.entries.values().filter(|d| d.can_decode).collect()
256 }
257
258 pub fn lossless_codecs(&self) -> Vec<&CodecDescriptor> {
260 self.entries.values().filter(|d| d.is_lossless).collect()
261 }
262
263 pub fn len(&self) -> usize {
265 self.entries.len()
266 }
267
268 pub fn is_empty(&self) -> bool {
270 self.entries.is_empty()
271 }
272
273 pub fn codec_ids(&self) -> Vec<CodecId> {
275 self.entries.keys().copied().collect()
276 }
277
278 pub fn default_registry() -> Self {
280 let mut reg = Self::new();
281
282 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 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 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 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 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}