|
|
|
|
@@ -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
|
|
|
|
|
|