review cleanup

This commit is contained in:
Timo
2025-05-16 11:32:32 +02:00
parent abd66f50db
commit 2012b09845
8 changed files with 61 additions and 57 deletions

View File

@@ -13,7 +13,7 @@ A few aspects of Element Call's interface can be controlled through a global API
These functions must be used in conjunction with the `controlledOutput` URL parameter in order to have any effect. These functions must be used in conjunction with the `controlledOutput` URL parameter in order to have any effect.
- `controls.setAvailableOutputDevices(devices: { id: string, name: string, forEarpiece?: boolean }[]): void` Sets the list of available audio outputs. `forEarpiece` is used on ios only. - `controls.setAvailableOutputDevices(devices: { id: string, name: string, forEarpiece?: boolean }[]): void` Sets the list of available audio outputs. `forEarpiece` is used on ios only.
It flags the device that should be used if the user selects earpice mode. This should be the main (stereo loudspeaker) of the device. It flags the device that should be used if the user selects earpiece mode. This should be the main stereo loudspeaker of the device.
- `controls.onOutputDeviceSelect: ((id: string) => void) | undefined` Callback called whenever the user or application selects a new audio output. - `controls.onOutputDeviceSelect: ((id: string) => void) | undefined` Callback called whenever the user or application selects a new audio output.
- `controls.setOutputDevice(id: string): void` Sets the selected audio device in EC menu. This should be used if the os decides to automatically switch to bluetooth. - `controls.setOutputDevice(id: string): void` Sets the selected audio device in EC menu. This should be used if the os decides to automatically switch to bluetooth.
- `controls.setOutputEnabled(enabled: boolean)` Enables/disables all audio output from the application. This can be useful for temporarily pausing audio while the controlling application is switching output devices. Output is enabled by default. - `controls.setOutputEnabled(enabled: boolean)` Enables/disables all audio output from the application. This can be useful for temporarily pausing audio while the controlling application is switching output devices. Output is enabled by default.

View File

@@ -26,7 +26,7 @@ export interface OutputDevice {
export const setPipEnabled$ = new Subject<boolean>(); export const setPipEnabled$ = new Subject<boolean>();
export const setAvailableOutputDevices$ = new Subject<OutputDevice[]>(); export const setAvailableOutputDevices$ = new Subject<OutputDevice[]>();
export const setOutputDevice$ = new Subject<string>(); export const setOutputDevice$ = new Subject<string>();
export const setOutputDisabled$ = new Subject<boolean>(); export const setOutputEnabled$ = new Subject<boolean>();
window.controls = { window.controls = {
canEnterPip(): boolean { canEnterPip(): boolean {
@@ -51,8 +51,8 @@ window.controls = {
setOutputDevice$.next(id); setOutputDevice$.next(id);
}, },
setOutputEnabled(enabled: boolean): void { setOutputEnabled(enabled: boolean): void {
if (!setOutputDisabled$.observed) if (!setOutputEnabled$.observed)
throw new Error("Output controls are disabled"); throw new Error("Output controls are disabled");
setOutputDisabled$.next(!enabled); setOutputEnabled$.next(!enabled);
}, },
}; };

View File

@@ -77,6 +77,27 @@ export interface MediaDevices extends Omit<InputDevices, "usingNames"> {
audioOutput: MediaDeviceHandle; audioOutput: MediaDeviceHandle;
} }
function useSelectedId(
available: Map<string, DeviceLabel>,
preferredId: string | undefined,
): string | undefined {
return 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.
return (preferredId !== undefined && available.has(preferredId)) ||
(available.size === 1 && available.has(""))
? preferredId
: available.keys().next().value;
}
return undefined;
}, [available, preferredId]);
}
/** /**
* Hook to get access to a mediaDevice handle for a kind. This allows to list * Hook to get access to a mediaDevice handle for a kind. This allows to list
* the available devices, read and set the selected device. * the available devices, read and set the selected device.
@@ -84,17 +105,17 @@ export interface MediaDevices extends Omit<InputDevices, "usingNames"> {
* @param setting The setting this handles selection should be synced with. * @param setting The setting this handles selection should be synced with.
* @param usingNames If the hook should query device names for the associated * @param usingNames If the hook should query device names for the associated
* list. * list.
* @returns A handle for the choosen kind. * @returns A handle for the chosen kind.
*/ */
function useMediaDeviceHandle( function useMediaDeviceHandle(
kind: MediaDeviceKind, kind: MediaDeviceKind,
setting: Setting<string | undefined>, setting: Setting<string | undefined>,
usingNames: boolean, usingNames: boolean,
): MediaDeviceHandle { ): MediaDeviceHandle {
// Make sure we don't needlessly reset to a device observer without names,
// once permissions are already given
const hasRequestedPermissions = useRef(false); const hasRequestedPermissions = useRef(false);
const requestPermissions = usingNames || hasRequestedPermissions.current; 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; hasRequestedPermissions.current ||= usingNames;
// We use a bare device observer here rather than one of the fancy device // We use a bare device observer here rather than one of the fancy device
@@ -153,22 +174,7 @@ function useMediaDeviceHandle(
); );
const [preferredId, select] = useSetting(setting); const [preferredId, select] = useSetting(setting);
const selectedId = useMemo(() => { const selectedId = useSelectedId(available, preferredId);
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.
return (preferredId !== undefined && available.has(preferredId)) ||
(available.size === 1 && available.has(""))
? preferredId
: available.keys().next().value;
}
return undefined;
}, [available, preferredId]);
const selectedGroupId = useObservableEagerState( const selectedGroupId = useObservableEagerState(
useMemo( useMemo(
() => () =>
@@ -337,21 +343,7 @@ function useControlledOutput(): MediaDeviceHandle {
setOutputDevice$.subscribe((id) => setPreferredId(id)); setOutputDevice$.subscribe((id) => setPreferredId(id));
}, [setPreferredId]); }, [setPreferredId]);
const selectedId = useMemo(() => { const selectedId = useSelectedId(available, preferredId);
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.
return (preferredId !== undefined && available.has(preferredId)) ||
(available.size === 1 && available.has(""))
? preferredId
: available.keys().next().value;
}
return undefined;
}, [available, preferredId]);
const [asEarpice, setAsEarpiece] = useState(false); const [asEarpice, setAsEarpiece] = useState(false);

