Measured on 6.45.0, Apple TV 4K 3rd gen / tvOS 26.6, loopback-HLS VOD on the
native path. Asset: 1080p H.264 MKV, ~35 Mbit/s, ~89 min, DTS audio, PGS subs.
After the origin comes back and the reader is demonstrably serving again,
playbackPhase stays .stalled(reconnecting: true) — through normal playback,
indefinitely, until some later unrelated recovery happens to clear it. The
longest we have measured is 454.8 s, of which 298 s were with the picture
playing normally.
The rig
The origin is reached through a local reverse proxy we can kill and restart, so
only the media plane dies; nothing else about the session changes. Killing it
produces Could not connect to the server on the reader within milliseconds on
this asset, because at ~35 Mbit/s a 16 MB read-ahead is only about 4 s and the
reader is essentially always connected.
What we measured
Three arms, one session, minutes apart. The discriminator is whether the
recovery restarted the producer, which is visible in the reader's generation
counters.
| arm |
outage |
producer restart on recovery |
reader generations |
phase cleared after |
| A |
~140 s |
yes |
reset to pump gen=10 / prefetch gen=4 |
304.6 s, and only on a later restart-free recovery |
| B |
~133 s |
yes |
reset to gen=1 |
still stalled 298 s later; see C |
| C (control) |
12 s |
no |
continued pump gen=87, 88, 89 |
8 s |
Arm B in full:
+0.000 cut
+0.168 phase -> .stalled(reconnecting: true)
+48 picture stops (read-ahead exhausted)
+133 origin restored
+133.44 pump gen=1 first data after 27ms <- reader healthy, generations RESET
+431 picture playing normally, reader at gen=91, ~90 clean fetches,
phase STILL .stalled(reconnecting: true)
Arm C is the same session five minutes later. A 12 s outage is too brief to
wedge the producer, so no restart: generations continued rather than resetting,
and the phase cleared 8 s after the origin returned.
The mechanism, from your own diagnostics
On the recovery in arm B:
[HLSVideoEngine] restart at idx=286: old producer wedged in a read past 5s;
aborted it and reopened a fresh demuxer
[HLSVideoEngine] producer restarted at idx=286 (restart took 5150ms;
stopWait=5005ms reopen=126ms seek=18ms build=0ms)
The aborted producer is the one that owned the reconnecting state. The fresh
demuxer was never stalled, so it has nothing to clear, and the phase keeps
reporting the reader that no longer exists.
Why a host cannot work around it
playbackPhase is the only signal a host has for "the source is delivering".
The obvious alternative — treat an advancing clock as proof — is one we
deliberately rejected after two device traces of our own: read-ahead plays on
for tens of seconds after a source dies, so a clock can advance over a dead
source; and a seek that cannot land jumps the clock while parking the phase on
.seeking. So we clear our own reconnect state only when the phase is neither
stalled nor seeking, and a phase that never leaves stalled is unrecoverable
from outside the engine.
The user-visible cost on our side is a "Reconnecting" indicator that stays up
over normally playing video, and — because we drive a group-playback feature
off the same signal — a peer that keeps telling the room it is buffering long
after it is fine.
The ask
When a producer restart replaces the reader, the stall state should go with it:
either clear it as part of the swap, or let the fresh producer publish a
non-stalled phase once it is serving. A "reader is delivering" signal that does
not depend on which producer instance owns it would also solve it from our side.
Caveat on version
We are pinned to 6.45.0 and have not yet tested 6.46.0 or 6.47.0, because we are
holding the pin while a retest on another issue is outstanding. 6.46.0 changes
producer cutting, so it may well move this. Happy to bump and re-run both arms
if that would be more useful than the measurements above.
The rig is reproducible on demand here and we can run whatever would help,
including a longer soak or a variant that wedges the producer without killing
the origin.
Measured on 6.45.0, Apple TV 4K 3rd gen / tvOS 26.6, loopback-HLS VOD on the
native path. Asset: 1080p H.264 MKV, ~35 Mbit/s, ~89 min, DTS audio, PGS subs.
After the origin comes back and the reader is demonstrably serving again,
playbackPhasestays.stalled(reconnecting: true)— through normal playback,indefinitely, until some later unrelated recovery happens to clear it. The
longest we have measured is 454.8 s, of which 298 s were with the picture
playing normally.
The rig
The origin is reached through a local reverse proxy we can kill and restart, so
only the media plane dies; nothing else about the session changes. Killing it
produces
Could not connect to the serveron the reader within milliseconds onthis asset, because at ~35 Mbit/s a 16 MB read-ahead is only about 4 s and the
reader is essentially always connected.
What we measured
Three arms, one session, minutes apart. The discriminator is whether the
recovery restarted the producer, which is visible in the reader's generation
counters.
pump gen=10/prefetch gen=4gen=1pump gen=87, 88, 89Arm B in full:
Arm C is the same session five minutes later. A 12 s outage is too brief to
wedge the producer, so no restart: generations continued rather than resetting,
and the phase cleared 8 s after the origin returned.
The mechanism, from your own diagnostics
On the recovery in arm B:
The aborted producer is the one that owned the reconnecting state. The fresh
demuxer was never stalled, so it has nothing to clear, and the phase keeps
reporting the reader that no longer exists.
Why a host cannot work around it
playbackPhaseis the only signal a host has for "the source is delivering".The obvious alternative — treat an advancing clock as proof — is one we
deliberately rejected after two device traces of our own: read-ahead plays on
for tens of seconds after a source dies, so a clock can advance over a dead
source; and a seek that cannot land jumps the clock while parking the phase on
.seeking. So we clear our own reconnect state only when the phase is neitherstalled nor seeking, and a phase that never leaves stalled is unrecoverable
from outside the engine.
The user-visible cost on our side is a "Reconnecting" indicator that stays up
over normally playing video, and — because we drive a group-playback feature
off the same signal — a peer that keeps telling the room it is buffering long
after it is fine.
The ask
When a producer restart replaces the reader, the stall state should go with it:
either clear it as part of the swap, or let the fresh producer publish a
non-stalled phase once it is serving. A "reader is delivering" signal that does
not depend on which producer instance owns it would also solve it from our side.
Caveat on version
We are pinned to 6.45.0 and have not yet tested 6.46.0 or 6.47.0, because we are
holding the pin while a retest on another issue is outstanding. 6.46.0 changes
producer cutting, so it may well move this. Happy to bump and re-run both arms
if that would be more useful than the measurements above.
The rig is reproducible on demand here and we can run whatever would help,
including a longer soak or a variant that wedges the producer without killing
the origin.