use std::collections::HashMap;
use oximedia_core::CodecId;
pub type Fourcc = [u8; 4];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodecDirection {
EncodeOnly,
DecodeOnly,
Both,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodecProfile {
pub name: String,
pub id: u32,
pub description: Option<String>,
}
impl CodecProfile {
pub fn new(name: impl Into<String>, id: u32) -> Self {
Self {
name: name.into(),
id,
description: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
}
#[derive(Debug, Clone)]
pub struct CodecDescriptor {
pub codec_id: CodecId,
pub name: String,
pub long_name: Option<String>,
pub fourccs: Vec<Fourcc>,
pub can_encode: bool,
pub can_decode: bool,
pub is_lossless: bool,
pub profiles: Vec<CodecProfile>,
pub max_bit_depth: u8,
}
impl CodecDescriptor {
pub fn new(codec_id: CodecId) -> Self {
Self {
name: codec_id.name().to_string(),
codec_id,
long_name: None,
fourccs: Vec::new(),
can_encode: false,
can_decode: false,
is_lossless: codec_id.is_lossless(),
profiles: Vec::new(),
max_bit_depth: 8,
}
}
pub fn with_long_name(mut self, s: impl Into<String>) -> Self {
self.long_name = Some(s.into());
self
}
pub fn with_fourcc(mut self, fourcc: Fourcc) -> Self {
self.fourccs.push(fourcc);
self
}
pub fn with_direction(mut self, dir: CodecDirection) -> Self {
match dir {
CodecDirection::EncodeOnly => {
self.can_encode = true;
self.can_decode = false;
}
CodecDirection::DecodeOnly => {
self.can_encode = false;
self.can_decode = true;
}
CodecDirection::Both => {
self.can_encode = true;
self.can_decode = true;
}
}
self
}
pub fn with_lossless(mut self, lossless: bool) -> Self {
self.is_lossless = lossless;
self
}
pub fn with_profile(mut self, profile: CodecProfile) -> Self {
self.profiles.push(profile);
self
}
pub fn with_max_bit_depth(mut self, depth: u8) -> Self {
self.max_bit_depth = depth;
self
}
pub fn has_fourcc(&self, fourcc: &Fourcc) -> bool {
self.fourccs.contains(fourcc)
}
pub fn profile_by_name(&self, name: &str) -> Option<&CodecProfile> {
let lower = name.to_lowercase();
self.profiles
.iter()
.find(|p| p.name.to_lowercase() == lower)
}
pub fn profile_by_id(&self, id: u32) -> Option<&CodecProfile> {
self.profiles.iter().find(|p| p.id == id)
}
}
#[derive(Debug, Default)]
pub struct CodecRegistry {
entries: HashMap<CodecId, CodecDescriptor>,
name_index: HashMap<String, CodecId>,
fourcc_index: HashMap<Fourcc, CodecId>,
}
impl CodecRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, desc: CodecDescriptor) {
let id = desc.codec_id;
if let Some(old) = self.entries.get(&id) {
self.name_index.remove(&old.name);
for fc in &old.fourccs {
self.fourcc_index.remove(fc);
}
}
self.name_index.insert(desc.name.clone(), id);
for fc in &desc.fourccs {
self.fourcc_index.insert(*fc, id);
}
self.entries.insert(id, desc);
}
pub fn remove(&mut self, id: CodecId) -> Option<CodecDescriptor> {
let desc = self.entries.remove(&id)?;
self.name_index.remove(&desc.name);
for fc in &desc.fourccs {
self.fourcc_index.remove(fc);
}
Some(desc)
}
pub fn lookup_by_id(&self, id: CodecId) -> Option<&CodecDescriptor> {
self.entries.get(&id)
}
pub fn lookup_by_name(&self, name: &str) -> Option<&CodecDescriptor> {
let key = name.to_lowercase();
let id = self.name_index.get(&key)?;
self.entries.get(id)
}
pub fn lookup_by_fourcc(&self, fourcc: &Fourcc) -> Option<&CodecDescriptor> {
let id = self.fourcc_index.get(fourcc)?;
self.entries.get(id)
}
pub fn encoders(&self) -> Vec<&CodecDescriptor> {
self.entries.values().filter(|d| d.can_encode).collect()
}
pub fn decoders(&self) -> Vec<&CodecDescriptor> {
self.entries.values().filter(|d| d.can_decode).collect()
}
pub fn lossless_codecs(&self) -> Vec<&CodecDescriptor> {
self.entries.values().filter(|d| d.is_lossless).collect()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn codec_ids(&self) -> Vec<CodecId> {
self.entries.keys().copied().collect()
}
pub fn default_registry() -> Self {
let mut reg = Self::new();
reg.register(
CodecDescriptor::new(CodecId::Av1)
.with_long_name("AOMedia Video 1")
.with_fourcc(*b"AV01")
.with_fourcc(*b"av01")
.with_direction(CodecDirection::Both)
.with_max_bit_depth(12)
.with_profile(CodecProfile::new("Main", 0).with_description("8/10-bit 4:2:0"))
.with_profile(CodecProfile::new("High", 1).with_description("8/10-bit 4:4:4"))
.with_profile(
CodecProfile::new("Professional", 2)
.with_description("8/10/12-bit 4:0:0/4:2:2/4:4:4"),
),
);
reg.register(
CodecDescriptor::new(CodecId::Vp9)
.with_long_name("Google VP9")
.with_fourcc(*b"VP90")
.with_fourcc(*b"vp09")
.with_direction(CodecDirection::Both)
.with_max_bit_depth(12)
.with_profile(CodecProfile::new("Profile 0", 0).with_description("8-bit 4:2:0"))
.with_profile(
CodecProfile::new("Profile 1", 1).with_description("8-bit 4:2:2/4:4:4"),
)
.with_profile(CodecProfile::new("Profile 2", 2).with_description("10/12-bit 4:2:0"))
.with_profile(
CodecProfile::new("Profile 3", 3).with_description("10/12-bit 4:2:2/4:4:4"),
),
);
reg.register(
CodecDescriptor::new(CodecId::Vp8)
.with_long_name("Google VP8")
.with_fourcc(*b"VP80")
.with_fourcc(*b"vp08")
.with_direction(CodecDirection::Both)
.with_max_bit_depth(8)
.with_profile(CodecProfile::new("Baseline", 0)),
);
reg.register(
CodecDescriptor::new(CodecId::Theora)
.with_long_name("Xiph.org Theora")
.with_fourcc(*b"theo")
.with_direction(CodecDirection::Both)
.with_max_bit_depth(8)
.with_profile(CodecProfile::new("VP3 Compatible", 0)),
);
reg.register(
CodecDescriptor::new(CodecId::Ffv1)
.with_long_name("FFV1 Lossless Video Codec")
.with_fourcc(*b"FFV1")
.with_direction(CodecDirection::Both)
.with_lossless(true)
.with_max_bit_depth(16)
.with_profile(CodecProfile::new("Version 0", 0))
.with_profile(CodecProfile::new("Version 1", 1))
.with_profile(CodecProfile::new("Version 3", 3).with_description("Multithreaded")),
);
reg.register(
CodecDescriptor::new(CodecId::Png)
.with_long_name("PNG Lossless Image")
.with_fourcc(*b"png ")
.with_direction(CodecDirection::Both)
.with_lossless(true)
.with_max_bit_depth(16),
);
reg.register(
CodecDescriptor::new(CodecId::Opus)
.with_long_name("Opus Interactive Audio Codec")
.with_fourcc(*b"Opus")
.with_direction(CodecDirection::Both)
.with_max_bit_depth(16)
.with_profile(CodecProfile::new("SILK", 0).with_description("Speech optimised"))
.with_profile(CodecProfile::new("CELT", 1).with_description("Music/wideband"))
.with_profile(
CodecProfile::new("Hybrid", 2).with_description("SILK+CELT combined"),
),
);
reg.register(
CodecDescriptor::new(CodecId::Vorbis)
.with_long_name("Xiph.org Vorbis")
.with_fourcc(*b"vorb")
.with_direction(CodecDirection::Both)
.with_max_bit_depth(16),
);
reg.register(
CodecDescriptor::new(CodecId::Flac)
.with_long_name("Free Lossless Audio Codec")
.with_fourcc(*b"fLaC")
.with_direction(CodecDirection::Both)
.with_lossless(true)
.with_max_bit_depth(32),
);
reg.register(
CodecDescriptor::new(CodecId::Pcm)
.with_long_name("Raw PCM Audio")
.with_fourcc(*b"pcm ")
.with_direction(CodecDirection::Both)
.with_lossless(true)
.with_max_bit_depth(32),
);
reg
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_registry() -> CodecRegistry {
CodecRegistry::default_registry()
}
#[test]
fn test_lookup_by_id_av1() {
let reg = make_registry();
let desc = reg.lookup_by_id(CodecId::Av1).expect("AV1 registered");
assert_eq!(desc.name, "av1");
assert!(desc.can_encode);
assert!(desc.can_decode);
}
#[test]
fn test_lookup_by_name_case_insensitive() {
let reg = make_registry();
assert!(reg.lookup_by_name("AV1").is_some());
assert!(reg.lookup_by_name("av1").is_some());
assert!(reg.lookup_by_name("Vp9").is_some());
}
#[test]
fn test_lookup_by_fourcc() {
let reg = make_registry();
let desc = reg
.lookup_by_fourcc(b"AV01")
.expect("AV01 FOURCC registered");
assert_eq!(desc.codec_id, CodecId::Av1);
}
#[test]
fn test_lookup_missing_codec() {
let reg = make_registry();
assert!(reg.lookup_by_id(CodecId::H263).is_none());
assert!(reg.lookup_by_name("nonexistent").is_none());
}
#[test]
fn test_encoders_decoders() {
let reg = make_registry();
let encoders = reg.encoders();
let decoders = reg.decoders();
assert!(!encoders.is_empty(), "should have at least one encoder");
assert!(!decoders.is_empty(), "should have at least one decoder");
assert_eq!(encoders.len(), decoders.len());
}
#[test]
fn test_lossless_codecs() {
let reg = make_registry();
let lossless = reg.lossless_codecs();
let ids: Vec<_> = lossless.iter().map(|d| d.codec_id).collect();
assert!(ids.contains(&CodecId::Flac));
assert!(ids.contains(&CodecId::Pcm));
assert!(ids.contains(&CodecId::Ffv1));
}
#[test]
fn test_register_and_remove() {
let mut reg = CodecRegistry::new();
reg.register(CodecDescriptor::new(CodecId::Av1).with_direction(CodecDirection::DecodeOnly));
assert_eq!(reg.len(), 1);
let removed = reg.remove(CodecId::Av1).expect("should remove");
assert_eq!(removed.codec_id, CodecId::Av1);
assert!(reg.is_empty());
assert!(reg.lookup_by_name("av1").is_none());
}
#[test]
fn test_replace_existing_entry() {
let mut reg = CodecRegistry::new();
reg.register(
CodecDescriptor::new(CodecId::Vp9)
.with_direction(CodecDirection::DecodeOnly)
.with_fourcc(*b"VP90"),
);
reg.register(
CodecDescriptor::new(CodecId::Vp9)
.with_direction(CodecDirection::Both)
.with_fourcc(*b"VP90"),
);
let desc = reg.lookup_by_id(CodecId::Vp9).expect("should be present");
assert!(desc.can_encode);
assert_eq!(reg.len(), 1, "replacement should not duplicate");
}
#[test]
fn test_profile_lookup() {
let reg = make_registry();
let desc = reg.lookup_by_id(CodecId::Av1).expect("AV1 registered");
let main = desc.profile_by_name("main").expect("Main profile exists");
assert_eq!(main.id, 0);
let prof2 = desc.profile_by_id(2).expect("Profile 2 exists");
assert_eq!(prof2.name, "Professional");
}
#[test]
fn test_codec_ids_all_present() {
let reg = make_registry();
let ids = reg.codec_ids();
assert!(ids.contains(&CodecId::Av1));
assert!(ids.contains(&CodecId::Opus));
assert!(ids.contains(&CodecId::Flac));
}
#[test]
fn test_has_fourcc() {
let desc = CodecDescriptor::new(CodecId::Vp8).with_fourcc(*b"VP80");
assert!(desc.has_fourcc(b"VP80"));
assert!(!desc.has_fourcc(b"VP90"));
}
}