diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 636f5a6d..8a19395b 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -17,9 +17,10 @@ import { type JSX, } from "react"; import { createMediaDeviceObserver } from "@livekit/components-core"; -import { combineLatest, map, startWith } from "rxjs"; +import { combineLatest, distinctUntilChanged, map, startWith } from "rxjs"; import { useObservable, useObservableEagerState } from "observable-hooks"; import { logger } from "matrix-js-sdk/lib/logger"; +import { isEqual } from "lodash-es"; import { useSetting, @@ -140,7 +141,13 @@ function useMediaDeviceHandle( kind, () => logger.error("Error creating MediaDeviceObserver"), requestPermissions, - ).pipe(startWith([])), + // This Observable emits new values whenever the browser fires a + // MediaDevices 'devicechange' event. One would think, innocently, that + // a 'devicechange' event means the devices have changed. But as of the + // time of writing, we are seeing mobile Safari firing spurious + // 'devicechange' events (where no change has actually occurred) when + // we call MediaDevices.getUserMedia. So, filter by deep equality. + ).pipe(startWith([]), distinctUntilChanged(isEqual)), [kind, requestPermissions], ); const available = useObservableEagerState( diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 72079783..f5b47cdd 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -53,6 +53,7 @@ import { useTrackProcessorSync, } from "../livekit/TrackProcessorContext"; import { usePageTitle } from "../usePageTitle"; +import { useLatest } from "../useLatest"; interface Props { client: MatrixClient; @@ -159,13 +160,14 @@ export const LobbyView: FC = ({ ], ); + const latestMuteStates = useLatest(muteStates); const onError = useCallback( (error: Error) => { logger.error("Error while creating preview Tracks:", error); - muteStates.audio.setEnabled?.(false); - muteStates.video.setEnabled?.(false); + latestMuteStates.current.audio.setEnabled?.(false); + latestMuteStates.current.video.setEnabled?.(false); }, - [muteStates.audio, muteStates.video], + [latestMuteStates], ); const tracks = usePreviewTracks(localTrackOptions, onError); diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 6e24fb07..5e0f6f6d 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -60,7 +60,7 @@ function useMuteState( // Determine the default value once devices are actually connected (prev) => prev ?? (device.available.size > 0 ? enabledByDefault() : undefined), - [device], + [device.available.size], ); return useMemo( () => @@ -70,7 +70,7 @@ function useMuteState( enabled: enabled ?? false, setEnabled: setEnabled as Dispatch>, }, - [device, enabled, setEnabled], + [device.available.size, enabled, setEnabled], ); }