diff --git a/src/mux/decode_ts/tests.rs b/src/mux/decode_ts/tests.rs index b184402d..68a1701b 100644 --- a/src/mux/decode_ts/tests.rs +++ b/src/mux/decode_ts/tests.rs @@ -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 diff --git a/src/mux/ebml.rs b/src/mux/ebml.rs index a8396f8d..4f5e1693 100644 --- a/src/mux/ebml.rs +++ b/src/mux/ebml.rs @@ -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; diff --git a/src/mux/mkv.rs b/src/mux/mkv.rs index dff006f2..a4fb0b64 100644 --- a/src/mux/mkv.rs +++ b/src/mux/mkv.rs @@ -2061,7 +2061,8 @@ impl MkvMuxer { 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 = (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 diff --git a/src/mux/mkvstream.rs b/src/mux/mkvstream.rs index 1924f9a2..9497143a 100644 --- a/src/mux/mkvstream.rs +++ b/src/mux/mkvstream.rs @@ -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 { let mut comp = Vec::new(); ebml::write_uint(&mut comp, CONTENT_COMP_ALGO, algo).unwrap();