feat: jam output device — single-speaker, everyone else controls (#953)
add output_client_id to jam state so only one participant's browser
plays audio. everyone else sees the queue, controls playback, adds
tracks — but their browser doesn't produce sound.
backend:
- output_client_id/output_did in jam state, set_output command
- auto-set output to host on first WS sync
- clear output + pause when output device disconnects or leaves
- fix _close_ws_for_did race: clear output before popping client_id
- validate jam_id in set_output to prevent cross-jam spoofing
frontend playback fixes (discovered during integration):
- autoplay policy: queue.play()/pause() set player.paused synchronously
alongside jam bridge call — WS round-trip broke gesture context
- audio event fight: onplay/onpause handlers skip during jam —
drift correction seeking fired onpause, which paused playback
- output transfer: explicitly pause audio when isOutputDevice flips
false — was returning early without stopping the audio element
frontend UI:
- output status in queue panel and player stripe
- "play here" button for non-output devices
- speaker badge on output participant's avatar
- non-output progress bar interpolation (250ms interval)
12 new tests covering output lifecycle, cross-client commands,
WS replacement race condition, and jam_id validation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
authored by
zzstoatzz.io