diff --git a/docs/controls.md b/docs/controls.md index af90ab12..2bc17b24 100644 --- a/docs/controls.md +++ b/docs/controls.md @@ -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. diff --git a/src/controls.ts b/src/controls.ts index f97601af..72f80260 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -19,6 +19,7 @@ export interface Controls { export interface OutputDevice { id: string; name: string; + forEarpiece?: boolean; } export const setPipEnabled$ = new Subject(); diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 6bca910f..38fcce6b 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -71,6 +71,16 @@ interface InputDevices { export interface MediaDevices extends Omit { 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 = ({ children }) => { }; function useControlledOutput(): MediaDevice { + const showEarpiece = useShowEarpiece(); + const available = useObservableEagerState( useObservable(() => setOutputDevices$.pipe( startWith([]), - map( - (devices) => - new Map( - devices.map(({ id, name }) => [id, { type: "name", name }]), - ), - ), + map((devices) => { + const devicesMap = new Map( + 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], ); }