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