Place Blu-ray clips by the playlist's marks, not by guessing at PTS jumps

A seamless-branching title's PlayItems do not chain contiguously: one
clip's OUT can sit after the next clip's IN, where the disc stores the
join twice, or before it, where the playlist skips material. The mux
never saw those marks — its own header said so — and inferred seams from
PTS jumps instead.

Inference cannot recover this. A forward jump is ambiguous: it means the
playlist skipped, or it means frames were lost to damaged media, and
compressing the latter would falsify timing on exactly the rips that most
need it faithful. An overlap smaller than the B-frame reorder threshold
is invisible to inference entirely, and its duplicate content then
collided in the muxer, where the monotonic nudge flattened a run of audio
onto the tick floor and put sound ahead of picture for the rest of the
film.

Measured on one 11-PlayItem title: the file declared 7893.385 s, which is
what the playlist says the title is, and carried packets to 8029.298 s.
Both numbers came from the same program on the same disc. Four skips
totalling 135.9 s became dead timeline, and a 1.79 s overlap put audio
1.8 s ahead at the half-hour mark. Five of forty-seven titles were
affected; every single-clip title was exact.

So the marks are read. Each clip contributes exactly out - in, laid end to
end, so the output runs as long as the playlist says and a join never
rewinds. Titles without usable marks — DVD, HD-DVD, file sources — keep
the inference path unchanged, and clips that already chain contiguously
produce a constant offset, which is pinned by a test.
This commit is contained in:
Matthew Jackson
2026-08-05 15:24:31 -07:00
parent dc7c3a7db5
commit dfd2f023d0
4 changed files with 445 additions and 3 deletions
+6 -2
View File
@@ -742,7 +742,7 @@ impl DemuxSink {
tracks,
ref_video_track,
ref_first_pts_ns: None,
timeline: TimelineContinuity::new(),
timeline: TimelineContinuity::with_clips(&title.clips),
finished: false,
})
}
@@ -866,7 +866,11 @@ impl Stream for DemuxSink {
// an audio ES before the video ES, in which case track 0 is audio and a
// non-video epoch driver would ratchet the frontier on sparse/lagging PTS.
let drives = Some(frame.track) == self.ref_video_track;
let pts = self.timeline.adjust(frame.pts, drives);
// See `MkvMuxer::write_frame`: `None` is material outside the
// playlist's clip marks and is dropped rather than emitted.
let Some(pts) = self.timeline.map(frame.pts, drives) else {
return Ok(());
};
if drives {
// Delay reference: recorded here, not in the track's `TrackOut`, so
// it survives the `audio://` / `sub://` kind filter dropping the