View File

@@ -24,8 +24,7 @@ import {
type MatrixRTCSession, type MatrixRTCSession,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useObservable, useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import { startWith } from "rxjs";
import type { IWidgetApiRequest } from "matrix-widget-api"; import type { IWidgetApiRequest } from "matrix-widget-api";
import { import {
@@ -66,11 +65,10 @@ import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
import { import {
useNewMembershipManager as useNewMembershipManagerSetting, useNewMembershipManager as useNewMembershipManagerSetting,
useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting,
muteAllAudio as muteAllAudioSetting,
useSetting, useSetting,
} from "../settings/settings"; } from "../settings/settings";
import { useTypedEventEmitter } from "../useEvents"; import { useTypedEventEmitter } from "../useEvents";
import { setOutputDisabled$ } from "../controls.ts"; import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
declare global { declare global {
interface Window { interface Window {
@@ -107,12 +105,9 @@ export const GroupCallView: FC<Props> = ({
const [externalError, setExternalError] = useState<ElementCallError | null>( const [externalError, setExternalError] = useState<ElementCallError | null>(
null, null,
); );
const muteAllAudioControlled = useObservableEagerState(
useObservable(() => setOutputDisabled$.pipe(startWith(false))),
);
const [muteAllAudioFromSetting] = useSetting(muteAllAudioSetting);
const muteAllAudio = muteAllAudioControlled || muteAllAudioFromSetting;
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const muteAllAudio = useObservableEagerState(muteAllAudio$);
const leaveSoundContext = useLatest( const leaveSoundContext = useLatest(
useAudioContext({ useAudioContext({
sounds: callEventAudioSounds, sounds: callEventAudioSounds,

View File

@@ -25,7 +25,7 @@ import {
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import classNames from "classnames"; import classNames from "classnames";
import { BehaviorSubject, map, startWith } from "rxjs"; import { BehaviorSubject, map } 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 { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport"; import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
@@ -96,7 +96,6 @@ import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
import { import {
debugTileLayout as debugTileLayoutSetting, debugTileLayout as debugTileLayoutSetting,
useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting,
muteAllAudio as muteAllAudioSetting,
developerMode as developerModeSetting, developerMode as developerModeSetting,
useSetting, useSetting,
} from "../settings/settings"; } from "../settings/settings";
@@ -104,7 +103,7 @@ import { ReactionsReader } from "../reactions/ReactionsReader";
import { ConnectionLostError } from "../utils/errors.ts"; import { ConnectionLostError } from "../utils/errors.ts";
import { useTypedEventEmitter } from "../useEvents.ts"; import { useTypedEventEmitter } from "../useEvents.ts";
import { MatrixAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx"; import { MatrixAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx";
import { setOutputDisabled$ } from "../controls.ts"; import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
@@ -223,11 +222,7 @@ export const InCallView: FC<InCallViewProps> = ({
room: livekitRoom, room: livekitRoom,
}); });
const muteAllAudioControlled = useObservableEagerState( const muteAllAudio = useObservableEagerState(muteAllAudio$);
useObservable(() => setOutputDisabled$.pipe(startWith(false))),
);
const [muteAllAudioFromSetting] = useSetting(muteAllAudioSetting);
const muteAllAudio = muteAllAudioControlled || muteAllAudioFromSetting;
// This seems like it might be enough logic to use move it into the call view model? // This seems like it might be enough logic to use move it into the call view model?
const [didFallbackToRoomKey, setDidFallbackToRoomKey] = useState(false); const [didFallbackToRoomKey, setDidFallbackToRoomKey] = useState(false);

View File

@@ -133,6 +133,6 @@ export const muteAllAudio = new Setting<boolean>("mute-all-audio", false);
export const alwaysShowSelf = new Setting<boolean>("always-show-self", true); export const alwaysShowSelf = new Setting<boolean>("always-show-self", true);
export const alwaysShowIphoneEarpiece = new Setting<boolean>( export const alwaysShowIphoneEarpiece = new Setting<boolean>(
"always-show-iphone-earpice", "always-show-iphone-earpiece",
false, false,
); );

View File

@@ -0,0 +1,22 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { combineLatest, map, startWith } from "rxjs";
import { setOutputEnabled$ } from "../controls";
import { muteAllAudio as muteAllAudioSetting } from "../settings/settings";
/**
* This can transition into sth more complete: `GroupCallViewModel.ts`
*/
export const muteAllAudio$ = combineLatest([
setOutputEnabled$,
muteAllAudioSetting.value$,
]).pipe(
startWith([false, muteAllAudioSetting.getValue()]),
map(([outputEndabled, settingsMute]) => !outputEndabled || settingsMute),
);

View File

@@ -140,7 +140,7 @@ test("will use the correct volume level", async () => {
expect(testAudioContext.pan.pan.setValueAtTime).toHaveBeenCalledWith(0, 0); expect(testAudioContext.pan.pan.setValueAtTime).toHaveBeenCalledWith(0, 0);
}); });
test("will use the pan if earpice is selected", async () => { test("will use the pan if earpiece is selected", async () => {
const { findByText } = render( const { findByText } = render(
<MediaDevicesContext.Provider <MediaDevicesContext.Provider
value={{ value={{