filmcraft/crates/codecs/tests/draft_decoders.rs

120 lines
4.3 KiB
Rust

//! Draft decoding of VP9, HEVC and AV1 through the media stack (as `h264_draft.rs` for H.264):
//! off by default, approximate frames only reach draft requests, and decoding for an export after
//! draft playback (same source, same GOP cache) still matches ffmpeg bit for bit.
mod common;
use common::*;
use filmcraft_media::FrameRequest;
use filmcraft_media::cancel::with_draft;
/// Play `name` without and with draft decoding, then "export" it: every non-draft request must
/// be bit-exact with ffmpeg, and draft playback must have produced some approximate frames.
fn check(name: &str, args: &[&str], w: usize, h: usize) {
let ff = filmcraft_testkit::require_ffmpeg!();
let Some(file) = fixture(&ff, name, args) else { return };
let raw = ffmpeg_frames(&ff, &file, "yuv420p");
let fsize = w * h * 3 / 2;
let n = raw.len() / fsize;
assert!(n >= 16, "{name}: {n} frames");
let reference = |i: usize| raw_planes(&raw[i * fsize..(i + 1) * fsize], w, h, w / 2, h / 2, 1);
// Default: nothing is a draft, every frame exact.
let src = filmcraft_codecs::open_bytes(name, bytes(&file)).expect("open");
let rate = src.info().frame_rate();
let g0 = filmcraft_codecs::gop_stats();
for i in 0..n {
let f = src.video_frame(FrameRequest { time: rate.tick_of(i as i64), scale: 0.5 }).expect("frame");
assert_eq!(max_diff(&planes(&f), &reference(i)), 0, "{name}: frame {i} without draft mode");
}
assert_eq!(filmcraft_codecs::gop_stats().draft, g0.draft, "{name}: draft decoding is off by default");
// Draft playback at 1/2 resolution on a fresh source: non-reference frames are drafts.
let src = filmcraft_codecs::open_bytes(name, bytes(&file)).expect("open");
let g1 = filmcraft_codecs::gop_stats();
let mut approximate = 0;
for i in 0..n {
let f = with_draft(true, || src.video_frame(FrameRequest { time: rate.tick_of(i as i64), scale: 0.5 })).expect("frame");
approximate += (max_diff(&planes(&f), &reference(i)) > 0) as usize;
}
let drafts = filmcraft_codecs::gop_stats().draft - g1.draft;
assert!(drafts > 0 && approximate > 0 && approximate as u64 <= drafts, "{name}: {drafts} draft frames, {approximate} differ");
// Then an export from the same source (no draft hint): every frame is exact again.
for i in 0..n {
let f = src.video_frame(FrameRequest::full(rate.tick_of(i as i64))).expect("frame");
assert_eq!(max_diff(&planes(&f), &reference(i)), 0, "{name}: export frame {i} after draft playback");
}
}
/// One test (the GOP cache statistics are process-wide, so the codecs run one after another).
#[test]
fn draft_playback_never_reaches_exports() {
vp9();
hevc();
av1();
}
fn vp9() {
// Two temporal layers: every other frame refreshes no reference slot. (25 fps: WebM stores
// millisecond timestamps.)
check(
"draft_vp9.webm",
&[
"-f",
"lavfi",
"-i",
"testsrc2=s=320x240:r=25:d=1,noise=alls=10:allf=t",
"-c:v",
"libvpx-vp9",
"-deadline",
"realtime",
"-speed",
"8",
"-b:v",
"600k",
"-ts-parameters",
"ts_number_layers=2:ts_target_bitrate=300,600:ts_rate_decimator=2,1:ts_periodicity=2:ts_layer_id=0,1:ts_layering_mode=2",
"-pix_fmt",
"yuv420p",
],
320,
240,
);
}
fn hevc() {
// B-pyramid: non-reference B pictures (sub-layer non-reference NAL types).
check(
"draft_hevc.mp4",
&[
"-f",
"lavfi",
"-i",
"testsrc2=s=320x240:r=25:d=1,noise=alls=10:allf=t",
"-c:v",
"libx265",
"-preset",
"fast",
"-x265-params",
"bframes=4:b-pyramid=1:keyint=24:log-level=error",
"-pix_fmt",
"yuv420p",
"-tag:v",
"hvc1",
],
320,
240,
);
}
fn av1() {
// SVT-AV1 hierarchical GOP: the top layer refreshes no reference slot.
check(
"draft_av1.mp4",
&["-f", "lavfi", "-i", "testsrc2=s=320x240:r=25:d=1,noise=alls=10:allf=t", "-c:v", "libsvtav1", "-preset", "8", "-crf", "35", "-pix_fmt", "yuv420p"],
320,
240,
);
}