diff --git a/docs/controls.md b/docs/controls.md index 7e84d78d..fbe3b90f 100644 --- a/docs/controls.md +++ b/docs/controls.md @@ -17,3 +17,5 @@ These functions must be used in conjunction with the `controlledOutput` URL para - `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.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")`) diff --git a/locales/en/app.json b/locales/en/app.json index 0b4c0599..e8a86fcc 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -173,6 +173,7 @@ "devices": { "camera": "Camera", "camera_numbered": "Camera {{n}}", + "change_device_button": "Change audio device", "default": "Default", "default_named": "Default <2>({{name}})", "earpiece": "Earpiece", diff --git a/src/controls.ts b/src/controls.ts index cc45f881..86af57a5 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -15,6 +15,7 @@ export interface Controls { setOutputDevice(id: string): void; onOutputDeviceSelect?: (id: string) => void; setOutputEnabled(enabled: boolean): void; + showNativeOutputDevicePicker?: () => void; } export interface OutputDevice { diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 96f6b1de..75663140 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -77,6 +77,22 @@ export interface MediaDevices extends Omit { audioOutput: MediaDeviceHandle; } +/** + * An observable that represents if we should display the devices menu for iOS. + * This implies the following + * - hide any input devices (they do not work anyhow on ios) + * - Show a button to show the native output picker instead. + * - Only show the earpice toggle option if the earpiece is available: + * `setAvailableOutputDevices$.includes((d)=>d.forEarpiece)` + */ +export const iosDeviceMenu$ = alwaysShowIphoneEarpieceSetting.value$.pipe( + startWith( + alwaysShowIphoneEarpieceSetting.getValue() || + navigator.userAgent.includes("iPhone"), + ), + map((v) => v || navigator.userAgent.includes("iPhone")), +); + function useSelectedId( available: Map, preferredId: string | undefined, @@ -304,41 +320,36 @@ export const MediaDevicesProvider: FC = ({ children }) => { }; function useControlledOutput(): MediaDeviceHandle { - const { available, physicalDeviceForEarpiceMode } = useObservableEagerState( - useObservable(() => { - const showEarpice$ = alwaysShowIphoneEarpieceSetting.value$.pipe( - startWith(alwaysShowIphoneEarpieceSetting.getValue()), - map((v) => v || navigator.userAgent.includes("iPhone")), - ); - const outputDeviceData$ = setAvailableOutputDevices$.pipe( - startWith([]), - map((devices) => { - const physicalDeviceForEarpiceMode = devices.find( - (d) => d.forEarpiece, - ); - return { - devicesMap: new Map( - devices.map(({ id, name }) => [id, { type: "name", name }]), - ), - physicalDeviceForEarpiceMode, - }; - }), - ); + const { available, deviceForEarpiece: physicalDeviceForEarpiceMode } = + useObservableEagerState( + useObservable(() => { + const outputDeviceData$ = setAvailableOutputDevices$.pipe( + startWith([]), + map((devices) => { + const deviceForEarpiece = devices.find((d) => d.forEarpiece); + return { + devicesMap: new Map( + devices.map(({ id, name }) => [id, { type: "name", name }]), + ), + deviceForEarpiece, + }; + }), + ); - return combineLatest([outputDeviceData$, showEarpice$]).pipe( - map(([{ devicesMap, physicalDeviceForEarpiceMode }, showEarpiece]) => { - let available = devicesMap; - if (showEarpiece && !!physicalDeviceForEarpiceMode) { - available = new Map([ - ...devicesMap.entries(), - [EARPIECE_CONFIG_ID, { type: "earpiece" }], - ]); - } - return { available, physicalDeviceForEarpiceMode }; - }), - ); - }), - ); + return combineLatest([outputDeviceData$, iosDeviceMenu$]).pipe( + map(([{ devicesMap, deviceForEarpiece }, iosShowEarpiece]) => { + let available = devicesMap; + if (iosShowEarpiece && !!deviceForEarpiece) { + available = new Map([ + ...devicesMap.entries(), + [EARPIECE_CONFIG_ID, { type: "earpiece" }], + ]); + } + return { available, deviceForEarpiece }; + }), + ); + }), + ); const [preferredId, setPreferredId] = useSetting(audioOutputSetting); useEffect(() => { setOutputDevice$.subscribe((id) => setPreferredId(id)); diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 1c97a87d..519a48ed 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -8,8 +8,9 @@ Please see LICENSE in the repository root for full details. import { type FC, type ReactNode, useState } from "react"; import { useTranslation } from "react-i18next"; import { type MatrixClient } from "matrix-js-sdk"; -import { Root as Form, Separator } from "@vector-im/compound-web"; +import { Button, Root as Form, Separator } from "@vector-im/compound-web"; import { type Room as LivekitRoom } from "livekit-client"; +import { useObservableEagerState } from "observable-hooks"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; @@ -19,6 +20,7 @@ import { FeedbackSettingsTab } from "./FeedbackSettingsTab"; import { useMediaDevices, useMediaDeviceNames, + iosDeviceMenu$, } from "../livekit/MediaDevicesContext"; import { widget } from "../widget"; import { @@ -101,6 +103,7 @@ export const SettingsModal: FC = ({ const [showDeveloperSettingsTab] = useSetting(developerMode); const { available: isRageshakeAvailable } = useSubmitRageshake(); + const iosDeviceMenu = useObservableEagerState(iosDeviceMenu$); const audioTab: Tab = { key: "audio", @@ -108,13 +111,25 @@ export const SettingsModal: FC = ({ content: ( <>
- - t("settings.devices.microphone_numbered", { n }) - } - /> + {!iosDeviceMenu && ( + + t("settings.devices.microphone_numbered", { n }) + } + /> + )} + {iosDeviceMenu && ( + + )}