// Session-only per-share audio volume. Mirror of `participantVolumes` but // NOT persisted — when the user leaves the call or restarts the app, these // reset to default. Intentional: the relevant trackSid is ephemeral anyway, // and users don't expect screen-share volume to survive between sessions. // // Keys are participantIds (LiveKit identity). There's one screen-share per // participant at a time in LiveKit, so keying by id keeps the API aligned // with how the context menu surfaces the control ("Dennis's share"). const DEFAULT_VOLUME = 1; // Matches Discord's slider range — up to 200% via the WebAudio GainNode in // remoteAudioPipelines. HTMLMediaElement.volume only goes to 1.0, so the // above-100% values are only meaningful on the WebAudio output path. const MAX_VOLUME = 2; type VolumeMap = Record; type Listener = (map: VolumeMap) => void; let current: VolumeMap = {}; const listeners = new Set(); function clamp(v: number): number { if (!Number.isFinite(v)) return DEFAULT_VOLUME; if (v < 0) return 0; if (v > MAX_VOLUME) return MAX_VOLUME; return v; } function notify(): void { for (const fn of listeners) fn(current); } export function getScreenShareVolume(userId: string): number { return current[userId] ?? DEFAULT_VOLUME; } export function setScreenShareVolume(userId: string, volume: number): void { const next = clamp(volume); if (next === (current[userId] ?? DEFAULT_VOLUME)) return; current = { ...current, [userId]: next }; applyToAttachedElements(userId, next); notify(); } export function subscribeScreenShareVolumes(fn: Listener): () => void { listeners.add(fn); return () => { listeners.delete(fn); }; } // Reset on call end — called from CallContext when CallState.idle triggers. export function clearScreenShareVolumes(): void { if (Object.keys(current).length === 0) return; current = {}; notify(); } // Live-apply to any