use std::collections::HashMap;
use oxideav_core::{
CodecCapabilities, CodecId, CodecParameters, CodecPreferences, CodecTag, Error, Result,
};
use crate::{Decoder, DecoderFactory, Encoder, EncoderFactory};
pub type CodecProbe = fn(&[u8]) -> bool;
#[derive(Clone, Copy)]
pub struct TagClaim {
pub priority: u8,
pub probe: Option<CodecProbe>,
}
#[derive(Clone)]
pub struct CodecImplementation {
pub caps: CodecCapabilities,
pub make_decoder: Option<DecoderFactory>,
pub make_encoder: Option<EncoderFactory>,
}
#[derive(Default)]
pub struct CodecRegistry {
impls: HashMap<CodecId, Vec<CodecImplementation>>,
tag_claims: HashMap<CodecTag, Vec<(CodecId, TagClaim)>>,
}
impl CodecRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, id: CodecId, implementation: CodecImplementation) {
self.impls.entry(id).or_default().push(implementation);
}
pub fn register_decoder_impl(
&mut self,
id: CodecId,
caps: CodecCapabilities,
factory: DecoderFactory,
) {
self.register(
id,
CodecImplementation {
caps: caps.with_decode(),
make_decoder: Some(factory),
make_encoder: None,
},
);
}
pub fn register_encoder_impl(
&mut self,
id: CodecId,
caps: CodecCapabilities,
factory: EncoderFactory,
) {
self.register(
id,
CodecImplementation {
caps: caps.with_encode(),
make_decoder: None,
make_encoder: Some(factory),
},
);
}
pub fn register_both(
&mut self,
id: CodecId,
caps: CodecCapabilities,
decode: DecoderFactory,
encode: EncoderFactory,
) {
self.register(
id,
CodecImplementation {
caps: caps.with_decode().with_encode(),
make_decoder: Some(decode),
make_encoder: Some(encode),
},
);
}
pub fn register_decoder(&mut self, id: CodecId, factory: DecoderFactory) {
let caps = CodecCapabilities::audio(id.as_str()).with_decode();
self.register_decoder_impl(id, caps, factory);
}
pub fn register_encoder(&mut self, id: CodecId, factory: EncoderFactory) {
let caps = CodecCapabilities::audio(id.as_str()).with_encode();
self.register_encoder_impl(id, caps, factory);
}
pub fn has_decoder(&self, id: &CodecId) -> bool {
self.impls
.get(id)
.map(|v| v.iter().any(|i| i.make_decoder.is_some()))
.unwrap_or(false)
}
pub fn has_encoder(&self, id: &CodecId) -> bool {
self.impls
.get(id)
.map(|v| v.iter().any(|i| i.make_encoder.is_some()))
.unwrap_or(false)
}
pub fn make_decoder_with(
&self,
params: &CodecParameters,
prefs: &CodecPreferences,
) -> Result<Box<dyn Decoder>> {
let candidates = self
.impls
.get(¶ms.codec_id)
.ok_or_else(|| Error::CodecNotFound(params.codec_id.to_string()))?;
let mut ranked: Vec<&CodecImplementation> = candidates
.iter()
.filter(|i| i.make_decoder.is_some() && !prefs.excludes(&i.caps))
.filter(|i| caps_fit_params(&i.caps, params, false))
.collect();
ranked.sort_by_key(|i| prefs.effective_priority(&i.caps));
let mut last_err: Option<Error> = None;
for imp in ranked {
match (imp.make_decoder.unwrap())(params) {
Ok(d) => return Ok(d),
Err(e) => last_err = Some(e),
}
}
Err(last_err.unwrap_or_else(|| {
Error::CodecNotFound(format!(
"no decoder for {} accepts the requested parameters",
params.codec_id
))
}))
}
pub fn make_encoder_with(
&self,
params: &CodecParameters,
prefs: &CodecPreferences,
) -> Result<Box<dyn Encoder>> {
let candidates = self
.impls
.get(¶ms.codec_id)
.ok_or_else(|| Error::CodecNotFound(params.codec_id.to_string()))?;
let mut ranked: Vec<&CodecImplementation> = candidates
.iter()
.filter(|i| i.make_encoder.is_some() && !prefs.excludes(&i.caps))
.filter(|i| caps_fit_params(&i.caps, params, true))
.collect();
ranked.sort_by_key(|i| prefs.effective_priority(&i.caps));
let mut last_err: Option<Error> = None;
for imp in ranked {
match (imp.make_encoder.unwrap())(params) {
Ok(e) => return Ok(e),
Err(e) => last_err = Some(e),
}
}
Err(last_err.unwrap_or_else(|| {
Error::CodecNotFound(format!(
"no encoder for {} accepts the requested parameters",
params.codec_id
))
}))
}
pub fn make_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
self.make_decoder_with(params, &CodecPreferences::default())
}
pub fn make_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
self.make_encoder_with(params, &CodecPreferences::default())
}
pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
self.impls
.iter()
.filter(|(_, v)| v.iter().any(|i| i.make_decoder.is_some()))
.map(|(id, _)| id)
}
pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
self.impls
.iter()
.filter(|(_, v)| v.iter().any(|i| i.make_encoder.is_some()))
.map(|(id, _)| id)
}
pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
self.impls
.iter()
.flat_map(|(id, v)| v.iter().map(move |i| (id, i)))
}
pub fn claim_tag(
&mut self,
id: CodecId,
tag: CodecTag,
priority: u8,
probe: Option<CodecProbe>,
) {
let entry = self.tag_claims.entry(tag).or_default();
entry.push((id, TagClaim { priority, probe }));
entry.sort_by_key(|(_, claim)| std::cmp::Reverse(claim.priority));
}
pub fn resolve_tag(&self, tag: &CodecTag, probe_data: Option<&[u8]>) -> Option<&CodecId> {
let claims = self.tag_claims.get(tag)?;
for (id, claim) in claims {
match (claim.probe, probe_data) {
(None, _) => return Some(id),
(Some(_), None) => return Some(id),
(Some(p), Some(d)) => {
if p(d) {
return Some(id);
}
}
}
}
None
}
pub fn claims_for_tag(&self, tag: &CodecTag) -> &[(CodecId, TagClaim)] {
self.tag_claims
.get(tag)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn all_tag_claims(&self) -> impl Iterator<Item = (&CodecTag, &CodecId, &TagClaim)> {
self.tag_claims
.iter()
.flat_map(|(tag, claims)| claims.iter().map(move |(id, c)| (tag, id, c)))
}
}
fn caps_fit_params(caps: &CodecCapabilities, p: &CodecParameters, for_encode: bool) -> bool {
let _ = for_encode; if let (Some(max), Some(w)) = (caps.max_width, p.width) {
if w > max {
return false;
}
}
if let (Some(max), Some(h)) = (caps.max_height, p.height) {
if h > max {
return false;
}
}
if let (Some(max), Some(br)) = (caps.max_bitrate, p.bit_rate) {
if br > max {
return false;
}
}
if let (Some(max), Some(sr)) = (caps.max_sample_rate, p.sample_rate) {
if sr > max {
return false;
}
}
if let (Some(max), Some(ch)) = (caps.max_channels, p.channels) {
if ch > max {
return false;
}
}
true
}
#[cfg(test)]
mod tag_tests {
use super::*;
use oxideav_core::CodecCapabilities;
fn looks_like_msmpeg4(data: &[u8]) -> bool {
!data.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01])
}
fn looks_like_mpeg4_part2(data: &[u8]) -> bool {
data.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01])
}
#[test]
fn resolve_single_claim_no_probe() {
let mut reg = CodecRegistry::new();
reg.claim_tag(CodecId::new("flac"), CodecTag::fourcc(b"FLAC"), 10, None);
assert_eq!(
reg.resolve_tag(&CodecTag::fourcc(b"FLAC"), None)
.map(|c| c.as_str()),
Some("flac"),
);
}
#[test]
fn resolve_missing_tag_returns_none() {
let reg = CodecRegistry::new();
assert!(reg.resolve_tag(&CodecTag::fourcc(b"????"), None).is_none());
}
#[test]
fn priority_highest_wins() {
let mut reg = CodecRegistry::new();
reg.claim_tag(CodecId::new("low"), CodecTag::fourcc(b"TEST"), 1, None);
reg.claim_tag(CodecId::new("high"), CodecTag::fourcc(b"TEST"), 10, None);
reg.claim_tag(CodecId::new("mid"), CodecTag::fourcc(b"TEST"), 5, None);
assert_eq!(
reg.resolve_tag(&CodecTag::fourcc(b"TEST"), None)
.map(|c| c.as_str()),
Some("high"),
);
}
#[test]
fn probe_chooses_matching_bitstream() {
let mut reg = CodecRegistry::new();
reg.claim_tag(
CodecId::new("msmpeg4v3"),
CodecTag::fourcc(b"DIV3"),
10,
Some(looks_like_msmpeg4),
);
reg.claim_tag(
CodecId::new("mpeg4video"),
CodecTag::fourcc(b"DIV3"),
5,
Some(looks_like_mpeg4_part2),
);
let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
assert_eq!(
reg.resolve_tag(&CodecTag::fourcc(b"DIV3"), Some(&mpeg4_part2))
.map(|c| c.as_str()),
Some("mpeg4video"),
);
assert_eq!(
reg.resolve_tag(&CodecTag::fourcc(b"DIV3"), Some(&ms_mpeg4))
.map(|c| c.as_str()),
Some("msmpeg4v3"),
);
}
#[test]
fn probed_claims_without_probe_data_fall_back_to_priority() {
let mut reg = CodecRegistry::new();
reg.claim_tag(
CodecId::new("msmpeg4v3"),
CodecTag::fourcc(b"DIV3"),
10,
Some(looks_like_msmpeg4),
);
reg.claim_tag(
CodecId::new("mpeg4video"),
CodecTag::fourcc(b"DIV3"),
5,
Some(looks_like_mpeg4_part2),
);
assert_eq!(
reg.resolve_tag(&CodecTag::fourcc(b"DIV3"), None)
.map(|c| c.as_str()),
Some("msmpeg4v3"),
);
}
#[test]
fn fallback_no_probe_catches_everything() {
let mut reg = CodecRegistry::new();
reg.claim_tag(
CodecId::new("picky"),
CodecTag::fourcc(b"MAYB"),
10,
Some(|_| false), );
reg.claim_tag(CodecId::new("fallback"), CodecTag::fourcc(b"MAYB"), 1, None);
assert_eq!(
reg.resolve_tag(&CodecTag::fourcc(b"MAYB"), Some(b"hello"))
.map(|c| c.as_str()),
Some("fallback"),
);
}
#[test]
fn claims_for_tag_returns_ordered_list() {
let mut reg = CodecRegistry::new();
reg.claim_tag(CodecId::new("a"), CodecTag::fourcc(b"XYZ1"), 1, None);
reg.claim_tag(CodecId::new("b"), CodecTag::fourcc(b"XYZ1"), 9, None);
reg.claim_tag(CodecId::new("c"), CodecTag::fourcc(b"XYZ1"), 5, None);
let claims: Vec<_> = reg
.claims_for_tag(&CodecTag::fourcc(b"XYZ1"))
.iter()
.map(|(id, c)| (id.as_str().to_string(), c.priority))
.collect();
assert_eq!(
claims,
vec![
("b".to_string(), 9),
("c".to_string(), 5),
("a".to_string(), 1),
],
);
}
#[test]
fn fourcc_case_insensitive_lookup() {
let mut reg = CodecRegistry::new();
reg.claim_tag(CodecId::new("vid"), CodecTag::fourcc(b"div3"), 10, None);
assert!(reg.resolve_tag(&CodecTag::fourcc(b"DIV3"), None).is_some());
assert!(reg.resolve_tag(&CodecTag::fourcc(b"div3"), None).is_some());
assert!(reg.resolve_tag(&CodecTag::fourcc(b"DiV3"), None).is_some());
}
#[test]
fn wave_format_and_matroska_tags_work() {
let mut reg = CodecRegistry::new();
reg.claim_tag(CodecId::new("mp3"), CodecTag::wave_format(0x0055), 10, None);
reg.claim_tag(
CodecId::new("h264"),
CodecTag::matroska("V_MPEG4/ISO/AVC"),
10,
None,
);
assert_eq!(
reg.resolve_tag(&CodecTag::wave_format(0x0055), None)
.map(|c| c.as_str()),
Some("mp3"),
);
assert_eq!(
reg.resolve_tag(&CodecTag::matroska("V_MPEG4/ISO/AVC"), None)
.map(|c| c.as_str()),
Some("h264"),
);
}
#[test]
fn mp4_object_type_tag_works() {
let mut reg = CodecRegistry::new();
reg.claim_tag(
CodecId::new("aac"),
CodecTag::mp4_object_type(0x40),
10,
None,
);
assert_eq!(
reg.resolve_tag(&CodecTag::mp4_object_type(0x40), None)
.map(|c| c.as_str()),
Some("aac"),
);
}
#[test]
fn suppress_unused_caps() {
let _ = CodecCapabilities::audio("dummy");
}
}