Split out of #189 (deep-window follow-up by @kskchaitanya1993). The #189 holdback fix itself is verified there (loopback -16832 = 0 across a 2.5 min run with the live window deepened to ~22s, comfortably past the ~18s holdback runway). This issue tracks what remains: the session still cycles through the #168 reroute dance roughly every 13s instead of settling into stable play.
Observed (5.18.5-family, Apple TV 4K 3rd gen, tvOS 26.5)
HEVC-in-MPEG-TS live channel, test-bed loops a fixed segment pool and injects a real [DISCONTINUITY] each lap. Over 2.5 min: 8 #168 reroutes, 0 host ladder re-tunes, playback never leaves the engine, it just loops:
#9 load 192.168.x/hevc-hdr/master.m3u8 -> readyToPlay -> playing (native, audio-only)
#9 master advertises video but AVPlayer built no video track after grace
#168 reroute -> #10 load 127.0.0.1/media.m3u8 (loopback)
live seg-0 ... seg-1 [DISCONTINUITY] ... seg-2 ... seg-3
#11 load 192.168.x/hevc-hdr/master.m3u8 <- origin master again, native bypass
#11 built no video track -> #168 reroute -> #12 loopback -> repeat
Root cause: two engine-side defects composing into a loop
Defect A: the #168 verdict is not remembered. The video-carriage watchdog (RemoteHLSIngestFallback.Watchdog) discovers "this master advertises video but AVPlayer will never build a video track for its MPEG-TS carriage" at runtime, at the cost of a full native mount, readyToPlay, and the 8-tick (~4s) grace. The verdict is then thrown away. Every subsequent load() of the same URL with nativeRemoteHLS re-mounts the doomed native bypass and re-pays the whole discovery cost. Nothing in the engine records that this master is a known reroute case.
Defect B: the rerouted ingest session has no in-engine recovery. rerouteRemoteHLSOntoLiveIngest loads the loopback session as .custom(HLSLiveIngestReader), so HLSVideoEngine gets sourceReopenableByURL: false (AetherEngine+Loading.swift, session construction). That flag routes every live pump exit (eof, readError, keyframeStarvation) straight past the URL-reopen machinery into onLiveSourceReset ("URL reopen not possible, requesting host retune", HLSVideoEngine+LiveReopen.swift). The ingest reader itself is also designed to go terminal rather than recover: 6 consecutive empty playlist refreshes throw HLSIngestError.ingestStalled (HLSLiveIngestReader.swift), a contract written for phase-1 direct-live where the host had chosen the path itself and owns a fallback ladder. For a #168-rerouted session that contract is wrong twice over: the reader was created by the engine from a URL plus headers and is trivially reconstructible in-engine, and the host never opted into the loopback path in the first place.
How the loop closes. The engine publishes liveSourceReset; a host that answers a live-source reset by re-tuning the same channel URL (a perfectly reasonable response) lands back on the native bypass, which is deterministically doomed for this master (Defect A), which reroutes again (#168), which builds a fresh ingest session that dies the same way (Defect B). Note the #N load <origin> mounts are NativeAVPlayerHost session logs executing that host retune; no engine path reloads the origin URL on its own. So every layer behaves as designed and the composition still loops, with roughly 5s of black/audio-only per lap burned on re-discovering a known verdict.
Why this matters beyond the test-bed
The looped fixed pool is what triggers the ingest death here, but real origins hit the same terminal funnel: ad-boundary and program-transition discontinuities, encoder restarts, and any CDN hiccup that outlives the reader's ~12s refresh-retry deadline. On a rerouted session, each of those today costs a full native-remount dance instead of an in-session recovery.
Proposed fix (two layers)
- Reroute verdict memory. Cache master URLs whose video-carriage watchdog fired (bounded, process-scoped, expiring so an origin that fixes its packaging is not permanently exiled).
load() with nativeRemoteHLS consults the cache and routes a known reroute case directly onto the live-ingest loopback path, skipping the doomed mount and the ~4s grace entirely. This alone collapses the cycle cost and makes any host retune reland on the working path immediately.
- Bounded in-engine revive for engine-created ingest sessions. When the custom reader is the engine's own
HLSLiveIngestReader, an eof/readError/terminal-stall pump exit should first attempt a bounded fresh-reader revive inside the session (rebuild the reader from URL plus headers, rejoin at the live edge, seam carries EXT-X-DISCONTINUITY), mirroring the URL-reopen machinery and the AE#169 bounded-revive pattern. Only an exhausted budget delegates to liveSourceReset.
Evidence wanted to pin the exact terminal trigger
@kskchaitanya1993 offered the raw log around a few cycles in #189, and this is where it pays off: the decisive window is between the loopback mount (#10 load 127.0.0.1/...) and the next origin mount (#11 load 192.168.x/...). The lines that discriminate the candidates:
[HLSIngest] terminal: ... (which HLSIngestError, e.g. ingestStalled would implicate the pool wrap resetting MEDIA-SEQUENCE and starving the playlist tracker)
[HLSVideoEngine] live custom-source pump exited (reason=...); URL reopen not possible, requesting host retune
[HLSVideoEngine] live source replayed from start after reconnect; requesting host retune
[AetherEngine] onLiveSourceReset -> publishing liveSourceReset to host
Also useful: confirmation of how the host app answers liveSourceReset (re-tune same URL?), which would complete the loop mechanism end to end. The fix targets the whole class either way; the exact trigger determines which revive path Layer 2 must cover first and what the regression test encodes.
Split out of #189 (deep-window follow-up by @kskchaitanya1993). The #189 holdback fix itself is verified there (loopback
-16832= 0 across a 2.5 min run with the live window deepened to ~22s, comfortably past the ~18s holdback runway). This issue tracks what remains: the session still cycles through the#168reroute dance roughly every 13s instead of settling into stable play.Observed (5.18.5-family, Apple TV 4K 3rd gen, tvOS 26.5)
HEVC-in-MPEG-TS live channel, test-bed loops a fixed segment pool and injects a real
[DISCONTINUITY]each lap. Over 2.5 min: 8#168reroutes, 0 host ladder re-tunes, playback never leaves the engine, it just loops:Root cause: two engine-side defects composing into a loop
Defect A: the
#168verdict is not remembered. The video-carriage watchdog (RemoteHLSIngestFallback.Watchdog) discovers "this master advertises video but AVPlayer will never build a video track for its MPEG-TS carriage" at runtime, at the cost of a full native mount, readyToPlay, and the 8-tick (~4s) grace. The verdict is then thrown away. Every subsequentload()of the same URL withnativeRemoteHLSre-mounts the doomed native bypass and re-pays the whole discovery cost. Nothing in the engine records that this master is a known reroute case.Defect B: the rerouted ingest session has no in-engine recovery.
rerouteRemoteHLSOntoLiveIngestloads the loopback session as.custom(HLSLiveIngestReader), soHLSVideoEnginegetssourceReopenableByURL: false(AetherEngine+Loading.swift, session construction). That flag routes every live pump exit (eof,readError,keyframeStarvation) straight past the URL-reopen machinery intoonLiveSourceReset("URL reopen not possible, requesting host retune",HLSVideoEngine+LiveReopen.swift). The ingest reader itself is also designed to go terminal rather than recover: 6 consecutive empty playlist refreshes throwHLSIngestError.ingestStalled(HLSLiveIngestReader.swift), a contract written for phase-1 direct-live where the host had chosen the path itself and owns a fallback ladder. For a#168-rerouted session that contract is wrong twice over: the reader was created by the engine from a URL plus headers and is trivially reconstructible in-engine, and the host never opted into the loopback path in the first place.How the loop closes. The engine publishes
liveSourceReset; a host that answers a live-source reset by re-tuning the same channel URL (a perfectly reasonable response) lands back on the native bypass, which is deterministically doomed for this master (Defect A), which reroutes again (#168), which builds a fresh ingest session that dies the same way (Defect B). Note the#N load <origin>mounts areNativeAVPlayerHostsession logs executing that host retune; no engine path reloads the origin URL on its own. So every layer behaves as designed and the composition still loops, with roughly 5s of black/audio-only per lap burned on re-discovering a known verdict.Why this matters beyond the test-bed
The looped fixed pool is what triggers the ingest death here, but real origins hit the same terminal funnel: ad-boundary and program-transition discontinuities, encoder restarts, and any CDN hiccup that outlives the reader's ~12s refresh-retry deadline. On a rerouted session, each of those today costs a full native-remount dance instead of an in-session recovery.
Proposed fix (two layers)
load()withnativeRemoteHLSconsults the cache and routes a known reroute case directly onto the live-ingest loopback path, skipping the doomed mount and the ~4s grace entirely. This alone collapses the cycle cost and makes any host retune reland on the working path immediately.HLSLiveIngestReader, aneof/readError/terminal-stall pump exit should first attempt a bounded fresh-reader revive inside the session (rebuild the reader from URL plus headers, rejoin at the live edge, seam carriesEXT-X-DISCONTINUITY), mirroring the URL-reopen machinery and the AE#169 bounded-revive pattern. Only an exhausted budget delegates toliveSourceReset.Evidence wanted to pin the exact terminal trigger
@kskchaitanya1993 offered the raw log around a few cycles in #189, and this is where it pays off: the decisive window is between the loopback mount (
#10 load 127.0.0.1/...) and the next origin mount (#11 load 192.168.x/...). The lines that discriminate the candidates:[HLSIngest] terminal: ...(whichHLSIngestError, e.g.ingestStalledwould implicate the pool wrap resettingMEDIA-SEQUENCEand starving the playlist tracker)[HLSVideoEngine] live custom-source pump exited (reason=...); URL reopen not possible, requesting host retune[HLSVideoEngine] live source replayed from start after reconnect; requesting host retune[AetherEngine] onLiveSourceReset -> publishing liveSourceReset to hostAlso useful: confirmation of how the host app answers
liveSourceReset(re-tune same URL?), which would complete the loop mechanism end to end. The fix targets the whole class either way; the exact trigger determines which revive path Layer 2 must cover first and what the regression test encodes.