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:
@@ -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
@@ -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
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user