🔧(frontend) sync persisted device ids with the actual selected devices

Now that the exact deviceId constraint has been dropped, the browser
can pick a different device than the one persisted in localStorage
(for example when the persisted device is no longer available).

Sync the persisted ids in localStorage with the device id that was
actually selected on the started track, so the local cache stays
consistent with what the app is really using.
This commit is contained in:
lebaudantoine
2026-08-07 20:28:27 +02:00
committed by aleb_the_flash
parent aaa51a4457
commit 751d029ac9
5 changed files with 66 additions and 0 deletions
+1
View File
@@ -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
@@ -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)
@@ -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
}
@@ -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])
}
@@ -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) {
<>
<RoomMetadataSynchronizer />
<ConnectionObserver />
<SyncDevicePreferences />
<MediaStateObserver />
<ChatProvider />
<VideoResolutionSubscription />