libfreemkv 0.31.5: do not force monotonic block timestamps on video

B-frame video PTS is legitimately non-monotonic in decode/storage order; the
audio-oriented monotonic nudge was clobbering it to prev+1ms, which decoders
flagged as non-monotonic DTS (thousands per title). Apply the nudge to
audio/subtitle only; video keeps its true PES PTS. + regression test.
This commit is contained in:
Matthew Jackson
2026-06-08 09:00:35 -07:00
parent f79c2a0aa9
commit 41a6d89cd1
3 changed files with 88 additions and 7 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog # Changelog
## 0.31.5 (2026-06-08)
### Fixed
- MKV mux: stop forcing strictly-monotonic block timestamps on the **video**
track. The monotonic nudge (added for audio PES that collide on a
millisecond) was clobbering B-frame video PTS — which are legitimately
non-monotonic in decode/storage order — to prev+1ms. A `copy` remux
preserved the wrong value, but decoding derived DTS from the HEVC POC and
found them colliding, emitting thousands of "non monotonically increasing
dts" warnings per title. Video now keeps its true PES PTS (Matroska
SimpleBlock permits non-monotonic block timestamps); audio/subtitle tracks
still get the nudge. Verified: a re-mux drops the warning count to zero.
## 0.31.4 (2026-06-08) ## 0.31.4 (2026-06-08)
Test cleanup — no runtime changes. Removed 144 unit tests flagged in adversarial Test cleanup — no runtime changes. Removed 144 unit tests flagged in adversarial
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "libfreemkv" name = "libfreemkv"
version = "0.31.4" version = "0.31.5"
edition = "2024" edition = "2024"
rust-version = "1.86" rust-version = "1.86"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
+73 -6
View File
@@ -270,6 +270,26 @@ fn monotonic_ts(prev: Option<i64>, pts_ms: i64) -> i64 {
} }
} }
/// Per-track block timestamp. The strictly-monotonic nudge is applied to
/// AUDIO/SUBTITLE tracks only; VIDEO (track 0) is returned UNCHANGED.
///
/// With B-frames, a video frame's presentation PTS is legitimately
/// non-monotonic in decode/storage order (a B-frame sits between its anchors,
/// below the frame stored just before it). Forcing it strictly-increasing
/// clobbers those PTS to prev+1ms — a `copy` remux preserves the (wrong) value,
/// but a decoder derives DTS from the HEVC POC and finds them colliding
/// ("non monotonically increasing dts", thousands per title). Matroska
/// SimpleBlock permits non-monotonic block timestamps (signed block-relative
/// offsets), so video keeps its true PES PTS; only no-reorder tracks (audio,
/// subtitles), where a same-millisecond collision IS a real defect, get nudged.
fn block_ts(track_idx: usize, prev: Option<i64>, pts_ms: i64) -> i64 {
if track_idx == 0 {
pts_ms
} else {
monotonic_ts(prev, pts_ms)
}
}
/// Encode a Matroska track number as an EBML VINT into a stack buffer, /// Encode a Matroska track number as an EBML VINT into a stack buffer,
/// returning the buffer and the used length. Track numbers are small (1-based, /// returning the buffer and the used length. Track numbers are small (1-based,
/// a handful of tracks), so 1 byte covers `< 0x80` and 2 bytes covers the rest; /// a handful of tracks), so 1 byte covers `< 0x80` and 2 bytes covers the rest;
@@ -550,12 +570,19 @@ impl<W: Write + Seek> MkvMuxer<W> {
// kept keyframe are clamped to t=0 rather than corrupting the timeline. // kept keyframe are clamped to t=0 rather than corrupting the timeline.
let pts_ms = (raw_ms - base).max(0); let pts_ms = (raw_ms - base).max(0);
// Enforce strictly-monotonic per-track block timestamps. Some audio PES // Strictly-monotonic block timestamps — AUDIO/SUBTITLE ONLY. Some audio
// PTS truncate to the same millisecond as the previous frame (or, rarely, // PES PTS truncate to the same millisecond as the previous frame (or
// tick back 1ms), which surfaces as "non-monotonic DTS" and is rejected // tick back 1ms); nudge those to prev+1ms (sub-frame, inaudible).
// by ffmpeg/strict players. Nudge to prev+1ms — sub-frame, inaudible, //
// and A/V sync is unaffected at millisecond granularity. // VIDEO (track 0) is EXEMPT: with B-frames, presentation PTS is
let pts_ms = monotonic_ts(self.last_pts_ms.get(&track_idx).copied(), pts_ms); // legitimately non-monotonic in decode/storage order (a B-frame's PTS
// sits between its anchors, below the frame stored before it). Forcing
// it strictly-increasing clobbers those PTS to prev+1ms, which a `copy`
// remux preserves but a decoder rejects — it derives DTS from the HEVC
// POC and finds them colliding ("non monotonically increasing dts").
// Matroska SimpleBlock permits non-monotonic block timestamps (negative
// block-relative offsets), so leave the true PES PTS intact for video.
let pts_ms = block_ts(track_idx, self.last_pts_ms.get(&track_idx).copied(), pts_ms);
let needs_new_cluster = !self.cluster_open let needs_new_cluster = !self.cluster_open
|| (is_video_key && (pts_ms - self.cluster_ts_ms) >= CLUSTER_DURATION_MS); || (is_video_key && (pts_ms - self.cluster_ts_ms) >= CLUSTER_DURATION_MS);
@@ -974,6 +1001,46 @@ mod tests {
assert_eq!(out, [1000, 1001, 1002, 1003, 1032, 1033, 1064]); assert_eq!(out, [1000, 1001, 1002, 1003, 1032, 1033, 1064]);
} }
#[test]
fn block_ts_exempts_video_from_monotonic_nudge() {
// VIDEO (track 0) keeps its true PTS even when non-monotonic in storage
// order — a B-frame whose presentation PTS sits below the frame stored
// before it must NOT be nudged to prev+1ms (that clobbering is what
// produced the "non monotonically increasing dts" flood on decode).
assert_eq!(
block_ts(0, Some(1040), 1000),
1000,
"video B-frame PTS preserved"
);
assert_eq!(
block_ts(0, Some(1000), 1000),
1000,
"video dup-ms PTS preserved"
);
// A realistic decode-order GOP (I, then B-frames dipping below it):
// every value passes through untouched for video.
let gop = [1000i64, 960, 920, 1080, 1040];
let mut prev = None;
let out: Vec<i64> = gop
.iter()
.map(|&p| {
let t = block_ts(0, prev, p);
prev = Some(t);
t
})
.collect();
assert_eq!(out, gop, "video timestamps must be left exactly as-is");
// AUDIO/SUBTITLE (track != 0) still get the strictly-monotonic nudge —
// a same-ms collision there is a real defect.
assert_eq!(block_ts(1, Some(1000), 1000), 1001, "audio dup-ms nudged");
assert_eq!(
block_ts(2, Some(1001), 1000),
1002,
"subtitle back-tick nudged"
);
}
#[test] #[test]
fn mkv_multiple_tracks() { fn mkv_multiple_tracks() {
let buf = Cursor::new(Vec::new()); let buf = Cursor::new(Vec::new());