Add an online mode that plays the sounds on the viewer - #45
Merged
Merged
Conversation
Design document for issue #40: route buzzers, daily double, timeout and waiting music to the viewer tab, which is the one screen-shared during online events. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Online mode moves the game sounds to the viewer, which is the tab that gets screen-shared during online events. This commit only introduces the switch and exposes it to the front-end; nothing reads it yet. The dev entrypoint takes no arguments, so run.py honours CEOPARDY_ONLINE to give `make run` the same capability. Refs #40 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The host asks the server to play a sound and the server relays it on the /game namespace; which window actually makes noise stays a client-side decision. Buzzers and the daily double already had a broadcast to ride on, but the timeout and thinking music were purely host-local clicks, so a channel is needed for them regardless. One generic event covers all five. The thinking music is persisted as a State row since it is the only sound with duration: a viewer joining or reloading mid-break resumes it from /api/v1/state rather than sitting in silence. Refs #40 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All sound handling lived inline in HostView, which made it impossible for the viewer to play anything. It now sits in one composable that both views use, with a single isAudioSink() predicate deciding which window makes noise: the host normally, the viewer in online mode, and never the viewer iframe the host embeds in its footer drawer. The host no longer plays sounds directly; it asks the server to broadcast a cue and reacts to the broadcast like any other client. That also fixes the daily-double sound firing off the REST response, which left a second host window silent. Two long-standing bugs fall out of the move: the waiting music now loops instead of playing once, and its state lives on the server so a client joining mid-break hears it. The preload map that was built and never read is gone. Refs #40 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The viewer becomes the audio sink and gets a one-time "Click to enable sound" overlay, because browsers refuse to play audio until the tab has seen a user gesture. The operator clicks it once while setting up the screen share and it never reappears, so it stays off the crowd screen. The waiting music follows server state rather than the one-shot event, so a client joining or reloading mid-break picks it up. Its cue also mirrors into the local state, since POST /api/v1/sound broadcasts the cue without a full state refresh. Refs #40 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refs #40 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
obilodeau
commented
Sep 6, 2026
obilodeau
commented
Sep 6, 2026
The sound names and their URLs were written out twice, once in Python for validation and once in TypeScript for playback, with only a comment asking the next person to keep them in sync. The back-end now owns the registry and hands it to the front-end in /api/v1/state, so adding a sound is a one-line change to SOUND_FILES. Names and filenames stay a mapping rather than a directory listing: several of them differ (dailydouble, thinking), so scanning the directory would rename half the sounds. Unlocking audio now uses an inline silent clip instead of borrowing buzzer1.wav, so it no longer depends on a sound file being present -- most of them are gitignored for licensing. Refs #40 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It now looks like a system dialog instead of an in-game thing
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.
Closes #40.
During an online event the tab we screen-share is the viewer, but every
sound has always played on the host machine — so the audience heard nothing.
ceopardy serve --onlineflips that: the viewer becomes the audio sink andthe host goes quiet.
How it works
The host no longer plays sounds directly. It asks the server to broadcast a
cue on the
/gamenamespace, and each client decides for itself whether itis the one that should make noise:
That iframe guard matters — the host footer drawer embeds a live viewer, so
without it online mode would make the host louder rather than silent.
A pure host→viewer Socket.IO design isn't possible here: the server
registers only
connect/disconnectand Flask-SocketIO drops client-emittedevents it has no handler for. Since
timeoutand the waiting music had noserver round-trip at all, a channel was needed regardless, so this uses one
generic
soundevent behind a RESTPOST /api/v1/sound, matching thedocumented client→server-is-REST convention.
The viewer shows a one-time "Click to enable sound" overlay, because
browsers won't play audio until the tab has seen a user gesture. It's
dismissed during screen-share setup and never returns.
Drive-by fixes
Extracting the audio layer surfaced three existing bugs, all fixed here:
broadcast, leaving a second host window silent;
preloadmap was built on every import and never read.Testing
make cigreen (38 tests, 22 of them new for the payload validator).five cues broadcast to a connected viewer, bad payloads rejected 400 with
no broadcast, and the waiting-music flag surviving in
/api/v1/stateforclients that join mid-break.
ONLINE_MODEconfirmed reaching the front-end through/api/v1/state.Audio playback itself still wants a human ear — worth one pass with two
windows before merging.
The design write-up is in
docs/plans/2026-09-06-online-mode-viewer-sound.md.🤖 Generated with Claude Code