tests: pin pictures emitted without their reference at a start, join or gap resync

Thirteen ignored tests, each failing on current code, cover H.264 (stream
start, join and gap resync onto an open GOP; inter pictures before the
first random-access picture), HEVC (a forward-PTS join and a gap resync
onto a CRA hand its RASL to the decoder; pictures before the first IRAP),
VC-1 (an open entry point at the start or after a gap; BROKEN_LINK), the
MPEG-2 gap resync onto an open GOP, and the seam plan keeping a picture
whose reference it dropped at a clip's IN mark. Two passing tests pin
the cases that are already right: a closed VC-1 entry point and RASL
after a seamless HEVC join.
This commit is contained in:
Matthew Jackson
2026-10-02 09:48:16 -07:00
parent 8a0cb2adbf
commit 8d15f5211e
5 changed files with 347 additions and 0 deletions
+100
View File
@@ -908,6 +908,106 @@ mod tests {
);
}
// --- Pictures a decoder cannot reconstruct from the stream's own data ---
/// One non-IDR picture AU: `hdr` = NAL header byte, `body` = slice-header byte
/// (0x88 I, 0x98 P, 0x9C B), at `frame` 24 fps frame periods (90 kHz).
fn pic_au(hdr: u8, body: u8, frame: i64) -> PesPacket {
make_pes(vec![0x00, 0x00, 0x01, hdr, body, 0x00], Some(frame * 3750))
}
/// An open GOP in decode order `I3 B1 B2 P6 B4 B5` (display frame numbers),
/// offset by `base` frames. B1/B2 precede the I in display order: leading
/// pictures predicted from the anchor BEFORE this GOP.
fn open_gop(base: i64) -> Vec<PesPacket> {
vec![
pic_au(0x61, 0x88, base + 3),
pic_au(0x01, 0x9C, base + 1),
pic_au(0x01, 0x9C, base + 2),
pic_au(0x41, 0x98, base + 6),
pic_au(0x01, 0x9C, base + 4),
pic_au(0x01, 0x9C, base + 5),
]
}
/// Emitted frames' display frame numbers.
fn frame_nos(frames: &[Frame]) -> Vec<i64> {
frames
.iter()
.map(|f| (f.pts_ns * 24 + 500_000_000) / 1_000_000_000)
.collect()
}
#[test]
#[ignore = "defect: an open GOP at the stream start emits its leading B-pictures, whose forward reference precedes the title"]
fn an_open_gop_at_the_stream_start_drops_its_leading_b_pictures() {
let mut p = H264Parser::new();
let f: Vec<Frame> = open_gop(0).iter().flat_map(|x| p.parse(x)).collect();
assert_eq!(
frame_nos(&f),
[3, 6, 4, 5],
"B1/B2 reference a picture the output does not hold"
);
}
#[test]
#[ignore = "defect: a join onto an open GOP emits its leading B-pictures, predicted from the previous clip's anchor"]
fn a_join_onto_an_open_gop_drops_its_leading_b_pictures() {
// Clip 1 ends on a complete open GOP; clip 2's PTS restarts far away, so
// its leading B1/B2 reference a picture of the clip the playlist skipped.
let mut p = H264Parser::new();
let mut pes = vec![make_pes(vec![0x00, 0x00, 0x01, 0x65, 0x88, 0x00], Some(0))];
pes.extend(open_gop(0).into_iter().skip(3));
pes.extend(open_gop(10_000));
let f: Vec<Frame> = pes.iter().flat_map(|x| p.parse(x)).collect();
assert_eq!(
frame_nos(&f)[4..],
[10_003, 10_006, 10_004, 10_005],
"clip 2's leading Bs reference clip 1's tail, not their own source"
);
}
#[test]
#[ignore = "defect: inter pictures before the stream's first random-access picture are emitted"]
fn pictures_before_the_first_random_access_picture_are_dropped() {
let mut p = H264Parser::new();
let mut pes = vec![pic_au(0x41, 0x98, 0), pic_au(0x01, 0x9C, 1)];
pes.push(make_pes(
vec![0x00, 0x00, 0x01, 0x65, 0x88, 0x00],
Some(2 * 3750),
));
let f: Vec<Frame> = pes.iter().flat_map(|x| p.parse(x)).collect();
assert!(f[0].keyframe, "the first emitted picture is the IDR");
}
#[test]
#[ignore = "defect: after a gap the ResyncGate resumes on an open-GOP I-picture and passes its leading B-pictures, whose forward reference the gate dropped"]
fn a_gap_resync_onto_an_open_gop_emits_no_leading_b_pictures() {
// IDR, then a P that follows lost data (and the B predicted from it), then
// an open GOP continuing the same clock: its B1/B2 reference the dropped P.
let mut p = H264Parser::new();
let mut pes = vec![make_pes(
vec![0x00, 0x00, 0x01, 0x65, 0x88, 0x00],
Some(7 * 3750),
)];
let mut lost = pic_au(0x41, 0x98, 9);
lost.discontinuity = true;
pes.push(lost);
pes.push(pic_au(0x01, 0x9C, 8));
pes.extend(open_gop(10));
let mut gate = crate::mux::resync::ResyncGate::new();
let f: Vec<Frame> = pes
.iter()
.flat_map(|x| p.parse(x))
.filter(|f| gate.admit(true, f.discontinuity, f.keyframe))
.collect();
assert_eq!(
frame_nos(&f),
[7, 13, 16, 14, 15],
"B1/B2 reference the P the gate dropped"
);
}
// The escaped slice header must be unescaped at the parse call site: a second slice with
// first_mb_in_slice=65535 (00 00 03 escape) is intra only when read unescaped.
#[test]
+92
View File
@@ -2352,6 +2352,98 @@ mod tests {
);
}
/// Whether `frames` hand a decoder a RASL picture it must decode: one that
/// follows a CRA (not a BLA, whose RASL a decoder discards) in decode order.
fn emits_decodable_rasl_after_cra(frames: &[Frame]) -> bool {
let mut last_irap = None;
for f in frames {
let t = nal_type_of(&nals_of(&f.data)[0]);
match t {
16..=21 => last_irap = Some(t),
8 | 9 if last_irap == Some(NAL_CRA_NUT) => return true,
_ => {}
}
}
false
}
#[test]
#[ignore = "defect: a non-seamless join whose PTS jumps FORWARD is not detected, so its CRA stays CRA and its RASL decode against the previous clip"]
fn a_forward_pts_join_onto_a_cra_does_not_hand_its_rasl_to_the_decoder() {
// Clip 1: CRA + trailing pictures at ~0 s. Clip 2 starts 100 s later in
// the source clock (e.g. a PlayItem that skips part of a clip, or a clip
// whose STC runs ahead) with a CRA whose RASL reference a picture of
// the skipped material.
let mut p = HevcParser::new();
let mut frames = Vec::new();
frames.extend(p.parse(&make_pes(cra_au(&[0x01]), Some(0))));
frames.extend(p.parse(&make_pes(nal_bytes(1, &[0xD0]), Some(3750))));
frames.extend(p.parse(&make_pes(nal_bytes(1, &[0xD0]), Some(7500))));
let t = 100 * 90_000;
let clip2 = frames.len();
frames.extend(p.parse(&make_pes(cra_au(&[0x02]), Some(t))));
frames.extend(p.parse(&make_pes(nal_bytes(9, &[0xE0]), Some(t - 7500))));
frames.extend(p.parse(&make_pes(nal_bytes(8, &[0xE0]), Some(t - 3750))));
assert!(
!emits_decodable_rasl_after_cra(&frames[clip2..]),
"clip 2's RASL reference a picture this output does not hold"
);
}
#[test]
fn rasl_after_a_seamless_join_cra_stay_decodable() {
// Characterises the seamless case: PTS continuous across the join, so the
// RASL reference the previous clip's tail, which the output holds.
let mut p = HevcParser::new();
let mut frames = Vec::new();
frames.extend(p.parse(&make_pes(cra_au(&[0x01]), Some(0))));
frames.extend(p.parse(&make_pes(nal_bytes(1, &[0xD0]), Some(3750))));
frames.extend(p.parse(&make_pes(cra_au(&[0x02]), Some(4 * 3750))));
frames.extend(p.parse(&make_pes(nal_bytes(9, &[0xE0]), Some(2 * 3750))));
assert!(
emits_decodable_rasl_after_cra(&frames),
"a seamless join keeps its CRA and RASL"
);
}
#[test]
#[ignore = "defect: after a gap the ResyncGate resumes on a CRA and passes its RASL, whose reference the gate dropped"]
fn a_gap_resync_onto_a_cra_does_not_hand_its_rasl_to_the_decoder() {
// IDR, a TRAIL_R that follows lost data, then a CRA (same clock) whose RASL
// reference the dropped TRAIL_R. The CRA is not first in the bitstream, so
// a decoder decodes the RASL.
let mut p = HevcParser::new();
let mut lost = make_pes(nal_bytes(1, &[0xD0]), Some(3750));
lost.discontinuity = true;
let pes = [
make_pes(nal_bytes(19, &[0x80]), Some(0)),
lost,
make_pes(cra_au(&[0x02]), Some(4 * 3750)),
make_pes(nal_bytes(9, &[0xE0]), Some(2 * 3750)),
make_pes(nal_bytes(8, &[0xE0]), Some(3 * 3750)),
];
let mut gate = crate::mux::resync::ResyncGate::new();
let frames: Vec<Frame> = pes
.iter()
.flat_map(|x| p.parse(x))
.filter(|f| gate.admit(true, f.discontinuity, f.keyframe))
.collect();
assert!(
!emits_decodable_rasl_after_cra(&frames),
"the RASL reference the picture the gate dropped"
);
}
#[test]
#[ignore = "defect: pictures before the stream's first IRAP are emitted with no reference"]
fn pictures_before_the_first_irap_are_dropped() {
let mut p = HevcParser::new();
let mut frames = Vec::new();
frames.extend(p.parse(&make_pes(nal_bytes(1, &[0xD0]), Some(0))));
frames.extend(p.parse(&make_pes(cra_au(&[0x01]), Some(3750))));
assert!(frames[0].keyframe, "the first emitted picture is the IRAP");
}
/// Test 2: a CRA with NO boundary marker is left unchanged (CRA stays CRA).
#[test]
fn cra_without_boundary_unchanged() {
+35
View File
@@ -1055,6 +1055,41 @@ mod tests {
);
}
#[test]
#[ignore = "defect: after a gap the ResyncGate resumes on an open GOP's I-picture and passes its leading B-pictures, whose forward reference the gate dropped"]
fn a_gap_resync_onto_an_open_gop_emits_no_leading_b_pictures() {
// GOP 1 (closed) loses data before P5: P5 and the Bs after it reference
// the lost picture. GOP 2 is open and PTS-continuous (no join): its leading
// B0/B1 reference GOP 1's P5, which the gate dropped.
let mut pes = gop_ibbp(seq_gop(true, false), Some(90_000));
pes[3].discontinuity = true;
pes.extend(gop_ibbp(gop_flags(false, false), None));
let mut gate = crate::mux::resync::ResyncGate::new();
let out: Vec<Frame> = run(pes)
.into_iter()
.filter(|f| gate.admit(true, f.discontinuity, f.keyframe))
.collect();
let types: Vec<_> = out
.iter()
.map(|f| f.coding.unwrap().coding_type())
.collect();
// GOP 1: I2 B0 B1 kept; P5 B3 B4 dropped. GOP 2: I2 kept, B0 B1 lack P5
// (dropped), P5 B3 B4 decode from GOP 2's own anchors.
assert_eq!(
types,
[
CodingType::I,
CodingType::B,
CodingType::B,
CodingType::I,
CodingType::P,
CodingType::B,
CodingType::B,
],
"no emitted picture may reference one the gate dropped"
);
}
// --- Sequence header parsing ---
#[test]
+94
View File
@@ -558,6 +558,100 @@ mod tests {
data
}
// --- Pictures a decoder cannot reconstruct from the stream's own data ---
/// A progressive advanced-profile entry-point I-picture AU (sequence header,
/// entry-point header with the given BROKEN_LINK / CLOSED_ENTRY, frame)
/// followed, in decode order, by B B P (SMPTE 421M §6.2.1: with CLOSED_ENTRY
/// 0 the Bs after the entry-point I may predict from the anchor before it).
fn entry_gop(broken_link: bool, closed_entry: bool) -> Vec<Vec<u8>> {
let seq = [0x00, 0x00, 0x01, SC_SEQUENCE_HEADER, 0xC0, 0, 0, 0, 0, 0];
let ep0 = (u8::from(broken_link) << 7) | (u8::from(closed_entry) << 6);
let ep = [0x00, 0x00, 0x01, SC_ENTRY_POINT, ep0, 0x00];
let pic = |ptype: u8| vec![0x00, 0x00, 0x01, SC_FRAME, ptype];
vec![
[&seq[..], &ep[..], &pic(0xC0)].concat(),
pic(0x80),
pic(0x80),
pic(0x00),
]
}
fn coding_types(aus: Vec<Vec<u8>>) -> Vec<CodingType> {
let mut p = Vc1Parser::new();
aus.into_iter()
.flat_map(|au| p.parse(&make_pes(au, Some(0))))
.map(|f| f.coding.unwrap().coding_type())
.collect()
}
#[test]
#[ignore = "defect: an open entry point (CLOSED_ENTRY=0) at the stream start emits its leading B-pictures, whose forward reference precedes the title"]
fn an_open_entry_point_at_the_stream_start_drops_its_leading_b_pictures() {
assert_eq!(
coding_types(entry_gop(false, false)),
[CodingType::I, CodingType::P]
);
}
#[test]
fn a_closed_entry_point_at_the_stream_start_keeps_every_picture() {
assert_eq!(
coding_types(entry_gop(false, true)),
[CodingType::I, CodingType::B, CodingType::B, CodingType::P]
);
}
#[test]
#[ignore = "defect: BROKEN_LINK mid-stream is not read; the Bs it marks undecodable are emitted"]
fn a_broken_link_drops_the_leading_b_pictures_mid_stream() {
let mut aus = entry_gop(false, true);
aus.extend(entry_gop(true, false));
assert_eq!(
coding_types(aus),
[
CodingType::I,
CodingType::B,
CodingType::B,
CodingType::P,
CodingType::I,
CodingType::P
]
);
}
#[test]
#[ignore = "defect: after a gap the ResyncGate resumes on an open entry point and passes its leading B-pictures, whose forward reference the gate dropped"]
fn a_gap_resync_onto_an_open_entry_point_emits_no_leading_b_pictures() {
let mut aus = entry_gop(false, true);
aus.extend(entry_gop(false, false));
let mut p = Vc1Parser::new();
let mut gate = crate::mux::resync::ResyncGate::new();
let types: Vec<CodingType> = aus
.into_iter()
.enumerate()
.flat_map(|(i, au)| {
let mut pes = make_pes(au, Some(0));
// The first entry segment's P follows lost data.
pes.discontinuity = i == 3;
p.parse(&pes)
})
.filter(|f| gate.admit(true, f.discontinuity, f.keyframe))
.map(|f| f.coding.unwrap().coding_type())
.collect();
assert_eq!(
types,
[
CodingType::I,
CodingType::B,
CodingType::B,
CodingType::I,
CodingType::P
],
"the second entry's Bs reference the P the gate dropped"
);
}
// --- sequence header detection ---
#[test]
+26
View File
@@ -955,6 +955,32 @@ mod tests {
);
}
// A PlayItem's IN mark need not sit on a GOP's I-picture: a player decodes from the
// preceding entry point and suppresses display until IN. The plan trims by each frame's
// own PTS, in decode order, so it drops the I and keeps the pictures predicted from it.
#[test]
#[ignore = "defect: the seam plan drops a reference picture outside a clip's marks while keeping the pictures predicted from it"]
fn the_in_mark_never_keeps_a_picture_whose_reference_it_dropped() {
const F: i64 = 41_708_333; // one 23.976 fps frame (ns)
let clips = vec![crate::disc::Clip {
feed_span: Some((0, 1_000_000)),
clip_id: "00001".into(),
in_time: 45_000, // IN = 1 s, mid-GOP
out_time: 450_000,
duration_secs: 9.0,
source_packets: 0,
}];
let mut plan = SeamPlan::from_clips(&clips).expect("one clip");
// Decode order: I (display 0.958 s, before IN), then P predicted from it
// (display 1.083 s, after IN).
let i_kept = plan.place(S - F, 0, true, Some(0)).is_some();
let p_kept = plan.place(S + 2 * F, 0, true, Some(100)).is_some();
assert!(
i_kept || !p_kept,
"the P is kept but its reference I was dropped (i_kept={i_kept}, p_kept={p_kept})"
);
}
#[test]
fn a_repeated_clip_shares_one_span_and_is_still_trusted() {
let mut clips = clips_with_spans();