M2 tests: synthetic I_PCM/long-term/MMCO/gap streams, avcC, robustness, stats

- hand-built CAVLC and CABAC bitstreams (test-only CABAC encoder) with I_PCM
  and P_Skip macroblocks exercising long-term references, MMCO 1-6,
  frame_num gaps and reference list modification (idc 0 and 2); expected
  output is known exactly and cross-checked with ffmpeg when available
- avcC configuration + length-prefixed samples with pts round trip
- corrupted / truncated / garbage input never panics or deadlocks
  (single- and multi-threaded)
- Decoder::stats() coverage counters and an ignored coverage report test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-09-30 06:44:39 -04:00
parent 374569e65e
commit 20f0d08311
7 changed files with 661 additions and 4 deletions

View File

@ -45,5 +45,11 @@ fn main() {
}
write(dec.flush());
let dt = t0.elapsed().as_secs_f64();
if let Some(e) = dec.take_error() {
eprintln!("decode error: {e}");
}
if std::env::var("H264_STATS").is_ok() {
eprintln!("{:#?}", dec.stats());
}
eprintln!("{n} frames in {dt:.3}s ({:.1} fps)", n as f64 / dt);
}

View File

@ -5,7 +5,7 @@ use crate::error::{Result, ensure};
/// Table 9-44 rangeTabLPS[pStateIdx][qCodIRangeIdx].
#[rustfmt::skip]
static RANGE_TAB_LPS: [[u8; 4]; 64] = [
pub(crate) static RANGE_TAB_LPS: [[u8; 4]; 64] = [
[128, 176, 208, 240], [128, 167, 197, 227], [128, 158, 187, 216], [123, 150, 178, 205],
[116, 142, 169, 195], [111, 135, 160, 185], [105, 128, 152, 175], [100, 122, 144, 166],
[95, 116, 137, 158], [90, 110, 130, 150], [85, 104, 123, 142], [81, 99, 117, 135],
@ -44,7 +44,7 @@ static TRANS_IDX_MPS: [u8; 64] = [
/// Combined state transition: index = (pStateIdx << 1 | valMPS) * 2 + bin_is_lps.
/// Each context is stored as `pStateIdx << 1 | valMPS`.
static NEXT_STATE: [[u8; 2]; 128] = {
pub(crate) static NEXT_STATE: [[u8; 2]; 128] = {
let mut t = [[0u8; 2]; 128];
let mut s = 0;
while s < 128 {

View File

@ -9,7 +9,7 @@
use crate::dpb::{Dpb, Output, OutputMeta};
use crate::error::{Error, Result, ensure, invalid, unsupported};
use crate::params::{Pps, Sps};
use crate::picture::{Frame, FrameRef, MbState, Planes, RefPic};
use crate::picture::{Frame, FrameRef, MbKind, MbState, Planes, RefPic};
use crate::slice::{NalHeader, Poc, PocState, SliceHeader, SliceType, nal_type};
use crate::slicedec::{PicState, SliceDecoder};
use crate::transform::LevelScale;
@ -39,10 +39,37 @@ struct PendingPic {
slices: Vec<SliceJob>,
}
/// Counters describing what the decoder has seen (useful to check test coverage).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DecodeStats {
pub pictures: u64,
pub slices_cavlc: u64,
pub slices_cabac: u64,
pub slices_i: u64,
pub slices_p: u64,
pub slices_b: u64,
pub mb_i4x4: u64,
pub mb_i8x8: u64,
pub mb_i16x16: u64,
pub mb_pcm: u64,
pub mb_p_skip: u64,
pub mb_b_skip: u64,
pub mb_b_direct16x16: u64,
pub mb_inter: u64,
/// Inter macroblocks using the 8x8 transform.
pub mb_inter_8x8_transform: u64,
pub mmco_ops: u64,
pub long_term_marks: u64,
pub frame_num_gaps: u64,
pub weighted_slices: u64,
pub temporal_direct_slices: u64,
}
/// State shared with decoding jobs.
#[derive(Default)]
struct Shared {
error: Mutex<Option<Error>>,
stats: Mutex<DecodeStats>,
/// Recycled picture buffers.
pool: Mutex<Vec<PicState>>,
}
@ -227,6 +254,17 @@ impl Decoder {
pics
}
/// Statistics accumulated so far (macroblock counts include finished pictures only).
pub fn stats(&self) -> DecodeStats {
self.shared.stats.lock().map(|s| s.clone()).unwrap_or_default()
}
fn stat(&self, f: impl FnOnce(&mut DecodeStats)) {
if let Ok(mut s) = self.shared.stats.lock() {
f(&mut s);
}
}
/// First error reported by a decoding job since the last call (also returned by
/// [`Decoder::decode`]); useful after [`Decoder::flush`].
pub fn take_error(&mut self) -> Option<Error> {
@ -317,7 +355,31 @@ impl Decoder {
ensure!(!refs[1].is_empty(), "empty RefPicList1 in B slice");
}
}
let weighted = (pps.weighted_pred && sh.slice_type.is_p()) || (pps.weighted_bipred_idc != 0 && sh.slice_type.is_b());
let temporal = sh.slice_type.is_b() && !sh.direct_spatial_mv_pred;
let (cabac, st, mmcos, lt) = (
pps.entropy_coding_mode,
sh.slice_type,
sh.mmcos.len() as u64,
sh.long_term_reference as u64 + sh.mmcos.iter().filter(|m| m.op == 3 || m.op == 6).count() as u64,
);
pending.slices.push(SliceJob { sh, pps, sps, rbsp, refs, ls });
self.stat(|s| {
if cabac {
s.slices_cabac += 1;
} else {
s.slices_cavlc += 1;
}
match st {
SliceType::I | SliceType::Si => s.slices_i += 1,
SliceType::P | SliceType::Sp => s.slices_p += 1,
SliceType::B => s.slices_b += 1,
}
s.weighted_slices += weighted as u64;
s.temporal_direct_slices += temporal as u64;
s.mmco_ops += mmcos;
s.long_term_marks += lt;
});
Ok(())
}
@ -369,6 +431,7 @@ impl Decoder {
(Arc::new(Frame::from_planes(id, 0, &planes)), meta.clone())
};
let mut upd = |fnum: u32| poc_state.update_gap_frame(fnum, sps);
self.shared.stats.lock().map(|mut s| s.frame_num_gaps += 1).ok();
self.dpb.fill_frame_num_gap(self.prev_ref_frame_num, sh.frame_num, max, sps.max_num_ref_frames as usize, &mut make, &mut upd);
self.prev_ref_frame_num = (sh.frame_num + max - 1) % max;
}
@ -456,6 +519,25 @@ fn run_job(frame: FrameRef, slices: Vec<SliceJob>, shared: &Shared) {
Some(Error::Invalid("internal error while decoding a picture".into()))
}
};
if let Ok(mut s) = shared.stats.lock() {
s.pictures += 1;
for st in &pic.mbs {
match st.kind {
MbKind::I4x4 => s.mb_i4x4 += 1,
MbKind::I8x8 => s.mb_i8x8 += 1,
MbKind::I16x16 => s.mb_i16x16 += 1,
MbKind::IPcm => s.mb_pcm += 1,
MbKind::PSkip => s.mb_p_skip += 1,
MbKind::BSkip => s.mb_b_skip += 1,
MbKind::BDirect16x16 => s.mb_b_direct16x16 += 1,
MbKind::Inter => s.mb_inter += 1,
MbKind::None => {}
}
if st.transform_8x8 && !st.kind.is_intra() {
s.mb_inter_8x8_transform += 1;
}
}
}
if let (Some(e), Ok(mut slot)) = (err, shared.error.lock()) {
slot.get_or_insert(e);
}

View File

@ -36,10 +36,12 @@ pub mod params;
mod picture;
pub mod slice;
mod slicedec;
#[cfg(test)]
mod synth_tests;
mod tables;
mod transform;
pub use decoder::Decoder;
pub use decoder::{DecodeStats, Decoder};
pub use error::{Error, Result};
/// Colour description from the VUI (ITU-T H.273 code points).

View File

@ -0,0 +1,407 @@
//! Hand-built bitstreams exercising features libx264 never emits: I_PCM (CAVLC and CABAC), long-term
//! references, MMCO operations, frame_num gaps and reference list modification. Every picture consists
//! of I_PCM and P_Skip macroblocks only, so the expected output is known exactly (I_PCM samples are
//! lossless, P_Skip copies the selected reference with a zero motion vector, and deblocking cannot
//! modify samples at these QPs).
use crate::Decoder;
use crate::cabac::{Cabac, NEXT_STATE, RANGE_TAB_LPS};
use filmcraft_bitstream::{BitWriter, escape_rbsp};
const MB_W: usize = 4;
const MB_H: usize = 3;
const NMB: usize = MB_W * MB_H;
fn nal(ref_idc: u8, t: u8, rbsp: &[u8]) -> Vec<u8> {
let mut v = vec![0, 0, 0, 1, (ref_idc << 5) | t];
v.extend(escape_rbsp(rbsp));
v
}
fn sps(profile: u8, max_refs: u32, gaps: bool) -> Vec<u8> {
let mut w = BitWriter::new();
w.write_bits(profile as u32, 8);
w.write_bits(0, 8);
w.write_bits(30, 8);
w.write_ue(0); // sps id
w.write_ue(0); // log2_max_frame_num - 4 -> 16
w.write_ue(2); // POC type 2 (output order = decoding order)
w.write_ue(max_refs);
w.write_bit(gaps);
w.write_ue(MB_W as u32 - 1);
w.write_ue(MB_H as u32 - 1);
w.write_bit(true); // frame_mbs_only
w.write_bit(true); // direct_8x8_inference
w.write_bit(false); // cropping
w.write_bit(false); // vui
w.rbsp_trailing();
nal(3, 7, &w.finish())
}
fn pps(cabac: bool) -> Vec<u8> {
let mut w = BitWriter::new();
w.write_ue(0);
w.write_ue(0);
w.write_bit(cabac);
w.write_bit(false);
w.write_ue(0); // slice groups
w.write_ue(0); // num_ref_idx_l0_default - 1
w.write_ue(0);
w.write_bit(false); // weighted_pred
w.write_bits(0, 2);
w.write_se(0); // pic_init_qp - 26
w.write_se(0);
w.write_se(0); // chroma_qp_index_offset
w.write_bit(true); // deblocking_filter_control_present
w.write_bit(false);
w.write_bit(false);
w.rbsp_trailing();
nal(3, 8, &w.finish())
}
#[derive(Default, Clone)]
struct Pic {
idr: bool,
frame_num: u32,
ref_idc: u8,
long_term_ref: bool,
/// ref_pic_list_modification entries (idc, value)
mods: Vec<(u32, u32)>,
/// MMCOs: (op, a, b) with a = difference_of_pic_nums_minus1 / long_term_pic_num /
/// max_long_term_frame_idx_plus1 and b = long_term_frame_idx
mmcos: Vec<(u32, u32, u32)>,
/// Per MB: Some(samples) for I_PCM, None for P_Skip.
mbs: Vec<Option<Vec<u8>>>,
}
impl Pic {
fn is_p(&self) -> bool {
!self.idr && self.mbs.iter().any(|m| m.is_none())
}
}
fn slice_header(w: &mut BitWriter, p: &Pic, cabac: bool) {
w.write_ue(0); // first_mb
w.write_ue(if p.is_p() { 5 } else { 7 });
w.write_ue(0); // pps id
w.write_bits(p.frame_num, 4);
if p.idr {
w.write_ue(0);
}
if p.is_p() {
w.write_bit(false); // num_ref_idx_override
w.write_bit(!p.mods.is_empty());
if !p.mods.is_empty() {
for &(idc, v) in &p.mods {
w.write_ue(idc);
w.write_ue(v);
}
w.write_ue(3);
}
}
if p.ref_idc != 0 {
if p.idr {
w.write_bit(false);
w.write_bit(p.long_term_ref);
} else {
w.write_bit(!p.mmcos.is_empty());
if !p.mmcos.is_empty() {
for &(op, a, b) in &p.mmcos {
w.write_ue(op);
match op {
1 => w.write_ue(a),
2 => w.write_ue(a),
3 => {
w.write_ue(a);
w.write_ue(b);
}
4 => w.write_ue(a),
6 => w.write_ue(b),
_ => {}
}
}
w.write_ue(0);
}
}
}
if cabac && p.is_p() {
w.write_ue(0); // cabac_init_idc
}
w.write_se(0); // slice_qp_delta
w.write_ue(0); // disable_deblocking_filter_idc
w.write_se(0);
w.write_se(0);
}
/// Minimal CABAC encoder (9.3.4, informative) writing into a BitWriter.
struct Enc {
low: u32,
range: u32,
outstanding: u32,
first: bool,
ctx: [u8; 1024],
}
impl Enc {
fn new(qp: i32, init: usize) -> Self {
let c = Cabac::new(&[0, 0, 0, 0], 0, qp, init).unwrap();
Enc { low: 0, range: 510, outstanding: 0, first: true, ctx: c.ctx }
}
fn init(&mut self) {
self.low = 0;
self.range = 510;
self.outstanding = 0;
self.first = true;
}
fn put(&mut self, w: &mut BitWriter, b: bool) {
if self.first {
self.first = false;
} else {
w.write_bit(b);
}
while self.outstanding > 0 {
w.write_bit(!b);
self.outstanding -= 1;
}
}
fn renorm(&mut self, w: &mut BitWriter) {
while self.range < 256 {
if self.low < 256 {
self.put(w, false);
} else if self.low >= 512 {
self.low -= 512;
self.put(w, true);
} else {
self.low -= 256;
self.outstanding += 1;
}
self.range <<= 1;
self.low <<= 1;
}
}
fn encode(&mut self, w: &mut BitWriter, ctx_idx: usize, bin: u32) {
let s = self.ctx[ctx_idx] as usize;
let lps = RANGE_TAB_LPS[s >> 1][((self.range >> 6) & 3) as usize] as u32;
self.range -= lps;
if bin != (s & 1) as u32 {
self.low += self.range;
self.range = lps;
self.ctx[ctx_idx] = NEXT_STATE[s][1];
} else {
self.ctx[ctx_idx] = NEXT_STATE[s][0];
}
self.renorm(w);
}
fn terminate(&mut self, w: &mut BitWriter, bin: bool) {
self.range -= 2;
if bin {
self.low += self.range;
// EncodeFlush
self.range = 2;
self.renorm(w);
self.put(w, (self.low >> 9) & 1 != 0);
w.write_bits(((self.low >> 7) & 3) | 1, 2);
} else {
self.renorm(w);
}
}
}
fn write_pcm(w: &mut BitWriter, samples: &[u8]) {
w.align_zero();
w.write_bytes(samples);
}
fn slice_nal(p: &Pic, cabac: bool) -> Vec<u8> {
let mut w = BitWriter::new();
slice_header(&mut w, p, cabac);
if !cabac {
let mut run = 0;
for mb in &p.mbs {
match mb {
None => run += 1,
Some(s) => {
if p.is_p() {
w.write_ue(run);
run = 0;
w.write_ue(30); // I_PCM in a P slice
} else {
w.write_ue(25);
}
write_pcm(&mut w, s);
}
}
}
if run > 0 {
w.write_ue(run);
}
w.rbsp_trailing();
} else {
while !w.is_byte_aligned() {
w.write_bit(true); // cabac_alignment_one_bit
}
let mut e = Enc::new(26, if p.is_p() { 0 } else { 3 });
let skip = |i: usize| p.mbs[i].is_none();
for (i, mb) in p.mbs.iter().enumerate() {
let (x, y) = (i % MB_W, i / MB_W);
let a = if x > 0 { Some(i - 1) } else { None };
let b = if y > 0 { Some(i - MB_W) } else { None };
if p.is_p() {
let inc = a.map(|n| !skip(n) as usize).unwrap_or(0) + b.map(|n| !skip(n) as usize).unwrap_or(0);
e.encode(&mut w, 11 + inc, skip(i) as u32);
}
if let Some(s) = mb {
if p.is_p() {
e.encode(&mut w, 14, 1); // intra prefix
e.encode(&mut w, 17, 1); // not I_NxN
} else {
// neighbours are I_PCM (not I_NxN) -> condTerm 1
let inc = a.is_some() as usize + b.is_some() as usize;
e.encode(&mut w, 3 + inc, 1);
}
e.terminate(&mut w, true); // I_PCM
write_pcm(&mut w, s);
e.init();
}
e.terminate(&mut w, i + 1 == NMB); // end_of_slice_flag
}
w.align_zero();
}
let t = if p.idr { 5 } else { 1 };
nal(p.ref_idc, t, &w.finish())
}
fn random_mb(seed: &mut u32) -> Vec<u8> {
(0..384)
.map(|_| {
*seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
(*seed >> 24) as u8
})
.collect()
}
/// Expected cropped planes of a picture made of PCM blocks (`None` = copy from `reference`).
fn render(mbs: &[Option<Vec<u8>>], reference: Option<&(Vec<u8>, Vec<u8>, Vec<u8>)>) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let (w, h) = (MB_W * 16, MB_H * 16);
let (mut y, mut u, mut v) = match reference {
Some(r) => r.clone(),
None => (vec![0; w * h], vec![0; w * h / 4], vec![0; w * h / 4]),
};
for (i, mb) in mbs.iter().enumerate() {
let Some(s) = mb else { continue };
let (x0, y0) = ((i % MB_W) * 16, (i / MB_W) * 16);
for r in 0..16 {
y[(y0 + r) * w + x0..(y0 + r) * w + x0 + 16].copy_from_slice(&s[r * 16..r * 16 + 16]);
}
for r in 0..8 {
let o = (y0 / 2 + r) * (w / 2) + x0 / 2;
u[o..o + 8].copy_from_slice(&s[256 + r * 8..256 + r * 8 + 8]);
v[o..o + 8].copy_from_slice(&s[320 + r * 8..320 + r * 8 + 8]);
}
}
(y, u, v)
}
fn build_and_check(cabac: bool, threads: usize) {
let mut seed = 99u32;
let pcm_pic = |seed: &mut u32| (0..NMB).map(|_| Some(random_mb(seed))).collect::<Vec<_>>();
let a = pcm_pic(&mut seed);
let b = pcm_pic(&mut seed);
let mixed: Vec<Option<Vec<u8>>> = (0..NMB).map(|i| if i % 3 == 1 { Some(random_mb(&mut seed)) } else { None }).collect();
let skip = vec![None; NMB];
let pics = vec![
// 0: IDR, all PCM, marked long-term (LongTermFrameIdx 0)
Pic { idr: true, frame_num: 0, ref_idc: 3, long_term_ref: true, mbs: a.clone(), ..Default::default() },
// 1: all PCM, short-term
Pic { frame_num: 1, ref_idc: 2, mbs: b.clone(), ..Default::default() },
// 2: P_Skip from the long-term picture via modification idc 2 (default list would use pic 1)
Pic { frame_num: 2, ref_idc: 2, mods: vec![(2, 0)], mbs: skip.clone(), ..Default::default() },
// 3: frame_num gap (3, 4 missing); P_Skip from pic 2 (PicNum 2) via idc 0: 5 - (2 + 1) = 2
Pic { frame_num: 5, ref_idc: 2, mods: vec![(0, 2)], mbs: skip.clone(), ..Default::default() },
// 4: mixed PCM / P_Skip from pic 1 (PicNum 1: 6 - 5); MMCO 4 raises MaxLongTermFrameIdx to 2,
// MMCO 2 drops long-term 0, MMCO 3 makes pic 1 (difference 6 - 1 - 1 = 4) long-term index 1
Pic { frame_num: 6, ref_idc: 2, mods: vec![(0, 4)], mmcos: vec![(4, 3, 0), (2, 0, 0), (3, 4, 1)], mbs: mixed.clone(), ..Default::default() },
// 5: non-reference P_Skip from long-term index 1 (= pic 1)
Pic { frame_num: 7, ref_idc: 0, mods: vec![(2, 1)], mbs: skip.clone(), ..Default::default() },
// 6: P_Skip with the default list: short-term by descending PicNum -> pic 4 first
Pic { frame_num: 7, ref_idc: 2, mmcos: vec![(1, 0, 0), (6, 0, 2)], mbs: skip.clone(), ..Default::default() },
// 7: MMCO 5 picture made of PCM, then 8: P_Skip referencing it (only reference left)
Pic { frame_num: 8, ref_idc: 2, mmcos: vec![(5, 0, 0)], mbs: pcm_pic(&mut seed), ..Default::default() },
];
let mut stream = sps(if cabac { 77 } else { 66 }, 8, true);
stream.extend(pps(cabac));
for p in &pics {
stream.extend(slice_nal(p, cabac));
}
let after5 = Pic { frame_num: 1, ref_idc: 2, mbs: skip.clone(), ..Default::default() };
stream.extend(slice_nal(&after5, cabac));
// expected output
let pa = render(&a, None);
let pb = render(&b, None);
let p2 = pa.clone();
let p3 = p2.clone();
let p4 = render(&mixed, Some(&pb));
let p5 = pb.clone();
let p6 = p4.clone();
let p7 = render(&pics[7].mbs, None);
let p8 = p7.clone();
let expected = [pa, pb, p2, p3, p4, p5, p6, p7, p8];
if threads == 1 {
cross_check_ffmpeg(&stream, &expected, if cabac { "synth_cabac" } else { "synth_cavlc" });
}
let mut dec = Decoder::with_threads(threads);
let mut out = dec.decode(&stream, 0).unwrap();
out.extend(dec.flush());
assert!(dec.take_error().is_none());
assert_eq!(out.len(), expected.len(), "cabac={cabac}");
for (i, (p, e)) in out.iter().zip(expected.iter()).enumerate() {
assert_eq!((p.width as usize, p.height as usize), (MB_W * 16, MB_H * 16));
assert!(p.y == e.0 && p.u == e.1 && p.v == e.2, "picture {i} differs (cabac={cabac}, threads={threads})");
}
let s = dec.stats();
assert!(s.mb_pcm > 0 && s.frame_num_gaps == 1 && s.long_term_marks >= 3, "{s:?}");
}
type Planes3 = (Vec<u8>, Vec<u8>, Vec<u8>);
/// Validate the hand-built stream and its expected output with ffmpeg when available.
fn cross_check_ffmpeg(stream: &[u8], expected: &[Planes3], name: &str) {
let ff = std::path::Path::new("/opt/homebrew/bin/ffmpeg");
if !ff.exists() {
eprintln!("ffmpeg not found; skipping oracle check of {name}");
return;
}
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/h264");
std::fs::create_dir_all(&dir).unwrap();
let src = dir.join(format!("{name}.{}.h264", std::process::id()));
let dst = dir.join(format!("{name}.{}.yuv", std::process::id()));
std::fs::write(&src, stream).unwrap();
let ok = std::process::Command::new(ff)
.args(["-hide_banner", "-loglevel", "error", "-y", "-i"])
.arg(&src)
.args(["-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", "yuv420p"])
.arg(&dst)
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(ok, "ffmpeg failed to decode {name}");
let got = std::fs::read(&dst).unwrap();
let _ = std::fs::remove_file(&src);
let _ = std::fs::remove_file(&dst);
let want: Vec<u8> = expected.iter().flat_map(|(y, u, v)| y.iter().chain(u).chain(v).copied()).collect();
assert!(got == want, "ffmpeg output differs from the expected output of {name} ({} vs {} bytes)", got.len(), want.len());
}
#[test]
fn pcm_long_term_mmco_gaps_cavlc() {
build_and_check(false, 1);
build_and_check(false, 4);
}
#[test]
fn pcm_long_term_mmco_gaps_cabac() {
build_and_check(true, 1);
build_and_check(true, 4);
}

View File

@ -51,3 +51,43 @@ fixture_tests!(
qp50,
qp1,
);
/// Print which coding tools each fixture exercises (run with `-- --ignored --nocapture`).
#[test]
#[ignore]
fn coverage_report() {
for f in common::FIXTURES {
let Some((h264, _)) = common::ensure(f) else { return };
let data = std::fs::read(h264).unwrap();
let mut dec = filmcraft_h264::Decoder::new();
for au in common::split_access_units(&data) {
dec.decode(au, 0).unwrap();
}
dec.flush();
let s = dec.stats();
println!(
"{:<20} pics {:>3} cavlc/cabac {:>3}/{:<3} I/P/B {:>3}/{:>3}/{:>3} i4 {:>6} i8 {:>6} i16 {:>6} pcm {:>4} pskip {:>6} bskip {:>6} bdirect {:>5} inter {:>6} t8 {:>6} mmco {:>3} lt {} gaps {} wp {} tdirect {}",
f.name,
s.pictures,
s.slices_cavlc,
s.slices_cabac,
s.slices_i,
s.slices_p,
s.slices_b,
s.mb_i4x4,
s.mb_i8x8,
s.mb_i16x16,
s.mb_pcm,
s.mb_p_skip,
s.mb_b_skip,
s.mb_b_direct16x16,
s.mb_inter,
s.mb_inter_8x8_transform,
s.mmco_ops,
s.long_term_marks,
s.frame_num_gaps,
s.weighted_slices,
s.temporal_direct_slices
);
}
}

View File

@ -0,0 +1,120 @@
//! avcC / length-prefixed input and resilience against damaged streams.
mod common;
use filmcraft_bitstream::annexb_nals;
use filmcraft_h264::Decoder;
/// Build an avcC record and length-prefixed samples from an Annex-B stream.
fn to_avcc(data: &[u8]) -> (Vec<u8>, Vec<Vec<u8>>) {
let mut sps = Vec::new();
let mut pps = Vec::new();
let mut samples = Vec::new();
for au in common::split_access_units(data) {
let mut sample = Vec::new();
for nal in annexb_nals(au) {
match nal[0] & 0x1f {
7 => sps.push(nal.to_vec()),
8 => pps.push(nal.to_vec()),
_ => {
sample.extend((nal.len() as u32).to_be_bytes());
sample.extend_from_slice(nal);
}
}
}
if !sample.is_empty() {
samples.push(sample);
}
}
sps.dedup();
pps.dedup();
let s0 = &sps[0];
let mut avcc = vec![1, s0[1], s0[2], s0[3], 0xff, 0xe0 | sps.len() as u8];
for s in &sps {
avcc.extend((s.len() as u16).to_be_bytes());
avcc.extend_from_slice(s);
}
avcc.push(pps.len() as u8);
for p in &pps {
avcc.extend((p.len() as u16).to_be_bytes());
avcc.extend_from_slice(p);
}
(avcc, samples)
}
#[test]
fn avcc_input_matches_reference() {
for name in ["main_cabac_b", "baseline_qcif"] {
let f = common::fixture(name);
let Some((h264, yuv)) = common::ensure(f) else { return };
let (avcc, samples) = to_avcc(&std::fs::read(h264).unwrap());
let mut dec = Decoder::from_avcc(&avcc).unwrap();
assert_eq!(dec.nal_length_size(), Some(4));
let mut pics = Vec::new();
for (i, s) in samples.iter().enumerate() {
pics.extend(dec.decode(s, i as i64 * 1000).unwrap());
}
pics.extend(dec.flush());
let reference = std::fs::read(yuv).unwrap();
common::compare(&pics, &reference, f.width as usize, f.height as usize).unwrap();
// pts are passed through reordering
let mut pts: Vec<i64> = pics.iter().map(|p| p.pts).collect();
pts.sort();
assert_eq!(pts, (0..samples.len() as i64).map(|i| i * 1000).collect::<Vec<_>>());
let p0 = &pics[0];
assert!(p0.key);
assert_eq!((p0.y_stride, p0.uv_stride), (f.width as usize, f.width as usize / 2));
}
}
fn xorshift(seed: &mut u64) -> u64 {
*seed ^= *seed << 13;
*seed ^= *seed >> 7;
*seed ^= *seed << 17;
*seed
}
/// Damaged streams must never panic or hang; errors are fine.
#[test]
fn corrupted_streams_do_not_panic() {
for name in ["main_cabac_b", "baseline_cif_noise", "cavlc_b", "bpyramid"] {
let f = common::fixture(name);
let Some((h264, _)) = common::ensure(f) else { return };
let data = std::fs::read(h264).unwrap();
let mut seed = 0x1234_5678_9abc_def0u64 ^ name.len() as u64;
for round in 0..12 {
let mut d = data.clone();
// flip bytes after the parameter sets
for _ in 0..(1 + round * 3) {
let pos = 64 + (xorshift(&mut seed) as usize) % (d.len() - 64);
d[pos] ^= 1 << (xorshift(&mut seed) % 8);
}
if round % 4 == 3 {
d.truncate(d.len() * 2 / 3);
}
for threads in [1, 4] {
let mut dec = Decoder::with_threads(threads);
for au in common::split_access_units(&d) {
let _ = dec.decode(au, 0);
}
let _ = dec.flush();
let _ = dec.take_error();
}
}
}
}
#[test]
fn garbage_input_is_rejected_gracefully() {
let mut seed = 42u64;
for len in [0usize, 1, 5, 100, 5000] {
let mut g: Vec<u8> = (0..len).map(|_| xorshift(&mut seed) as u8).collect();
if len > 10 {
g[..4].copy_from_slice(&[0, 0, 0, 1]);
}
let mut dec = Decoder::new();
let _ = dec.decode(&g, 0);
let _ = dec.flush();
}
assert!(Decoder::from_avcc(&[1, 2, 3]).is_err());
}