feat(call): in-call soundboard, quality controls, room call-permissions
CI / Build & Quality Checks (push) Successful in 10m49s
CI / Trigger Desktop Build (push) Successful in 8s

Element Call is now consumed as our self-built fork
(@lotusguild/element-call-embedded); wire up its previously-dormant
capabilities and document the fork as live.

Soundboard (P5-15): a call-bar button plays user-uploaded audio clips into the
call as a real published track (io.lotus.inject_audio) plus local playback.
Clips are uploadable like emoji/sticker packs, stored in io.lotus.soundboard
account data (synced across devices). Gated by a Settings toggle + volume.

Quality controls (P5-31): per-user mic/screenshare bitrate + screenshare
framerate (Settings -> Calls), applied via io.lotus.set_quality clamped to any
room cap. Room admins set caps and hard call-permissions (allow_screenshare /
allow_camera) in Room Settings -> Voice; the call bar hides blocked buttons.

- New: CallSoundboard, useSoundboard, soundboardClips; RoomQuality,
  useCallQuality, callQuality (+ unit tests).
- Optimistic-write RoomQuality admin UI (no stale-state clobber).
- Docs: mark EC fork live across README/FEATURES/TODO/BUGS/TESTING; add D2
  manual-test steps.

Numeric quality caps are client-cooperative; screenshare/camera permissions are
hard-enforced server-side (see LotusGuild/matrix voice-limit-guard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 22:34:17 -04:00
parent 02b2ce8109
commit 7c06b27c73
22 changed files with 1259 additions and 120 deletions
+38
View File
@@ -7,6 +7,17 @@ export enum CallControlEvent {
StateUpdate = 'state_update',
}
/**
* [lotus #7 / P5-31] Payload for the fork's `io.lotus.set_quality` action.
* All fields optional; `null` clears that cap. Bits/sec for bitrates, fps for
* framerate.
*/
export type LotusQualityPayload = {
audioMaxBitrate?: number | null;
screenshareMaxBitrate?: number | null;
screenshareMaxFramerate?: number | null;
};
export class CallControl extends EventEmitter implements CallControlState {
private state: CallControlState;
@@ -358,6 +369,33 @@ export class CallControl extends EventEmitter implements CallControlState {
this.call.transport.send('io.lotus.focus_participant', { userId: null }).catch(() => undefined);
}
/**
* [lotus #3 / P5-15] Inject a soundboard clip into the call so other
* participants hear it. The fork publishes it as a separate LiveKit audio
* track (`io.lotus.inject_audio`) rather than splicing the mic. `url` must be
* an https/blob URL the widget can fetch WITHOUT credentials — the host
* resolves an mxc clip to a `blob:` object URL first (authenticated media
* can't be fetched cross-realm by the widget). `volume` is 01.
*
* The local user does not hear their own published track, so callers should
* also play the clip locally for feedback.
*/
public injectAudio(url: string, volume = 1): void {
this.call.transport.send('io.lotus.inject_audio', { url, volume }).catch(() => undefined);
}
/**
* [lotus #7 / P5-31] Apply audio/screenshare encoding limits to the local
* published tracks (the fork's `io.lotus.set_quality` action, via
* `RTCRtpSender.setParameters` — no republish). Bitrates are bits/sec,
* framerate is fps. A field set to `null` clears that cap. Settings are
* sticky fork-side (re-applied on every re-publish / reconnect). Values are
* clamped fork-side, so out-of-range input can't brick the encoder.
*/
public setQuality(settings: LotusQualityPayload): void {
this.call.transport.send('io.lotus.set_quality', settings).catch(() => undefined);
}
public dispose() {
this.bodyMutationObserver.disconnect();
this.controlMutationObserver.disconnect();
+5
View File
@@ -179,6 +179,11 @@ export class CallEmbed {
// - transparent background so the room wallpaper shows through natively
lotusCallState: 'true',
lotusTransparent: 'true',
// [lotus #3 / P5-15] Arm the fork's audio-inject handler so the in-call
// soundboard can publish clips into the call. Dormant until the host
// sends io.lotus.inject_audio (only on an explicit user click), so
// arming it for every call is safe.
lotusAudioInject: 'true',
});
if (denoiseMode === 'ml') {