Skip to content

Add an online mode that plays the sounds on the viewer - #45

Merged
obilodeau merged 9 commits into
mainfrom
online-mode-viewer-sound
Sep 8, 2026
Merged

obilodeau merged 9 commits into
mainfrom
online-mode-viewer-sound

Conversation

@obilodeau

Copy link
Copy Markdown
Owner

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 --online flips that: the viewer becomes the audio sink and
the host goes quiet.

How it works

The host no longer plays sounds directly. It asks the server to broadcast a
cue on the /game namespace, and each client decides for itself whether it
is the one that should make noise:

// useSound.ts
if (window.self !== window.top) return false;  // the host's embedded viewer iframe
return game.onlineMode ? !game.isHost : game.isHost;

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/disconnect and Flask-SocketIO drops client-emitted
events it has no handler for. Since timeout and the waiting music had no
server round-trip at all, a channel was needed regardless, so this uses one
generic sound event behind a REST POST /api/v1/sound, matching the
documented 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:

  • the waiting music never looped, so it played once and stopped;
  • the daily-double sound fired off the REST response instead of the
    broadcast, leaving a second host window silent;
  • a preload map was built on every import and never read.

Testing

  • make ci green (38 tests, 22 of them new for the payload validator).
  • Verified end to end against a running server with a Socket.IO client: all
    five cues broadcast to a connected viewer, bad payloads rejected 400 with
    no broadcast, and the waiting-music flag surviving in /api/v1/state for
    clients that join mid-break.
  • ONLINE_MODE confirmed 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

obilodeau and others added 6 commits September 6, 2026 20:31
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>
Comment thread frontend/src/composables/useSound.ts Outdated
Comment thread frontend/src/composables/useSound.ts Outdated
obilodeau and others added 3 commits September 6, 2026 21:40
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
@obilodeau
obilodeau merged commit aabdccf into main Sep 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an online mode option with sound playing on the viewer instead of the host

1 participant