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.
- `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.
- `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.
- `showNativeOutputDevicePicker: () => void`. This callback will be code by the webview if the user presses the output button in the settings menu.
This button is only shown on ios. (`userAgent.includes("IPhone")`)
- `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")`)

View File

@@ -63,7 +63,7 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
| `controlledMediaDevices` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the [global JS controls for audio output devices](./controls.md#audio-devices) should be enabled, allowing the list of output devices to be controlled by the app hosting Element Call. |
| `controlledMediaDevices` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the [global JS controls for audio output devices](./controls.md#audio-devices) should be enabled, allowing the list of output devices to be controlled by the app hosting Element Call. |
| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. |
| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. |
| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) |

View File

@@ -32,17 +32,13 @@ 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 that has been set during loading to be be available immediately once loaded.
export const setAvailableOutputDevices$ = new BehaviorSubject<OutputDevice[]>(
[],
);
// We want the devices that have been set during loading to be available immediately once loaded.
export const availableOutputDevices$ = new BehaviorSubject<OutputDevice[]>([]);
// 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.
export const setOutputDevice$ = new BehaviorSubject<string | undefined>(
undefined,
);
// We want the device that has been set during loading to be available immediately once loaded.
export const outputDevice$ = new BehaviorSubject<string | 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.
*
* 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);
},
setAvailableOutputDevices(devices: OutputDevice[]): void {
setAvailableOutputDevices$.next(devices);
availableOutputDevices$.next(devices);
},
setOutputDevice(id: string): void {
setOutputDevice$.next(id);
outputDevice$.next(id);
},
setOutputEnabled(enabled: boolean): void {
if (!setOutputEnabled$.observed)
throw new Error(
"Output controls are disabled. No setOutputEnabled$ observer",
);
setOutputEnabled$.next(!enabled);
setOutputEnabled$.next(enabled);
},
};

View File

@@ -29,7 +29,7 @@ import {
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
type Setting,
} from "../settings/settings";
import { setAvailableOutputDevices$, setOutputDevice$ } from "../controls";
import { outputDevice$, availableOutputDevices$ } from "../controls";
import { useUrlParams } from "../UrlParams";
// 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)
* - Show a button to show the native output picker instead.
* - 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(
startWith(
alwaysShowIphoneEarpieceSetting.getValue() ||
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
* the available devices, read and set the selected device.
* @param kind audio input, output or video output.
* @param setting The setting this handles selection should be synced with.
* @param kind Audio input, output or video output.
* @param setting The setting this handle's selection should be synced with.
* @param usingNames If the hook should query device names for the associated
* list.
* @returns A handle for the chosen kind.
@@ -320,7 +316,7 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
function useControlledOutput(): MediaDeviceHandle {
const { available } = useObservableEagerState(
useObservable(() => {
const outputDeviceData$ = setAvailableOutputDevices$.pipe(
const outputDeviceData$ = availableOutputDevices$.pipe(
map((devices) => {
const deviceForEarpiece = devices.find((d) => d.forEarpiece);
const deviceMapTuple: [string, DeviceLabel][] = devices.map(
@@ -339,8 +335,9 @@ function useControlledOutput(): MediaDeviceHandle {
}),
);
return combineLatest([outputDeviceData$, iosDeviceMenu$]).pipe(
map(([{ devicesMap, deviceForEarpiece }, iosShowEarpiece]) => {
return combineLatest(
[outputDeviceData$, iosDeviceMenu$],
({ devicesMap, deviceForEarpiece }, iosShowEarpiece) => {
let available = devicesMap;
if (iosShowEarpiece && !!deviceForEarpiece) {
available = new Map([
@@ -349,15 +346,16 @@ function useControlledOutput(): MediaDeviceHandle {
]);
}
return { available, deviceForEarpiece };
}),
},
);
}),
);
const [preferredId, setPreferredId] = useSetting(audioOutputSetting);
useEffect(() => {
setOutputDevice$.subscribe((id) => {
const subscription = outputDevice$.subscribe((id) => {
if (id) setPreferredId(id);
});
return (): void => subscription.unsubscribe();
}, [setPreferredId]);
const selectedId = useSelectedId(available, preferredId);
@@ -365,9 +363,10 @@ function useControlledOutput(): MediaDeviceHandle {
const [asEarpiece, setAsEarpiece] = useState(false);
useEffect(() => {
// In earpiece mode we just sent the EARPIECE_CONFIG_ID to the native code
// This only happens on ios where we use the native picker.
// So this only is needed so that ios can know if the proximity sensor should be used or not.
// Let the hosting application know which output device has been selected.
// 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.onOutputDeviceSelect?.(selectedId);
setAsEarpiece(selectedId === EARPIECE_CONFIG_ID);
}, [selectedId]);

View File

@@ -104,7 +104,7 @@ export const SettingsModal: FC<Props> = ({
const [showDeveloperSettingsTab] = useSetting(developerMode);
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$);
// In controlled devices we will not show the input section
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.
*/
import { combineLatest, map, startWith } from "rxjs";
import { combineLatest, startWith } from "rxjs";
import { setOutputEnabled$ } from "../controls";
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`
*/
export const muteAllAudio$ = combineLatest([
setOutputEnabled$,
muteAllAudioSetting.value$,
]).pipe(
startWith([true, muteAllAudioSetting.getValue()]),
map(([outputEnabled, settingsMute]) => !outputEnabled || settingsMute),
export const muteAllAudio$ = combineLatest(
[setOutputEnabled$.pipe(startWith(true)), muteAllAudioSetting.value$],
(outputEnabled, settingsMute) => !outputEnabled || settingsMute,
);