Skip to content

fix(video): sequential EVENT startup - serve from the first finalized segment, skip the spacing scan (#370) - #373

Merged
superuser404notfound merged 2 commits into
superuser404notfound:mainfrom
tschuegy:fix/370-sequential-startup-gate
Aug 14, 2026
Merged

fix(video): sequential EVENT startup - serve from the first finalized segment, skip the spacing scan (#370)#373
superuser404notfound merged 2 commits into
superuser404notfound:mainfrom
tschuegy:fix/370-sequential-startup-gate

Conversation

@tschuegy

Copy link
Copy Markdown
Contributor

Fixes #370. Two startup costs that together kept a sequential-origin session from ever starting on a slow or stalling origin — AVPlayer's first playlist GET was held the full 30 s and the asset load died on -1008/CoreMedia -12884 while ~12 s of media already sat on disk.

Commit 1: serve the EVENT playlist from its first finalized segment

waitForSequentialStartupSegments reused LiveEdgePolicy.minStartupSegments = 2, a sliding-window constant whose -12888 rationale (a 1-segment live window with holdback that can't fit) does not apply to an append-only EVENT playlist: it starts at media sequence 0, grows monotonically, and #EXT-X-SODALITE-REFRESH already defeats the unchanged-playlist patience. The demand was also steeper than it looks: a sequential duration is only final when the NEXT segment's ledger opens (real EXTINF = nextStart − start), so "2 published durations" meant 3 segment opens ≈ 12-18 s of media through the origin. The gate is now sequentialStartupSegments = 1 on the provider (LiveEdgePolicy untouched — its constant stays correct for live).

The publish lag itself stays, deliberately: published EVENT entries must not mutate, and capture / duration-known already coincide at the earliest knowable moment — the fix is to stop demanding two of them, not to publish provisional durations that would need correcting.

Also: a pump that dies before publishing anything now releases a held startup GET immediately (abortSequentialStartupWait, called from both fatal VOD surfaces) instead of letting the server thread sit out the rest of its timeout while the session is already surfacing failure.

Commit 2: plan without spending the origin's prefix on the spacing scan

For the uniform-stride fallback plan, measureKeyframeSpacing starts with a seek — a silent no-op on the non-seekable sequential pb — and then consumes up to 20000 packets / 30 s of content from the single byte-0-only connection. Those packets never reach the pump, so the session both started late and silently dropped the archive's first GOP(s). In both field traces the measurement (0.48 s and 2.0 s) fell below the 4 s floor anyway, so the scan bought nothing. Sequential plans now go straight to the target stride; the #358 holes the scan exists to soften don't bite this path, because the append playlist gives zero-duration holes no URI and its EXTINF is real by construction. Live and seekable VOD keep the scan.

Tests

  • SequentialAppendPlaylistTests: one finalized segment releases the gate; an empty session still times out; EOF with zero segments still releases (the ENDLIST arm); an aborted wait returns immediately instead of sitting out its timeout.
  • Full suite green on macOS: 1831 Swift Testing + 504 XCTest, 0 failures.

Test plan

  • macOS: swift test (full suite, counts above).
  • Device: Apple TV 4K, tvOS 26.6, SRF Xtream timeshift (H.264 720p50 AAC) against a throttled/stalling origin — the field session that never started; verified through the Syravo client after the next release is pinned.

Fully independent of the #368/#369 PRs; the three-way combination is integration-tested locally (full suite green on the merged tree).

tschuegy and others added 2 commits August 13, 2026 22:33
…d segment (superuser404notfound#370)

The startup gate reused LiveEdgePolicy.minStartupSegments = 2, a
sliding-window constant whose -12888 rationale does not apply to an
append-only EVENT playlist (media sequence 0, refresh counter already
defeats the unchanged-playlist patience). Because a sequential duration
is only final when the NEXT segment's ledger opens, demanding 2
durations really demanded 3 segment opens (~12-18 s of media) before
AVPlayer's held playlist GET was answered; on a stalling origin the GET
sat out the full 30 s and the asset load died on -12884 with ~12 s of
media already on disk. A one-segment EVENT playlist is legal HLS.

The publish lag itself stays: a segment's real EXTINF is nextStart -
start, published EVENT entries must not mutate, and capture and
duration-known already coincide at the earliest knowable moment - the
fix is to stop demanding two of them, not to publish provisional
durations.

A pump that dies before publishing anything now also releases a held
startup GET immediately (abortSequentialStartupWait) instead of letting
the server thread sit out the rest of its timeout while the session is
already surfacing failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he spacing scan (superuser404notfound#370)

measureKeyframeSpacing starts with a seek - a silent no-op on the
non-seekable sequential pb - and then consumes up to 20000 packets /
30 s of content from the single byte-0-only connection. Those packets
never reach the pump, so the session both started late and silently
dropped the archive's first GOP(s); in both field traces the measured
spacing (0.48 s, 2.0 s) fell below the 4 s floor anyway, so the scan
bought nothing. Sequential plans now go straight to the target stride.
The superuser404notfound#358 holes the scan exists to soften don't bite this path: the
append playlist gives zero-duration holes no URI and its EXTINF is real
by construction.

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

Copy link
Copy Markdown
Owner

Merged as d619d6b. Thanks for this one, and in particular for the trace that makes both costs legible: the #65 ledger lines are what turn "2 published durations" into a measurable 3 segment opens, and the served tail (one EXTINF after the full 30 s hold) is the direct witness that the constant was the gate.

Two things pulled in afterwards, in 244de44:

The release now belongs to the failure surface. abortSequentialStartupWait covered the two surfaces this trace ran through. A sequential origin reaches two more: a .muxerFailed exit revives through requestRestart, which a sequential origin refuses into onVODSourceFailed, and the AE#366 moov-prime revive ends on its own exhaustion cap. The AE#366 shape in particular fires before the first duration is published (an E-AC-3 archive whose first segment carries no audio packet), so the held GET still sat out its remaining ~30 s on a session that had already surfaced failure. Every VOD failure now goes through one surfaceVODSourceFailure that releases the wait with it, including the AE#169 read-error cap that a sequential origin does not reach today, so the property holds by construction rather than per call site.

The gate counts advertisable entries. buildMediaPlaylistText gives a zero-duration entry no URI on EVENT, so a gate counting raw appends can answer the held GET with a playlist that renders empty, which is the -12888 the gate exists to prevent. emitSequentialReport's ordering makes index 0 a real duration today, so this is a guard and not a live defect, but the second entry that used to make it unreachable is exactly what this change spends.

Two things that held up on re-reading, worth recording here: the plan-side skip costs nothing in cut geometry, because cuts are keyframe-gated either way, so a stride below the GOP yields the same segments and only leaves index holes the append playlist already renders away. And since initialProducerBaseIndex stays 0 on a sequential origin, there is no seek between planning and prod.start(), so the scan's consumed prefix really was lost content and not just lost time.

Full suite green on the merged tree (1845 Swift Testing tests, 268 suites). Unreleased so far; the issue stays open for the device retest against the next tagged build.

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