Compare commits

...

4 Commits

Author SHA1 Message Date
Timo
33f0934a14 lint 2025-06-04 15:54:19 +02:00
Timo
6000b9590e Always ask for permission and dont rerequest devices if permissions change 2025-06-04 14:21:56 +02:00
Timo
a08efde3c6 try another fix for the infinit toggle issue ios 2025-06-04 12:53:45 +02:00
Timo
4704510c5a disable startUsingDeviceNames 2025-06-04 12:53:26 +02:00
4 changed files with 38 additions and 76 deletions

View File

@@ -8,16 +8,14 @@ Please see LICENSE in the repository root for full details.
import {
type FC,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
type JSX,
} from "react";
import { createMediaDeviceObserver } from "@livekit/components-core";
import { combineLatest, map, startWith } from "rxjs";
import { combineLatest, filter, map, pairwise, startWith } from "rxjs";
import { useObservable, useObservableEagerState } from "observable-hooks";
import { logger } from "matrix-js-sdk/lib/logger";
@@ -66,9 +64,6 @@ export interface MediaDeviceHandle {
interface InputDevices {
audioInput: MediaDeviceHandle;
videoInput: MediaDeviceHandle;
startUsingDeviceNames: () => void;
stopUsingDeviceNames: () => void;
usingNames: boolean;
}
export interface MediaDevices extends Omit<InputDevices, "usingNames"> {
@@ -120,14 +115,7 @@ function useSelectedId(
function useMediaDeviceHandle(
kind: MediaDeviceKind,
setting: Setting<string | undefined>,
usingNames: boolean,
): MediaDeviceHandle {
const hasRequestedPermissions = useRef(false);
const requestPermissions = usingNames || hasRequestedPermissions.current;
// Make sure we don't needlessly reset to a device observer without names,
// once permissions are already given
hasRequestedPermissions.current ||= usingNames;
// We use a bare device observer here rather than one of the fancy device
// selection hooks from @livekit/components-react, because
// useMediaDeviceSelect expects a room or track, which we don't have here, and
@@ -136,13 +124,34 @@ function useMediaDeviceHandle(
// kind, which then results in multiple permissions requests.
const deviceObserver$ = useMemo(
() =>
createMediaDeviceObserver(
kind,
() => logger.error("Error creating MediaDeviceObserver"),
requestPermissions,
).pipe(startWith([])),
[kind, requestPermissions],
createMediaDeviceObserver(kind, () =>
logger.error("Error creating MediaDeviceObserver"),
).pipe(
startWith(undefined, []),
// Convert to a tuple of previous and next value.
pairwise(),
// Filter out consecutive updates that don't change the available devices.
// createMediaDeviceObserver can will emit each time, 'devicechange' is emitted.
// On safari this happens each time we call `getUserMedia`.
filter(([prev, n]) => {
// The only way to get undefined is with startWith for `prev`.
const next = n!;
if (prev === undefined) return true;
if (prev.length !== next.length) return true;
if (prev.length === 0) return false;
return !next.every(
(d, i) =>
d.deviceId === prev[i].deviceId && d.label === prev[i].label,
);
}),
// Convert back to non-pairwise observable
map(([, next]) => next!),
// Use startWith at the end to ensure that the observable
// always emits an initial value that does not get filtered out.
),
[kind],
);
const available = useObservableEagerState(
useMemo(
() =>
@@ -173,6 +182,13 @@ function useMediaDeviceHandle(
["", { type: "default", name: availableRaw[0]?.label || null }],
...available,
]);
const availablePrint = Array.from(available.entries()).map(
([id, label]) =>
`id:${id === "" ? '""' : id} label:${(label as { name?: string }).name}\n`,
);
logger.info(
`Media devices changed.\nkind ${kind}\n(deviceObserver$ updated): ${availablePrint}`,
);
// Note: creating virtual default input devices would be another problem
// entirely, because requesting a media stream from deviceId "" won't
// automatically track the default device.
@@ -223,43 +239,19 @@ export const devicesStub: MediaDevices = {
audioInput: deviceStub,
audioOutput: deviceStub,
videoInput: deviceStub,
startUsingDeviceNames: () => {},
stopUsingDeviceNames: () => {},
};
export const MediaDevicesContext = createContext<MediaDevices>(devicesStub);
function useInputDevices(): InputDevices {
// Counts the number of callers currently using device names.
const [numCallersUsingNames, setNumCallersUsingNames] = useState(0);
const usingNames = numCallersUsingNames > 0;
const audioInput = useMediaDeviceHandle(
"audioinput",
audioInputSetting,
usingNames,
);
const videoInput = useMediaDeviceHandle(
"videoinput",
videoInputSetting,
usingNames,
);
const startUsingDeviceNames = useCallback(
() => setNumCallersUsingNames((n) => n + 1),
[setNumCallersUsingNames],
);
const stopUsingDeviceNames = useCallback(
() => setNumCallersUsingNames((n) => n - 1),
[setNumCallersUsingNames],
);
const audioInput = useMediaDeviceHandle("audioinput", audioInputSetting);
const videoInput = useMediaDeviceHandle("videoinput", videoInputSetting);
return {
audioInput,
videoInput,
startUsingDeviceNames,
stopUsingDeviceNames,
usingNames,
};
}
@@ -268,20 +260,13 @@ interface Props {
}
export const MediaDevicesProvider: FC<Props> = ({ children }) => {
const {
audioInput,
videoInput,
startUsingDeviceNames,
stopUsingDeviceNames,
usingNames,
} = useInputDevices();
const { audioInput, videoInput } = useInputDevices();
const { controlledAudioDevices } = useUrlParams();
const webViewAudioOutput = useMediaDeviceHandle(
"audiooutput",
audioOutputSetting,
usingNames,
);
const controlledAudioOutput = useControlledOutput();
@@ -292,8 +277,6 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
? controlledAudioOutput
: webViewAudioOutput,
videoInput,
startUsingDeviceNames,
stopUsingDeviceNames,
}),
[
audioInput,
@@ -301,8 +284,6 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
controlledAudioOutput,
webViewAudioOutput,
videoInput,
startUsingDeviceNames,
stopUsingDeviceNames,
],
);
@@ -396,17 +377,6 @@ export const useMediaDevices = (): MediaDevices =>
* default because it may involve requesting additional permissions from the
* user.
*/
export const useMediaDeviceNames = (
context: MediaDevices,
enabled = true,
): void =>
useEffect(() => {
if (enabled) {
context.startUsingDeviceNames();
return context.stopUsingDeviceNames;
}
}, [context, enabled]);
/**
* A convenience hook to get the audio node configuration for the earpiece.
* It will check the `useAsEarpiece` of the `audioOutput` device and return

View File

@@ -106,8 +106,6 @@ function mockMediaDevices(
videoInput: mockDevices(
camera ? new Map([[mockCamera.deviceId, mockCamera]]) : new Map(),
),
startUsingDeviceNames: (): void => {},
stopUsingDeviceNames: (): void => {},
};
}

View File

@@ -19,7 +19,6 @@ import { ProfileSettingsTab } from "./ProfileSettingsTab";
import { FeedbackSettingsTab } from "./FeedbackSettingsTab";
import {
useMediaDevices,
useMediaDeviceNames,
iosDeviceMenu$,
} from "../livekit/MediaDevicesContext";
import { widget } from "../widget";
@@ -98,7 +97,6 @@ export const SettingsModal: FC<Props> = ({
};
const devices = useMediaDevices();
useMediaDeviceNames(devices, open);
const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting);
const [soundVolumeRaw, setSoundVolumeRaw] = useState(soundVolume);
const [showDeveloperSettingsTab] = useSetting(developerMode);

View File

@@ -126,8 +126,6 @@ test("will use the correct device", () => {
useAsEarpiece: false,
},
videoInput: deviceStub,
startUsingDeviceNames: () => {},
stopUsingDeviceNames: () => {},
}}
>
<TestComponentWrapper />
@@ -161,8 +159,6 @@ test("will use the pan if earpiece is selected", async () => {
useAsEarpiece: true,
},
videoInput: deviceStub,
startUsingDeviceNames: () => {},
stopUsingDeviceNames: () => {},
}}
>
<TestComponentWrapper />