Skip to content

Recover web audio when a stream drops mid-round - #2404

Merged
taahamahdi merged 2 commits into
masterfrom
web-audio-resiliency
Aug 15, 2026
Merged

Recover web audio when a stream drops mid-round#2404
taahamahdi merged 2 commits into
masterfrom
web-audio-resiliency

Conversation

@taahamahdi

Copy link
Copy Markdown
Collaborator

Problem

A web listener's <audio> element had no recovery path. An error, a truncated body, or a silent stall meant silence for the rest of the round, with no affordance for the viewer.

This was reported live from a prod game. nginx access logs showed a complete body delivered for every round of that session (and no ffmpeg failures), so playback died browser-side after the bytes arrived — exactly the case nothing handled.

Change

Re-fetching a stream URL is always safe: the server seeks each GET to the live position. So every failure mode now takes the same path — re-request and resume in sync with the room.

Recovery triggers:

  • error
  • ended landing earlier than the playback should have finished (truncated body)
  • the stalled event
  • a decode-progress watchdog — nothing decoded for 5s while the element believes it is playing (this is the case that fires no event at all)
  • online, and the tab becoming visible again

Bounded so a genuinely dead playback can't hammer the server: retries stop at the playback's own deadline (playbackDurationSec is now passed into handleRoundAudio), after 4 backed-off attempts, or after 6 total fetches per playback. Past that, stalled surfaces and the sound widget offers a manual "Sound stopped — tap to resume" (activity.web.sound.retry, filled in for all locales). Retries append ?a=<n> so the media cache can't replay the dead response.

Also fixes a misclassification: any play() rejection was treated as an autoplay block, showing an "Enable sound" pill that wouldn't help and blocking auto-recovery. Only NotAllowedError is a permission problem now — everything else is a dropout.

Server side, stream starts (with seek / remaining), 404 and 410 lookups, and clients disconnecting mid-body are now logged. Previously nothing was logged unless ffmpeg itself failed, which is why the original report had no server-side trail.

Verification

Driven in headless Chrome against a fake audio server, in real wall-clock time:

  • Stream hangs mid-body (3s of audio, socket held open, no error/ended — the reported symptom): previously permanent silence; now re-fetches at +3.9s and plays through to the end.
  • Permanently broken stream: stops after 6 fetches and raises stalled, so the manual pill appears.
  • Healthy stream: single fetch, plays to the end, no spurious retries.

tsc (both projects), prettier, lint-ci, lint-i18n (0 warnings), vite build, and the 71 web unit tests all pass.

A web listener's <audio> element had no recovery path: an error, a
truncated body, or a silent stall meant silence for the rest of the round
with nothing the viewer could do about it. Reported live from a prod
game; nginx showed complete bodies delivered for every round, so the
playback died browser-side after the bytes arrived.

Re-fetching a stream URL is always safe — the server seeks each GET to
the live position — so every failure mode now takes the same path:
re-request and resume in sync with the room. Triggered by `error`, an
`ended` that lands earlier than the playback should, the `stalled` event,
a decode-progress watchdog (nothing decoded for 5s while playing),
`online`, and the tab becoming visible again.

Bounded so a genuinely dead playback can't hammer the server: retries
stop at the playback's own deadline (playbackDurationSec is now passed
into handleRoundAudio), after 4 backed-off attempts, or after 6 total
fetches per playback — then `stalled` surfaces and the sound widget
offers a manual "tap to resume". Retries append `?a=<n>` so the media
cache can't replay the dead response.

Also fixes a misclassification: any play() rejection was treated as an
autoplay block, showing an "Enable sound" pill that wouldn't help and
blocking auto-recovery. Only NotAllowedError is a permission problem now.

Server side, log stream starts (with seek/remaining), 404/410 lookups,
and clients that disconnect mid-body, so the next occurrence leaves a
trail — previously nothing was logged unless ffmpeg itself failed.
@taahamahdi
taahamahdi requested a review from Brainicism as a code owner August 15, 2026 07:41
@taahamahdi
taahamahdi enabled auto-merge (squash) August 15, 2026 08:05
@taahamahdi
taahamahdi merged commit 92c0bde into master Aug 15, 2026
6 checks passed
@taahamahdi
taahamahdi deleted the web-audio-resiliency branch August 15, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant