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). |
…ls with that answer (#796) 7.38.0 typed the refusal in the reader, but the live fail-fast after the probe dropped it, so a 503 from a metered panel reached the host as liveSourceUnavailable and load threw DemuxerError.openFailed(-5). It now goes through publishLoadFailure (sourceRateLimited / sourceRefused with the status) and load throws the reader's httpStatus. A live open that reaches nothing keeps liveSourceUnavailable. Reported by @cmcpherson274 against 7.44.0 on an Apple TV 4K. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…n answers with a status fails with that answer (#796) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
|
Thank you for the measurements. They are right, and the gap was in the engine, not in your app. The reader has typed the refusal since 7.38.0, but the live fail-fast after the probe threw that away and published Fixed on main in
A test against a local origin that answers 503 and 403 covers both kinds. PR #818 reworks this same spot more broadly; this smaller change closes the gap in the meantime. It ships with the next release. Since your app already keeps the behaviour as it is, the only change you should see is the kind: a 503 window will read as |
Failed initial live probes keep liveSourceUnavailable with the original URL error in underlyingDomain/underlyingCode; HTTP and TLS refusals stay typed. AVIO readers capture the diagnostics context at init and hand it to every delegate. HLS reasons are finite and pinned by a literal drift test, refused AES keys keep playlistUnreachable(status:), and source network events are immutable with open Role/Phase values. A transport failure at open is not a reason to fail it: the open and the read loop ride resets and timeouts out on the reconnect ladder as before, and the reader only records the last URL error. The live start attaches that record when it finally gives up (superuser404notfound#796's typed status path is kept). Only a TLS trust failure, which no reconnect can fix, fails the open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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)