Repository navigation
fix(live): a live stream read off its URL rides out an IPTV panel's refusals - #796
Conversation
…efusals A connection-limited panel answers a live TS stream's reconnects with 509 while another device on the account holds its last slot, for seconds to tens of seconds. The reader climbed its 0.5, 1, 2, 4, 8 s ladder, gave up after six refusals, and each refusal armed the origin's request pacer, which the reload after it waited out (up to 10 s a request). A live stream's 429/503/509 is now asked again every second, or after the origin's Retry-After up to 5 s, for up to 60 s, the way the live HLS ingest asks for its playlist; its refusals arm no pacer and lower no budget, and the books still say the origin refused so the revive arm can tell metered from lost. A live open the origin answers with a status fails typed with that status at once, where the read loop used to spend the open budget on the same request and hand back an untyped failure. LiveStreamRefusalRetryTests.
…he wall clock The three loopback cases bounded the gaps between the origin's requests (a second each, Retry-After stretching them to 2 and 5 s, a count within the budget), and CI's runner took three times as long as a Mac to run the same suite, so the bounds decided what the tests reported. They now run with the reconnect backoff scaled down, as the other reader suites do, and assert what came through, how many requests it took, and that the origin's books hold no refusal; the wait itself is a pure rule (AVIOReader.liveRefusalWait) tested as one.
…l-retries # Conflicts: # CHANGELOG.md
eb10eba
into
superuser404notfound:main
…ils typed #796 throws a live open's first 4xx/5xx typed at once. Before it, the read loop gave a hard 5xx on a never-productive source one more attempt, which is what a 500 or 502 during a tuner or transcode spin-up needs. Refusals (429/503/509) and 4xx still fail at once. The live refusal budget's test seam is now read once at reader init, like the other reader hooks, so a parallel test cannot change it under a running reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
|
Merged, thank you. This closes the gap #778 left on the raw TS path: the same panel refusal is now ridden out on both readers, and a refused live open finally reaches the host with its status. Follow-up on main in
One note for other integrators: a 509 at the open (a zap while the previous channel's slot is still held) now fails as It ships with the next release. |
|
Released in 7.38.0. Thanks again for this. |
|
A data point from an Apple TV on the typed refusal at a live open, which reached us in 7.38.0 and which 7.44.0 carries. When the origin answers a plain-URL live open with 503, the open now fails at once, as 7.38.0's release notes say; the kind our app reads afterwards is Setup. Apple TV 4K (3rd generation, What we see, on every refused open: the reader's first connection ends with 503 after 4 to 20 ms ( and
On full screen on the Apple TV, in a 25 s or 60 s window, our own three retries (after 1, 3 and 8 s) were refused at 1.19 to 1.29, 4.45 to 4.59 and 12.65 to 12.80 s after the first refused open, each failing typed, and there our app stopped; in a 5 s window the fourth open played. On 7.36.0 the same refusals were retried inside the reader: in a 5 s window the picture came 12.5 s after the tune, and longer windows ran into our own 20 s start limit. The sentence in 7.38.0's release notes this bears on: "A live open the origin answers with a status fails typed with that status at once ( We have chosen to keep the engine's new behaviour as it is in our app; this is only a note for hosts that key a retry on 7.44.0 is our app's engine pin as of today (2026-10-21). |
Summary
A live stream read straight off its URL (the continuous MPEG-TS form an IPTV panel serves for a channel) is one connection for as long as it plays. A connection-limited panel answers that connection's reconnects with 509 while another device on the account holds the last slot, for seconds to tens of seconds, and serves again the moment the slot is free.
AVIOReaderhandles that refusal the way it handles a metered VOD origin, which costs a live viewer a frozen picture and then a long black: this gives a live stream the same steady retry the live HLS ingest got in #778, and types a refused live open.What changed
The reader's refusal handling on a live stream, before this change:
backoffDelay), andrecordRateLimitAndShouldGiveUpgives up after six consecutive refusals: about 16 s, inside the refusal this is about, and the read fails withEIO.OriginRequestBudget.noteRefusal, which arms the origin's request pacer (a quiet of 2, 4, 8, then 15 s, then one request every 2 s until 60 s without a refusal) and halves the learned concurrency for the rest of the process. The host's reload, or the producer's reopen, then waits up to 10 s for its first request (proceeded without waiting the pacer out after 10004ms) against an origin that would serve it.awaitFirstPersistentDatareturns with the connection ended, the live path has no refusal branch (AE#693's is!isLive), and the open "proceeds to read-loop reconnect", so the demuxer's open spends the ladder on the same request and fails with an untyped read error the host cannot name.With this change, on a live stream only:
liveRefusalRetryInterval,liveRefusalMaximumRetryWait), for up to 60 s from the first refusal of the streak (liveRefusalRetryBudget), then the read fails withEIOas before. The budget is a while rather than a count because at one attempt a second a count of six is spent in six seconds.noteRefusalWitnessed, soHLSVideoEngine+LiveReopen'srefusedRecentlystill tells a metered source from a lost one. The retry's own cadence, at most one small request a second while refused, is its pacing; the same reasoning as fix(live): the HLS ingest rides out an origin's refusals instead of ending the session #778, and like there, a ceiling the host declares (maxConcurrentSourceRequests) still binds it.AVIOReaderError.httpStatus(status)at once, sopublishLoadFailurereportssourceRateLimitedorsourceRefusedwith the status and the host retries with the right words. A connection that ended with no status (a transport failure) still goes to the read loop.Non-live sources keep the ladder, the count and the pacer exactly as they are.
Test plan
swift test.LiveStreamRefusalRetryTests), and an IPTV panel's live MPEG-TS channel (H.264 + AAC, SDR) for the open and the steady state throughaetherctl play --live --fast-zap --dvr-window 600 --max-concurrent-requests 1on both the native and the software route.LiveStreamRefusalRetryTests, new: the wait rule as a pure function (a second; Retry-After 2 → 2 s; Retry-After 30 → 5 s); with the reconnect backoff scaled down as the other reader suites do, three 509s mid-stream are retried until the stream continues, with no refusal on the origin's books and no pacer; a stream refused past the budget (shortened through a test seam) is asked again more than the old count allowed and then ends withEIO; a 509 and a 403 at the open throw typed on the first request. Judged by outcome and request count, not the wall clock: the first version of this PR bounded the gaps and CI's runner, three times slower than a Mac on this suite, failed them. The mid-stream, budget and open cases fail onmain.Checklist
CHANGELOG.mdupdatedfeat(...),fix(...),chore(...))liveRefusalRetryBudgetForTestingis internal)