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

@@ -26,7 +26,7 @@ export interface OutputDevice {
export const setPipEnabled$ = new Subject<boolean>();
export const setAvailableOutputDevices$ = new Subject<OutputDevice[]>();
export const setOutputDevice$ = new Subject<string>();
export const setOutputDisabled$ = new Subject<boolean>();
export const setOutputEnabled$ = new Subject<boolean>();
window.controls = {
canEnterPip(): boolean {
@@ -51,8 +51,8 @@ window.controls = {
setOutputDevice$.next(id);
},
setOutputEnabled(enabled: boolean): void {
if (!setOutputDisabled$.observed)
if (!setOutputEnabled$.observed)
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;
}
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
* 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 usingNames If the hook should query device names for the associated
* list.
* @returns A handle for the choosen kind.
* @returns A handle for the chosen kind.
*/
function useMediaDeviceHandle(
kind: MediaDeviceKind,
setting: Setting<string | undefined>,
usingNames: boolean,
): MediaDeviceHandle {
// Make sure we don't needlessly reset to a device observer without names,
// once permissions are already given
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
@@ -153,22 +174,7 @@ function useMediaDeviceHandle(
);
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.
return (preferredId !== undefined && available.has(preferredId)) ||
(available.size === 1 && available.has(""))
? preferredId
: available.keys().next().value;
}
return undefined;
}, [available, preferredId]);
const selectedId = useSelectedId(available, preferredId);
const selectedGroupId = useObservableEagerState(
useMemo(
() =>
@@ -337,21 +343,7 @@ function useControlledOutput(): MediaDeviceHandle {
setOutputDevice$.subscribe((id) => setPreferredId(id));
}, [setPreferredId]);
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.
return (preferredId !== undefined && available.has(preferredId)) ||
(available.size === 1 && available.has(""))
? preferredId
: available.keys().next().value;
}
return undefined;
}, [available, preferredId]);
const selectedId = useSelectedId(available, preferredId);
const [asEarpice, setAsEarpiece] = useState(false);

View File

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

View File

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