Skip to content

fix(stream): never go live without browser audio; end silent connections - #825

Merged
anneoneone merged 2 commits into
mainfrom
fix/silent-go-live
Oct 30, 2026
Merged

anneoneone merged 2 commits into
mainfrom
fix/silent-go-live

Conversation

@anneoneone

@anneoneone anneoneone commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

The host panel no longer goes on air when the browser sends no audio, and the backend ends a stream connection whose first audio chunk does not arrive within 15 s.

Found during show 48 (2026-10-30). The status said "live" and "recording" for half an hour, but /live.mp3 played silence. Every session from robi's browser ended with 0 chunks / 0 bytes received. Liquidsoap never got a source, and no recording segment was written.

Issue: none filed.

What changes:

  • Frontend: Go Live refuses without a captured input, and stops the socket when the recorder fails to start. A suspended audio context is resumed. The host sees an error when no audio was sent 5 s after start. The test broadcast cannot be passed while "0 chunks sent".
  • Backend: the stream ends with an error frame if no audio chunk arrives within 15 s (STREAM_FIRST_AUDIO_TIMEOUT_SECS). The recording keeps its reconnect grace.

Both parts reach hosts only with a backend deploy: the host panel at https://admin.live.moafunk.de is built into the backend image. Prod still runs 7093fe5.

Validation:

  • Frontend: npm test -- --run 186 passed, npm run lint ok, npm run typecheck ok, npm run build ok.
  • Backend: cargo test 116 passed, 4 ignored. cargo fmt --check clean. cargo clippy --all-targets has no warnings in new code; the existing ones stay.
  • Regression: the 3 new capture behaviour tests fail against main's useAudioCapture.ts. connection_without_audio_is_ended fails against main's stream_ws.rs (no frame, timeout).
  • Not tested in a real browser. The suspended-context cause is likely, but it is not confirmed on robi's machine.
Implementation and review

This is a fix to main, not architecture-epic work, so the epic fields do not apply.

Epic: n/a
Executor: n/a
Lane: n/a
Reviewer: n/a
Leaf IDs: n/a

Evidence from prod (show 48, 19:55–20:23 UTC).

  • Backend 7093fe5. All sessions, test and live, logged Stream ended for user 'robi': 0 chunks / 0 bytes received.
  • The producer ffmpeg exited on stop with EBML header parsing failed (empty stdin).
  • No Liquidsoap harbor log at all. /live.mp3 measured −91 dB (mksafe blank).
  • The recording folder stayed empty. The finalize then failed on R2 with complete_multipart_upload: service error (0-byte artifact).
  • robi's last good show (2026-10-13) sent 24,353 chunks on the same image 7093fe5, so with the same backend and the same host panel. No code changed on our side.
  • The rehearsal (LiveSetupTest) checks that the input track is live and still sent 0 chunks. So the track was live but produced no data. This points to a suspended AudioContext: MediaRecorder records the gain node's destination stream, which gives no audio while the context is suspended. The context was created after await getUserMedia and was never resumed.

Frontend (useAudioCapture.ts, FlowOnAir.vue, LiveSetupTest.vue).

  • resumeAudioContext() runs after setupAudioProcessing, in startRecording, and on the user clicks for test and reconnect. resume() only works from a user gesture. The countdown auto-start is not one, so the reconnect click is the recovery path.
  • startRecording refuses when the device track has ended. It checks the device stream, because the processed stream's track stays live.
  • A 5 s watchdog per recorder reports NO_AUDIO_DATA_MESSAGE through onError when no non-empty blob was sent. It is cleared on stop and restart.
  • handleGoLive checks audioDeviceOk before it connects, wires setOnError to the visible error line, and calls stopStream() when startRecording() returns false. The order stays connect → record, so the first chunk with the WebM header is not lost.
  • The "Stream Ended" screen shows the error, because a backend-ended session lands there.

Backend (stream_ws.rs, config.rs).

  • While chunks_received == 0, receiver.next() waits with timeout_at(first_audio_deadline). On timeout it sends NO_AUDIO_ERROR and breaks the loop.
  • The loop is left as a normal drop: the stream stops unless it was replaced, and the recording gets the 30 s reconnect grace.
  • After the first chunk the loop is unchanged. Gaps later in the show do not trigger it.
  • A replaced socket that never sent audio is also closed after 15 s; the superseded check leaves the current stream alone.
  • Config.stream_first_audio_timeout_secs (serde default 15). Config is only built through serde, so the new field breaks no struct literal.

Tests.

  • frontend/tests/useAudioCapture.test.ts: fake AudioContext (starts suspended) and MediaRecorder. Covers resume, ended track, watchdog fires on empty blobs, stays quiet with data, stays quiet after stop.
  • backend/src/broadcast_tests.rs: connection_without_audio_is_ended, first_audio_chunk_lifts_the_no_audio_deadline. Both use the real router and ffmpeg with a 1 s limit.

GitNexus impact (upstream): all LOW. handle_stream_socket has 1 caller, startRecording 1 (restartRecording), setupAudioProcessing 2, handleGoLive 1. detect_changes lists only the stream_ws_handler flows. The index was built on dev/312-interim, so some symbol names in its output are off.

Not in this PR.

  • Reconnect (manual ⟳ and auto) opens a new producer but keeps the running MediaRecorder. The new ffmpeg probably gets WebM clusters without the EBML header. Suspected, not reproduced.
  • dev/312-interim needs its own port through the epic process.
  • Finalizing an empty recording still tries an R2 upload and logs complete_multipart_upload: service error.

Show 48 was "on air" for half an hour with silence. The browser opened
the stream socket but sent no audio, and nothing told the host.

- Go Live refuses without a captured input. It stops the socket when
  the recorder fails to start.
- Resume a suspended AudioContext at capture, test, go-live and
  reconnect. A suspended context sends no audio.
- Show an error when the recorder sent no audio 5 s after start.
- The test broadcast cannot be passed while 0 chunks were sent.
Pings keep the stream socket open without audio. Then the status says
"live" and recording, but Icecast plays silence (show 48).

If the first audio chunk does not arrive within 15 s, the backend sends
an error frame and ends the stream like a dropped connection. The
recording keeps its reconnect grace.

The limit is STREAM_FIRST_AUDIO_TIMEOUT_SECS (default 15).
@vercel

vercel Bot commented Oct 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
live-moafunk-de Ready Ready Preview Oct 30, 2026 8:30pm UTC

This branch was successfully deployed

1 active deployment
Preview — 1c41c3aa Deployed Oct 30, 2026 by vercel[bot]
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