#![allow(clippy::needless_range_loop)]
pub mod alpha;
pub mod bitstream;
pub mod dct;
pub mod decoder;
pub mod encoder;
pub mod entropy;
pub mod frame;
pub mod quant;
pub mod slice;
use oxideav_core::{CodecCapabilities, CodecId, CodecTag, PixelFormat};
use oxideav_core::{CodecInfo, CodecRegistry, RuntimeContext};
pub const CODEC_ID_STR: &str = "prores";
pub const PRORES_FOURCCS: [&[u8; 4]; 6] = [b"apco", b"apcs", b"apcn", b"apch", b"ap4h", b"ap4x"];
pub const PRORES_RAW_FOURCCS: [&[u8; 4]; 2] = [b"aprn", b"aprh"];
pub fn is_prores_raw_fourcc(fourcc: &[u8; 4]) -> bool {
let mut upper = [0u8; 4];
for i in 0..4 {
upper[i] = fourcc[i].to_ascii_uppercase();
}
matches!(&upper, b"APRN" | b"APRH")
}
pub fn codec_id_for_fourcc(fourcc: &[u8; 4]) -> Option<CodecId> {
let mut upper = [0u8; 4];
for i in 0..4 {
upper[i] = fourcc[i].to_ascii_uppercase();
}
match &upper {
b"APCO" | b"APCS" | b"APCN" | b"APCH" | b"AP4H" | b"AP4X" => {
Some(CodecId::new(CODEC_ID_STR))
}
_ => None,
}
}
pub fn profile_for_fourcc(fourcc: &[u8; 4]) -> Option<frame::Profile> {
let mut upper = [0u8; 4];
for i in 0..4 {
upper[i] = fourcc[i].to_ascii_uppercase();
}
Some(match &upper {
b"APCO" => frame::Profile::Proxy,
b"APCS" => frame::Profile::Lt,
b"APCN" => frame::Profile::Standard,
b"APCH" => frame::Profile::Hq,
b"AP4H" => frame::Profile::Prores4444,
b"AP4X" => frame::Profile::Prores4444Xq,
_ => return None,
})
}
pub fn fourcc_for_profile(profile: frame::Profile) -> &'static [u8; 4] {
profile.fourcc()
}
pub fn register_codecs(reg: &mut CodecRegistry) {
let caps = CodecCapabilities::video("prores_sw")
.with_lossy(true)
.with_intra_only(true)
.with_pixel_format(PixelFormat::Yuv422P)
.with_pixel_format(PixelFormat::Yuv444P)
.with_pixel_format(PixelFormat::Yuv422P10Le)
.with_pixel_format(PixelFormat::Yuv444P10Le)
.with_pixel_format(PixelFormat::Yuv422P12Le)
.with_pixel_format(PixelFormat::Yuv444P12Le)
.with_pixel_format(PixelFormat::Yuv422P16Le)
.with_pixel_format(PixelFormat::Yuv444P16Le)
.with_pixel_format(PixelFormat::Yuva422P)
.with_pixel_format(PixelFormat::Yuva444P)
.with_pixel_format(PixelFormat::Yuva422P10Le)
.with_pixel_format(PixelFormat::Yuva444P10Le)
.with_pixel_format(PixelFormat::Yuva422P12Le)
.with_pixel_format(PixelFormat::Yuva444P12Le)
.with_pixel_format(PixelFormat::Yuva422P16Le)
.with_pixel_format(PixelFormat::Yuva444P16Le);
reg.register(
CodecInfo::new(CodecId::new(CODEC_ID_STR))
.capabilities(caps)
.decoder(decoder::make_decoder)
.encoder(encoder::make_encoder)
.tags([
CodecTag::fourcc(b"APCO"),
CodecTag::fourcc(b"APCS"),
CodecTag::fourcc(b"APCN"),
CodecTag::fourcc(b"APCH"),
CodecTag::fourcc(b"AP4H"),
CodecTag::fourcc(b"AP4X"),
]),
);
}
pub fn register(ctx: &mut RuntimeContext) {
register_codecs(&mut ctx.codecs);
}
oxideav_core::register!("prores", register);
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::frame::VideoPlane;
use oxideav_core::{CodecId, CodecParameters, Frame, MediaType, PixelFormat, VideoFrame};
fn synthetic_gradient(width: u32, height: u32) -> VideoFrame {
let w = width as usize;
let h = height as usize;
let cw = w / 2;
let mut y = vec![0u8; w * h];
let mut cb = vec![0u8; cw * h];
let mut cr = vec![0u8; cw * h];
for j in 0..h {
for i in 0..w {
y[j * w + i] = ((i + j) * 255 / (w + h)).min(255) as u8;
}
}
for j in 0..h {
for i in 0..cw {
cb[j * cw + i] = (128 + ((i as i32 - cw as i32 / 2) * 2).clamp(-64, 64)) as u8;
cr[j * cw + i] = (128 + ((j as i32 - h as i32 / 2) * 2).clamp(-64, 64)) as u8;
}
}
let _ = width;
let _ = height;
VideoFrame {
pts: Some(0),
planes: vec![
VideoPlane { stride: w, data: y },
VideoPlane {
stride: cw,
data: cb,
},
VideoPlane {
stride: cw,
data: cr,
},
],
}
}
fn psnr(orig: &[u8], decoded: &[u8]) -> f64 {
assert_eq!(orig.len(), decoded.len());
let mut mse = 0.0f64;
for (a, b) in orig.iter().zip(decoded.iter()) {
let d = *a as f64 - *b as f64;
mse += d * d;
}
mse /= orig.len() as f64;
if mse == 0.0 {
return 120.0;
}
10.0 * (255.0f64 * 255.0 / mse).log10()
}
#[test]
fn rdd36_encoder_decoder_roundtrip_psnr() {
let width = 64u32;
let height = 48u32;
let original = synthetic_gradient(width, height);
let mut enc_params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
enc_params.media_type = MediaType::Video;
enc_params.width = Some(width);
enc_params.height = Some(height);
enc_params.pixel_format = Some(PixelFormat::Yuv422P);
let mut reg = oxideav_core::CodecRegistry::new();
register_codecs(&mut reg);
let mut encoder = reg.first_encoder(&enc_params).expect("make_encoder");
encoder
.send_frame(&Frame::Video(original.clone()))
.expect("send_frame");
let pkt = encoder.receive_packet().expect("receive_packet");
let dec_params = enc_params.clone();
let mut decoder = reg.first_decoder(&dec_params).expect("make_decoder");
decoder.send_packet(&pkt).expect("send_packet");
let frame = decoder.receive_frame().expect("receive_frame");
let decoded = match frame {
Frame::Video(v) => v,
_ => panic!("expected video frame"),
};
assert_eq!(decoded.planes.len(), 3);
for (i, (o, d)) in original
.planes
.iter()
.zip(decoded.planes.iter())
.enumerate()
{
assert_eq!(o.data.len(), d.data.len(), "plane {i} size mismatch");
let p = psnr(&o.data, &d.data);
assert!(p > 30.0, "plane {i} PSNR too low: {p:.2} dB (want > 30)");
eprintln!("plane {i} PSNR = {p:.2} dB");
}
}
#[test]
fn registry_caps_advertise_every_supported_pixel_format() {
let mut reg = oxideav_core::CodecRegistry::new();
register_codecs(&mut reg);
let impls = reg.implementations(&CodecId::new(CODEC_ID_STR));
assert_eq!(impls.len(), 1);
let advertised = &impls[0].caps.accepted_pixel_formats;
let expected = [
PixelFormat::Yuv422P,
PixelFormat::Yuv444P,
PixelFormat::Yuv422P10Le,
PixelFormat::Yuv444P10Le,
PixelFormat::Yuv422P12Le,
PixelFormat::Yuv444P12Le,
PixelFormat::Yuv422P16Le,
PixelFormat::Yuv444P16Le,
PixelFormat::Yuva422P,
PixelFormat::Yuva444P,
PixelFormat::Yuva422P10Le,
PixelFormat::Yuva444P10Le,
PixelFormat::Yuva422P12Le,
PixelFormat::Yuva444P12Le,
PixelFormat::Yuva422P16Le,
PixelFormat::Yuva444P16Le,
];
assert_eq!(advertised.len(), expected.len());
for pf in expected {
assert!(
advertised.contains(&pf),
"capabilities must advertise {pf:?}"
);
let mut p = CodecParameters::video(CodecId::new(CODEC_ID_STR));
p.media_type = MediaType::Video;
p.width = Some(64);
p.height = Some(48);
p.pixel_format = Some(pf);
assert!(reg.first_decoder(&p).is_ok(), "decoder for {pf:?}");
assert!(reg.first_encoder(&p).is_ok(), "encoder for {pf:?}");
}
}
#[test]
fn decoder_registered() {
let mut reg = oxideav_core::CodecRegistry::new();
register_codecs(&mut reg);
assert!(reg.has_decoder(&CodecId::new(CODEC_ID_STR)));
assert!(reg.has_encoder(&CodecId::new(CODEC_ID_STR)));
}
#[test]
fn register_via_runtime_context_installs_codec_factory() {
let mut ctx = oxideav_core::RuntimeContext::new();
register(&mut ctx);
assert!(ctx.codecs.has_decoder(&CodecId::new(CODEC_ID_STR)));
assert!(ctx.codecs.has_encoder(&CodecId::new(CODEC_ID_STR)));
}
#[test]
fn codec_id_for_fourcc_maps_all_six() {
for fc in PRORES_FOURCCS {
assert_eq!(codec_id_for_fourcc(fc), Some(CodecId::new(CODEC_ID_STR)));
}
}
#[test]
fn codec_id_for_fourcc_is_case_insensitive() {
assert_eq!(
codec_id_for_fourcc(b"APCH"),
Some(CodecId::new(CODEC_ID_STR))
);
assert_eq!(
codec_id_for_fourcc(b"apch"),
Some(CodecId::new(CODEC_ID_STR))
);
assert_eq!(
codec_id_for_fourcc(b"ApCh"),
Some(CodecId::new(CODEC_ID_STR))
);
}
#[test]
fn codec_id_for_fourcc_rejects_non_prores() {
assert_eq!(codec_id_for_fourcc(b"avc1"), None);
assert_eq!(codec_id_for_fourcc(b"hvc1"), None);
assert_eq!(codec_id_for_fourcc(b"mp4v"), None);
assert_eq!(codec_id_for_fourcc(b"alac"), None);
assert_eq!(codec_id_for_fourcc(b"av01"), None);
}
#[test]
fn prores_raw_fourcc_does_not_resolve_to_standard_prores() {
for fc in PRORES_RAW_FOURCCS {
assert_eq!(codec_id_for_fourcc(fc), None, "raw fourcc {fc:?}");
assert_eq!(profile_for_fourcc(fc), None, "raw fourcc {fc:?}");
}
}
#[test]
fn is_prores_raw_fourcc_detects_aprn_aprh_case_insensitive() {
for fc in PRORES_RAW_FOURCCS {
assert!(is_prores_raw_fourcc(fc), "lower {fc:?}");
let mut up = *fc;
up.make_ascii_uppercase();
assert!(is_prores_raw_fourcc(&up), "upper {up:?}");
}
assert!(is_prores_raw_fourcc(b"ApRh"));
for fc in PRORES_FOURCCS {
assert!(!is_prores_raw_fourcc(fc), "standard {fc:?}");
}
assert!(!is_prores_raw_fourcc(b"avc1"));
assert!(!is_prores_raw_fourcc(b"av01"));
assert!(!is_prores_raw_fourcc(b"aprx"));
}
#[test]
fn decode_packet_rejects_prores_raw_marker_with_unsupported() {
let mut raw = Vec::new();
raw.extend_from_slice(&16u32.to_be_bytes()); raw.extend_from_slice(b"aprh"); raw.extend_from_slice(&[0u8; 8]); let err = decoder::decode_packet(&raw, None).expect_err("must reject ProRes RAW");
let msg = err.to_string();
assert!(
msg.contains("ProRes RAW"),
"error should name ProRes RAW, got: {msg}"
);
let mut other = Vec::new();
other.extend_from_slice(&16u32.to_be_bytes());
other.extend_from_slice(b"junk");
other.extend_from_slice(&[0u8; 8]);
let err2 = decoder::decode_packet(&other, None).expect_err("must reject non-ProRes");
assert!(
!err2.to_string().contains("ProRes RAW"),
"non-ProRes bytes should not be reported as ProRes RAW"
);
}
#[test]
fn profile_for_fourcc_roundtrips_via_profile_fourcc() {
for p in [
frame::Profile::Proxy,
frame::Profile::Lt,
frame::Profile::Standard,
frame::Profile::Hq,
frame::Profile::Prores4444,
frame::Profile::Prores4444Xq,
] {
let fc = p.fourcc();
assert_eq!(profile_for_fourcc(fc), Some(p), "fourcc roundtrip");
let mut up = *fc;
up.make_ascii_uppercase();
assert_eq!(profile_for_fourcc(&up), Some(p));
}
assert_eq!(profile_for_fourcc(b"mp4v"), None);
}
#[test]
fn fourcc_for_profile_inverts_profile_for_fourcc() {
for p in [
frame::Profile::Proxy,
frame::Profile::Lt,
frame::Profile::Standard,
frame::Profile::Hq,
frame::Profile::Prores4444,
frame::Profile::Prores4444Xq,
] {
let fc = fourcc_for_profile(p);
assert_eq!(fc, p.fourcc(), "canonical fourcc for {p:?}");
assert!(
PRORES_FOURCCS.contains(&fc),
"{fc:?} must be one of the six carriage FourCCs"
);
assert_eq!(
profile_for_fourcc(fc),
Some(p),
"fourcc_for_profile -> profile_for_fourcc round-trip for {p:?}"
);
}
}
#[test]
fn fourcc_for_profile_returns_lowercase_canonical_bytes() {
assert_eq!(fourcc_for_profile(frame::Profile::Proxy), b"apco");
assert_eq!(fourcc_for_profile(frame::Profile::Lt), b"apcs");
assert_eq!(fourcc_for_profile(frame::Profile::Standard), b"apcn");
assert_eq!(fourcc_for_profile(frame::Profile::Hq), b"apch");
assert_eq!(fourcc_for_profile(frame::Profile::Prores4444), b"ap4h");
assert_eq!(fourcc_for_profile(frame::Profile::Prores4444Xq), b"ap4x");
}
#[test]
fn registry_recognizes_prores_fourcc_tags() {
use oxideav_core::stream::{CodecResolver, ProbeContext};
use oxideav_core::CodecTag;
let mut reg = oxideav_core::CodecRegistry::new();
register_codecs(&mut reg);
for fc in PRORES_FOURCCS {
let tag = CodecTag::fourcc(fc);
let ctx = ProbeContext::new(&tag);
let id = reg.resolve_tag(&ctx).expect("resolve_tag");
assert_eq!(id, CodecId::new(CODEC_ID_STR), "fourcc {fc:?}");
}
}
fn synthetic_gradient_444(width: u32, height: u32) -> VideoFrame {
let w = width as usize;
let h = height as usize;
let mut y = vec![0u8; w * h];
let mut cb = vec![0u8; w * h];
let mut cr = vec![0u8; w * h];
for j in 0..h {
for i in 0..w {
y[j * w + i] = ((i + j) * 255 / (w + h)).min(255) as u8;
cb[j * w + i] = (128 + ((i as i32 - w as i32 / 2) * 2).clamp(-64, 64)) as u8;
cr[j * w + i] = (128 + ((j as i32 - h as i32 / 2) * 2).clamp(-64, 64)) as u8;
}
}
VideoFrame {
pts: Some(0),
planes: vec![
VideoPlane { stride: w, data: y },
VideoPlane {
stride: w,
data: cb,
},
VideoPlane {
stride: w,
data: cr,
},
],
}
}
#[test]
fn rdd36_encoder_decoder_roundtrip_psnr_4444() {
let width = 64u32;
let height = 48u32;
let original = synthetic_gradient_444(width, height);
let mut enc_params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
enc_params.media_type = MediaType::Video;
enc_params.width = Some(width);
enc_params.height = Some(height);
enc_params.pixel_format = Some(PixelFormat::Yuv444P);
let mut reg = oxideav_core::CodecRegistry::new();
register_codecs(&mut reg);
let mut encoder = reg.first_encoder(&enc_params).expect("make_encoder");
encoder
.send_frame(&Frame::Video(original.clone()))
.expect("send_frame");
let pkt = encoder.receive_packet().expect("receive_packet");
let dec_params = enc_params.clone();
let mut decoder = reg.first_decoder(&dec_params).expect("make_decoder");
decoder.send_packet(&pkt).expect("send_packet");
let frame = decoder.receive_frame().expect("receive_frame");
let decoded = match frame {
Frame::Video(v) => v,
_ => panic!("expected video frame"),
};
assert_eq!(decoded.planes.len(), 3);
for (i, (o, d)) in original
.planes
.iter()
.zip(decoded.planes.iter())
.enumerate()
{
assert_eq!(o.data.len(), d.data.len(), "plane {i} size mismatch");
let p = psnr(&o.data, &d.data);
assert!(
p > 30.0,
"4444 plane {i} PSNR too low: {p:.2} dB (want > 30)"
);
eprintln!("4444 plane {i} PSNR = {p:.2} dB");
}
}
}