Files
Toju/toju-app/src/app/domains/voice-session/infrastructure/util/voice-settings-storage.util.ts
2026-05-23 15:28:40 +02:00

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));
}