import { STORAGE_KEY_VOICE_SETTINGS } from '../../../../core/constants'; import { DEFAULT_LATENCY_PROFILE, DEFAULT_SCREEN_SHARE_QUALITY, LATENCY_PROFILES, SCREEN_SHARE_QUALITIES, type LatencyProfile, type ScreenShareQuality } from '../../../../shared-kernel'; export interface VoiceSettings { inputDevice: string; outputDevice: string; inputVolume: number; outputVolume: number; audioBitrate: number; latencyProfile: LatencyProfile; includeSystemAudio: boolean; noiseReduction: boolean; screenShareQuality: ScreenShareQuality; askScreenShareQuality: boolean; } export const DEFAULT_VOICE_SETTINGS: VoiceSettings = { inputDevice: '', outputDevice: '', inputVolume: 100, outputVolume: 100, audioBitrate: 96, latencyProfile: DEFAULT_LATENCY_PROFILE, includeSystemAudio: false, noiseReduction: true, screenShareQuality: DEFAULT_SCREEN_SHARE_QUALITY, askScreenShareQuality: true }; export function loadVoiceSettingsFromStorage(): VoiceSettings { if (cachedSettings) { return { ...cachedSettings }; } try { const raw = localStorage.getItem(STORAGE_KEY_VOICE_SETTINGS); if (!raw) { cachedSettings = { ...DEFAULT_VOICE_SETTINGS }; return { ...cachedSettings }; } cachedSettings = normaliseVoiceSettings(JSON.parse(raw) as Partial); return { ...cachedSettings }; } catch { cachedSettings = { ...DEFAULT_VOICE_SETTINGS }; return { ...cachedSettings }; } } export function saveVoiceSettingsToStorage(patch: Partial): VoiceSettings { const nextSettings = normaliseVoiceSettings({ ...loadVoiceSettingsFromStorage(), ...patch }); cachedSettings = nextSettings; schedulePersist(nextSettings); return { ...nextSettings }; } let cachedSettings: VoiceSettings | null = null; let pendingSettings: VoiceSettings | null = null; let persistScheduled = false; function schedulePersist(settings: VoiceSettings): void { pendingSettings = settings; if (persistScheduled) { return; } persistScheduled = true; const runner = (): void => { persistScheduled = false; if (!pendingSettings) { return; } const toPersist = pendingSettings; pendingSettings = null; try { localStorage.setItem(STORAGE_KEY_VOICE_SETTINGS, JSON.stringify(toPersist)); } catch { // ignore quota / privacy errors } }; type IdleCallbackHandle = number; interface IdleDeadline { didTimeout: boolean; timeRemaining(): number; } type IdleRequest = (cb: (deadline: IdleDeadline) => void, opts?: { timeout: number }) => IdleCallbackHandle; interface MaybeIdleGlobal { requestIdleCallback?: IdleRequest; } const idleGlobal = (typeof globalThis === 'undefined' ? {} : globalThis) as MaybeIdleGlobal; if (typeof idleGlobal.requestIdleCallback === 'function') { idleGlobal.requestIdleCallback(() => runner(), { timeout: 1000 }); } else { setTimeout(runner, 0); } } function normaliseVoiceSettings(raw: Partial): VoiceSettings { return { inputDevice: typeof raw.inputDevice === 'string' ? raw.inputDevice : DEFAULT_VOICE_SETTINGS.inputDevice, outputDevice: typeof raw.outputDevice === 'string' ? raw.outputDevice : DEFAULT_VOICE_SETTINGS.outputDevice, inputVolume: clampNumber(raw.inputVolume, 0, 100, DEFAULT_VOICE_SETTINGS.inputVolume), outputVolume: clampNumber(raw.outputVolume, 0, 200, DEFAULT_VOICE_SETTINGS.outputVolume), audioBitrate: clampNumber(raw.audioBitrate, 32, 256, DEFAULT_VOICE_SETTINGS.audioBitrate), latencyProfile: LATENCY_PROFILES.includes(raw.latencyProfile as LatencyProfile) ? raw.latencyProfile as LatencyProfile : DEFAULT_VOICE_SETTINGS.latencyProfile, includeSystemAudio: typeof raw.includeSystemAudio === 'boolean' ? raw.includeSystemAudio : DEFAULT_VOICE_SETTINGS.includeSystemAudio, noiseReduction: typeof raw.noiseReduction === 'boolean' ? raw.noiseReduction : DEFAULT_VOICE_SETTINGS.noiseReduction, screenShareQuality: SCREEN_SHARE_QUALITIES.includes(raw.screenShareQuality as ScreenShareQuality) ? raw.screenShareQuality as ScreenShareQuality : DEFAULT_VOICE_SETTINGS.screenShareQuality, askScreenShareQuality: typeof raw.askScreenShareQuality === 'boolean' ? raw.askScreenShareQuality : DEFAULT_VOICE_SETTINGS.askScreenShareQuality }; } function clampNumber( value: unknown, min: number, max: number, fallback: number ): number { if (typeof value !== 'number' || Number.isNaN(value)) { return fallback; } return Math.max(min, Math.min(max, value)); }