This commit is contained in:
Timo
2025-05-21 12:51:00 +02:00
parent 435a7d0adb
commit a056a28423
6 changed files with 33 additions and 41 deletions

View File

@@ -12,10 +12,10 @@ A few aspects of Element Call's interface can be controlled through a global API
These functions must be used in conjunction with the `controlledMediaDevices` URL parameter in order to have any effect. These functions must be used in conjunction with the `controlledMediaDevices` URL parameter in order to have any effect.
- `controls.setAvailableOutputDevices(devices: { id: string, name: string, forEarpiece?: boolean, isEarpiece?: boolean isSpeaker?: boolean, isExternalHeadset?, boolean;}[]): void` Sets the list of available audio outputs. `forEarpiece` is used on ios only. - `controls.setAvailableOutputDevices(devices: { id: string, name: string, forEarpiece?: boolean, isEarpiece?: boolean isSpeaker?: boolean, isExternalHeadset?, 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 earpiece 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 Element Call's menu. This should be used if the OS decides to automatically switch to Bluetooth, for example.
- `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.
- `showNativeOutputDevicePicker: () => void`. This callback will be code by the webview if the user presses the output button in the settings menu. - `showNativeOutputDevicePicker: (() => void) | undefined`. Callback called whenever the user presses the output button in the settings menu.
This button is only shown on ios. (`userAgent.includes("IPhone")`) This button is only shown on iOS. (`userAgent.includes("iPhone")`)

View File

