/* Copyright 2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { combineLatest, filter, map, merge, of, pairwise, startWith, Subject, switchMap, type Observable, } from "rxjs"; import { createMediaDeviceObserver } from "@livekit/components-core"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { audioInput as audioInputSetting, audioOutput as audioOutputSetting, videoInput as videoInputSetting, alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, } from "../settings/settings"; import { type ObservableScope } from "./ObservableScope"; import { outputDevice$ as controlledOutputSelection$, availableOutputDevices$ as controlledAvailableOutputDevices$, } from "../controls"; import { getUrlParams } from "../UrlParams"; import { platform } from "../Platform"; // This hardcoded id is used in EX ios! It can only be changed in coordination with // the ios swift team. const EARPIECE_CONFIG_ID = "earpiece-id"; const logger = rootLogger.getChild("[MediaDevices]"); export type DeviceLabel = | { type: "name"; name: string } | { type: "number"; number: number } | { type: "default"; name: string | null }; export type AudioOutputDeviceLabel = DeviceLabel | { type: "earpiece" }; export interface SelectedDevice { id: string; } export interface SelectedAudioInputDevice extends SelectedDevice { /** * Emits whenever we think that this audio input device has logically changed * to refer to a different hardware device. */ hardwareDeviceChange$: Observable; } export interface SelectedAudioOutputDevice extends SelectedDevice { /** * Whether this device is a "virtual earpiece" device. If so, we should output * on a single channel of the device at a reduced volume. */ virtualEarpiece: boolean; } export interface MediaDevice { /** * A map from available device IDs to labels. */ available$: Observable>; /** * The selected device. */ selected$: Observable; /** * Selects a new device. */ select(id: string): void; } /** * 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 earpiece toggle option if the earpiece is available: * `availableOutputDevices$.includes((d)=>d.forEarpiece)` */ export const iosDeviceMenu$ = platform === "ios" ? of(true) : alwaysShowIphoneEarpieceSetting.value$; function availableRawDevices$( kind: MediaDeviceKind, updateAvailableDeviceRequests$: Observable, scope: ObservableScope, ): Observable { return updateAvailableDeviceRequests$.pipe( startWith(false), switchMap((withPermissions) => createMediaDeviceObserver( kind, (e) => logger.error("Error creating MediaDeviceObserver", e), withPermissions, ), ), startWith([]), scope.state(), ); } function buildDeviceMap( availableRaw: MediaDeviceInfo[], ): Map { return new Map( availableRaw.map((d, i) => [ d.deviceId, d.label ? { type: "name", name: d.label } : { type: "number", number: i + 1 }, ]), ); } function selectDevice$