Place a non-advancing mark table by provenance

Marks that do not advance across a title are normal, not a defect: each
clip file carries its own STC, so one clip's IN has no ordering
relationship to the previous clip's. Refusing those tables dropped
exactly the branched titles this type exists for onto the inference path
that cannot read them, and that is the overrun.

Lifting it was tried once before every track carried a source offset, and
nine audio and subtitle tracks with nothing to place them by pinned
themselves to clip 0 and dropped most of the title. Every parser stamps
provenance now, so the clip comes from the byte offset and the marks keep
one job: whether a frame lies inside its own clip's [in, out].

Still gated on spans_trusted. Without usable spans there is no offset to
place by and inference is all that is left, so a table inference cannot
read is still refused rather than silently truncating a title.

A frame reaching the timestamp path under a plan now says so once per
track. It is not expected any more, and it is how a mostly-dropped track
reached a user without a single line in the log.
This commit is contained in:
Matthew Jackson
2026-08-07 07:49:53 -07:00
parent d4a32d3a26
commit 991977f297
+88 -41
View File
@@ -207,34 +207,38 @@ impl SeamPlan {
); );
return None; return None;
} }
// Inferring a clip from a timestamp assumes the marks are points on // Marks that do not advance across the title are NORMAL, not a
// ONE clock that advances across the whole title: a join is // defect: each clip file carries its own STC, so one clip's IN has
// recognised by a frame landing at the next clip's IN, and a frame // no ordering relationship to the previous clip's at all.
// is assigned to a clip by falling inside its [in, out].
// //
// Not every title is like that — a playlist whose clips each restart // Inferring a clip from a timestamp cannot read such a table — a
// their own STC has marks that all cover the same low values. Under // crossing can be missed, and a missed crossing strands the track
// such a table a crossing can be missed, and a missed crossing // on a clip it has already left — so this was refused outright.
// STRANDS the track on the clip it was on: every later frame then // That refusal was about INFERENCE, and it cost the titles it was
// falls outside that clip's marks and is dropped, silently, for the // meant to protect: they fell back to the very path that cannot
// rest of the title. // read them, with a timeline running minutes past the real length.
// //
// That reasoning is entirely about INFERENCE, so the refusal belongs // Provenance does not read the marks across clips. The clip comes
// to inference. With trusted spans the clip comes from the byte // from the byte offset a frame was read at, and each clip's
// offset and the marks are never read across clips: each clip's // `offset_ns` maps its own private clock onto the output timeline —
// `offset_ns` maps its own private clock onto the output timeline, // exactly right for an independent STC. Lifting the refusal was
// which is exactly the right operation for a restarting STC. // tried once BEFORE every track carried a source offset, and nine
// Refusing unconditionally turned this type off on most of the // audio and subtitle tracks with nothing to place them by pinned
// branched discs on hand — the very titles it was written for — // themselves to clip 0 and dropped most of the title. Now they all
// leaving them on the inference path with a timeline running // carry one, so the marks keep a single job: deciding whether a
// minutes past the title's real length. // frame lies inside its own clip's [in, out].
if let Some(prev) = out.last() //
// `spans_trusted` still gates it. Without usable spans there is no
// byte offset to place by and inference is all that is left, so a
// table inference cannot read must still be refused.
if !spans_trusted
&& let Some(prev) = out.last()
&& in_ns <= prev.in_ns && in_ns <= prev.in_ns
{ {
tracing::info!( tracing::info!(
target: "freemkv::mux", target: "freemkv::mux",
"no seam plan: the clips' marks do not advance across the \ "no seam plan: the clips' marks do not advance and the feed \
title, and only the video track carries provenance" spans cannot be trusted, so nothing can place them"
); );
return None; return None;
} }
@@ -436,6 +440,22 @@ impl SeamPlan {
return Some(raw_ns.saturating_add(c.offset_ns)); return Some(raw_ns.saturating_add(c.offset_ns));
} }
// Reaching here means this frame carried no byte offset, so it is being
// placed by reading the marks ACROSS clips. Every demuxed source now
// stamps provenance on every track, so under a plan this is not
// expected — and on a table whose marks do not advance the reasoning
// below is meaningless and can strand the track for the rest of the
// title. Say so once per track rather than let it be silent: a mostly
// dropped track already reached a user once this way.
if src_byte.is_none() && self.cursors[track].last_raw_ns.is_none() {
tracing::info!(
target: "freemkv::mux",
track,
"track has no source offset under a seam plan; placing it from \
timestamps, which is only reliable while the marks advance"
);
}
// Bounded by the clip count, so a wild PTS cannot spin here. // Bounded by the clip count, so a wild PTS cannot spin here.
while clip + 1 < self.clips.len() { while clip + 1 < self.clips.len() {
let cur = self.clips[clip]; let cur = self.clips[clip];
@@ -1349,26 +1369,17 @@ mod tests {
} }
} }
/// A table whose marks do not advance across the title is REFUSED, and the /// Marks that do not advance across the title are normal — each clip file
/// reason is not that the marks are unreadable in principle — it is that /// carries its own STC — and are now PLACED, because every track carries a
/// only the VIDEO parsers stamp a frame's source byte. Every audio and /// source byte offset and the clip comes from that, not from the marks.
/// subtitle parser hardcodes `source: None`, so those tracks have no
/// provenance at all and can only be placed from their timestamps.
/// ///
/// Accepting such a table therefore places video correctly by byte and /// This was refused before, which dropped exactly the branched titles the
/// leaves nine audio/subtitle tracks with nothing to place them by. Tried /// seam plan exists for onto the inference path that cannot read them.
/// on a real 22-clip title, that pinned every one of them to clip 0 and
/// dropped every frame past its OUT mark — most of the title.
///
/// Lifting this refusal requires giving the audio and subtitle parsers
/// provenance first, stamped from the PES that STARTED each access unit.
#[test] #[test]
fn marks_that_do_not_advance_are_refused_while_provenance_is_video_only() { fn marks_that_do_not_advance_are_placed_by_provenance() {
const N: u32 = 4; const N: u32 = 4;
const SEG: u32 = 600 * 45_000; const SEG: u32 = 600 * 45_000;
const CLIP_BYTES: u64 = 4_000_000_000; const CLIP_BYTES: u64 = 4_000_000_000;
// Trusted, tiling, distinct spans -- and still refused, because the
// spans only help the one track that carries a byte offset.
let clips: Vec<crate::disc::Clip> = (0..N) let clips: Vec<crate::disc::Clip> = (0..N)
.map(|i| crate::disc::Clip { .map(|i| crate::disc::Clip {
clip_id: format!("{i:05}"), clip_id: format!("{i:05}"),
@@ -1379,10 +1390,46 @@ mod tests {
feed_span: Some((i as u64 * CLIP_BYTES, (i as u64 + 1) * CLIP_BYTES)), feed_span: Some((i as u64 * CLIP_BYTES, (i as u64 + 1) * CLIP_BYTES)),
}) })
.collect(); .collect();
assert!( let mut plan =
SeamPlan::from_clips(&clips).is_none(), SeamPlan::from_clips(&clips).expect("a non-advancing table is placeable by byte");
"a non-advancing table must be refused while only video has provenance"
); // The SAME raw timestamp appears in every clip — unresolvable from
// timestamps, exact from the byte offset.
let seg_ns = mpls_ticks_to_ns(SEG);
let raw = mpls_ticks_to_ns(SEG / 2);
let mut last: Option<i64> = None;
for i in 0..N {
let byte = i as u64 * CLIP_BYTES + CLIP_BYTES / 2;
let out = plan
.place(raw, 0, true, Some(byte))
.unwrap_or_else(|| panic!("clip {i} did not place"));
assert_eq!(out, i as i64 * seg_ns + seg_ns / 2, "clip {i}");
if let Some(l) = last {
assert!(out > l, "output moved backwards at clip {i}");
}
last = Some(out);
}
assert_eq!(plan.total_ns(), N as i64 * seg_ns);
}
/// Without usable spans there is no byte offset to place by, so inference
/// is all that is left — and a table inference cannot read must still be
/// refused rather than silently truncating the title.
#[test]
fn marks_that_do_not_advance_are_still_refused_without_spans() {
const N: u32 = 4;
const SEG: u32 = 600 * 45_000;
let clips: Vec<crate::disc::Clip> = (0..N)
.map(|i| crate::disc::Clip {
clip_id: format!("{i:05}"),
in_time: 0,
out_time: SEG,
duration_secs: 600.0,
source_packets: 0,
feed_span: None,
})
.collect();
assert!(SeamPlan::from_clips(&clips).is_none());
} }
/// The refusal must SURVIVE where it is actually load-bearing: a /// The refusal must SURVIVE where it is actually load-bearing: a