mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-16 06:17:10 +00:00
Merge branch 'livekit' into toger5/track-processor-blur
This commit is contained in:
@@ -16,7 +16,8 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { createMediaDeviceObserver } from "@livekit/components-core";
|
||||
import { type Observable } from "rxjs";
|
||||
import { map, startWith } from "rxjs";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import {
|
||||
@@ -26,11 +27,25 @@ import {
|
||||
videoInput as videoInputSetting,
|
||||
type Setting,
|
||||
} from "../settings/settings";
|
||||
import { isFirefox } from "../Platform";
|
||||
|
||||
export type DeviceLabel =
|
||||
| { type: "name"; name: string }
|
||||
| { type: "number"; number: number }
|
||||
| { type: "default"; name: string | null };
|
||||
|
||||
export interface MediaDevice {
|
||||
available: MediaDeviceInfo[];
|
||||
/**
|
||||
* A map from available device IDs to labels.
|
||||
*/
|
||||
available: Map<string, DeviceLabel>;
|
||||
selectedId: string | undefined;
|
||||
/**
|
||||
* The group ID of the selected device.
|
||||
*/
|
||||
// This is exposed sort of ad-hoc because it's only needed for knowing when to
|
||||
// restart the tracks of default input devices, and ideally this behavior
|
||||
// would be encapsulated somehow…
|
||||
selectedGroupId: string | undefined;
|
||||
select: (deviceId: string) => void;
|
||||
}
|
||||
|
||||
@@ -42,26 +57,10 @@ export interface MediaDevices {
|
||||
stopUsingDeviceNames: () => void;
|
||||
}
|
||||
|
||||
// Cargo-culted from @livekit/components-react
|
||||
function useObservableState<T>(
|
||||
observable: Observable<T> | undefined,
|
||||
startWith: T,
|
||||
): T {
|
||||
const [state, setState] = useState<T>(startWith);
|
||||
useEffect(() => {
|
||||
// observable state doesn't run in SSR
|
||||
if (typeof window === "undefined" || !observable) return;
|
||||
const subscription = observable.subscribe(setState);
|
||||
return (): void => subscription.unsubscribe();
|
||||
}, [observable]);
|
||||
return state;
|
||||
}
|
||||
|
||||
function useMediaDevice(
|
||||
kind: MediaDeviceKind,
|
||||
setting: Setting<string | undefined>,
|
||||
usingNames: boolean,
|
||||
alwaysDefault: boolean = false,
|
||||
): MediaDevice {
|
||||
// Make sure we don't needlessly reset to a device observer without names,
|
||||
// once permissions are already given
|
||||
@@ -75,49 +74,97 @@ function useMediaDevice(
|
||||
// useMediaDevices provides no way to request device names.
|
||||
// Tragically, the only way to get device names out of LiveKit is to specify a
|
||||
// kind, which then results in multiple permissions requests.
|
||||
const deviceObserver = useMemo(
|
||||
const deviceObserver$ = useMemo(
|
||||
() =>
|
||||
createMediaDeviceObserver(
|
||||
kind,
|
||||
() => logger.error("Error creating MediaDeviceObserver"),
|
||||
requestPermissions,
|
||||
),
|
||||
).pipe(startWith([])),
|
||||
[kind, requestPermissions],
|
||||
);
|
||||
const available = useObservableState(deviceObserver, []);
|
||||
const [preferredId, select] = useSetting(setting);
|
||||
const available = useObservableEagerState(
|
||||
useMemo(
|
||||
() =>
|
||||
deviceObserver$.pipe(
|
||||
map((availableRaw) => {
|
||||
// Sometimes browsers (particularly Firefox) can return multiple device
|
||||
// entries for the exact same device ID; using a map deduplicates them
|
||||
let available = new Map<string, DeviceLabel>(
|
||||
availableRaw.map((d, i) => [
|
||||
d.deviceId,
|
||||
d.label
|
||||
? { type: "name", name: d.label }
|
||||
: { type: "number", number: i + 1 },
|
||||
]),
|
||||
);
|
||||
// Create a virtual default audio output for browsers that don't have one.
|
||||
// Its device ID must be the empty string because that's what setSinkId
|
||||
// recognizes.
|
||||
if (
|
||||
kind === "audiooutput" &&
|
||||
available.size &&
|
||||
!available.has("") &&
|
||||
!available.has("default")
|
||||
)
|
||||
available = new Map([
|
||||
["", { type: "default", name: availableRaw[0]?.label || null }],
|
||||
...available,
|
||||
]);
|
||||
// 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.
|
||||
return available;
|
||||
}),
|
||||
),
|
||||
[kind, deviceObserver$],
|
||||
),
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
let selectedId: string | undefined = undefined;
|
||||
if (!alwaysDefault && available) {
|
||||
const [preferredId, select] = useSetting(setting);
|
||||
const selectedId = useMemo(() => {
|
||||
if (available.size) {
|
||||
// If the preferred device is available, use it. Or if every available
|
||||
// device ID is falsy, the browser is probably just being paranoid about
|
||||
// fingerprinting and we should still try using the preferred device.
|
||||
// Worst case it is not available and the browser will gracefully fall
|
||||
// back to some other device for us when requesting the media stream.
|
||||
// Otherwise, select the first available device.
|
||||
selectedId =
|
||||
available.some((d) => d.deviceId === preferredId) ||
|
||||
available.every((d) => d.deviceId === "")
|
||||
? preferredId
|
||||
: available.at(0)?.deviceId;
|
||||
return (preferredId !== undefined && available.has(preferredId)) ||
|
||||
(available.size === 1 && available.has(""))
|
||||
? preferredId
|
||||
: available.keys().next().value;
|
||||
}
|
||||
return undefined;
|
||||
}, [available, preferredId]);
|
||||
const selectedGroupId = useObservableEagerState(
|
||||
useMemo(
|
||||
() =>
|
||||
deviceObserver$.pipe(
|
||||
map(
|
||||
(availableRaw) =>
|
||||
availableRaw.find((d) => d.deviceId === selectedId)?.groupId,
|
||||
),
|
||||
),
|
||||
[deviceObserver$, selectedId],
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
available: available
|
||||
? // Sometimes browsers (particularly Firefox) can return multiple
|
||||
// device entries for the exact same device ID; deduplicate them
|
||||
[...new Map(available.map((d) => [d.deviceId, d])).values()]
|
||||
: [],
|
||||
return useMemo(
|
||||
() => ({
|
||||
available,
|
||||
selectedId,
|
||||
selectedGroupId,
|
||||
select,
|
||||
};
|
||||
}, [available, preferredId, select, alwaysDefault]);
|
||||
}),
|
||||
[available, selectedId, selectedGroupId, select],
|
||||
);
|
||||
}
|
||||
|
||||
export const deviceStub: MediaDevice = {
|
||||
available: [],
|
||||
available: new Map(),
|
||||
selectedId: undefined,
|
||||
selectedGroupId: undefined,
|
||||
select: () => {},
|
||||
};
|
||||
export const devicesStub: MediaDevices = {
|
||||
@@ -139,15 +186,6 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
||||
const [numCallersUsingNames, setNumCallersUsingNames] = useState(0);
|
||||
const usingNames = numCallersUsingNames > 0;
|
||||
|
||||
// Setting the audio device to something other than 'undefined' breaks echo-cancellation
|
||||
// and even can introduce multiple different output devices for one call.
|
||||
const alwaysUseDefaultAudio = isFirefox();
|
||||
|
||||
// On FF we dont need to query the names
|
||||
// (call enumerateDevices + create meadia stream to trigger permissions)
|
||||
// for ouput devices because the selector wont be shown on FF.
|
||||
const useOutputNames = usingNames && !isFirefox();
|
||||
|
||||
const audioInput = useMediaDevice(
|
||||
"audioinput",
|
||||
audioInputSetting,
|
||||
@@ -156,8 +194,7 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
||||
const audioOutput = useMediaDevice(
|
||||
"audiooutput",
|
||||
audioOutputSetting,
|
||||
useOutputNames,
|
||||
alwaysUseDefaultAudio,
|
||||
usingNames,
|
||||
);
|
||||
const videoInput = useMediaDevice(
|
||||
"videoinput",
|
||||
|
||||
@@ -310,18 +310,14 @@ export function useLiveKit(
|
||||
room.localParticipant.audioTrackPublications.values(),
|
||||
).find((d) => d.source === Track.Source.Microphone)?.track;
|
||||
|
||||
const defaultDevice = device.available.find(
|
||||
(d) => d.deviceId === "default",
|
||||
);
|
||||
if (
|
||||
defaultDevice &&
|
||||
activeMicTrack &&
|
||||
// only restart if the stream is still running: LiveKit will detect
|
||||
// when a track stops & restart appropriately, so this is not our job.
|
||||
// Plus, we need to avoid restarting again if the track is already in
|
||||
// the process of being restarted.
|
||||
activeMicTrack.mediaStreamTrack.readyState !== "ended" &&
|
||||
defaultDevice.groupId !==
|
||||
device.selectedGroupId !==
|
||||
activeMicTrack.mediaStreamTrack.getSettings().groupId
|
||||
) {
|
||||
// It's different, so restart the track, ie. cause Livekit to do another
|
||||
|
||||
Reference in New Issue
Block a user