Context
Companion to the sequential chunk-seam rebase issue (#368): even with the rebase in place, a timestamp leap that escapes it (corrupt stream, a jump inside one chunk, future regressions) currently turns the session into a 4-minute zombie instead of failing fast. Three independent containment gaps, all observed in one device trace (AetherEngine 6.25.1, tvOS 26.6, sequential-origin Xtream timeshift):
1. An inferred sample duration longer than a discontinuity is accepted
resolveVideoSampleDuration returns nextDts − dts unbounded. Across a 2^33 wrap the look-behind finalize computes 8 226 410 192 ticks (~91 404 s @ 90 kHz) and hands it to movenc:
[mp4 @ 0x15eb6e700] [error] Application provided duration: 8226410192 in stream 0 is invalid
finalizeAndWriteVideo then discards the write return code, so the packet is silently lost and nothing in the log names the failure at ERROR level from our side.
2. Discontinuity-scale fold runs are discarded, disarming the #358 recovery arms
SegmentCache.noteFolded drops runs wider than maxFoldRunLength = 64 ("a jump this wide is a restart or a seek, not a fold"). The field case was precisely such a run — 312 indices:
[HLSSegmentProducer] #358 plan indices 2...313 folded into seg-314 (no IRAP reached their boundary)
With the counters at 0, both #358 recovery arms (foldedTargetDecision reanchor and the engine's unrecoverable-gap handler) stay disarmed, so the designed fold-once-reanchor / fold-twice-fail path never runs and the session limps on until the #65 wedge breaker trips minutes later.
3. The backpressure park can wait on an index the playlist can never advertise
The advance-path park waits for the consumer to fetch head − bufferAheadSegments, but on a sequential EVENT playlist the advertised frontier is fed by this same pump's finalize reports — zero-duration fold holes get no URI. After a fold-to-tail the park waits for a fetch of an index that will never appear:
[HLSSegmentProducer] #65 backpressure PARK (advance) head=374 target=364 cacheTarget=54 highStored=61 cached=62 parked=12s (no playback progress)
The consumer meanwhile advances (cacheTarget 54→60, rendered clock 208→243 s) until it drains the listed window and stalls. Parking on one's own advertised frontier is a structural self-deadlock.
Proposed containment (PR follows)
Deliberately out of scope: re-latching OutputTimestampSanitizer after a huge jump. movenc itself latches monotonicity once a wrapped packet is accepted, so a sanitizer reset would only convert garbage timestamps into rejected writes; with the rebase (#368) the sanitizer never sees the leap, and without it the fold-count containment above fails the session cleanly.
Context
Companion to the sequential chunk-seam rebase issue (#368): even with the rebase in place, a timestamp leap that escapes it (corrupt stream, a jump inside one chunk, future regressions) currently turns the session into a 4-minute zombie instead of failing fast. Three independent containment gaps, all observed in one device trace (AetherEngine 6.25.1, tvOS 26.6, sequential-origin Xtream timeshift):
1. An inferred sample duration longer than a discontinuity is accepted
resolveVideoSampleDurationreturnsnextDts − dtsunbounded. Across a 2^33 wrap the look-behind finalize computes 8 226 410 192 ticks (~91 404 s @ 90 kHz) and hands it to movenc:finalizeAndWriteVideothen discards the write return code, so the packet is silently lost and nothing in the log names the failure at ERROR level from our side.2. Discontinuity-scale fold runs are discarded, disarming the #358 recovery arms
SegmentCache.noteFoldeddrops runs wider thanmaxFoldRunLength = 64("a jump this wide is a restart or a seek, not a fold"). The field case was precisely such a run — 312 indices:With the counters at 0, both #358 recovery arms (
foldedTargetDecisionreanchor and the engine's unrecoverable-gap handler) stay disarmed, so the designed fold-once-reanchor / fold-twice-fail path never runs and the session limps on until the #65 wedge breaker trips minutes later.3. The backpressure park can wait on an index the playlist can never advertise
The advance-path park waits for the consumer to fetch
head − bufferAheadSegments, but on a sequential EVENT playlist the advertised frontier is fed by this same pump's finalize reports — zero-duration fold holes get no URI. After a fold-to-tail the park waits for a fetch of an index that will never appear:The consumer meanwhile advances (cacheTarget 54→60, rendered clock 208→243 s) until it drains the listed window and stalls. Parking on one's own advertised frontier is a structural self-deadlock.
Proposed containment (PR follows)
resolveVideoSampleDurationat the discontinuity threshold (a sample longer than a discontinuity is definitionally invalid) and log the first rejected muxer write;onVODSourceFailedwithin seconds (the client's shifted-window retry is the designed recovery), on seekable VOD the reanchor genuinely works;Deliberately out of scope: re-latching
OutputTimestampSanitizerafter a huge jump. movenc itself latches monotonicity once a wrapped packet is accepted, so a sanitizer reset would only convert garbage timestamps into rejected writes; with the rebase (#368) the sanitizer never sees the leap, and without it the fold-count containment above fails the session cleanly.