SW path: fold PTS discontinuities on forward-only VOD, decouple audio decode-ahead from the video gate - #347
Open
tschuegy wants to merge 3 commits into
Conversation
A sequential-origin timeshift archive is a chunked recording whose every chunk restarts at PTS ~0 (device trace: an 89 s chunk ending at raw 2717.9 s, the next chunk opening at 0.04 s, with 44-76 junk bytes between chunks). FFmpeg's 33-bit wrap correction reads that backward jump as +92726 s, the renderer then waits 25 h for the frame's display time, and the video queue dies with -12080 - picture and sound freeze about 90 s in. The SW demux loop already folds exactly this shape for live sessions (accrue jumped-minus-expected into an offset, flush decoders at the seam); it was just gated on isLive. The gate widens to the new pure decision shouldFoldTimeline(isLive:sourceSeekable:): live folds as before, and a forward-only non-live source folds too, because its timeline is served as-is and a non-seekable pb offers no seek-based recovery. Seekable VOD keeps its trusted container timeline untouched. Seams now log per occurrence (chunked archives seam every minute or two) with a soft cap of 20 lines so a pathological source cannot flood the log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…[SWDiag] The combined demux loop paced everything on the video renderer's ~10-frame queue, so interleaved audio could never build more than ~0.3 s of lead over the synchronizer clock - any decode/deinterlace jitter beyond that starved the audio renderer and the master clock leapt, and a timeline gap let the free-running clock run away while the picture froze. Video packets now park in a bounded FIFO (drained at the renderer's pace) while audio keeps decoding up to AudioLookaheadPolicy.targetLeadSeconds ahead, and a genuine audio underrun pauses the clock for a rebuffer via the same policy the DVR feeder arm uses, with an everHadLead latch so exactly-realtime origins do not eat a spurious pause at session start. At a fold seam the parked pre-seam tail is decoded before the flush drops its reference chain. Live-without-ring sessions keep the historical lockstep pacing. The SW path also gains a 1 Hz [SWDiag] line (clock + delta, audio lead, parked depth, rebuffer state, and the display layer's own drop counter with per-second delta) - the native path has LagDiag, while SW sessions only had the 30 s memprobe, which is too coarse to see the clock leaps and layer-drop bursts a stuttering session is made of. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…state in [SWDiag] A replay session on tvOS 26.6 shows every existing counter clean (dclk 1.00, aLead 3-5 s, enq +50/s, layerDrop flat) while the picture visibly leaps forward every few seconds and the session eventually dies with FigVideoQueueRemote -12080 - the documented shape of "the display layer stops rendering (audio keeps going)". A layer that stalls and catches up displays frames LATE without dropping them, so the only place the stall can register is totalAccumulatedFrameDelay. The 1 Hz [SWDiag] line now carries that delay (with per-second delta), the corrupted-frame count, the queue-target status, the layer's superlayer/bounds state, and isReadyForDisplay, so the next device run pins whether the stall is queue-side, surface-side, or neither. Also explains the headless aetherctl artifact: a never-bound layer (surf=DETACHED) drops ~8 frames/s that a bound one does not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two SW-path weaknesses surfaced while chasing an IPTV catch-up bug on device (tvOS 26.6), both independent of the source that exposed them:
isLive, and non-live sessions run unprotected. A forward-only VOD source (chunked timeshift archive) restarted its timestamps at PTS ~0 every ~85 s; FFmpeg's 33-bit wrap correction read the -2718 s jump as +92726 s, the renderer waited 25 hours for the frame's display time, and the video queue died withFigVideoQueueRemote -12080- picture and sound frozen ~90 s into every session.AudioLookaheadPolicyfor exactly this; the combined loop had nothing.Change
fix(sw)fold - the discontinuity fold gate widens to the new pure decisionshouldFoldTimeline(isLive:sourceSeekable:): live folds as before, and a forward-only non-live source folds too (its timeline is served as-is and a non-seekable pb offers no seek-based recovery). Seekable VOD keeps its trusted container timeline untouched. Seams log per occurrence with a soft cap of 20 lines.fix(sw)audio decoupling - video packets park in a bounded FIFO (drained at the renderer's pace) while audio keeps decoding up toAudioLookaheadPolicy.targetLeadSecondsahead of the clock, and a genuine audio underrun pauses the clock for a rebuffer via the same policy the DVR feeder uses, with aneverHadLeadlatch so exactly-realtime origins do not eat a spurious pause at session start. At a fold seam the parked pre-seam tail is decoded before the flush drops its reference chain. Live-without-ring sessions keep the historical lockstep pacing.diag(sw)- the SW path gains a 1 Hz[SWDiag]line (clock + delta, decoded-audio lead, parked depth, rebuffer state, the display layer's own drop counter, accumulated render delay with per-second delta, queue-target status, surface state,isReadyForDisplay). The native path has LagDiag; SW sessions only had the 30 s memprobe, which is too coarse to see the clock leaps and layer-drop bursts a stuttering session is made of. It also names a headless-harness artifact: a never-bound layer (surf=DETACHED) drops ~8 frames/s that a bound one does not.Verification
SWTimelineFoldTestspins the fold decision; full suite green (1632 tests).[SWDiag]on a healthy session readsdclk=1.00,aLead3-5 s sawtooth,layerDropflat,delay=0.00.aetherctl play: clock exact across 5+ minutes, no rebuffer churn on fast origins, rebuffer pause/resume behaves on origins that deliver below realtime.🤖 Generated with Claude Code