import { LotusQualityPayload } from '../plugins/call/CallControl'; import { CallAudioBitrate, ScreenshareBitrate, ScreenshareFramerate } from '../state/settings'; /** * [P5-31] Room-level quality caps, stored in the `io.lotus.room_quality` state * event. Admins set a ceiling every client must stay under. Values mirror the * user-setting units (kbps / fps); `undefined`/absent = no cap. * * NOTE: the client applies these as a best-effort UX cap. Hard enforcement for * ALL Matrix clients is a server-side follow-up (a `voice-limit-guard`-style * sidecar on LXC 151 that reads this event) — see LOTUS_TODO.md P5-31. */ export type RoomQualityContent = { // Numeric caps: client-cooperative only (our fork honors them; the SFU cannot // enforce publisher bitrate/fps — LiveKit forwards, never transcodes). audio_max_kbps?: number; screenshare_max_kbps?: number; screenshare_max_fps?: number; // Publish-source policy: HARD-enforced server-side for ALL clients by the // voice-limit-guard (it re-signs the LiveKit JWT's canPublishSources). // Absent/true = allowed; only an explicit false forbids. allow_screenshare?: boolean; allow_camera?: boolean; }; // Selectable options (kbps / fps), shared by the settings UI and the room-admin // UI so they stay in sync. Values are strings so they satisfy SettingsSelect's // `T extends string` constraint; parsed to numbers in buildQualityPayload. export const AUDIO_BITRATE_OPTIONS: { value: CallAudioBitrate; label: string }[] = [ { value: 'auto', label: 'Auto' }, { value: '32', label: '32 kbps' }, { value: '64', label: '64 kbps' }, { value: '96', label: '96 kbps' }, { value: '128', label: '128 kbps' }, { value: '256', label: '256 kbps' }, ]; export const SCREENSHARE_BITRATE_OPTIONS: { value: ScreenshareBitrate; label: string }[] = [ { value: 'auto', label: 'Auto' }, { value: '500', label: '0.5 Mbps' }, { value: '1500', label: '1.5 Mbps' }, { value: '3000', label: '3 Mbps' }, { value: '8000', label: '8 Mbps' }, ]; export const SCREENSHARE_FRAMERATE_OPTIONS: { value: ScreenshareFramerate; label: string }[] = [ { value: 'auto', label: 'Auto' }, { value: '15', label: '15 fps' }, { value: '30', label: '30 fps' }, { value: '60', label: '60 fps' }, ]; /** Lower of two caps, treating `undefined` as "no cap on that side". */ const minCap = (a: number | undefined, b: number | undefined): number | undefined => { if (a === undefined) return b; if (b === undefined) return a; return Math.min(a, b); }; /** Parse a setting value ('auto' | numeric string) to a number or undefined. */ const num = (v: string): number | undefined => { if (v === 'auto') return undefined; const n = parseInt(v, 10); return Number.isFinite(n) ? n : undefined; }; type QualitySettings = { callAudioBitrate: CallAudioBitrate; screenshareBitrate: ScreenshareBitrate; screenshareFramerate: ScreenshareFramerate; }; /** * Build the `io.lotus.set_quality` payload from the user's settings, clamped by * any room-level cap. Every field is always present so clearing a setting back * to 'auto' sends an explicit `null` that resets the fork-side cap (otherwise a * previously-applied cap would stick for the rest of the call). */ export const buildQualityPayload = ( settings: QualitySettings, roomCaps?: RoomQualityContent, ): LotusQualityPayload => { const userAudio = num(settings.callAudioBitrate); const userSsBitrate = num(settings.screenshareBitrate); const userSsFps = num(settings.screenshareFramerate); const audioKbps = minCap(userAudio, roomCaps?.audio_max_kbps); const ssKbps = minCap(userSsBitrate, roomCaps?.screenshare_max_kbps); const ssFps = minCap(userSsFps, roomCaps?.screenshare_max_fps); return { audioMaxBitrate: audioKbps === undefined ? null : audioKbps * 1000, screenshareMaxBitrate: ssKbps === undefined ? null : ssKbps * 1000, screenshareMaxFramerate: ssFps === undefined ? null : ssFps, }; };