diff --git a/CHANGELOG.md b/CHANGELOG.md index e0e7501b..8144b79b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to - ♻️(frontend) encapsulate error tracking behind a telemetry module - ♻️(frontend) encapsulate PostHog capture calls in the telemetry module +- 🔧(frontend) sync persisted device ids with the actual selected devices ### Fixed diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index 5bc75070..3d83c767 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -55,6 +55,7 @@ import { import { saveUsername, userStore } from '@/stores/user' import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice' +import { useSyncTrackDeviceId } from '../livekit/hooks/useSyncTrackDeviceId' import { useSnapshot } from 'valtio' import { useUser } from '@/features/auth/api/useUser' import { useConfig } from '@/api/useConfig' @@ -274,6 +275,8 @@ export const Join = ({ const videoTrack = dynamicVideoTrack || previewVideoTrack const audioTrack = dynamicAudioTrack || previewAudioTrack + useSyncTrackDeviceId(audioTrack, saveAudioInputDeviceId) + useSyncTrackDeviceId(videoTrack, saveVideoInputDeviceId) const videoEl = useRef(null) const isVideoInitiated = useRef(false) diff --git a/src/frontend/src/features/rooms/livekit/components/SyncDevicePreferences.tsx b/src/frontend/src/features/rooms/livekit/components/SyncDevicePreferences.tsx new file mode 100644 index 00000000..ba02c17d --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/SyncDevicePreferences.tsx @@ -0,0 +1,20 @@ +import { useLocalParticipant } from '@livekit/components-react' +import type { LocalTrack } from 'livekit-client' +import { useSyncTrackDeviceId } from '../hooks/useSyncTrackDeviceId' +import { + saveAudioInputDeviceId, + saveVideoInputDeviceId, +} from '@/stores/userChoices' + +export const SyncDevicePreferences = () => { + const { cameraTrack, microphoneTrack } = useLocalParticipant() + useSyncTrackDeviceId( + cameraTrack?.track as LocalTrack | undefined, + saveVideoInputDeviceId + ) + useSyncTrackDeviceId( + microphoneTrack?.track as LocalTrack | undefined, + saveAudioInputDeviceId + ) + return null +} diff --git a/src/frontend/src/features/rooms/livekit/hooks/useSyncTrackDeviceId.ts b/src/frontend/src/features/rooms/livekit/hooks/useSyncTrackDeviceId.ts new file mode 100644 index 00000000..7ce924b9 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/useSyncTrackDeviceId.ts @@ -0,0 +1,40 @@ +import { useEffect } from 'react' +import { TrackEvent, type LocalTrack } from 'livekit-client' + +/** + * Keeps the persisted preference aligned with the track's actual device. + * The track is the source of truth, not the store. + * + * Syncs on mount and TrackEvent.Restarted, covering acquisition/restart, + * setDeviceId switches, unmute re-acquisition, and browser fallback. + * + * Skip persisting the raw "default" alias: it means "follow the OS default" + * and resolving it would pin the preference to a concrete device. + */ +export const useSyncTrackDeviceId = ( + track: LocalTrack | undefined, + save: (deviceId: string) => void +) => { + useEffect(() => { + if (!track) return + let cancelled = false + const sync = () => { + track + .getDeviceId(false) + .then((deviceId) => { + if (!cancelled && deviceId) { + save(deviceId) + } + }) + .catch(() => { + // A track without settings (ended, screen share) has no id to sync. + }) + } + sync() + track.on(TrackEvent.Restarted, sync) + return () => { + cancelled = true + track.off(TrackEvent.Restarted, sync) + } + }, [track, save]) +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index e6bfe693..1859fff3 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -26,6 +26,7 @@ import { PipRoomPlaceholder } from '@/features/pip/components/PipRoomPlaceholder import { StageLayout } from '@/features/layout/components/StageLayout' import { PinAnnouncer } from '@/features/layout/components/PinAnnouncer' import { ChatProvider } from '@/features/chat/components/ChatProvider' +import { SyncDevicePreferences } from '@/features/rooms/livekit/components/SyncDevicePreferences' /** * @public @@ -65,6 +66,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) { <> +