157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
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<VoiceSettings>);
|
|
return { ...cachedSettings };
|
|
} catch {
|
|
cachedSettings = { ...DEFAULT_VOICE_SETTINGS };
|
|
return { ...cachedSettings };
|
|
}
|
|
}
|
|
|
|
export function saveVoiceSettingsToStorage(patch: Partial<VoiceSettings>): 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>): 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));
|
|
}
|