mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
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:
@@ -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]
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user