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

@@ -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,
);