mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
143 lines
6.1 KiB
Rust
143 lines
6.1 KiB
Rust
//! Encoder → decoder round trips and robustness against corrupt data (no ffmpeg needed).
|
|
|
|
use craft_video::dnx::{ChromaFormat, DecodeOptions, Encoder, EncoderConfig, Frame, Profile, decode_frame, decode_frame_with, probe};
|
|
|
|
/// A synthetic picture: gradients, a ring pattern and some hashed texture.
|
|
fn picture(w: u32, h: u32, chroma: ChromaFormat, depth: u8, seed: u32) -> Frame {
|
|
let mut f = Frame::new(w, h, chroma, depth, false);
|
|
let max = (1u32 << depth) - 1;
|
|
let s = depth as u32 - 8;
|
|
let hash = |x: u32, y: u32| (x.wrapping_mul(73856093) ^ y.wrapping_mul(19349663) ^ seed.wrapping_mul(83492791)) % 17;
|
|
for y in 0..h {
|
|
for x in 0..w {
|
|
let r = (((x as f32 - w as f32 / 2.0).powi(2) + (y as f32 - h as f32 / 2.0).powi(2)).sqrt() / 6.0).sin();
|
|
let v = (16 << s) as f32 + (219 << s) as f32 * (0.5 + 0.3 * r + 0.2 * (x as f32 / w as f32 - 0.5)) + (hash(x, y) << s) as f32;
|
|
f.y[(y * w + x) as usize] = (v as u32).min(max) as u16;
|
|
}
|
|
}
|
|
let cw = f.chroma_width();
|
|
for y in 0..f.chroma_height() {
|
|
for x in 0..cw {
|
|
let i = (y * cw + x) as usize;
|
|
f.cb[i] = (((128 + (x * 90 / cw) as i32 - 45) as u32) << s) as u16;
|
|
f.cr[i] = (((128 + (y * 80 / f.chroma_height()) as i32 - 40) as u32) << s) as u16;
|
|
}
|
|
}
|
|
f
|
|
}
|
|
|
|
fn psnr(a: &[u16], b: &[u16], peak: f64) -> f64 {
|
|
let mse: f64 = a.iter().zip(b).map(|(&x, &y)| (x as f64 - y as f64).powi(2)).sum::<f64>() / a.len() as f64;
|
|
if mse == 0.0 { 99.0 } else { 10.0 * (peak * peak / mse).log10() }
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_all_profiles() {
|
|
for (profile, depth, w, h) in [
|
|
(Profile::Lb, 8, 640, 360),
|
|
(Profile::Sq, 8, 1280, 720),
|
|
(Profile::Hq, 8, 720, 486),
|
|
(Profile::Hqx, 10, 1000, 562),
|
|
(Profile::Hqx, 12, 512, 288),
|
|
(Profile::R444, 10, 400, 300),
|
|
(Profile::R444, 12, 64, 48),
|
|
] {
|
|
let mut cfg = EncoderConfig::new(profile, w, h);
|
|
cfg.bit_depth = depth;
|
|
let chroma = cfg.chroma();
|
|
let mut enc = Encoder::with_config(cfg).unwrap();
|
|
for seed in 0..2 {
|
|
let src = picture(w, h, chroma, depth, seed);
|
|
let data = enc.encode(&src).unwrap();
|
|
assert_eq!(data.len() as u32, enc.frame_size());
|
|
assert_eq!(&data[data.len() - 4..], &[0x60, 0x0D, 0xC0, 0xDE]);
|
|
let hdr = probe(&data).unwrap();
|
|
assert_eq!((hdr.cid, hdr.width, hdr.lines, hdr.bit_depth), (profile.cid(), w, h, depth));
|
|
let dec = decode_frame(&data).unwrap();
|
|
let single = decode_frame_with(&data, &DecodeOptions { threads: false, ..Default::default() }).unwrap();
|
|
assert_eq!(dec, single, "threaded and single-threaded decodes differ");
|
|
assert_eq!((dec.width, dec.height, dec.chroma, dec.bit_depth, dec.rgb), (w, h, chroma, depth, false));
|
|
let peak = ((1u32 << depth) - 1) as f64;
|
|
let p = psnr(&src.y, &dec.y, peak);
|
|
let pc = psnr(&src.cb, &dec.cb, peak);
|
|
let min = if profile == Profile::Lb { 30.0 } else { 38.0 };
|
|
assert!(p > min && pc > min, "{profile:?} {depth}-bit {w}x{h}: PSNR {p:.1}/{pc:.1}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_bad_input() {
|
|
let mut enc = Encoder::new(Profile::Hq, 64, 64).unwrap();
|
|
assert!(enc.encode(&Frame::new(32, 64, ChromaFormat::Yuv422, 8, false)).is_err());
|
|
assert!(enc.encode(&Frame::new(64, 64, ChromaFormat::Yuv444, 8, false)).is_err());
|
|
assert!(Encoder::with_config(EncoderConfig { bit_depth: 10, ..EncoderConfig::new(Profile::Hq, 64, 64) }).is_err());
|
|
assert!(decode_frame(&[0u8; 100]).is_err());
|
|
assert!(decode_frame(&[0u8; 1000]).is_err());
|
|
// An unsupported raster is an error, not a panic.
|
|
assert!(Encoder::new(Profile::Hq, 0, 0).is_err());
|
|
assert!(Encoder::new(Profile::Hq, 20_000, 64).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_streams_never_panic() {
|
|
let mut enc = Encoder::new(Profile::Hq, 256, 128).unwrap();
|
|
let good = enc.encode(&picture(256, 128, ChromaFormat::Yuv422, 8, 1)).unwrap();
|
|
let mut seed = 0x1234_5678u32;
|
|
let mut rnd = || {
|
|
seed ^= seed << 13;
|
|
seed ^= seed >> 17;
|
|
seed ^= seed << 5;
|
|
seed
|
|
};
|
|
for i in 0..400 {
|
|
let mut d = good.clone();
|
|
let n = 1 + rnd() % 8;
|
|
for _ in 0..n {
|
|
// mostly the payload, sometimes the header
|
|
let pos = if i % 4 == 0 { rnd() as usize % 0x280 } else { 0x280 + rnd() as usize % (d.len() - 0x280) };
|
|
d[pos] ^= 1 << (rnd() % 8);
|
|
}
|
|
if i % 5 == 0 {
|
|
d.truncate(rnd() as usize % d.len());
|
|
}
|
|
let _ = decode_frame(&d);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn encoder_describes_its_track() {
|
|
let enc = Encoder::new(Profile::Hq, 1920, 1080).unwrap();
|
|
let params = enc.codec_params();
|
|
assert_eq!(params.codec, craft_media::CodecId::Dnxhd);
|
|
assert_eq!(params.tag, Some(*b"AVdh"));
|
|
assert_eq!((params.width, params.height), (Some(1920), Some(1080)));
|
|
assert!(params.extradata.is_empty());
|
|
}
|
|
|
|
/// filmcraft #4101: scan-line offsets that repeat or do not increase let one coded row stand in
|
|
/// for every row, so a small payload could fill a whole raster.
|
|
#[test]
|
|
fn repeated_or_decreasing_scan_line_offsets_are_refused() {
|
|
let mut enc = Encoder::new(Profile::Hq, 256, 128).unwrap();
|
|
let good = enc.encode(&picture(256, 128, ChromaFormat::Yuv422, 8, 2)).unwrap();
|
|
assert!(decode_frame(&good).is_ok());
|
|
let rows = probe(&good).unwrap().mb_rows as usize;
|
|
assert!(rows > 2);
|
|
let at = |s: usize| craft_video::dnx::header::SCAN_INDEX_OFFSET + 4 * s;
|
|
let get = |d: &[u8], s: usize| <[u8; 4]>::try_from(&d[at(s)..at(s) + 4]).unwrap();
|
|
// every row pointing at the first row's data
|
|
let mut repeated = good.clone();
|
|
for s in 1..rows {
|
|
let first = get(&good, 0);
|
|
repeated[at(s)..at(s) + 4].copy_from_slice(&first);
|
|
}
|
|
assert!(decode_frame(&repeated).is_err());
|
|
// two rows swapped
|
|
let mut swapped = good.clone();
|
|
let (a, b) = (get(&good, 1), get(&good, 2));
|
|
swapped[at(1)..at(1) + 4].copy_from_slice(&b);
|
|
swapped[at(2)..at(2) + 4].copy_from_slice(&a);
|
|
assert!(decode_frame(&swapped).is_err());
|
|
}
|