Fixes#47 (the remaining part; #52 already refused up front with not_permitted).
Problem: soundboard clips publish as a separate Unknown-source track. Tokens for rooms with a call policy (io.lotus.room_quality) only allow listed sources, so LiveKit refuses that track. The soundboard therefore didn't work in any policy room, not just audio-only ones.
Now (soundboardMode() picks per room):
"track": unrestricted rooms keep the separate track, unchanged.
"mic": where the token allows the microphone, the clip is mixed into the published microphone for its duration.
The track currently on the mic's RTCRtpSender (the denoised one, when noise suppression is on) and the clip feed a MediaStreamAudioDestinationNode. The sender switches to the mix with replaceTrack, and the original goes back when the clip ends.
The server still sees one ordinary microphone track.
Muting mid-clip stops the clip (#13: muted means silent).
If LiveKit swapped its own track in meanwhile (device change, denoise toggle), the restore leaves it alone.
null: no microphone grant either, so still not_permitted.
Verified
Unit tests, 5 new (13/13 in the file): the mode table; the mix path (no separate publish, sender swapped, clip started, original restored and the mixed track stopped on end); mute mid-clip restores; a LiveKit-swapped track isn't overwritten; no mic grant → not_permitted.
Full suite under Node 24: 756 passed. tsc, oxlint and knip are clean.
Two-person call in an audio-only voice room (allow_camera: false, allow_screenshare: false), both with a silent fake mic, Audit Main pressing "Beep":
Bob's received audio energy from Audit Main: 0 → 0.040 during the clip → 0 after.
Audit Main's outgoing mic track: 4659b8c5 → d999d550 (the mix) → back to 4659b8c5.
No "soundboard isn't available", and no permission errors.
Fixes #47 (the remaining part; #52 already refused up front with `not_permitted`).
**Problem:** soundboard clips publish as a separate `Unknown`-source track. Tokens for rooms with a call policy (`io.lotus.room_quality`) only allow listed sources, so LiveKit refuses that track. The soundboard therefore didn't work in **any** policy room, not just audio-only ones.
**Now** (`soundboardMode()` picks per room):
- `"track"`: unrestricted rooms keep the separate track, unchanged.
- `"mic"`: where the token allows the microphone, the clip is **mixed into the published microphone** for its duration.
- The track currently on the mic's `RTCRtpSender` (the denoised one, when noise suppression is on) and the clip feed a `MediaStreamAudioDestinationNode`. The sender switches to the mix with `replaceTrack`, and the original goes back when the clip ends.
- The server still sees one ordinary microphone track.
- **Muting mid-clip stops the clip** (#13: muted means silent).
- If LiveKit swapped its own track in meanwhile (device change, denoise toggle), the restore leaves it alone.
- `null`: no microphone grant either, so still `not_permitted`.
**Verified**
- Unit tests, 5 new (13/13 in the file): the mode table; the mix path (no separate publish, sender swapped, clip started, original restored and the mixed track stopped on end); mute mid-clip restores; a LiveKit-swapped track isn't overwritten; no mic grant → `not_permitted`.
- Full suite under Node 24: 756 passed. tsc, oxlint and knip are clean.
- **Two-person call** in an audio-only voice room (`allow_camera: false, allow_screenshare: false`), both with a silent fake mic, Audit Main pressing "Beep":
- Bob's received audio energy from Audit Main: **0 → 0.040 during the clip → 0 after**.
- Audit Main's outgoing mic track: `4659b8c5` → `d999d550` (the mix) → back to `4659b8c5`.
- No "soundboard isn't available", and no permission errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
Clips publish as a separate Unknown-source track, which tokens limited to
listed sources (any room with io.lotus.room_quality) refuse, so #52 could
only answer not_permitted. Where the token allows the microphone, the
clip is now mixed into the published mic for its duration: the track on
the mic's RTCRtpSender (post-denoise) and the clip feed a
MediaStreamAudioDestinationNode, the sender switches to the mix with
replaceTrack, and the original goes back when the clip ends. Muting
mid-clip stops it; a track LiveKit swapped in meanwhile is left alone.
not_permitted remains for tokens without the microphone.
Fixes#47
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #47 (the remaining part; #52 already refused up front with
not_permitted).Problem: soundboard clips publish as a separate
Unknown-source track. Tokens for rooms with a call policy (io.lotus.room_quality) only allow listed sources, so LiveKit refuses that track. The soundboard therefore didn't work in any policy room, not just audio-only ones.Now (
soundboardMode()picks per room):"track": unrestricted rooms keep the separate track, unchanged."mic": where the token allows the microphone, the clip is mixed into the published microphone for its duration.RTCRtpSender(the denoised one, when noise suppression is on) and the clip feed aMediaStreamAudioDestinationNode. The sender switches to the mix withreplaceTrack, and the original goes back when the clip ends.null: no microphone grant either, so stillnot_permitted.Verified
not_permitted.allow_camera: false, allow_screenshare: false), both with a silent fake mic, Audit Main pressing "Beep":4659b8c5→d999d550(the mix) → back to4659b8c5.🤖 Generated with Claude Code
https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5