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..c4a585ed --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/useSyncTrackDeviceId.ts @@ -0,0 +1,44 @@ +import { useEffect } from 'react' +import { TrackEvent, type LocalTrack } from 'livekit-client' + +/** + * Keeps the persisted device preference aligned with the device the track + * is ACTUALLY using — the track is the ground truth, not the store. + * + * Syncs on mount and on every TrackEvent.Restarted, which covers all the + * moments the underlying device can change on the join screen: + * - initial acquisition where LiveKit resolved the 'default' alias + * (getDeviceId(normalize=true) resolves it to the concrete id via + * livekit's DeviceManager — this replaces the former + * useResolveInitiallyDefaultDeviceId, which never fired after init), + * - a device switch via setDeviceId, + * - an unmute re-acquisition, + * - the browser falling back to another device. + */ +export const useSyncTrackDeviceId = ( + track: LocalTrack | undefined, + save: (deviceId: string) => void +) => { + useEffect(() => { + if (!track) return + let cancelled = false + const sync = () => { + track + .getDeviceId() + .then((deviceId) => { + if (!cancelled && deviceId && deviceId !== 'default') { + 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) { <> +