review cleanup
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
22
src/state/MuteAllAudioModel.ts
Normal file
22
src/state/MuteAllAudioModel.ts
Normal 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),
|
||||||
|
);
|
||||||
@@ -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={{
|
||||||
|
|||||||
Reference in New Issue
Block a user