From 62e8354d03f4cd68c933f7115592cfef0dd8e374 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Thu, 9 Jul 2026 12:36:33 -0400 Subject: [PATCH] feat(voice): persist A/V device preferences in localStorage --- web/src/components/DeviceSettingsModal.tsx | 2 + web/src/stores/voice.ts | 46 ++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/web/src/components/DeviceSettingsModal.tsx b/web/src/components/DeviceSettingsModal.tsx index 403b994..ea113b0 100644 --- a/web/src/components/DeviceSettingsModal.tsx +++ b/web/src/components/DeviceSettingsModal.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { useVoiceStore } from '../stores/voice.ts'; +import { saveDevicePref } from '../stores/voice.ts'; interface Props { onClose: () => void; @@ -48,6 +49,7 @@ export function DeviceSettingsModal({ onClose }: Props) { if (!room) return; try { await room.switchActiveDevice(kind, deviceId); + saveDevicePref(kind, deviceId); if (kind === 'audioinput') setActiveAudioInput(deviceId); if (kind === 'audiooutput') setActiveAudioOutput(deviceId); if (kind === 'videoinput') setActiveVideoInput(deviceId); diff --git a/web/src/stores/voice.ts b/web/src/stores/voice.ts index a8977f9..a9d43d6 100644 --- a/web/src/stores/voice.ts +++ b/web/src/stores/voice.ts @@ -64,6 +64,42 @@ interface VoiceState { dismissWhisper: (timestamp: number) => void; } +const DEVICE_PREFS_KEY = 'dc_voice_device_prefs'; + +interface DevicePrefs { + audioInput?: string; + audioOutput?: string; + videoInput?: string; +} + +function loadDevicePrefs(): DevicePrefs { + try { + return JSON.parse(localStorage.getItem(DEVICE_PREFS_KEY) || '{}'); + } catch { + return {}; + } +} + +function saveDevicePrefs(prefs: DevicePrefs) { + localStorage.setItem(DEVICE_PREFS_KEY, JSON.stringify(prefs)); +} + +export function saveDevicePref(kind: MediaDeviceKind, deviceId: string) { + const prefs = loadDevicePrefs(); + if (kind === 'audioinput') prefs.audioInput = deviceId; + else if (kind === 'audiooutput') prefs.audioOutput = deviceId; + else if (kind === 'videoinput') prefs.videoInput = deviceId; + saveDevicePrefs(prefs); +} + +export function getSavedDeviceId(kind: MediaDeviceKind): string | undefined { + const prefs = loadDevicePrefs(); + if (kind === 'audioinput') return prefs.audioInput; + if (kind === 'audiooutput') return prefs.audioOutput; + if (kind === 'videoinput') return prefs.videoInput; + return undefined; +} + function participantToVoice(p: Participant): VoiceParticipant { const micPub = p.getTrackPublication(Track.Source.Microphone); const camPub = p.getTrackPublication(Track.Source.Camera); @@ -215,6 +251,16 @@ export const useVoiceStore = create((set, get) => ({ console.warn("[voice] failed to set initial mute state:", micErr); } + // Restore saved device preferences + const prefs = loadDevicePrefs(); + try { + if (prefs.audioInput) await room.switchActiveDevice('audioinput', prefs.audioInput); + if (prefs.audioOutput) await room.switchActiveDevice('audiooutput', prefs.audioOutput); + if (prefs.videoInput) await room.switchActiveDevice('videoinput', prefs.videoInput); + } catch (devErr) { + console.warn("[voice] failed to restore saved devices:", devErr); + } + set({ isJoining: false }); // Notify WebSocket peers