//, done-when 1 - the control that lets a participant change which microphone or camera the // visit is using, from inside the visit, without rejoining. // // WHY IT HAS TO EXIST: the app took `devices[0]` and never asked. The first-enumerated input is // whatever the machine happens to list first - commonly a virtual device (OBS, DroidCam, a conferencing // tool's virtual mic) or a disconnected headset. A patient in that situation is silent for the entire // visit, is told they blocked their own microphone, and has no control anywhere in the product to // switch to the one that works. Their only recourse was to re-order Windows sound devices mid-visit, // which does not even help, because nothing here ever read the OS default. // // Pure presentation, like everything else in this folder: no SDK, no transport, no fetch. import { useCallback, useEffect, useState } from 'react'; import { useVideoServiceContext } from '../../services/VideoService'; import type { DeviceChoice } from '../../services/video/deviceChoices'; import type { MediaKind } from '../../services/video/mediaFailure'; const LABEL: Record = { microphone: 'Microphone', camera: 'Camera' }; export const DevicePicker = () => { const { call, micDeviceId, cameraDeviceId, listInputs, selectInput, micAvailable, cameraAvailable, cameraFailure } = useVideoServiceContext(); const [devices, setDevices] = useState<{ microphones: DeviceChoice[]; cameras: DeviceChoice[] }>( { microphones: [], cameras: [] }, ); const [busy, setBusy] = useState(false); // Devices are acquired DURING the join, and enumerating before it completes returns the // label-less list a browser reports without permission - a menu of "Microphone 1, Microphone 2" // that tells nobody anything. `active` is the only phase where this list is worth showing. const connected = call.phase === 'active'; const refresh = useCallback(() => { if (!connected) return; void listInputs().then(setDevices); }, [connected, listInputs]); // Re-read on every open rather than once: a headset plugged in mid-visit is exactly the case // somebody opens this for, and a list captured at join could never contain it. useEffect(refresh, [refresh]); const change = async (kind: MediaKind, deviceId: string) => { if (!deviceId) return; setBusy(true); try { await selectInput(kind, deviceId); } finally { setBusy(false); } // The switch may have changed what is available (a second busy device is the ordinary next // move for someone whose first one was), so re-read rather than trusting the stale list. refresh(); }; if (!connected) return null; // A kind with NOTHING to choose between renders no control at all. One entry is still worth // showing - it names what the visit is using, which is the question this card was raised about - // but an empty list means "there is no such device", and a disabled empty menu says that worse // than the failure notice already does. const rows: Array<{ kind: MediaKind; choices: DeviceChoice[]; current?: string }> = [ { kind: 'microphone', choices: devices.microphones, current: micDeviceId }, { kind: 'camera', choices: devices.cameras, current: cameraDeviceId }, ].filter((row) => row.choices.length > 0) as any; if (rows.length === 0) return null; // Folded to one line so the call keeps its room; a blocked device opens it, because the warning // above names this control as the fix. const blocked = !micAvailable || (!cameraAvailable && !!cameraFailure); return (
Microphone and camera
{rows.map(({ kind, choices, current }) => ( ))}
); };