Problem
Requesting the mic today is a separate, generic control, and camera/mic preview doesn't exist before promotion — a candidate has no way to see themselves or know their setup works until they're already live. Promotion itself re-acquires media from scratch via activateMedia()'s setCameraEnabled/setMicrophoneEnabled calls, which is a second getUserMedia() moment that can flicker/fail independently of the first.
Scope narrowed (2026-08-22)
The composer-integrated request design bullet below is implemented — real-device speaker-entry-friction testing made removing the standalone "Request the mic" control (and its separate justification form) urgent enough to pull forward, alongside #27. This issue now covers only what's left: candidate readiness (local media pre-acquisition), the persistent self-preview, and promotion without reacquiring media.
Scope narrowed further (2026-08-22)
Candidate readiness, the persistent self-preview, and promotion without reacquiring media are all implemented — see the comment below and DECISIONS.md for the full design and reasoning. This issue now covers only what's left: role-specific dominant video and a mic activity indicator.
Design
Composer-integrated request ✅ shipped: the chat composer has a mic-mode toggle (🎤) — submitting in mic-mode posts the request through the existing authoritative requestToSpeak path, replacing the standalone "Request the mic" control entirely. micRequestMode is a controlled prop lifted to EventRoom (not local to the composer), since tapping an empty seat that turns out to have a queue (issue #27) needs to switch this same composer into request mode from outside it.
Readiness without a second gesture ✅ shipped: submitting the mic-mode composer is itself the real user gesture that calls createLocalTracks({ audio: true, video: true }) once (useLiveRoomConnection.prepareLocalMedia), acquiring both tracks under Safari's gesture requirement — one combined permission prompt, not two.
Persistent self-preview, spatially stable ✅ shipped: a new SelfPreview component, always mounted whenever localVideoTrack is held, renders into #20's fixed corner slot, attaching the locally held track via .attach() — the same component/track instance through candidate readiness, promotion, and (per this issue's own explicit non-goal below) still just the local feed once actually speaking.
Promotion without reacquiring ✅ shipped: useLiveRoomConnection's publish path checks for already-prepared tracks first and calls publishTrack() on them directly, skipping a second getUserMedia() call entirely. Falls back to the existing gesture-gated setCameraEnabled/setMicrophoneEnabled path, unchanged, for anyone who never pre-acquired (issue #27's direct join).
Role-specific dominant video (partial, narrow) and mic activity indicator — remaining, unchanged from original design. The former is deliberately deferred rather than built now — the self-preview stays the local feed only; making the other speaker's video dominant once the local user is speaking is a larger visual redesign left to a later pass (possibly #18), not silently expanded into this one.
Explicit non-goal
No candidate snapshot/profile-preview icons for other waiting candidates. No change to #23's automatic-promotion scope — confirmed: resolveClaimDecision (the shared eligibility rule checkPromotionEligibility/claimOpenSeat both use) depends only on queue rank, never on media/readiness state, so this issue's remaining work required no changes there. No new server-side "readiness" field — local track possession is the whole signal (see DECISIONS.md).
Depends on
#20 (video-first room shell) — the self-preview slot must exist. Shipped.
Blocks
Nothing currently — #23's automatic promotion doesn't depend on any signal this issue produces (see the non-goal note above; that dependency assumed a design that changed once #23 was actually implemented).
Problem
Requesting the mic today is a separate, generic control, and camera/mic preview doesn't exist before promotion — a candidate has no way to see themselves or know their setup works until they're already live. Promotion itself re-acquires media from scratch via
activateMedia()'ssetCameraEnabled/setMicrophoneEnabledcalls, which is a secondgetUserMedia()moment that can flicker/fail independently of the first.Scope narrowed (2026-08-22)
The composer-integrated request design bullet below is implemented — real-device speaker-entry-friction testing made removing the standalone "Request the mic" control (and its separate justification form) urgent enough to pull forward, alongside #27. This issue now covers only what's left: candidate readiness (local media pre-acquisition), the persistent self-preview, and promotion without reacquiring media.
Scope narrowed further (2026-08-22)
Candidate readiness, the persistent self-preview, and promotion without reacquiring media are all implemented — see the comment below and DECISIONS.md for the full design and reasoning. This issue now covers only what's left: role-specific dominant video and a mic activity indicator.
Design
Composer-integrated request ✅ shipped: the chat composer has a mic-mode toggle (
🎤) — submitting in mic-mode posts the request through the existing authoritativerequestToSpeakpath, replacing the standalone "Request the mic" control entirely.micRequestModeis a controlled prop lifted toEventRoom(not local to the composer), since tapping an empty seat that turns out to have a queue (issue #27) needs to switch this same composer into request mode from outside it.Readiness without a second gesture ✅ shipped: submitting the mic-mode composer is itself the real user gesture that calls
createLocalTracks({ audio: true, video: true })once (useLiveRoomConnection.prepareLocalMedia), acquiring both tracks under Safari's gesture requirement — one combined permission prompt, not two.Persistent self-preview, spatially stable ✅ shipped: a new
SelfPreviewcomponent, always mounted wheneverlocalVideoTrackis held, renders into #20's fixed corner slot, attaching the locally held track via.attach()— the same component/track instance through candidate readiness, promotion, and (per this issue's own explicit non-goal below) still just the local feed once actually speaking.Promotion without reacquiring ✅ shipped:
useLiveRoomConnection's publish path checks for already-prepared tracks first and callspublishTrack()on them directly, skipping a secondgetUserMedia()call entirely. Falls back to the existing gesture-gatedsetCameraEnabled/setMicrophoneEnabledpath, unchanged, for anyone who never pre-acquired (issue #27's direct join).Role-specific dominant video (partial, narrow) and mic activity indicator — remaining, unchanged from original design. The former is deliberately deferred rather than built now — the self-preview stays the local feed only; making the other speaker's video dominant once the local user is speaking is a larger visual redesign left to a later pass (possibly #18), not silently expanded into this one.
Explicit non-goal
No candidate snapshot/profile-preview icons for other waiting candidates. No change to #23's automatic-promotion scope — confirmed:
resolveClaimDecision(the shared eligibility rulecheckPromotionEligibility/claimOpenSeatboth use) depends only on queue rank, never on media/readiness state, so this issue's remaining work required no changes there. No new server-side "readiness" field — local track possession is the whole signal (see DECISIONS.md).Depends on
#20 (video-first room shell) — the self-preview slot must exist. Shipped.
Blocks
Nothing currently — #23's automatic promotion doesn't depend on any signal this issue produces (see the non-goal note above; that dependency assumed a design that changed once #23 was actually implemented).