mux: write PTS (presentation), not DTS, as the MKV block timecode
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.
This commit is contained in:
+11
-8
@@ -38,8 +38,11 @@ impl CodecParser for H264Parser {
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use DTS when available (monotonic for B-frame content), fall back to PTS
|
// MKV block timecodes are PRESENTATION timestamps; frames are stored in
|
||||||
let pts_ns = pes.dts.or(pes.pts).map(pts_to_ns).unwrap_or(0);
|
// 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
|
// Scan NAL units for SPS, PPS, and IDR detection
|
||||||
let mut keyframe = false;
|
let mut keyframe = false;
|
||||||
@@ -451,10 +454,10 @@ mod tests {
|
|||||||
assert!(frames.is_empty());
|
assert!(frames.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DTS preferred over PTS when present ---
|
// --- PTS (presentation) used for the MKV block timecode, not DTS ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dts_preferred_over_pts() {
|
fn pts_preferred_over_dts() {
|
||||||
let mut parser = H264Parser::new();
|
let mut parser = H264Parser::new();
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
@@ -464,13 +467,13 @@ mod tests {
|
|||||||
|
|
||||||
let pes = PesPacket {
|
let pes = PesPacket {
|
||||||
pid: 0x1011,
|
pid: 0x1011,
|
||||||
pts: Some(180000), // 2 seconds
|
pts: Some(180000), // 2 seconds (presentation)
|
||||||
dts: Some(90000), // 1 second
|
dts: Some(90000), // 1 second (decode)
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
let frames = parser.parse(&pes);
|
let frames = parser.parse(&pes);
|
||||||
assert_eq!(frames.len(), 1);
|
assert_eq!(frames.len(), 1);
|
||||||
// DTS should be used, not PTS
|
// PTS must be used — MKV block timecodes are presentation timestamps.
|
||||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
assert_eq!(frames[0].pts_ns, 2_000_000_000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-2
@@ -48,8 +48,13 @@ impl CodecParser for HevcParser {
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use DTS when available (monotonic for B-frame content), fall back to PTS
|
// MKV block timecodes are PRESENTATION timestamps; frames are stored
|
||||||
let pts_ns = pes.dts.or(pes.pts).map(pts_to_ns).unwrap_or(0);
|
// 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 data = &pes.data;
|
||||||
let mut keyframe = false;
|
let mut keyframe = false;
|
||||||
// Pre-size: output is ~input bytes with a few 4-byte length
|
// 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);
|
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 ---
|
// --- Dolby Vision enhancement layer ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -96,7 +96,11 @@ impl CodecParser for Mpeg2Parser {
|
|||||||
return Vec::new();
|
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 data = &pes.data;
|
||||||
let mut keyframe = false;
|
let mut keyframe = false;
|
||||||
let mut has_picture = false;
|
let mut has_picture = false;
|
||||||
|
|||||||
+11
-7
@@ -41,8 +41,11 @@ impl CodecParser for Vc1Parser {
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use DTS when available (monotonic for B-frame content), fall back to PTS
|
// MKV block timecodes are PRESENTATION timestamps; frames are stored in
|
||||||
let ts_ns = pes.dts.or(pes.pts).map(pts_to_ns).unwrap_or(0);
|
// 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_seq_header = false;
|
||||||
let mut has_entry_point = false;
|
let mut has_entry_point = false;
|
||||||
let mut frame_start: Option<usize> = None;
|
let mut frame_start: Option<usize> = None;
|
||||||
@@ -419,10 +422,10 @@ mod tests {
|
|||||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
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]
|
#[test]
|
||||||
fn dts_preferred_over_pts() {
|
fn pts_preferred_over_dts() {
|
||||||
let mut parser = Vc1Parser::new();
|
let mut parser = Vc1Parser::new();
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
@@ -431,13 +434,14 @@ mod tests {
|
|||||||
|
|
||||||
let pes = PesPacket {
|
let pes = PesPacket {
|
||||||
pid: 0x1011,
|
pid: 0x1011,
|
||||||
pts: Some(180000),
|
pts: Some(180000), // presentation
|
||||||
dts: Some(90000),
|
dts: Some(90000), // decode
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
let frames = parser.parse(&pes);
|
let frames = parser.parse(&pes);
|
||||||
assert_eq!(frames.len(), 1);
|
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 ---
|
// --- find_next_sc utility ---
|
||||||
|
|||||||
Reference in New Issue
Block a user