Skip to content

fix(video): contain a timestamp leap that escapes the timeline rebase (#369) - #372

Merged
superuser404notfound merged 2 commits into
superuser404notfound:mainfrom
tschuegy:fix/369-wrap-escape-containment
Aug 13, 2026
Merged

fix(video): contain a timestamp leap that escapes the timeline rebase (#369)#372
superuser404notfound merged 2 commits into
superuser404notfound:mainfrom
tschuegy:fix/369-wrap-escape-containment

Conversation

@tschuegy

Copy link
Copy Markdown
Contributor

Fixes #369. Companion to #368's rebase: defense in depth for a timestamp leap that escapes it (corrupt stream, a jump inside one chunk, future regressions). Today such a leap turns the session into a multi-minute zombie; with these three containments it either keeps playing sanely or fails cleanly within seconds onto the designed onVODSourceFailed surface.

The three containments

  1. resolveVideoSampleDuration gets a cap. The look-behind delta across a 2^33 wrap IS the wrap (device: 8226410192 ticks ≈ 91404 s), which movenc rejects — and the write rc was dropped on the floor, so the packet vanished silently. The inferred delta now falls back (existing duration, then fallback) when it exceeds the discontinuity threshold: a sample longer than a discontinuity is definitionally invalid, and reusing discontinuityThresholdSeconds keeps one shared definition. The first rejected muxer write is now logged.

  2. SegmentCache.noteFolded counts wide runs. The maxFoldRunLength guard dropped runs wider than 64 indices as "a restart or a seek" — but the field case was a discontinuity-scale cut leap (312 indices), and dropping it left every fold counter at 0, which is exactly what disarmed both VOD keyframe-aligned plan: the playlist advertises segment indices the producer never muxes, and the wedge recovery rebuilds the same hole #358 recovery arms for the folds most certain to trigger them. Wide runs now count (one Int per folded index, bounded by plan size); the constant survives as a log-classification threshold, and the fold site emits a distinct discontinuity-scale line. On a sequential origin the armed escalation reaches requestRestart → the sequential refusal → onVODSourceFailed within seconds (the client's shifted-window re-request is the designed recovery); on seekable VOD the reanchor genuinely works.

  3. The advance park cannot wait on an unadvertisable index. The park releases on a consumer fetch of head − bufferAheadSegments, but a sequential append playlist advertises only what this pump's OWN finalize reports have fed it — a release target beyond that frontier is waiting for oneself (field: parked at target=364 while the playlist ended at seg61, freezing it for good). The park is skipped while the target lies beyond the frontier (sequentialParkWouldSelfDeadlock, pure); the disk budget stays the resource bound, normal parking resumes as the frontier catches up, and negative startup targets are exempt (they release instantly and are no deadlock).

Deliberately out of scope

OutputTimestampSanitizer keeps latching after a huge jump. movenc latches monotonicity on its own once a wrapped packet is accepted, so a sanitizer re-latch would only convert garbage timestamps into rejected writes until the next muxer rotation — no better — and it would weaken the SSAI seam protection the class exists for. With #368 the sanitizer never sees the leap; without it, containment 2 fails the session cleanly.

Tests

Test plan

  • macOS: swift test (full suite, counts above).
  • Device: Apple TV 4K, tvOS 26.6, SRF Xtream timeshift (H.264 720p50 AC-3, field-coded, open-GOP) — the trace this containment is written against; verified through the Syravo client after the next release is pinned.

Applies cleanly with or without the #368 PR; the combination is integration-tested locally (full suite green on the merged tree).

tschuegy and others added 2 commits August 13, 2026 22:30
…superuser404notfound#369)

Three containment gaps, one field trace (a 2^33 wrap on a sequential
origin), each of which independently prolonged the failure:

- resolveVideoSampleDuration returned the look-behind delta unbounded, so
  across the wrap it handed movenc the wrap itself as a sample duration
  (device: 8226410192 ticks, rejected as 'Application provided duration
  ... is invalid') and the packet was silently lost - the write rc was
  dropped on the floor. The delta is now capped at the discontinuity
  threshold (a sample longer than a discontinuity is definitionally
  invalid; live rebases and the cap share one definition), and the first
  rejected write is logged.

- SegmentCache.noteFolded discarded fold runs wider than 64 indices as
  'a restart or a seek', but the widest folds are the ones most certain
  to make a consumer request a folded index; dropping the 312-index field
  run left every counter at 0 and disarmed both superuser404notfound#358 recovery arms. Wide
  runs now count (one Int per index, bounded by plan size) and the fold
  site classifies them as discontinuity-scale in the log.

- the advance-path backpressure park released only on a consumer fetch of
  head - bufferAheadSegments, but a sequential append playlist advertises
  only what THIS pump's finalize reports have fed it, so a target beyond
  that frontier waits for oneself (field: parked at target=364 while the
  playlist ended at seg61, freezing it for good). The park is skipped
  while the target lies beyond the advertisable frontier; the disk budget
  stays the resource bound and normal parking resumes as the frontier
  catches up. Negative startup targets are exempt - they release
  instantly and are no deadlock.

Deliberately unchanged: OutputTimestampSanitizer keeps latching. movenc
latches monotonicity on its own once a wrapped packet is accepted, so a
sanitizer reset would only convert garbage timestamps into rejected
writes, and it would weaken the SSAI seam protection the class exists
for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… device trace

The duration cap is pinned with the exact wrap values (363524400 ->
2^33, delta 8226410192), the fold-run counting with the field fold
(indices 2...313, repeat-across-restart increment, stored-index
exemption), and the park predicate with the field deadlock (target=364
vs frontier=61) plus the negative-startup-target exemption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@superuser404notfound
superuser404notfound merged commit 1ca9656 into superuser404notfound:main Aug 13, 2026
4 checks passed
@superuser404notfound

Copy link
Copy Markdown
Owner

Merged, thanks for the trace-anchored writeup, the three containments sit where the field log points and the pinned wrap values made the duration cap reviewable in one read. Full suite green on the merge result (1838 Swift Testing tests in 268 suites, 0 failures); the CHANGELOG conflict against the #368 follow-up was resolved by keeping both Unreleased entries.

Two follow-ups pushed on top (71b27b1, merged in 833e152), both in the same spirit as the PR:

The cap now also covers the duration the container DECLARES. resolveVideoSampleDuration capped the inferred delta only, and fell through to existingDuration > 0 ? existingDuration : fallback untouched. That branch is not a rare path here: it is exactly what runs when no forward delta exists, which is the EOF tail of the same wrapped stream, and movenc rejects a sample on the number, not on where the number came from. Pinned with the same 8226410192 ticks arriving as a declared duration.

The skipped park now hands its wedge detection to the disk park. This one is worth a note on the reasoning in section 3. The advance park is what catches a consumer that stopped advancing, and the #207 disk park documents in so many words that it needs no wedge breaker because "the extras eviction that follows the advancing playhead releases it". Skipping the advance park moves the pump straight into that assumption with the consumer possibly frozen: it races to the retention budget (2 GiB, so ~an hour of a 5 Mbps timeshift) and then holds in an unbreakable park for good. The skip is right, so the fix is to carry the detector across rather than to park: awaitPrefetchDiskBudgetRelease(detectWedge:) arms the same BackpressureWedgeDetector when the advance park was skipped, and its one-second poll matches the cadence the detector counts in. A trip ends the pump onto the existing re-anchor surface, which a sequential origin refuses into onVODSourceFailed, i.e. the failure the PR wants, in seconds instead of never.

One correction to the rationale, measured rather than argued, because it changes which path does the work:

The issue stays open for the device retest; the release will bundle this with #368 and, if it lands, #370.

superuser404notfound added a commit that referenced this pull request Aug 14, 2026
…ransport owns

CI on the #372 merge (run 31749495994) failed the short-body test at
`read > 0`: zero bytes reached the reader before the drop surfaced. Same
class as the 2026-08-11 flake that already relaxed `>= 1 MiB` to `> 0`.
The amount URLSession hands over before it reports a cut body is its own
to decide, and under load it can be nothing at all, so no floor is
honest. What the reader owns is the ceiling (never more than the server
sent) and the error it ends on, which is what this test now asserts.

The engine contract held in the failing run: `last == -5` passed there,
the amount was the only issue recorded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants