mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
tests: mux/codec pin flac, h264, mpeg2 caps and timing, reorder, read_ue, mp2 copy sources
This commit is contained in:
@@ -216,6 +216,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pes_dts_used_when_pts_absent() {
|
||||
let mut p = FlacParser::new();
|
||||
let mut pes = make_pes(make_flac_frame(100), None);
|
||||
pes.dts = Some(180_000);
|
||||
let f = p.parse(&pes);
|
||||
assert_eq!(f[0].pts_ns, pts_to_ns(180_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pes_discontinuity_propagates_to_frame() {
|
||||
let mut p = FlacParser::new();
|
||||
let mut pes = make_pes(make_flac_frame(100), Some(90_000));
|
||||
pes.discontinuity = true;
|
||||
assert!(p.parse(&pes)[0].discontinuity);
|
||||
assert!(!p.parse(&make_pes(make_flac_frame(100), Some(96_000)))[0].discontinuity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poisoned_track_drops_even_valid_frames() {
|
||||
let mut p = FlacParser::new();
|
||||
// A run of verified-corrupt frames poisons the track (200-AU verdict gate).
|
||||
for i in 0..400 {
|
||||
let mut bad = make_flac_frame(100);
|
||||
bad[20] ^= 0xFF;
|
||||
assert!(p.parse(&make_pes(bad, Some(i * 90))).is_empty());
|
||||
}
|
||||
assert!(p.tally.is_poisoned());
|
||||
let before = p.dropped_frames();
|
||||
assert!(
|
||||
p.parse(&make_pes(make_flac_frame(100), Some(1_000_000)))
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(p.dropped_frames(), before + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_flac_packet_passes_through() {
|
||||
// A packet without the FLAC sync isn't a frame we can validate — never
|
||||
|
||||
+40
-7
@@ -902,6 +902,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// 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]
|
||||
fn parse_unescapes_later_slice_header_before_reading_it() {
|
||||
let au = vec![
|
||||
0x00, 0x00, 0x01, 0x61, 0x88, 0x00, // slice 1: first_mb=0, I
|
||||
0x00, 0x00, 0x01, 0x61, 0x00, 0x00, 0x03, 0x80, 0x00, 0x30, // slice 2: 65535, I
|
||||
];
|
||||
let mut parser = H264Parser::new();
|
||||
let frames = parser.parse(&make_pes(au, Some(0)));
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(
|
||||
frames[0].keyframe,
|
||||
"every slice reads intra only if the escape byte is removed"
|
||||
);
|
||||
}
|
||||
|
||||
// The gap flag on a PES must reach the frame, or the resync gate never arms for H.264.
|
||||
#[test]
|
||||
fn pes_discontinuity_propagates_to_frame() {
|
||||
let au = vec![0x00, 0x00, 0x01, 0x61, 0x88, 0x00];
|
||||
for flag in [true, false] {
|
||||
let mut pes = make_pes(au.clone(), Some(0));
|
||||
pes.discontinuity = flag;
|
||||
let frames = H264Parser::new().parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].discontinuity, flag);
|
||||
}
|
||||
}
|
||||
|
||||
// Guard the promotion's precision: a non-intra (P) AU must NOT become a
|
||||
// keyframe, or resync would resume on inter-referenced dropped data.
|
||||
#[test]
|
||||
@@ -1141,9 +1171,11 @@ mod tests {
|
||||
for i in 0..30i64 {
|
||||
let au = annexb(0x65, &vec![0x88u8; 300_000]);
|
||||
let f = parser.parse(&make_pes(au, Some(3600 * (i + 1))));
|
||||
// The re-assert really happened (otherwise the count is vacuously 0).
|
||||
assert!(
|
||||
f[0].data.len() > 300_000,
|
||||
// The re-assert really happened (otherwise the count is vacuously 0):
|
||||
// length-prefixed slice + SPS (4+1+body) + PPS (4+1+2).
|
||||
assert_eq!(
|
||||
f[0].data.len(),
|
||||
(4 + 1 + 300_000) + (4 + 1 + big_sps.len()) + (4 + 1 + 2),
|
||||
"keyframe {i} must carry the re-asserted parameter sets"
|
||||
);
|
||||
}
|
||||
@@ -2205,8 +2237,9 @@ mod tests {
|
||||
/// extension bytes.
|
||||
#[test]
|
||||
fn avcc_high_profile_extension_carries_correct_values() {
|
||||
// profile_idc=100, chroma_format_idc=3 (4:4:4), depth_luma=2, depth_chroma=2.
|
||||
let sps = build_high_profile_sps(100, 3, 2, 2);
|
||||
// profile_idc=100, chroma_format_idc=3 (4:4:4), depth_luma=2, depth_chroma=5
|
||||
// (unequal, so a luma/chroma swap shows).
|
||||
let sps = build_high_profile_sps(100, 3, 2, 5);
|
||||
let mut parser = H264Parser::new();
|
||||
feed_sps_pps(&mut parser, &sps);
|
||||
|
||||
@@ -2217,8 +2250,8 @@ mod tests {
|
||||
assert_eq!(cp[ext_off + 1] & 0x07, 2, "bit_depth_luma_minus8 must be 2");
|
||||
assert_eq!(
|
||||
cp[ext_off + 2] & 0x07,
|
||||
2,
|
||||
"bit_depth_chroma_minus8 must be 2"
|
||||
5,
|
||||
"bit_depth_chroma_minus8 must be 5"
|
||||
);
|
||||
assert_eq!(
|
||||
cp[ext_off + 3],
|
||||
|
||||
@@ -487,6 +487,11 @@ mod provenance_guard {
|
||||
let mut i = 0;
|
||||
while let Some(p) = src[i..].find("Frame {") {
|
||||
let start = i + p;
|
||||
// Skip longer names ending in `Frame {` (e.g. `PesFrame {`).
|
||||
if start > 0 && (b[start - 1].is_ascii_alphanumeric() || b[start - 1] == b'_') {
|
||||
i = start + "Frame {".len();
|
||||
continue;
|
||||
}
|
||||
let open = start + src[start..].find('{').unwrap();
|
||||
let (mut depth, mut k) = (0usize, open);
|
||||
while k < b.len() {
|
||||
@@ -518,7 +523,7 @@ mod provenance_guard {
|
||||
// `source: None` fixtures are fine). Two loss spellings: explicit
|
||||
// `source: None`, or omitting it (Frame derives Default, so `..` yields None).
|
||||
let explicit_none = blk.contains("source: None");
|
||||
let no_source_field = !blk.contains("source:");
|
||||
let no_source_field = !blk.contains("source:") && !blk.contains("source,");
|
||||
if explicit_none || no_source_field {
|
||||
let line = src[..src.find(blk).unwrap_or(0)].lines().count() + 1;
|
||||
let how = if explicit_none {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use super::*;
|
||||
|
||||
// Spec quotes kept as reference text; the tests assert the behaviour directly.
|
||||
/// 13818-3 (2nd ed.) §2.5.3.1, the rule the whole module follows.
|
||||
#[allow(dead_code)]
|
||||
const SPEC_DETECTION: &str = "The MPEG-1 ancillary data field is initially assumed to contain \
|
||||
the coded multichannel extension. If the mandatory CRC-check yields a valid result, then \
|
||||
multichannel decoding will be started.";
|
||||
/// 13818-3 §2.5.1.3 (Layer II): the base frame's parts, in order.
|
||||
#[allow(dead_code)]
|
||||
const SPEC_BASE_FRAME: &str = "base_frame() { mpeg1_header() mpeg1_error_check() \
|
||||
mpeg1_audio_data() mc_extension_data_part1() mpeg1_ancillary_data() }";
|
||||
/// 13818-3 §2.5.2.14: what `mc_crc_check` covers.
|
||||
#[allow(dead_code)]
|
||||
const SPEC_MC_CRC: &str = "In Layer I and II, the calculation begins with the first bit of the \
|
||||
multichannel header and ends with the last bit of the scfsi field, but excluding the \
|
||||
mc_crc_check field itself.";
|
||||
@@ -778,7 +782,6 @@ fn frame_crc_is_checked_when_protection_bit_is_zero() {
|
||||
/// A frame whose mc_crc_check verifies is multichannel; one that does not, is not.
|
||||
#[test]
|
||||
fn multichannel_needs_a_valid_mc_crc_check() {
|
||||
let _ = (SPEC_DETECTION, SPEC_BASE_FRAME, SPEC_MC_CRC);
|
||||
let good = with_mc(STEREO_256, MC_3_2_LFE);
|
||||
// §2.5.3.1: "If the mandatory CRC-check yields a valid result, then multichannel decoding
|
||||
// will be started."
|
||||
@@ -1044,6 +1047,25 @@ fn missing_channels_copy_their_allocation_as_the_tables_say() {
|
||||
assert_eq!(src(0, 1, &st(1, 2, false), 0), "T1");
|
||||
// 3/0 '0' "T2": transmitted.
|
||||
assert_eq!(src(1, 0, &st(0, 0, false), 0), "read");
|
||||
// 3/2 '0011' T2 missing across every tc: 1|7 from Lo, 2|6 from Ro, the rest by dyn_cross_LR.
|
||||
for (tc, lr, want) in [
|
||||
(7, true, "T0"),
|
||||
(6, false, "T1"),
|
||||
(4, false, "T0"),
|
||||
(4, true, "T1"),
|
||||
] {
|
||||
assert_eq!(src(1, 2, &st(3, tc, lr), 0), want, "3/2 tc {tc} lr {lr}");
|
||||
}
|
||||
// 3/1 '011' T3 missing: tc 4|5 copy from Ro, else Lo unless tc<3 with dyn_cross_LR.
|
||||
for (tc, lr, want) in [
|
||||
(4, false, "T1"),
|
||||
(5, false, "T1"),
|
||||
(3, false, "T0"),
|
||||
(7, true, "T0"),
|
||||
(0, true, "T1"),
|
||||
] {
|
||||
assert_eq!(src(1, 1, &st(3, tc, lr), 1), want, "3/1 tc {tc} lr {lr}");
|
||||
}
|
||||
}
|
||||
|
||||
/// §2.5.2.13 centre "'11' centre bandwidth limited (Phantom coding)": "the subbands above
|
||||
@@ -1459,7 +1481,7 @@ fn random_bytes_never_panic() {
|
||||
let mut f: Vec<u8> = (0..len).map(|_| next() as u8).collect();
|
||||
if i % 2 == 0 && len >= 4 {
|
||||
f[0] = 0xFF;
|
||||
f[1] = 0xFC | (f[1] & 0x03); // syncword, ID 1, Layer II
|
||||
f[1] = 0xFC | (f[1] & 0x01); // syncword, ID 1, Layer II ('10')
|
||||
}
|
||||
if i % 3 == 0 {
|
||||
f = seeds[i % seeds.len()].clone();
|
||||
|
||||
+74
-4
@@ -1358,16 +1358,15 @@ mod tests {
|
||||
// --- parameter-set-only stream: seq header, no picture → no frame ---
|
||||
|
||||
#[test]
|
||||
fn sequence_header_only_emits_no_frame_but_captures_codec_private() {
|
||||
fn sequence_header_only_emits_no_frame_and_captures_no_codec_private() {
|
||||
let mut parser = Mpeg2Parser::new();
|
||||
let mut data = make_seq_header(1920, 1080, 3, 4);
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE, 0x14, 0x8A]);
|
||||
// No picture start code at all.
|
||||
let frames = parse_then_flush(&mut parser, &make_pes(data, Some(0)));
|
||||
assert!(frames.is_empty(), "no coded picture → no frame");
|
||||
// codec_private only captured when an AU is emitted; a header-only
|
||||
// stream emits nothing, so nothing is captured — and there is no frame
|
||||
// to need it. (Real streams always follow the header with a picture.)
|
||||
// A header-only AU returns before extract_seq_header: nothing is captured.
|
||||
assert!(parser.codec_private().is_none());
|
||||
}
|
||||
|
||||
// --- buffer cap: corrupt stream with no second boundary is force-flushed ---
|
||||
@@ -1387,6 +1386,77 @@ mod tests {
|
||||
assert!(frames[0].keyframe);
|
||||
}
|
||||
|
||||
// Frame-count cap: a run of pictures with no GOP/sequence boundary is force-flushed
|
||||
// during parse at MAX_PENDING_FRAMES, not held until EOF.
|
||||
#[test]
|
||||
fn gop_frame_cap_flushes_during_parse() {
|
||||
let mut parser = Mpeg2Parser::new();
|
||||
let mut data = Vec::new();
|
||||
for _ in 0..MAX_PENDING_FRAMES + 100 {
|
||||
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
||||
data.extend_from_slice(&[0xAA; 4]);
|
||||
}
|
||||
let out = parser.parse(&make_pes(data, Some(0)));
|
||||
assert_eq!(out.len(), MAX_PENDING_FRAMES, "cap flushed one full run");
|
||||
}
|
||||
|
||||
// Byte cap: few-but-huge pictures are force-flushed once the buffered bytes reach
|
||||
// MAX_PENDING_BYTES, even far below the frame cap.
|
||||
#[test]
|
||||
fn gop_byte_cap_flushes_during_parse() {
|
||||
let mut parser = Mpeg2Parser::new();
|
||||
let mut data = Vec::new();
|
||||
for _ in 0..2 {
|
||||
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
||||
data.extend(std::iter::repeat_n(0xAA, MAX_PENDING_BYTES / 2));
|
||||
}
|
||||
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
||||
data.extend_from_slice(&[0xAA; 4]);
|
||||
let out = parser.parse(&make_pes(data, Some(0)));
|
||||
assert_eq!(out.len(), 2, "byte cap flushed the two huge pictures");
|
||||
}
|
||||
|
||||
// Parser-level timing for a progressive sequence: rff counts 4 fields (tff=0) or 6 (tff=1)
|
||||
// only when the sequence extension's progressive_sequence is honoured.
|
||||
#[test]
|
||||
fn progressive_sequence_rff_durations_via_parser() {
|
||||
let mut parser = Mpeg2Parser::new();
|
||||
let field = 20_000_000u64; // 25 fps
|
||||
let mut au = make_seq_header(720, 576, 3, 3);
|
||||
au.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE, 0x10, 0x08]); // progressive_sequence
|
||||
au.extend_from_slice(&gop());
|
||||
for (ct, tr, tff) in [(1u8, 0u16, 0u8), (2, 1, 1)] {
|
||||
au.extend_from_slice(&make_picture_header_tr(ct, tr));
|
||||
au.extend_from_slice(&pic_coding_ext(tff, 1, 0, true));
|
||||
au.extend_from_slice(&[0xAA; 8]);
|
||||
}
|
||||
let f = parse_then_flush(&mut parser, &make_pes(au, Some(0)));
|
||||
assert_eq!(f.len(), 2);
|
||||
assert_eq!(f[0].duration_ns, Some(4 * field), "rff, tff=0 → 4 fields");
|
||||
assert_eq!(f[1].duration_ns, Some(6 * field), "rff, tff=1 → 6 fields");
|
||||
assert_eq!(f[1].pts_ns as u64, 4 * field);
|
||||
}
|
||||
|
||||
// Parser-level timing for field pictures: each occupies one field period.
|
||||
#[test]
|
||||
fn field_pictures_occupy_one_field_via_parser() {
|
||||
let mut parser = Mpeg2Parser::new();
|
||||
let field = 20_000_000u64; // 25 fps
|
||||
let mut au = make_seq_header(720, 576, 3, 3);
|
||||
au.extend_from_slice(&gop());
|
||||
for (ct, e2) in [(1u8, 0x01u8), (2, 0x02)] {
|
||||
au.extend_from_slice(&make_picture_header(ct));
|
||||
let mut ext = pic_coding_ext(0, 0, 0, false);
|
||||
ext[6] = e2;
|
||||
au.extend_from_slice(&ext);
|
||||
au.extend_from_slice(&[0xAA; 8]);
|
||||
}
|
||||
let f = parse_then_flush(&mut parser, &make_pes(au, Some(0)));
|
||||
assert_eq!(f.len(), 2);
|
||||
assert_eq!(f[0].duration_ns, Some(field));
|
||||
assert_eq!(f[1].duration_ns, Some(field));
|
||||
}
|
||||
|
||||
// --- parse_resolution: 12-bit field packing (ISO 13818-2 §6.2.2.1) ---
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -376,4 +376,38 @@ mod tests {
|
||||
sorted.dedup();
|
||||
assert_eq!(sorted.len(), all.len(), "no two frames share a display PTS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unanchored_gops_continue_after_the_previous_gop() {
|
||||
use CodingType::*;
|
||||
// GOPs 1-2 carry anchors (calibrating dur); GOPs 3-4 carry none and must
|
||||
// continue at origin + count*dur, not collide.
|
||||
let dur = 40_000_000i64;
|
||||
let mut r = SparsePtsReorder::new();
|
||||
let mut got: Vec<i64> = Vec::new();
|
||||
for anchor in [Some(0i64), Some(3 * dur), None, None] {
|
||||
for (k, ct) in [I, P, P].into_iter().enumerate() {
|
||||
let p = if k == 0 { anchor } else { None };
|
||||
got.extend(r.push(p, frame(ct, k == 0)).iter().map(|f| f.pts_ns));
|
||||
}
|
||||
}
|
||||
got.extend(r.flush().iter().map(|f| f.pts_ns));
|
||||
let want: Vec<i64> = (0..12).map(|i| i * dur).collect();
|
||||
assert_eq!(got, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_anchor_uses_fallback_duration() {
|
||||
use CodingType::*;
|
||||
let mut r = SparsePtsReorder::new();
|
||||
let mut got = Vec::new();
|
||||
for (k, ct) in [I, P, P].into_iter().enumerate() {
|
||||
got.extend(r.push((k == 0).then_some(1_000), frame(ct, k == 0)));
|
||||
}
|
||||
got.extend(r.flush());
|
||||
let pts: Vec<i64> = got.iter().map(|f| f.pts_ns).collect();
|
||||
let d = FALLBACK_FRAME_DUR_NS;
|
||||
assert_eq!(pts, vec![1_000, 1_000 + d, 1_000 + 2 * d]);
|
||||
assert!(got.iter().all(|f| f.duration_ns == Some(d as u64)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,4 +327,12 @@ mod tests {
|
||||
let data = [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
|
||||
assert_eq!(BitReader::new(&data).read_ue(), Some(u32::MAX >> 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_ue_thirty_two_leading_zeros_is_rejected() {
|
||||
// 32 zeros, a stop bit, 32 info bits: one past the cap, so None (not a code
|
||||
// whose `1 << 32` would overflow).
|
||||
let data = [0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00];
|
||||
assert_eq!(BitReader::new(&data).read_ue(), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user