From 83bfb721e74460a8635f2391861161fa0602824b Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 3 Jun 2025 17:22:20 -0400 Subject: [PATCH 1/2] Break loop in acquiring media on Safari --- src/room/LobbyView.tsx | 8 +++++--- src/room/MuteStates.ts | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) 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], ); } From 40b351d76d580dca20185151d8f23f85f1751abc Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 4 Jun 2025 11:23:20 -0400 Subject: [PATCH 2/2] Ignore spurious 'devicechange' events This gives us the additional insurance of breaking the Safari media acquisition loop at the source by admitting that they can be spurious in practice. Safari, why!? --- src/livekit/MediaDevicesContext.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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(