// The list a participant actually gets to choose from., done-when 1. // // A browser's device list is NOT a menu. It carries entries with empty ids (what a browser reports // before permission is granted - and `startAudioInput('')` is a silent no-op, i.e. a dead control), // entries with empty LABELS for the same reason, and the duplicate `default` / `communications` // aliases Windows adds for one physical microphone. On the machine that produced this card it also // carried an OBS Virtual Camera and two DroidCam sources ahead of the real hardware. // // The product used to take `devices[0]` and never ask, which is how a patient ends up silent for a // whole visit on a virtual microphone they did not know was selected. This is what makes asking // possible; the picker is what asks. import type { MediaKind } from './mediaFailure'; export interface DeviceChoice { deviceId: string; label: string; } const NOUN: Record = { microphone: 'Microphone', camera: 'Camera' }; export function deviceChoices( devices: Array<{ deviceId?: string; label?: string }> | undefined, kind: MediaKind, ): DeviceChoice[] { const seen = new Set(); const choices: DeviceChoice[] = []; (devices ?? []).forEach((device) => { const deviceId = device?.deviceId; // An entry with no id cannot be selected. Rendering it is a control that does nothing. if (!deviceId || seen.has(deviceId)) return; seen.add(deviceId); choices.push({ deviceId, // Positional, and numbered over the KEPT list rather than the raw one, so the names a user // sees are 1..n with no gaps where a dropped entry used to be. label: device.label?.trim() || `${NOUN[kind]} ${choices.length + 1}`, }); }); return choices; }