mux: EditionFlagDefault uses its registered ID, SeekHead refuses an unreserved target; tests for decode_ts reorder clamp, VobSub stop, field order, late AAC CodecPrivate

This commit is contained in:
Matthew Jackson
2026-10-01 15:14:54 -07:00
parent c37ef82dbf
commit 92b14006a2
4 changed files with 187 additions and 6 deletions
+63
View File
@@ -351,6 +351,69 @@ fn h264_paff_pairs_come_from_the_slice_header() {
);
}
#[test]
fn start_up_spacing_is_floored_at_two_field_periods() {
// PAFF at R = 1 with no stated frame rate: T is the smallest first-field gap (1500),
// below 2 x the 1800-tick field delta, so the floor keeps the second field's start-up
// DTS below the next unit's.
let sps = H264Sps {
frame_mbs_only: false,
height_map_units: 34,
vui: Some(H264Vui {
restriction: Some((1, 2)),
..Default::default()
}),
..Default::default()
};
let (gap, field) = (1_500, 1_800);
let mut frames = Vec::new();
for u in 0..4i64 {
for bottom in [false, true] {
let mut es = if u == 0 && !bottom {
length_prefixed(&[sps.nal()])
} else {
Vec::new()
};
let slice = h264_slice(&sps, u == 0 && !bottom, 0, u as u32, Some(bottom));
es.extend(length_prefixed(&[slice]));
frames.push((B + u * gap + bottom as i64 * field, es));
}
}
// The start-up window: first field, its second field, and the next unit.
frames.truncate(3);
let mut d = DtsDeriver::for_codec(Codec::H264, None);
let dts = run(&mut d, &frames);
assert_eq!(dts[0], Some(B - 2 * field));
assert_eq!(d.counters(), DtsCounters::default(), "no order violation");
assert_conformant(&frames, &dts);
}
#[test]
fn reorder_depth_is_clamped_and_the_order_statistic_window_is_exact_at_the_clamp() {
// A corrupt SPS claiming 200 reordered frames is held to MAX_REORDER.
let sps = sps_timed(200);
let d = DtsDeriver::for_codec(Codec::H264, Some(&avcc(&sps.nal())));
assert_eq!(d.params().0, MAX_REORDER);
// In-order delivery at R = 16 over more than KEEP units: DTS_k is the PTS of unit k - 16.
let sps = sps_timed(MAX_REORDER as u32);
let mut d = DtsDeriver::for_codec(Codec::H264, Some(&avcc(&sps.nal())));
let n = 3 * KEEP as i64;
let frames: Vec<_> = (0..n)
.map(|k| (B + k * F, h264_frame(&sps, k == 0, k as u32)))
.collect();
let dts = run(&mut d, &frames);
let want: Vec<_> = (0..n)
.map(|k| Some(B + (k - MAX_REORDER as i64) * F))
.collect();
assert_eq!(dts, want);
assert_eq!(
d.core.top.len(),
KEEP,
"only KEEP unit PTS values are retained"
);
}
#[test]
fn h264_field_coded_r_upgrade_allots_one_slot_per_field() {
// 25 fps PAFF: clip 1 R = 0 (I/P), clip 2 IDR activates R = 2 (B-pyramid). The
+1 -1
View File
@@ -611,7 +611,7 @@ pub const CHAPTER_UID: u32 = 0x73C4;
pub const CHAPTER_TIME_START: u32 = 0x91;
pub const CHAPTER_DISPLAY: u32 = 0x80;
pub const CHAP_STRING: u32 = 0x85;
pub const EDITION_FLAG_DEFAULT: u32 = 0xDB;
pub const EDITION_FLAG_DEFAULT: u32 = 0x45DB;
pub const CHAPTER_FLAG_HIDDEN: u32 = 0x98;
pub const CHAPTER_FLAG_ENABLED: u32 = 0x4598;
pub const CHAP_LANGUAGE: u32 = 0x437C;
+102 -5
View File
@@ -2061,7 +2061,8 @@ impl<W: Write + Seek> MkvMuxer<W> {
None => return Err(crate::error::Error::MkvUnencodable.into()),
},
ebml::CUES => cues_offset,
_ => 0,
// The SeekHead reserves only the targets above.
_ => return Err(crate::error::Error::MkvUnencodable.into()),
};
self.writer
.seek(std::io::SeekFrom::Start(fixup.value_offset))?;
@@ -2975,6 +2976,47 @@ mod tests {
);
}
// A progressive leader on an interlaced MPEG-2 title leaves the reserved FieldOrder to be
// filled from the majority field order: TFF and BFF titles must not swap.
#[test]
fn reserved_field_order_follows_the_measured_majority() {
use crate::mux::codec::FieldOrder;
use std::sync::{Arc, Mutex};
for (scan, want) in [
(FieldOrder::Tff, ebml::FIELD_ORDER_TFF),
(FieldOrder::Bff, ebml::FIELD_ORDER_BFF),
] {
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let mut track = make_video_track();
track.codec_id = ebml::CODEC_MPEG2;
let mut muxer =
MkvMuxer::new(SharedWriter(shared.clone()), &[track], None, 0.0, &[]).unwrap();
let scans = [FieldOrder::Progressive, scan, scan, scan];
for (i, scan) in scans.into_iter().enumerate() {
let pts = i as i64 * 40_000_000;
muxer
.write_frame_at(
0,
pts,
i == 0,
&[1, 2, i as u8],
None,
None,
None,
Some(scan),
)
.unwrap();
}
muxer.finish().unwrap();
let data = shared.lock().unwrap().clone().into_inner();
let at = data
.windows(2)
.position(|w| w == [ebml::FIELD_ORDER as u8, 0x81])
.expect("FieldOrder element present");
assert_eq!(data[at + 2], want as u8);
}
}
#[test]
fn mkv_write_frame_creates_cluster() {
let buf = Cursor::new(Vec::new());
@@ -5528,6 +5570,27 @@ mod tests {
assert_eq!(groups[0].duration, 29_127, "SPU stop time, not a 1 s guess");
}
// The stop time is found past every command that precedes it in the first DCSQ.
#[test]
fn vobsub_stop_is_found_after_commands_with_arguments() {
let mut track = make_subtitle_track();
track.codec_id = ebml::CODEC_VOBSUB;
let mut spu = vec![0, 0, 0, 6, 0xAB, 0xCD];
// delay 0x0100, next = itself; STA_DSP, SET_COLOR, SET_CONTR, SET_DAREA, SET_DSPXA, STP_DSP.
spu.extend_from_slice(&[0x01, 0x00, 0, 6, 0x01]);
spu.extend_from_slice(&[0x03, 0x11, 0x22, 0x04, 0x33, 0x44]);
spu.extend_from_slice(&[0x05, 1, 2, 3, 4, 5, 6, 0x06, 1, 2, 3, 4, 0x02, 0xFF]);
let len = spu.len() as u16;
spu[..2].copy_from_slice(&len.to_be_bytes());
let data = mux_with_durations(&[track], &[(0, 0, true, spu, None)]);
let groups = all_block_groups(&data);
assert_eq!(groups.len(), 1);
assert_eq!(
groups[0].duration, 29_127,
"the real stop, not the 10 s cap"
);
}
// A VobSub SPU with no stop command ends at the next SPU on its track, else
// after a 10 s cap.
#[test]
@@ -5557,10 +5620,9 @@ mod tests {
);
let groups = all_block_groups(&data);
assert_eq!(groups.len(), 1);
assert!(
groups[0].duration > 10_000,
"a self-terminating cue must not be hidden after 1 s (got {} ticks)",
groups[0].duration
assert_eq!(
groups[0].duration, 300_000,
"a self-terminating cue lasts the 30 s fallback, not 1 s"
);
}
@@ -5703,6 +5765,41 @@ mod tests {
assert!(find_id(&data, ebml::CHANNELS).is_some(), "Channels present");
}
// A late AAC CodecPrivate fills the 16-byte reserve exactly, whatever its length: the
// TrackEntry still parses and the bytes after it are untouched.
#[test]
fn late_aac_codec_private_fills_the_reserve_exactly() {
for len in [2usize, 11, 12, 13] {
let mut aac = make_audio_track();
aac.codec_id = ebml::CODEC_AAC;
let mut muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[aac], None, 0.0, &[]).unwrap();
let cp: Vec<u8> = (1..=len as u8).collect();
assert!(muxer.set_codec_private(0, &cp).unwrap(), "len {len}");
let data = muxer.writer.into_inner();
let (te_start, te_size) = first_track_entry(&data);
let children = master_children(&data, te_start, te_size);
let (_, off, size) = *children
.iter()
.find(|(id, _, _)| *id == ebml::CODEC_PRIVATE)
.expect("CodecPrivate present");
assert_eq!(&data[off..off + size as usize], &cp[..], "len {len}");
let end = children.iter().map(|&(_, o, s)| o + s as usize).max();
assert_eq!(
end,
Some(te_start + te_size),
"len {len}: children fill the entry"
);
assert!(children.iter().any(|(id, _, _)| *id == ebml::AUDIO));
}
let mut aac = make_audio_track();
aac.codec_id = ebml::CODEC_AAC;
let mut muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[aac], None, 0.0, &[]).unwrap();
assert!(
!muxer.set_codec_private(0, &[0u8; 14]).unwrap(),
"does not fit"
);
}
#[test]
fn video_colour_element_emitted_only_when_hdr_metadata_present() {
// A video track with colour metadata (matrix/transfer) must emit the
+21
View File
@@ -7598,6 +7598,27 @@ mod readback_tests {
v
}
// EditionFlagDefault is the two-byte ID 0x45DB mkvmerge writes; the default edition wins.
#[test]
fn the_default_edition_is_read_by_its_registered_id() {
let atom = |name: &str| {
let mut a = Vec::new();
ebml::write_uint(&mut a, ebml::CHAPTER_TIME_START, 0).unwrap();
let mut d = Vec::new();
ebml::write_string(&mut d, ebml::CHAP_STRING, name).unwrap();
a.extend(el(ebml::CHAPTER_DISPLAY, &d));
el(ebml::CHAPTER_ATOM, &a)
};
let first = el(ebml::EDITION_ENTRY, &atom("first"));
let mut second = vec![0x45, 0xDB, 0x81, 0x01];
second.extend(atom("second"));
let mut body = first;
body.extend(el(ebml::EDITION_ENTRY, &second));
let chapters = parse_chapters(&body).unwrap();
assert_eq!(chapters.len(), 1);
assert_eq!(chapters[0].name, "second");
}
fn encoding(order: u64, algo: u64, settings: &[u8]) -> Vec<u8> {
let mut comp = Vec::new();
ebml::write_uint(&mut comp, CONTENT_COMP_ALGO, algo).unwrap();