add earpice mode

This commit is contained in:
Timo
2025-05-14 19:55:08 +02:00
parent f9b04ae38e
commit f69c75322f
3 changed files with 55 additions and 16 deletions

View File

@@ -12,6 +12,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.
- `controls.setOutputDevices(devices: { id: string, name: string }[]): void` Sets the list of available audio outputs.
- `controls.setOutputDevices(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.
- `controls.onOutputDeviceSelect: ((id: string) => void) | undefined` Callback called whenever the user or application selects a new audio output.
- `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.

View File

@@ -19,6 +19,7 @@ export interface Controls {
export interface OutputDevice {
id: string;
name: string;
forEarpiece?: boolean;
}
export const setPipEnabled$ = new Subject<boolean>();

View File

@@ -71,6 +71,16 @@ interface InputDevices {
export interface MediaDevices extends Omit<InputDevices, "usingNames"> {
audioOutput: MediaDevice;
}
function useShowEarpiece(): boolean {
const [alwaysShowIphoneEarpice] = useSetting(alwaysShowIphoneEarpieceSetting);
const m = useMemo(
() =>
(navigator.userAgent.match("iPhone")?.length ?? 0) > 0 ||
alwaysShowIphoneEarpice,
[alwaysShowIphoneEarpice],
);
return m;
}
function useMediaDevice(
kind: MediaDeviceKind,
@@ -79,7 +89,7 @@ function useMediaDevice(
): MediaDevice {
// Make sure we don't needlessly reset to a device observer without names,
// once permissions are already given
const [alwaysShowIphoneEarpice] = useSetting(alwaysShowIphoneEarpieceSetting);
const showEarpiece = useShowEarpiece();
const hasRequestedPermissions = useRef(false);
const requestPermissions = usingNames || hasRequestedPermissions.current;
hasRequestedPermissions.current ||= usingNames;
@@ -119,8 +129,6 @@ function useMediaDevice(
// recognizes.
// We also create this if we do not have any available devices, so that
// we can use the default or the earpiece.
const showEarpiece =
navigator.userAgent.match("iPhone") || alwaysShowIphoneEarpice;
if (
kind === "audiooutput" &&
!available.has("") &&
@@ -144,7 +152,7 @@ function useMediaDevice(
return available;
}),
),
[alwaysShowIphoneEarpice, deviceObserver$, kind],
[deviceObserver$, kind, showEarpiece],
),
);
@@ -298,20 +306,31 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
};
function useControlledOutput(): MediaDevice {
const showEarpiece = useShowEarpiece();
const available = useObservableEagerState(
useObservable(() =>
setOutputDevices$.pipe(
startWith<OutputDevice[]>([]),
map(
(devices) =>
new Map<string, DeviceLabel>(
devices.map(({ id, name }) => [id, { type: "name", name }]),
),
),
map((devices) => {
const devicesMap = new Map<string, DeviceLabel>(
devices.map(({ id, name }) => [id, { type: "name", name }]),
);
if (showEarpiece)
devicesMap.set(EARPIECE_CONFIG_ID, { type: "earpiece" });
return devicesMap;
}),
),
),
);
const [preferredId, select] = useSetting(audioOutputSetting);
const earpiceDevice = useObservableEagerState(
setOutputDevices$.pipe(
map((devices) => devices.find((d) => d.forEarpiece)),
),
);
const [preferredId, setPreferredId] = useSetting(audioOutputSetting);
const selectedId = useMemo(() => {
if (available.size) {
// If the preferred device is available, use it. Or if every available
@@ -327,19 +346,37 @@ function useControlledOutput(): MediaDevice {
}
return undefined;
}, [available, preferredId]);
useEffect(() => {
if (selectedId !== undefined)
window.controls.onOutputDeviceSelect?.(selectedId);
if (selectedId === EARPIECE_CONFIG_ID)
if (selectedId !== undefined)
window.controls.onOutputDeviceSelect?.(selectedId);
}, [selectedId]);
const [asEarpice, setAsEarpiece] = useState(false);
const select = useCallback(
(id: string) => {
if (id === EARPIECE_CONFIG_ID) {
setAsEarpiece(true);
if (earpiceDevice) setPreferredId(earpiceDevice.id);
} else {
setAsEarpiece(false);
setPreferredId(id);
}
},
[earpiceDevice, setPreferredId],
);
return useMemo(
() => ({
available,
available: available,
selectedId,
selectedGroupId: undefined,
select,
useAsEarpiece: asEarpice,
}),
[available, selectedId, select],
[available, selectedId, select, asEarpice],
);
}