Recover web audio when a stream drops mid-round - #2404
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A web listener's
<audio>element had no recovery path. Anerror, 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:
errorendedlanding earlier than the playback should have finished (truncated body)stalledeventonline, and the tab becoming visible againBounded so a genuinely dead playback can't hammer the server: retries stop at the playback's own deadline (
playbackDurationSecis now passed intohandleRoundAudio), after 4 backed-off attempts, or after 6 total fetches per playback. Past that,stalledsurfaces 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. OnlyNotAllowedErroris 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:
error/ended— the reported symptom): previously permanent silence; now re-fetches at +3.9s and plays through to the end.stalled, so the manual pill appears.tsc(both projects),prettier,lint-ci,lint-i18n(0 warnings),vite build, and the 71 web unit tests all pass.