mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f565fb8e3 | ||
|
|
98bfab47dc | ||
|
|
84ff12e4de |
+16
-4
@@ -6,6 +6,9 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, Subject } from "rxjs";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
const logger = rootLogger.getChild("[controlled-media]");
|
||||
|
||||
export interface Controls {
|
||||
canEnterPip(): boolean;
|
||||
@@ -44,10 +47,14 @@ export interface OutputDevice {
|
||||
export const setPipEnabled$ = new Subject<boolean>();
|
||||
// BehaviorSubject since the client might set this before we have subscribed (GroupCallView still in "loading" state)
|
||||
// We want the devices that have been set during loading to be available immediately once loaded.
|
||||
export const availableOutputDevices$ = new BehaviorSubject<OutputDevice[]>([]);
|
||||
export const controlledAvailableOutputDevices$ = new BehaviorSubject<
|
||||
OutputDevice[]
|
||||
>([]);
|
||||
// BehaviorSubject since the client might set this before we have subscribed (GroupCallView still in "loading" state)
|
||||
// We want the device that has been set during loading to be available immediately once loaded.
|
||||
export const outputDevice$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
export const controlledAudioDevice$ = new BehaviorSubject<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
/**
|
||||
* This allows the os to mute the call if the user
|
||||
* presses the volume down button when it is at the minimum volume.
|
||||
@@ -75,12 +82,17 @@ window.controls = {
|
||||
setPipEnabled$.next(false);
|
||||
},
|
||||
setAvailableAudioDevices(devices: OutputDevice[]): void {
|
||||
availableOutputDevices$.next(devices);
|
||||
logger.info("setAvailableAudioDevices called from native:", devices);
|
||||
|
||||
controlledAvailableOutputDevices$.next(devices);
|
||||
},
|
||||
setAudioDevice(id: string): void {
|
||||
outputDevice$.next(id);
|
||||
logger.info("setAudioDevice called from native", id);
|
||||
controlledAudioDevice$.next(id);
|
||||
},
|
||||
setAudioEnabled(enabled: boolean): void {
|
||||
logger.info("setAudioEnabled called from native:", enabled);
|
||||
|
||||
if (!setAudioEnabled$.observed)
|
||||
throw new Error(
|
||||
"Output controls are disabled. No setAudioEnabled$ observer",
|
||||
|
||||
@@ -18,7 +18,11 @@ import {
|
||||
} from "react";
|
||||
import { createMediaDeviceObserver } from "@livekit/components-core";
|
||||
import { combineLatest, distinctUntilChanged, map, startWith } from "rxjs";
|
||||
import { useObservable, useObservableEagerState } from "observable-hooks";
|
||||
import {
|
||||
useObservable,
|
||||
useObservableEagerState,
|
||||
useSubscription,
|
||||
} from "observable-hooks";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
||||
|
||||
@@ -30,7 +34,10 @@ import {
|
||||
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
|
||||
type Setting,
|
||||
} from "../settings/settings";
|
||||
import { outputDevice$, availableOutputDevices$ } from "../controls";
|
||||
import {
|
||||
controlledAudioDevice$,
|
||||
controlledAvailableOutputDevices$,
|
||||
} from "../controls";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
|
||||
// This hardcoded id is used in EX ios! It can only be changed in coordination with
|
||||
@@ -135,7 +142,7 @@ function useMediaDeviceHandle(
|
||||
// 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$ = useObservable(
|
||||
() =>
|
||||
createMediaDeviceObserver(
|
||||
kind,
|
||||
@@ -153,8 +160,9 @@ function useMediaDeviceHandle(
|
||||
),
|
||||
[kind, requestPermissions],
|
||||
);
|
||||
|
||||
const available = useObservableEagerState(
|
||||
useMemo(
|
||||
useObservable(
|
||||
() =>
|
||||
deviceObserver$.pipe(
|
||||
map((availableRaw) => {
|
||||
@@ -197,7 +205,7 @@ function useMediaDeviceHandle(
|
||||
const selectedId = useSelectedId(available, preferredId);
|
||||
|
||||
const selectedGroupId = useObservableEagerState(
|
||||
useMemo(
|
||||
useObservable(
|
||||
() =>
|
||||
deviceObserver$.pipe(
|
||||
map(
|
||||
@@ -326,9 +334,9 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
||||
function useControlledOutput(): MediaDeviceHandle {
|
||||
const { available } = useObservableEagerState(
|
||||
useObservable(() => {
|
||||
const outputDeviceData$ = availableOutputDevices$.pipe(
|
||||
const outputDeviceData$ = controlledAvailableOutputDevices$.pipe(
|
||||
map((devices) => {
|
||||
const deviceForEarpiece = devices.find((d) => d.forEarpiece);
|
||||
const hasDeviceForEarpiece = devices.find((d) => d.forEarpiece);
|
||||
const deviceMapTuple: [string, DeviceLabel][] = devices.map(
|
||||
({ id, name, isEarpiece, isSpeaker /*,isExternalHeadset*/ }) => {
|
||||
let deviceLabel: DeviceLabel = { type: "name", name };
|
||||
@@ -340,35 +348,34 @@ function useControlledOutput(): MediaDeviceHandle {
|
||||
);
|
||||
return {
|
||||
devicesMap: new Map<string, DeviceLabel>(deviceMapTuple),
|
||||
deviceForEarpiece,
|
||||
hasDeviceForEarpiece,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return combineLatest(
|
||||
[outputDeviceData$, iosDeviceMenu$],
|
||||
({ devicesMap, deviceForEarpiece }, iosShowEarpiece) => {
|
||||
({ devicesMap, hasDeviceForEarpiece }, iosShowEarpiece) => {
|
||||
let available = devicesMap;
|
||||
if (iosShowEarpiece && !!deviceForEarpiece) {
|
||||
if (iosShowEarpiece && !!hasDeviceForEarpiece) {
|
||||
available = new Map([
|
||||
...devicesMap.entries(),
|
||||
[EARPIECE_CONFIG_ID, { type: "earpiece" }],
|
||||
]);
|
||||
}
|
||||
return { available, deviceForEarpiece };
|
||||
return { available, deviceForEarpiece: hasDeviceForEarpiece };
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
const [preferredId, setPreferredId] = useSetting(audioOutputSetting);
|
||||
useEffect(() => {
|
||||
const subscription = outputDevice$.subscribe((id) => {
|
||||
if (id) setPreferredId(id);
|
||||
});
|
||||
return (): void => subscription.unsubscribe();
|
||||
}, [setPreferredId]);
|
||||
|
||||
const selectedId = useSelectedId(available, preferredId);
|
||||
const [preferredId, setPreferredId] = useSetting(audioOutputSetting);
|
||||
|
||||
useSubscription(controlledAudioDevice$, (id) => {
|
||||
if (id) setPreferredId(id);
|
||||
});
|
||||
|
||||
// const selectedId = useSelectedId(available, preferredId);
|
||||
|
||||
const [asEarpiece, setAsEarpiece] = useState(false);
|
||||
|
||||
@@ -377,23 +384,27 @@ function useControlledOutput(): MediaDeviceHandle {
|
||||
// This information is probably only of interest if the earpiece mode has been
|
||||
// selected - for example, Element X iOS listens to this to determine whether it
|
||||
// should enable the proximity sensor.
|
||||
if (selectedId) {
|
||||
window.controls.onAudioDeviceSelect?.(selectedId);
|
||||
if (preferredId) {
|
||||
logger.info(
|
||||
"[controlled-media] setAudioDeviceSelect called:",
|
||||
preferredId,
|
||||
);
|
||||
window.controls.onAudioDeviceSelect?.(preferredId);
|
||||
// Call deprecated method for backwards compatibility.
|
||||
window.controls.onOutputDeviceSelect?.(selectedId);
|
||||
window.controls.onOutputDeviceSelect?.(preferredId);
|
||||
}
|
||||
setAsEarpiece(selectedId === EARPIECE_CONFIG_ID);
|
||||
}, [selectedId]);
|
||||
setAsEarpiece(preferredId === EARPIECE_CONFIG_ID);
|
||||
}, [preferredId]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
available: available,
|
||||
selectedId,
|
||||
selectedId: preferredId,
|
||||
selectedGroupId: undefined,
|
||||
select: setPreferredId,
|
||||
useAsEarpiece: asEarpiece,
|
||||
}),
|
||||
[available, selectedId, setPreferredId, asEarpiece],
|
||||
[available, preferredId, setPreferredId, asEarpiece],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,6 @@ export function useLivekit(
|
||||
|
||||
// Only ever create the room once via useInitial.
|
||||
const room = useInitial(() => {
|
||||
logger.info("[LivekitRoom] Create LiveKit room");
|
||||
|
||||
let e2ee: E2EEManagerOptions | undefined;
|
||||
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
|
||||
logger.info("Created MatrixKeyProvider (per participant)");
|
||||
@@ -99,13 +97,18 @@ export function useLivekit(
|
||||
},
|
||||
audioCaptureDefaults: {
|
||||
...defaultLiveKitOptions.audioCaptureDefaults,
|
||||
deviceId: initialDevices.audioInput.selectedId,
|
||||
deviceId: controlledAudioDevices
|
||||
? undefined
|
||||
: initialDevices.audioInput.selectedId,
|
||||
},
|
||||
audioOutput: {
|
||||
deviceId: initialDevices.audioOutput.selectedId,
|
||||
deviceId: controlledAudioDevices
|
||||
? undefined
|
||||
: initialDevices.audioOutput.selectedId,
|
||||
},
|
||||
e2ee,
|
||||
};
|
||||
logger.info("[LivekitRoom] Create LiveKit room with options", roomOptions);
|
||||
// We have to create the room manually here due to a bug inside
|
||||
// @livekit/components-react. JSON.stringify() is used in deps of a
|
||||
// useEffect() with an argument that references itself, if E2EE is enabled
|
||||
@@ -157,7 +160,7 @@ export function useLivekit(
|
||||
);
|
||||
|
||||
const connectionState = useECConnectionState(
|
||||
initialDevices.audioInput.selectedId,
|
||||
controlledAudioDevices ? undefined : initialDevices.audioInput.selectedId,
|
||||
initialMuteStates.audio.enabled,
|
||||
room,
|
||||
sfuConfig,
|
||||
|
||||
+11
-2
@@ -54,6 +54,7 @@ import {
|
||||
} from "../livekit/TrackProcessorContext";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
@@ -99,6 +100,7 @@ export const LobbyView: FC<Props> = ({
|
||||
[muteStates],
|
||||
);
|
||||
|
||||
const { controlledAudioDevices } = useUrlParams();
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
const [settingsTab, setSettingsTab] = useState(defaultSettingsTab);
|
||||
|
||||
@@ -132,7 +134,11 @@ export const LobbyView: FC<Props> = ({
|
||||
// re-open the devices when they change (see below).
|
||||
const initialAudioOptions = useInitial(
|
||||
() =>
|
||||
muteStates.audio.enabled && { deviceId: devices.audioInput.selectedId },
|
||||
muteStates.audio.enabled && {
|
||||
deviceId: controlledAudioDevices
|
||||
? undefined
|
||||
: devices.audioInput.selectedId,
|
||||
},
|
||||
);
|
||||
|
||||
const { processor } = useTrackProcessor();
|
||||
@@ -148,13 +154,16 @@ export const LobbyView: FC<Props> = ({
|
||||
// which would cause the devices to be re-opened on the next render.
|
||||
audio: Object.assign({}, initialAudioOptions),
|
||||
video: muteStates.video.enabled && {
|
||||
deviceId: devices.videoInput.selectedId,
|
||||
deviceId: controlledAudioDevices
|
||||
? undefined
|
||||
: devices.videoInput.selectedId,
|
||||
processor: initialProcessor,
|
||||
},
|
||||
}),
|
||||
[
|
||||
initialAudioOptions,
|
||||
muteStates.video.enabled,
|
||||
controlledAudioDevices,
|
||||
devices.videoInput.selectedId,
|
||||
initialProcessor,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user