Skip to content

Rerouted live-ingest session death relands on the known-bad native path: #168 verdict not remembered, no in-engine revive #199

Description

@superuser404notfound

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)

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions