Skip to content

A producer restart that recovers a dead source leaves playbackPhase on .stalled(reconnecting:) indefinitely — the aborted producer owned the stall and the fresh one never clears it #433

Description

@rrgomes

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions