feat(frontend): shared pod-wide test audio

Make the "Test audio" connectivity check pod-wide instead of local to the
publisher. Shared on/off state is derived from the presence of the podman-beat
track across participants (self-syncing on join/leave). Any participant can
stop it: the owner unpublishes directly; non-owners send a new additive
BEAT_STOP data message that the owner honors. Drives the button label, status
line, and status waveform for everyone, and guards against rapid-double-click
double-publish and mid-start unmount leaks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kartikeya
2026-06-27 23:39:34 -07:00
parent e0757088ef
commit 7269098ea3
4 changed files with 230 additions and 24 deletions
+64
View File
@@ -0,0 +1,64 @@
# Shared Test Audio — pod-wide connectivity check
> Spec for the `frontend/src/livekit/useBeat.ts` + `PodView.tsx` test-audio
> behavior and the additive `BEAT_STOP` data message. Satisfies the
> documentation-first gate for those files.
## Why
The **Test audio** button is PodMan's pre-flight check that the LiveKit audio
path works for the whole pod — the same path the urgent Gemini-TTS voice
escalation rides on. Today the beat is published correctly but its on/off state
is **local to the publisher**: teammates can't see it's playing and can't stop
it. This makes it a shared, pod-wide toggle so a judge sees the state flip on
every screen at once.
## Behavior
- Any participant clicks **Test audio** → they publish the `podman-beat` audio
track (Web Audio, `lib/beat.ts`). Everyone auto-subscribes and hears it.
- The shared on/off state is **derived from the track's presence**, not a synced
flag — so it self-syncs across joins/leaves and can't drift from reality. The
publisher is the **owner**.
- Anyone can stop it:
- Owner clicks **Stop audio** → unpublishes its own track directly.
- Non-owner clicks **Stop (`<owner>`'s)** → sends `BEAT_STOP`; the owner
unpublishes. (LiveKit forbids unpublishing another participant's track, so a
request is the only way.)
- The Status card shows `publishing` / `<owner> playing` / `ready`, and the
waveform animates (`active`) for everyone while the test is live.
## State derivation (source of truth = the track)
`useBeat(room)` returns `{ on, by, mine }`, recomputed from the presence of a
track named `podman-beat` across `localParticipant` + `remoteParticipants` on
these events: `LocalTrackPublished/Unpublished`, `TrackPublished/Unpublished`,
`TrackSubscribed/Unsubscribed`, `ParticipantConnected/Disconnected`. Owner
disconnect and late-join sync therefore need no extra messaging.
## Contract (additive)
`shared/src/messages.ts` — one new message on the existing `podman.intervention`
data topic:
| { type: 'BEAT_STOP' } // any participant → owner: stop the shared beat
Additive to the `DataMessage` union; existing consumers ignore unknown types.
**No backend / API change.**
## Known limitation (LiveKit constraint)
A client can only unpublish **its own** tracks, so a non-owner's **Stop** is a
`BEAT_STOP` _request_ the owner must honor. If the owner disconnects **uncleanly**
(crash / network drop), the SFU keeps the track published until it times the
participant out — during that window the beat keeps playing and non-owners can't
stop it. A clean disconnect clears it immediately via `ParticipantDisconnected`.
Demo mitigation: have the same person who starts the test also stop it.
## Files
- `shared/src/messages.ts``BEAT_STOP` message (additive).
- `frontend/src/livekit/useBeat.ts``useBeat(room)` hook.
- `frontend/src/components/PodView.tsx` — button label, status line, waveform
`active` driven by the hook.
- `frontend/src/lib/beat.ts` — unchanged (existing Web-Audio beat source).