Skip to content

Live TS: high-water backpressure cycles healthy live connections, and reconnects request an unsatisfiable byte frontier #331

Description

@tschuegy

What happened?

Playing a live MPEG-TS stream (Xtream-style panel, LoadOptions.isLive = true) through the persistent AVIO reader, I expected one long-lived connection. Instead the reader's own backpressure cycles the connection forever, and against some panels the reconnect can never succeed at all:

  1. The 16 MB high-water end (Long-suspended data tasks holding full receive windows starve every Network.framework flow in the app process on tvOS — close-and-rerequest at high water would avoid the dormant state (field dose-response inside) #310) has no live branch, and live connections are open-ended by design - so the high-water end is the only thing that ever terminates a healthy live connection. Each end drains ~8 MB to low water and re-requests "at the frontier", but everything broadcast during the drain is gone: the demuxer rejoins on a corrupt TS packet. Panels serve a ring-buffer join burst at line rate on every (re)connect, so the burst refills the window immediately and each reconnect causes the next one - a steady cycle every ~9.5 MB with one mpegts "Packet corrupt" + h264 decode error per cycle, forever.

  2. The reconnect request itself carries Range: bytes=<frontier>-, but the frontier is reader bookkeeping, not a server-side byte address. A panel that ignores the offset and serves "from now" masks this; a panel that answers 416 to any offset it cannot satisfy turns every reconnect into an unrecoverable rejection loop: same offset ~1/s (read progress from the draining window keeps resetting the unproductive streak, so it never escalates), re-resolving the pin at streak 2 changes nothing (the offset is the problem, not the pin), and the session starves into CoreMedia -12888.

  3. Compounding (1): the 509 a connection-capped panel answers while the replaced connection's slot still lingers server-side is classified as a hard 5xx, so every cycle drops the pinned post-redirect URL and re-resolves through the portal - 500-1500 ms extra gap per cycle, and a second request against exactly the origin that has no room for it (the 519ae26 reasoning, left incomplete for 509).

Steps to reproduce

  1. LoadOptions with isLive = true; url = a live .ts from an Xtream-style panel (302 portal redirect to the stream host, max_connections = 1)
  2. engine.load(url:options:) and let it play
  3. Watch the [AVIOReader] log. Panel type A (ignores Range offsets): "pump window high water: 16MB ahead; ending the connection..." every ~30 s with one "[mpegts] Packet corrupt" per cycle, first reconnect attempt of each cycle 509s and drops the pin. Panel type B (rejects nonzero offsets; every response completes cleanly after a ~14 MB ring burst): "pump gen=N rejected response status=416 at offset X" repeating with the same X until the runway is gone and AVPlayer dies.

AetherEngine version or commit SHA

6.13.0 / 234cb57

Host app

Custom / my own integration

Platform

tvOS

OS version

tvOS 26.6

Device / chip

Apple TV 4K

Playback path

Native AVPlayer

Source media (for playback bugs)

Live MPEG-TS over http. Reproduced on both paths: H.264 High 1080i25 + AAC stereo, SDR (software path) and H.264 Baseline 1080p30 progressive + AAC stereo, SDR (native/loopback path).

Error codes / log lines

[AVIOReader] pump window high water: 16MB ahead; ending the connection, will re-request at the frontier once the consumer drains
[AVIOReader] pump gen=2 rejected response status=509 at offset 17829072
[AVIOReader] Dropped resolved URL cache (hard 509 from pinned URL)
[mpegts @ 0x11b2e8000] Packet corrupt (stream = 0, dts = 3137396400).
[h264 @ 0x12fb95400] error while decoding MB 9 6, bytestream -9
--- panel type B ---
[AVIOReader] pump gen=2 rejected response status=416 at offset 14472616
[AVIOReader] pump replacing a faulted connection at offset 14472616 with 7477KB of read-ahead left (streak=1 status=416, next attempt in 1.0s)
CoreMediaErrorDomain -12888 (Playlist File unchanged for longer than 1.5 * target duration)

Anything else

Root-caused in AVIOReader; a PR fixing all three follows (live high water absorbing the join burst once, live reconnects requesting bytes=0- like a join, 509 joining the 429/503 metering class). Verified on device against both panel types.

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