Merge pull request #3311 from element-hq/robin/break-safari-loop

Break loop in acquiring media on Safari
This commit is contained in:
Robin
2025-06-04 11:58:56 -04:00
committed by GitHub
3 changed files with 16 additions and 7 deletions

View File

@@ -17,9 +17,10 @@ import {
type JSX, type JSX,
} from "react"; } from "react";
import { createMediaDeviceObserver } from "@livekit/components-core"; 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 { useObservable, useObservableEagerState } from "observable-hooks";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { isEqual } from "lodash-es";
import { import {
useSetting, useSetting,
@@ -140,7 +141,13 @@ function useMediaDeviceHandle(
kind, kind,
() => logger.error("Error creating MediaDeviceObserver"), () => logger.error("Error creating MediaDeviceObserver"),
requestPermissions, 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<MediaDeviceInfo[]>(isEqual)),
[kind, requestPermissions], [kind, requestPermissions],
); );
const available = useObservableEagerState( const available = useObservableEagerState(

View File

@@ -53,6 +53,7 @@ import {
useTrackProcessorSync, useTrackProcessorSync,
} from "../livekit/TrackProcessorContext"; } from "../livekit/TrackProcessorContext";
import { usePageTitle } from "../usePageTitle"; import { usePageTitle } from "../usePageTitle";
import { useLatest } from "../useLatest";
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
@@ -159,13 +160,14 @@ export const LobbyView: FC<Props> = ({
], ],
); );
const latestMuteStates = useLatest(muteStates);
const onError = useCallback( const onError = useCallback(
(error: Error) => { (error: Error) => {
logger.error("Error while creating preview Tracks:", error); logger.error("Error while creating preview Tracks:", error);
muteStates.audio.setEnabled?.(false); latestMuteStates.current.audio.setEnabled?.(false);
muteStates.video.setEnabled?.(false); latestMuteStates.current.video.setEnabled?.(false);
}, },
[muteStates.audio, muteStates.video], [latestMuteStates],
); );
const tracks = usePreviewTracks(localTrackOptions, onError); const tracks = usePreviewTracks(localTrackOptions, onError);

View File

@@ -60,7 +60,7 @@ function useMuteState(
// Determine the default value once devices are actually connected // Determine the default value once devices are actually connected
(prev) => (prev) =>
prev ?? (device.available.size > 0 ? enabledByDefault() : undefined), prev ?? (device.available.size > 0 ? enabledByDefault() : undefined),
[device], [device.available.size],
); );
return useMemo( return useMemo(
() => () =>
@@ -70,7 +70,7 @@ function useMuteState(
enabled: enabled ?? false, enabled: enabled ?? false,
setEnabled: setEnabled as Dispatch<SetStateAction<boolean>>, setEnabled: setEnabled as Dispatch<SetStateAction<boolean>>,
}, },
[device, enabled, setEnabled], [device.available.size, enabled, setEnabled],
); );
} }