// Screen-share quality presets modelled on Discord's tiers. Values are the // upper bounds — LiveKit + WebRTC's congestion control dynamically drop to // lower spatial/temporal layers (SVC with VP9) when the uplink degrades, so // these numbers behave as "up to" caps, not constant bitrates. const STORAGE_KEY = 'chatapp.screenshare'; export type ScreenSharePreset = | 'auto' | '720p30' | '720p60' | '1080p30' | '1080p60' | '1440p60' | '4k60'; // Browser getDisplayMedia `displaySurface` hint. The OS picker honours this // to pre-filter the source list (monitor = whole display, window = single // window). `null` leaves everything selectable. export type DisplaySurfaceHint = 'monitor' | 'window' | null; export interface ScreenShareSettings { preset: ScreenSharePreset; displaySurface: DisplaySurfaceHint; // User-chosen framerate. `null` falls back to preset's default. framerateOverride: number | null; // Include system audio ("go live" style). On some hosts getDisplayMedia // can't capture system audio (macOS without special entitlements, some // Linux setups). If the browser ignores the `audio: true` request we // silently fall through to a video-only share. includeSystemAudio: boolean; // While system audio is being shared, mute the local playback of remote // call audio to prevent peers from hearing themselves echoed back via the // loopback capture. Defaults to true — Chromium's loopback grant in // Electron *should* exclude the app's own render output, but the // process-tree exclusion isn't always watertight (WebView2/Chromium audio // sessions can render in process owners outside the tree). The user can // opt out (e.g. when they route call audio to a separate output device // with setSinkId, in which case the default-render-endpoint capture // never sees it). duckRemoteAudioWhileSharing: boolean; } const DEFAULTS: ScreenShareSettings = { preset: 'auto', displaySurface: null, framerateOverride: null, includeSystemAudio: false, // Default off — we use Electron's `loopback` (not `loopbackWithMute`) // so the user keeps local audio while sharing. Auto-ducking remote // mic audio for echo prevention also kills the user's ability to // hear peers, which most users don't want. Opt-in only. duckRemoteAudioWhileSharing: false, }; export interface PresetParams { dims: { width: number; height: number } | null; // null = browser picks native framerate: number; bitrateKbps: number; label: string; } const PRESET_PARAMS: Record = { auto: { dims: null, framerate: 30, bitrateKbps: 8000, label: 'Auto (Original)' }, '720p30': { dims: { width: 1280, height: 720 }, framerate: 30, bitrateKbps: 2500, label: '720p · 30 fps', }, '720p60': { dims: { width: 1280, height: 720 }, framerate: 60, bitrateKbps: 3500, label: '720p · 60 fps', }, '1080p30': { dims: { width: 1920, height: 1080 }, framerate: 30, bitrateKbps: 4000, label: '1080p · 30 fps', }, '1080p60': { dims: { width: 1920, height: 1080 }, framerate: 60, bitrateKbps: 6000, label: '1080p · 60 fps', }, '1440p60': { dims: { width: 2560, height: 1440 }, framerate: 60, bitrateKbps: 8000, label: '1440p · 60 fps', }, '4k60': { dims: { width: 3840, height: 2160 }, framerate: 60, bitrateKbps: 10_000, label: '4K · 60 fps', }, }; export const PRESET_ORDER: ReadonlyArray = [ 'auto', '720p30', '720p60', '1080p30', '1080p60', '1440p60', '4k60', ]; export function getPresetParams(p: ScreenSharePreset): PresetParams { return PRESET_PARAMS[p]; } type Listener = (s: ScreenShareSettings) => void; const listeners = new Set(); let cached: ScreenShareSettings | null = null; function isPreset(v: unknown): v is ScreenSharePreset { return typeof v === 'string' && v in PRESET_PARAMS; } function read(): ScreenShareSettings { if (cached) return cached; try { const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) { cached = DEFAULTS; return cached; } const parsed = JSON.parse(raw) as Partial; cached = { preset: isPreset(parsed.preset) ? parsed.preset : DEFAULTS.preset, displaySurface: parsed.displaySurface === 'monitor' || parsed.displaySurface === 'window' ? parsed.displaySurface : DEFAULTS.displaySurface, framerateOverride: typeof parsed.framerateOverride === 'number' && parsed.framerateOverride > 0 ? parsed.framerateOverride : DEFAULTS.framerateOverride, includeSystemAudio: typeof parsed.includeSystemAudio === 'boolean' ? parsed.includeSystemAudio : DEFAULTS.includeSystemAudio, duckRemoteAudioWhileSharing: typeof parsed.duckRemoteAudioWhileSharing === 'boolean' ? parsed.duckRemoteAudioWhileSharing : DEFAULTS.duckRemoteAudioWhileSharing, }; return cached; } catch { cached = DEFAULTS; return cached; } } function write(s: ScreenShareSettings): void { cached = s; try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch { /* quota / private mode */ } for (const l of listeners) l(s); } export function getScreenShareSettings(): ScreenShareSettings { return read(); } export function updateScreenShareSettings( patch: Partial, ): ScreenShareSettings { const next = { ...read(), ...patch }; write(next); return next; } export function subscribeScreenShareSettings(listener: Listener): () => void { listeners.add(listener); return () => listeners.delete(listener); }