@@ -32,17 +32,13 @@ export interface OutputDevice {
*/ */
export const setPipEnabled$ = new Subject<boolean>(); export const setPipEnabled$ = new Subject<boolean>();
// BehaviorSubject since the client might set this before we have subscribed (GroupCallView still in "loading" state) // BehaviorSubject since the client might set this before we have subscribed (GroupCallView still in "loading" state)
// We want the that has been set during loading to be be available immediately once loaded. // We want the devices that have been set during loading to be available immediately once loaded.
export const setAvailableOutputDevices$ = new BehaviorSubject<OutputDevice[]>( export const availableOutputDevices$ = new BehaviorSubject<OutputDevice[]>([]);
[],
);
// BehaviorSubject since the client might set this before we have subscribed (GroupCallView still in "loading" state) // BehaviorSubject since the client might set this before we have subscribed (GroupCallView still in "loading" state)
// We want the that has been set during loading to be be available immediately once loaded. // We want the device that has been set during loading to be available immediately once loaded.
export const setOutputDevice$ = new BehaviorSubject<string | undefined>( export const outputDevice$ = new BehaviorSubject<string | undefined>(undefined);
undefined,
);
/** /**
* This is currently unused. It might be possible to allow the os to mute the call this way if the user * This allows the os to mute the call if the user
* presses the volume down button when it is at the minimum volume. * presses the volume down button when it is at the minimum volume.
* *
* This should also be used to display a darkened overlay screen letting the user know that audio is muted. * This should also be used to display a darkened overlay screen letting the user know that audio is muted.
@@ -62,16 +58,16 @@ window.controls = {
setPipEnabled$.next(false); setPipEnabled$.next(false);
}, },
setAvailableOutputDevices(devices: OutputDevice[]): void { setAvailableOutputDevices(devices: OutputDevice[]): void {
setAvailableOutputDevices$.next(devices); availableOutputDevices$.next(devices);
}, },
setOutputDevice(id: string): void { setOutputDevice(id: string): void {
setOutputDevice$.next(id); outputDevice$.next(id);
}, },
setOutputEnabled(enabled: boolean): void { setOutputEnabled(enabled: boolean): void {
if (!setOutputEnabled$.observed) if (!setOutputEnabled$.observed)
throw new Error( throw new Error(
"Output controls are disabled. No setOutputEnabled$ observer", "Output controls are disabled. No setOutputEnabled$ observer",
); );
setOutputEnabled$.next(!enabled); setOutputEnabled$.next(enabled);
}, },
}; };

View File

@@ -29,7 +29,7 @@ import {
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
type Setting, type Setting,
} from "../settings/settings"; } from "../settings/settings";
import { setAvailableOutputDevices$, setOutputDevice$ } from "../controls"; import { outputDevice$, availableOutputDevices$ } from "../controls";
import { useUrlParams } from "../UrlParams"; import { useUrlParams } from "../UrlParams";
// This hardcoded id is used in EX ios! It can only be changed in coordination with // This hardcoded id is used in EX ios! It can only be changed in coordination with
@@ -81,13 +81,9 @@ export interface MediaDevices extends Omit<InputDevices, "usingNames"> {
* - hide any input devices (they do not work anyhow on ios) * - hide any input devices (they do not work anyhow on ios)
* - Show a button to show the native output picker instead. * - Show a button to show the native output picker instead.
* - Only show the earpiece toggle option if the earpiece is available: * - Only show the earpiece toggle option if the earpiece is available:
* `setAvailableOutputDevices$.includes((d)=>d.forEarpiece)` * `availableOutputDevices$.includes((d)=>d.forEarpiece)`
*/ */
export const iosDeviceMenu$ = alwaysShowIphoneEarpieceSetting.value$.pipe( export const iosDeviceMenu$ = alwaysShowIphoneEarpieceSetting.value$.pipe(
startWith(
alwaysShowIphoneEarpieceSetting.getValue() ||
navigator.userAgent.includes("iPhone"),
),
map((v) => v || navigator.userAgent.includes("iPhone")), map((v) => v || navigator.userAgent.includes("iPhone")),
); );
@@ -115,8 +111,8 @@ function useSelectedId(
/** /**
* 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.
* @param kind audio input, output or video output. * @param kind Audio input, output or video output.
* @param setting The setting this handles selection should be synced with. * @param setting The setting this handle's 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 chosen kind. * @returns A handle for the chosen kind.
@@ -320,7 +316,7 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
function useControlledOutput(): MediaDeviceHandle { function useControlledOutput(): MediaDeviceHandle {
const { available } = useObservableEagerState( const { available } = useObservableEagerState(
useObservable(() => { useObservable(() => {
const outputDeviceData$ = setAvailableOutputDevices$.pipe( const outputDeviceData$ = availableOutputDevices$.pipe(
map((devices) => { map((devices) => {
const deviceForEarpiece = devices.find((d) => d.forEarpiece); const deviceForEarpiece = devices.find((d) => d.forEarpiece);
const deviceMapTuple: [string, DeviceLabel][] = devices.map( const deviceMapTuple: [string, DeviceLabel][] = devices.map(
@@ -339,8 +335,9 @@ function useControlledOutput(): MediaDeviceHandle {
}), }),
); );
return combineLatest([outputDeviceData$, iosDeviceMenu$]).pipe( return combineLatest(
map(([{ devicesMap, deviceForEarpiece }, iosShowEarpiece]) => { [outputDeviceData$, iosDeviceMenu$],
({ devicesMap, deviceForEarpiece }, iosShowEarpiece) => {
let available = devicesMap; let available = devicesMap;
if (iosShowEarpiece && !!deviceForEarpiece) { if (iosShowEarpiece && !!deviceForEarpiece) {
available = new Map([ available = new Map([
@@ -349,15 +346,16 @@ function useControlledOutput(): MediaDeviceHandle {
]); ]);
} }
return { available, deviceForEarpiece }; return { available, deviceForEarpiece };
}), },
); );
}), }),
); );
const [preferredId, setPreferredId] = useSetting(audioOutputSetting); const [preferredId, setPreferredId] = useSetting(audioOutputSetting);
useEffect(() => { useEffect(() => {
setOutputDevice$.subscribe((id) => { const subscription = outputDevice$.subscribe((id) => {
if (id) setPreferredId(id); if (id) setPreferredId(id);
}); });
return (): void => subscription.unsubscribe();
}, [setPreferredId]); }, [setPreferredId]);
const selectedId = useSelectedId(available, preferredId); const selectedId = useSelectedId(available, preferredId);
@@ -365,9 +363,10 @@ function useControlledOutput(): MediaDeviceHandle {
const [asEarpiece, setAsEarpiece] = useState(false); const [asEarpiece, setAsEarpiece] = useState(false);
useEffect(() => { useEffect(() => {
// In earpiece mode we just sent the EARPIECE_CONFIG_ID to the native code // Let the hosting application know which output device has been selected.
// This only happens on ios where we use the native picker. // This information is probably only of interest if the earpiece mode has been
// So this only is needed so that ios can know if the proximity sensor should be used or not. // selected - for example, Element X iOS listens to this to determine whether it
// should enable the proximity sensor.
if (selectedId) window.controls.onOutputDeviceSelect?.(selectedId); if (selectedId) window.controls.onOutputDeviceSelect?.(selectedId);
setAsEarpiece(selectedId === EARPIECE_CONFIG_ID); setAsEarpiece(selectedId === EARPIECE_CONFIG_ID);
}, [selectedId]); }, [selectedId]);

View File

@@ -104,7 +104,7 @@ export const SettingsModal: FC<Props> = ({
const [showDeveloperSettingsTab] = useSetting(developerMode); const [showDeveloperSettingsTab] = useSetting(developerMode);
const { available: isRageshakeAvailable } = useSubmitRageshake(); const { available: isRageshakeAvailable } = useSubmitRageshake();
// If we are on ios we will show a button to open the native picker. // If we are on iOS we will show a button to open the native audio device picker.
const iosDeviceMenu = useObservableEagerState(iosDeviceMenu$); const iosDeviceMenu = useObservableEagerState(iosDeviceMenu$);
// In controlled devices we will not show the input section // In controlled devices we will not show the input section
const { controlledMediaDevices } = useUrlParams(); const { controlledMediaDevices } = useUrlParams();

View File

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