feat(voice-store): add pruneStaleDevices to clear stale persisted device ids

This commit is contained in:
Jannis Braun
2026-04-27 20:08:42 +02:00
parent 770a77a473
commit 486448607e
+34
View File
@@ -89,6 +89,7 @@ interface VoiceState {
setInputDevice: (deviceId: string) => void;
setOutputDevice: (deviceId: string) => void;
setCameraDeviceId: (deviceId: string | null) => void;
pruneStaleDevices: () => Promise<void>;
pttActive: boolean;
setMuted: (muted: boolean) => void;
setPttActive: (active: boolean) => void;
@@ -322,6 +323,39 @@ export const useVoiceStore = create<VoiceState>()(
setCameraDeviceId: (deviceId) => set({ cameraDeviceId: deviceId }),
pruneStaleDevices: async () => {
try {
const devs = await navigator.mediaDevices.enumerateDevices();
// Firefox/Safari hide deviceIds pre-permission. If the enumerated set for a
// kind has zero non-empty deviceIds, skip pruning that kind — we cannot
// distinguish "stale" from "obscured".
const audioInIds = new Set(
devs.filter(d => d.kind === 'audioinput' && d.deviceId).map(d => d.deviceId)
);
const audioOutIds = new Set(
devs.filter(d => d.kind === 'audiooutput' && d.deviceId).map(d => d.deviceId)
);
const videoInIds = new Set(
devs.filter(d => d.kind === 'videoinput' && d.deviceId).map(d => d.deviceId)
);
const s = get();
const next: Partial<VoiceState> = {};
if (audioInIds.size > 0 && s.inputDeviceId !== 'default' && !audioInIds.has(s.inputDeviceId)) {
next.inputDeviceId = 'default';
}
if (audioOutIds.size > 0 && s.outputDeviceId !== 'default' && !audioOutIds.has(s.outputDeviceId)) {
next.outputDeviceId = 'default';
}
if (videoInIds.size > 0 && s.cameraDeviceId !== null && !videoInIds.has(s.cameraDeviceId)) {
next.cameraDeviceId = null;
}
if (Object.keys(next).length) set(next);
} catch {
// enumerateDevices unavailable (no MediaDevices API or permission policy denial) — skip
}
},
setMuted: (muted: boolean) => set({ isMuted: muted }),
setPttActive: (active: boolean) => set({ pttActive: active }),