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. 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.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. - `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 { export interface OutputDevice {
id: string; id: string;
name: string; name: string;
forEarpiece?: boolean;
} }
export const setPipEnabled$ = new Subject<boolean>(); export const setPipEnabled$ = new Subject<boolean>();

View File

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