From 5b702a76a760ba6ac3b329e4deca3fd3237dd746 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:01:13 -0700 Subject: [PATCH] mux: write PTS (presentation), not DTS, as the MKV block timecode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The video codec parsers (HEVC, H.264, VC-1, MPEG-2) used pes.dts.or(pes.pts) as each frame's timestamp. MKV block timecodes are presentation timestamps; frames are stored in decode order and the player reorders for display by timecode. Using DTS makes the timecode monotonic in storage order, presenting B-frames in decode order — visible motion judder / wrong frames on playback, and PTS-based seeking lands on the wrong frame. The compressed video was always byte-correct (verified by NAL-level diff against a known-good demux); this was purely a timestamp defect affecting every B-frame title. Fix: prefer PTS (pes.pts.or(pes.dts)). Verified on a real UHD iso->mkv: emitted PTS now reorders for B-frames identically to a reference muxer. Update the two tests that asserted the old DTS-preferred behavior and add an HEVC regression test pinning PTS as the block timecode. --- src/mux/codec/h264.rs | 19 +++++++++++-------- src/mux/codec/hevc.rs | 36 ++++++++++++++++++++++++++++++++++-- src/mux/codec/mpeg2.rs | 6 +++++- src/mux/codec/vc1.rs | 18 +++++++++++------- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/mux/codec/h264.rs b/src/mux/codec/h264.rs index 552282d..15e9913 100644 --- a/src/mux/codec/h264.rs +++ b/src/mux/codec/h264.rs @@ -38,8 +38,11 @@ impl CodecParser for H264Parser { return Vec::new(); } - // Use DTS when available (monotonic for B-frame content), fall back to PTS - let pts_ns = pes.dts.or(pes.pts).map(pts_to_ns).unwrap_or(0); + // MKV block timecodes are PRESENTATION timestamps; frames are stored in + // decode order and the player reorders by timecode. Use PTS, not DTS — + // DTS presents B-frames in decode order (visible judder) and breaks + // PTS-based seeking. Fall back to DTS only if PTS is absent. + let pts_ns = pes.pts.or(pes.dts).map(pts_to_ns).unwrap_or(0); // Scan NAL units for SPS, PPS, and IDR detection let mut keyframe = false; @@ -451,10 +454,10 @@ mod tests { assert!(frames.is_empty()); } - // --- DTS preferred over PTS when present --- + // --- PTS (presentation) used for the MKV block timecode, not DTS --- #[test] - fn dts_preferred_over_pts() { + fn pts_preferred_over_dts() { let mut parser = H264Parser::new(); let mut data = Vec::new(); @@ -464,13 +467,13 @@ mod tests { let pes = PesPacket { pid: 0x1011, - pts: Some(180000), // 2 seconds - dts: Some(90000), // 1 second + pts: Some(180000), // 2 seconds (presentation) + dts: Some(90000), // 1 second (decode) data, }; let frames = parser.parse(&pes); assert_eq!(frames.len(), 1); - // DTS should be used, not PTS - assert_eq!(frames[0].pts_ns, 1_000_000_000); + // PTS must be used — MKV block timecodes are presentation timestamps. + assert_eq!(frames[0].pts_ns, 2_000_000_000); } } diff --git a/src/mux/codec/hevc.rs b/src/mux/codec/hevc.rs index 6b1f76a..b9160d0 100644 --- a/src/mux/codec/hevc.rs +++ b/src/mux/codec/hevc.rs @@ -48,8 +48,13 @@ impl CodecParser for HevcParser { return Vec::new(); } - // Use DTS when available (monotonic for B-frame content), fall back to PTS - let pts_ns = pes.dts.or(pes.pts).map(pts_to_ns).unwrap_or(0); + // MKV block timecodes are PRESENTATION timestamps; frames are stored + // in decode order (the order they arrive here) and the player reorders + // for display by timecode. So use PTS, not DTS — using DTS makes the + // block timecode monotonic in storage order, which presents B-frames in + // decode order (visible judder / wrong frames) and breaks PTS-based + // seeking. Fall back to DTS only if PTS is somehow absent. + let pts_ns = pes.pts.or(pes.dts).map(pts_to_ns).unwrap_or(0); let data = &pes.data; let mut keyframe = false; // Pre-size: output is ~input bytes with a few 4-byte length @@ -583,6 +588,33 @@ mod tests { assert_eq!(frames[0].pts_ns, 1_000_000_000); } + // --- PTS (presentation), not DTS, drives the MKV block timecode --- + // Regression for B-frame presentation: writing DTS as the block timecode + // presents frames in decode order (visible judder) and breaks seeking. + + #[test] + fn pts_preferred_over_dts() { + let mut parser = HevcParser::new(); + + let mut data = Vec::new(); + data.extend_from_slice(&[0x00, 0x00, 0x01]); + data.extend_from_slice(&hevc_nal_header(1)); // TRAIL_R slice + data.extend_from_slice(&[0x10, 0x20]); + + let pes = PesPacket { + pid: 0x1011, + pts: Some(180000), // 2 s (presentation) + dts: Some(90000), // 1 s (decode) + data, + }; + let frames = parser.parse(&pes); + assert_eq!(frames.len(), 1); + assert_eq!( + frames[0].pts_ns, 2_000_000_000, + "block timecode must be PTS" + ); + } + // --- Dolby Vision enhancement layer --- #[test] diff --git a/src/mux/codec/mpeg2.rs b/src/mux/codec/mpeg2.rs index b10e194..f3783fe 100644 --- a/src/mux/codec/mpeg2.rs +++ b/src/mux/codec/mpeg2.rs @@ -96,7 +96,11 @@ impl CodecParser for Mpeg2Parser { return Vec::new(); } - let pts_ns = pes.dts.or(pes.pts).map(pts_to_ns).unwrap_or(0); + // MKV block timecodes are PRESENTATION timestamps; frames are stored in + // decode order and the player reorders by timecode. Use PTS, not DTS — + // DTS presents B-frames in decode order (visible judder) and breaks + // PTS-based seeking. Fall back to DTS only if PTS is absent. + let pts_ns = pes.pts.or(pes.dts).map(pts_to_ns).unwrap_or(0); let data = &pes.data; let mut keyframe = false; let mut has_picture = false; diff --git a/src/mux/codec/vc1.rs b/src/mux/codec/vc1.rs index 24dbde9..d1b9e88 100644 --- a/src/mux/codec/vc1.rs +++ b/src/mux/codec/vc1.rs @@ -41,8 +41,11 @@ impl CodecParser for Vc1Parser { return Vec::new(); } - // Use DTS when available (monotonic for B-frame content), fall back to PTS - let ts_ns = pes.dts.or(pes.pts).map(pts_to_ns).unwrap_or(0); + // MKV block timecodes are PRESENTATION timestamps; frames are stored in + // decode order and the player reorders by timecode. Use PTS, not DTS — + // DTS presents B-frames in decode order (visible judder) and breaks + // PTS-based seeking. Fall back to DTS only if PTS is absent. + let ts_ns = pes.pts.or(pes.dts).map(pts_to_ns).unwrap_or(0); let mut has_seq_header = false; let mut has_entry_point = false; let mut frame_start: Option = None; @@ -419,10 +422,10 @@ mod tests { assert_eq!(frames[0].pts_ns, 1_000_000_000); } - // --- DTS preferred over PTS --- + // --- PTS (presentation) used for the MKV block timecode, not DTS --- #[test] - fn dts_preferred_over_pts() { + fn pts_preferred_over_dts() { let mut parser = Vc1Parser::new(); let mut data = Vec::new(); @@ -431,13 +434,14 @@ mod tests { let pes = PesPacket { pid: 0x1011, - pts: Some(180000), - dts: Some(90000), + pts: Some(180000), // presentation + dts: Some(90000), // decode data, }; let frames = parser.parse(&pes); assert_eq!(frames.len(), 1); - assert_eq!(frames[0].pts_ns, 1_000_000_000); + // PTS must be used — MKV block timecodes are presentation timestamps. + assert_eq!(frames[0].pts_ns, 2_000_000_000); } // --- find_next_sc utility ---