Files
cinny/src/app/hooks/useCallQuality.ts
T
jared 7c06b27c73
CI / Build & Quality Checks (push) Successful in 10m49s
CI / Trigger Desktop Build (push) Successful in 8s
feat(call): in-call soundboard, quality controls, room call-permissions
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>
2026-06-30 22:34:17 -04:00

42 lines
1.9 KiB
TypeScript

import { useEffect } from 'react';
import { useAtomValue } from 'jotai';
import { CallEmbed } from '../plugins/call';
import { settingsAtom } from '../state/settings';
import { useStateEvent } from './useStateEvent';
import { StateEvent } from '../../types/matrix/room';
import { buildQualityPayload, RoomQualityContent } from '../utils/callQuality';
/**
* [P5-31] Apply the user's call quality settings (clamped by any room-level
* cap) to the Element Call fork via the `io.lotus.set_quality` widget action.
*
* The fork stores the settings and re-applies them on every (re)publish and
* reconnect, so we only need to (re)send when the payload changes or the widget
* becomes ready — no need to poll the track lifecycle here.
*/
export function useCallQuality(embed: CallEmbed): void {
const { callAudioBitrate, screenshareBitrate, screenshareFramerate } = useAtomValue(settingsAtom);
const roomQualityEvent = useStateEvent(embed.room, StateEvent.LotusRoomQuality);
const roomCaps = roomQualityEvent?.getContent<RoomQualityContent>();
// Depend on the primitive cap values (not the event object) so re-renders
// don't resend needlessly.
const audioCap = roomCaps?.audio_max_kbps;
const ssCap = roomCaps?.screenshare_max_kbps;
const fpsCap = roomCaps?.screenshare_max_fps;
useEffect(() => {
const payload = buildQualityPayload(
{ callAudioBitrate, screenshareBitrate, screenshareFramerate },
{ audio_max_kbps: audioCap, screenshare_max_kbps: ssCap, screenshare_max_fps: fpsCap },
);
const send = (): void => embed.control.setQuality(payload);
// Send now (settings are sticky fork-side even if tracks aren't up yet) and
// again once the widget signals ready, in case the transport wasn't up.
send();
const off = embed.onReady(send);
return off;
}, [embed, callAudioBitrate, screenshareBitrate, screenshareFramerate, audioCap, ssCap, fpsCap]);
}