// Why a microphone or camera could not be used, and therefore what to tell the person.. // // THE DEFECT: four different failures collapsed into ONE sentence - *"Your microphone is blocked - // allow microphone access in your browser, then rejoin."* Measured live 2026-08-14: Spencer set the // Windows default microphone, then the Edge default, then the site permission, over ~15 minutes. None // of them was the problem, and the app never consulted any of them. The real cause was a device another // browser on the same machine was already holding. // // THEN ON 2026-08-15 THE SAME COPY FIRED ON A CAUSE NOBODY HAD CONSIDERED: the page was `http://`, // where a browser does not DENY these devices - it does not offer them at all, and the permission // toggles are greyed out. The product told him to change a permission the browser had already refused // to even ask about. **That is the family in one sentence: telling someone to fix a setting that is // not the problem costs them the visit while they look in the wrong place.** // Forcing https is job. Saying something TRUE while an insecure origin is reachable is // this one's, and they are independent - this must not wait on that redirect. export type MediaFailureReason = 'insecure' | 'denied' | 'busy' | 'absent' | 'unknown'; export type MediaKind = 'microphone' | 'camera'; interface Facts { /** `window.isSecureContext`. Camera and microphone are secure-context-only features. */ secureContext: boolean; /** How many devices of this kind the browser enumerated. */ deviceCount?: number; /** Whatever `getUserMedia`/`startAudioInput` rejected with, if anything. */ error?: unknown; } /** Browser names for "you said no", read from the spec rather than from a message string. */ const DENIED = new Set(['NotAllowedError', 'PermissionDeniedError', 'SecurityError']); /** "The device exists and something else has it." */ const BUSY = new Set(['NotReadableError', 'TrackStartError', 'AbortError']); /** "There is no such device." */ const ABSENT = new Set(['NotFoundError', 'DevicesNotFoundError', 'OverconstrainedError']); export function classifyMediaFailure(facts: Facts): MediaFailureReason | undefined { // FIRST, AND IT HAS TO BE. On an insecure origin every other signal is a CONSEQUENCE rather than // a cause - the enumeration comes back empty AND the request rejects - so checking those first // reports "absent" and sends the user shopping for a microphone they already own. if (!facts.secureContext) return 'insecure'; const name = (facts.error as { name?: string } | undefined)?.name; if (name) { if (DENIED.has(name)) return 'denied'; if (BUSY.has(name)) return 'busy'; if (ABSENT.has(name)) return 'absent'; // Still a failure. It falls to the vaguest HONEST answer rather than to a confident wrong // one - the entire defect this module exists to fix was a confident wrong one. return 'unknown'; } // An empty enumeration is not an error: it means the device does not exist. if (facts.deviceCount === 0) return 'absent'; return undefined; } /** * One table, both devices - the noun is substituted rather than the sentences duplicated, so a * microphone sentence can never be shown for a camera failure. * * EVERY ROW SAYS SOMETHING DIFFERENT AND SOMETHING ACTIONABLE. Two identical rows would reproduce * the original defect exactly, which is why a test asserts they are all distinct. */ export function mediaFailureNotice(reason: MediaFailureReason, kind: MediaKind): string { switch (reason) { // Blames the PAGE, never the person, and deliberately contains none of "allow", "blocked" or // "permission": on an insecure origin the browser decided before the app was consulted, and the // permission control the old copy pointed at is greyed out. case 'insecure': return `This page is not secure, so the ${kind} cannot be used. Open the visit over https and rejoin.`; // done-when 3's control: a genuinely denied permission must STILL say to allow access. This // user's fix really is the browser setting, and offering them a device picker instead wastes // their visit. case 'denied': return `Your ${kind} is blocked — allow ${kind} access in your browser, then rejoin.`; case 'busy': return `Another app is using your ${kind}. Close it, or pick a different ${kind} below.`; case 'absent': return `We could not find a ${kind} on this device — no ${kind} is available for this visit.`; default: return `Your ${kind} could not be started. Try picking a different ${kind} below, or rejoin.`; } }