// Owns the Chime SDK and its observers. NO React - this is what makes the media lifecycle testable // without jsdom, lets surfaces be pure tile-binding consumers, and makes "the call survives // navigation" structurally true rather than carefully maintained. import { loadChimeSdk } from './chimeLoader'; import type { ChimeJoinInfo } from './videoContract'; import { classifyMediaFailure, type MediaFailureReason, type MediaKind } from './mediaFailure'; import { deviceChoices, type DeviceChoice } from './deviceChoices'; /** Task 9 - WHO is on the roster, not just how many. One entry per remote attendee, carrying the * SDK's `externalUserId` (its presence callback's THIRD argument) alongside the Chime attendee id it * has always tracked. Per the video contract, `externalUserId` IS the participant's stable FHIR * reference (`Practitioner/abc`, `Patient/xyz`) - this class only CARRIES it; resolving it to a name * or a neutral fallback (and never rendering it raw) is `MeetingParticipantsPanel`'s job, not this * one's. `undefined` when the SDK did not supply one for that attendee - the participant is still * tracked, just unresolvable beyond the panel's own "Someone" fallback. */ export interface RemoteMeetingParticipant { attendeeId: string; externalUserId?: string; } export interface CallSessionState { /** Every tile on screen, the local self-view included. This is the RENDER list. */ tiles: number[]; /** The tiles belonging to SOMEBODY ELSE - a strict subset of `tiles`. This is a LAYOUT fact and * nothing more. * * IT IS NOT A PRESENCE SIGNAL, and reading it as one was the Task 9 review's first defect. * A patient may join AUDIO-ONLY (this class deliberately degrades rather than refusing a visit * with no camera), in which case no remote video tile is ever created; and a patient may turn * their camera off mid-visit with a button we ship, which empties this list again. Either way * they are IN THE ROOM and talking. Use `remoteAttendees`. */ remoteTiles: number[]; /** - attendee id the numeric tile carrying that attendee's video. The link Chime * hands us on every tile update (`VideoTileState.boundAttendeeId`) and which this class used to * discard, leaving the main stage unable to feature the person who is actually speaking. * An absent key means that attendee has NO video tile - which is the camera-off case, not an error. */ tilesByAttendee: Record; /** EVERYONE ELSE IN THE MEETING, by Chime attendee id - the truthful "is anybody here yet" * signal, and the one "waiting for the patient" is derived from. * * The ring protocol is gone (Spencer, 2026-08-01): a host who starts a visit is `active` and * simply ALONE, so "waiting" is not a phase - it is an EMPTY ROOM in a live meeting. This comes * from `realtimeSubscribeToAttendeeIdPresence`, which fires for every attendee already present * when you join and then on every arrival and departure, with the local attendee EXCLUDED (see * `subscribeToPresence`). * * Task 9: now DERIVED from `remoteParticipants` below (`.map(p => p.attendeeId)`), kept only so * no existing consumer of the plain id list has to change. `remoteParticipants` is the one source * of truth. */ remoteAttendees: string[]; /** Task 9 - see `RemoteMeetingParticipant`. Same roster as `remoteAttendees`, one entry per * attendee, carrying `externalUserId` alongside the attendee id. */ remoteParticipants: RemoteMeetingParticipant[]; muted: boolean; /** Whether the local camera is CURRENTLY published. Never true without `cameraAvailable`: * claiming a camera we do not have inverts the toggle and shows everyone else a black tile. */ cameraOn: boolean; /** Whether a camera was actually acquired for this meeting (present, permitted, not in use by * another app). False audio-only. */ cameraAvailable: boolean; /** Whether a microphone was actually acquired. False this participant can hear but not speak. */ micAvailable: boolean; /** - WHY it is unavailable. Absent when the device works. * Four distinct causes used to render one sentence telling the user to change a browser * permission; two of them sent people to a setting that was not the problem, and on an insecure * origin the browser had already refused to ask, so that setting was greyed out. */ micFailure?: MediaFailureReason; cameraFailure?: MediaFailureReason; /** - WHICH device this visit is currently using, so the picker shows the live choice * rather than guessing at it. Absent until one has been selected. */ micDeviceId?: string; cameraDeviceId?: string; /** THE SILENT-CALL FLAG. Chime mixes EVERY remote participant into one dedicated