libfreemkv: rc.5.2 SOTL video — full Windows fps, opening-GOP proof, self-sufficient log-level 3
Three Silence-of-the-Lambs (R2 PAL SD-DVD) follow-ups for rc.5.2. SUB-TASK 1 — Windows Explorer showed 12.5 fps (half) for the 576i25 track. Root cause: the DefaultDecodedFieldDuration (20 ms field) element rc.5.1 added to "fix" Windows fps did the opposite. With FlagInterlaced=1 + DefaultDuration=40 ms + DefaultDecodedFieldDuration=20 ms, Explorer halved to 12.5 fps and MediaInfo flipped to VFR. MakeMKV's correct rip omits the field-duration element, keeps FlagInterlaced=1 + FieldOrder=TFF + full-frame DefaultDuration (40 ms), and Explorer shows 25 fps / MediaInfo CFR. Fix: MkvTrack::video now passes field_duration_ns == 0 so the element is no longer written; the 1/DefaultDuration = 25 fps signal (the only one tools trust) is the full-frame value. Interlace signalling (FlagInterlaced, FieldOrder=TFF) is retained — MediaInfo reads scan type from the MPEG-2 ES picture coding extension, so it still reports Interlaced / Top Field First. Tests pin the new TrackEntry elements (element present/absent + values). SUB-TASK 2 — opening "menu"/still-frame video. Traced the MPEG-2 opening-GOP path; the wrong/last seq header and PTS-floor-to-0 hypotheses are RULED OUT with file:line evidence: codecPrivate is the FIRST sequence header (read once at headers-ready, mkvstream.rs:115 + pipelined_stream.rs:289), DVD VOBU structure guarantees each title opens on seq header + I-frame (no mid-GOP open), the parser back-anchors leading still-frames to the disc's real timeline (mpeg2.rs:296-303), and the muxer anchors base on the opening keyframe's real PTS so the t=0 floor (mkv.rs:963) never corrupts it. Regression tests pin all three (parser + muxer level). SUB-TASK 3 — make --log-level 3 self-sufficient (diag.rs + minimal hooks). (a) dump the ACTUAL MKV TrackEntry elements written per track (tag=mkv.track: FlagInterlaced, FieldOrder, DefaultDuration, field duration, Display dims, codecPrivate hex) so Windows-fps-class metadata is verifiable from a log alone. (b) capture the first ~100 coded frames per track (raw) to <output>.opening.bin with a per-frame summary line (tag=mkv.opening.frame: track, key/delta, size, PTS) so opening-GOP/menu issues are diagnosable from a future log without the disc. Both gated to log-level 3; normal runs open no side file and record nothing. CI gate (Rust 1.86): fmt --check, clippy -D warnings, and test --tests all green.
This commit is contained in:
@@ -834,6 +834,61 @@ mod tests {
|
||||
assert!(f[0].keyframe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_au_keeps_disc_pts_and_opening_seq_header_no_zero_floor() {
|
||||
// SOTL SUB-TASK 2 regression (opening-GOP / still-frame open). A DVD
|
||||
// title opens on a VOBU that begins with a sequence header + I-frame; the
|
||||
// disc stamps that opening I-frame at its REAL (non-zero) timeline PTS,
|
||||
// not 0. The parser must (a) emit the opening I-frame with that real PTS
|
||||
// — never floored to 0 — and (b) capture THAT opening sequence header as
|
||||
// codec_private (read at headers-ready, before any later AU). Proves the
|
||||
// opening pictures are emitted with the correct seq header + PTS, ruling
|
||||
// out the "wrong/last seq header" and "PTS floored to t=0" hypotheses.
|
||||
let mut p = Mpeg2Parser::new();
|
||||
|
||||
// Opening AU: seq header (the codecPrivate) + GOP + I-frame TR0 carrying
|
||||
// the disc's real opening PTS (2 s here, i.e. NOT zero). 25 fps PAL.
|
||||
let mut a = make_seq_header(720, 576, 3, 3); // 16:9, 25 fps
|
||||
a.extend_from_slice(&gop());
|
||||
a.extend_from_slice(&make_picture_header_tr(PICTURE_TYPE_I, 0));
|
||||
a.extend_from_slice(&[0xAA; 20]);
|
||||
let mut frames = p.parse(&make_pes(a, Some(180_000))); // PTS = 2 s (90 kHz)
|
||||
assert!(frames.is_empty(), "first AU waits for the next boundary");
|
||||
|
||||
// Second picture (no PTS) closes the opening AU: the I-frame emits and
|
||||
// the opening sequence header is captured (headers-ready timing — the
|
||||
// consumer reads codec_private once the first AU drains).
|
||||
let mut b = make_picture_header_tr(3, 1);
|
||||
b.extend_from_slice(&[0xBB; 20]);
|
||||
frames.extend(p.parse(&make_pes(b, None)));
|
||||
|
||||
// codec_private is the OPENING sequence header (read at headers-ready,
|
||||
// before any later AU could replace it).
|
||||
let cp = p
|
||||
.codec_private()
|
||||
.expect("opening seq header captured at headers-ready");
|
||||
assert_eq!(
|
||||
&cp[..4],
|
||||
&[0x00, 0x00, 0x01, SEQ_HEADER_CODE],
|
||||
"codec_private is the opening sequence header"
|
||||
);
|
||||
assert_eq!(p.resolution(), Some((720, 576)), "576i opening header");
|
||||
assert_eq!(p.frame_rate(), Some((25, 1)), "25 fps opening header");
|
||||
|
||||
frames.extend(p.flush());
|
||||
|
||||
assert_eq!(frames.len(), 2);
|
||||
assert!(frames[0].keyframe, "opening picture is the I-frame");
|
||||
assert_eq!(
|
||||
frames[0].pts_ns, 2_000_000_000,
|
||||
"opening I-frame keeps the disc's real PTS (2 s), NOT floored to 0"
|
||||
);
|
||||
assert_eq!(
|
||||
frames[1].pts_ns, 2_040_000_000,
|
||||
"next frame is one 40 ms interval later on the real timeline"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Sequence header → codec_private ---
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user