/* 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, pairwise, startWith, Subject, switchMap, type Observable, tap, } from "rxjs"; import { createMediaDeviceObserver } from "@livekit/components-core"; import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { type RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc"; 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"; import { switchWhen } from "../utils/observable"; import { type Behavior, constant } from "./Behavior"; // 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"; export type DeviceLabel = | { type: "name"; name: string } | { type: "number"; number: number }; export type AudioOutputDeviceLabel = | DeviceLabel | { type: "speaker" } | { type: "earpiece" } | { type: "default"; name: string | null }; /** * Base selected-device value shared by all media kinds. * * `id` is the effective device identifier used by browser media APIs. */ export interface SelectedDevice { id: string; } /** * Selected audio input value with audio-input-specific metadata. */ 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; } /** * Selected audio output value with output-routing-specific metadata. */ 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; } /** * Common reactive contract for selectable input/output media devices (mic, speaker, camera). * * `Label` is the type used to represent a device in UI lists. * `Selected` is the type used to represent the active selection for a device kind. */ export interface MediaDevice { /** * Reactive map of currently available devices keyed by device ID. * * `Label` defines the UI-facing label data structure for each device type. */ available$: Behavior>; /** * The active device selection. * Can be `undefined` when no device is yet selected. * * When defined, `Selected` contains the selected device ID plus any * type-specific metadata. */ selected$: Behavior; /** * Requests selection of a device by ID. * * Implementations typically persist this preference and let `selected$` * converge to the effective device (which may differ if the requested ID is * unavailable). */ 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" ? constant(true) : alwaysShowIphoneEarpieceSetting.value$; function availableRawDevices$( kind: MediaDeviceKind, usingNames$: Behavior, scope: ObservableScope, logger: Logger, ): Behavior { const logError = (e: Error): void => logger.error("Error creating MediaDeviceObserver", e); const devices$ = createMediaDeviceObserver(kind, logError, false); const devicesWithNames$ = createMediaDeviceObserver(kind, logError, true); return scope.behavior( usingNames$.pipe( switchMap((withNames) => withNames ? // It might be that there is already a media stream running somewhere, // and so we can do without requesting a second one. Only switch to the // device observer that explicitly requests the names if we see that // names are in fact missing from the initial device enumeration. devices$.pipe( switchWhen( (devices, i) => i === 0 && devices.every((d) => !d.label), devicesWithNames$, ), ) : devices$, ), ), [], ); } 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$