From a9331962657725cc827948f9e8039d16f39582d0 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Sun, 7 Jun 2026 21:14:53 -0400 Subject: [PATCH] zebra-spaces: bare {video:true} retry on NotAllowedError (firefox constraint misclassification) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ticket 0002 telemetry 2026-06-08 00:50:52: fox removed the saved camera permission for the origin in Firefox/Fedora, clicked share- camera, got NotAllowedError instantly with NO prompt shown. With no saved deny, Firefox MUST prompt on a fresh request from a user gesture — its silent rejection signals the failure isn't permission but something else returning NotAllowedError, likely the constraints object misclassified on Firefox+Wayland+v4l2. Retry once with bare {video:true} after a NotAllowedError. If that works, the elaborate constraints (width/height/aspectRatio/frameRate ideals) were the culprit and we record the recovery. If THAT also NotAllowedErrors, surface the user-recovery notice (real permission denial). Self-disambiguating: the next signal-log line after a click tells us which case fired without further telemetry roundtrips. --- web/zebra-spaces.html | 52 ++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index f805fcd..6975f49 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -4632,23 +4632,39 @@ async function sfuPublishCamera(){ logLine('err','camera open retry failed: '+(e2.name||'Error')+' — '+e2.message); fsm.send('FAILED', { error: e2.message }); fsm.send('DONE'); return; } + } else if (e.name === 'NotAllowedError'){ + /* Firefox on Fedora/Wayland: NotAllowedError fires WITHOUT + * showing a permission prompt even when site permission is + * unset/removed (fox 2026-06-08 ticket 0002). That signature + * is firefox-canonical for "no prompt fired" — and one known + * cause is the constraints object: certain hardware + driver + * combos make Firefox reject {width/height/aspectRatio/frameRate} + * ideals as if they were a permission denial. + * + * Retry once with bare {video:true}. If THAT works, the + * elaborate constraints were the culprit. If THAT also fires + * NotAllowedError, it's a real permission denial — surface + * the recovery notice. */ + logLine('', 'camera open retry: NotAllowedError → bare {video:true}'); + try { + stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); + logLine('', 'camera open recovered with bare {video:true} — firefox constraint misclassification'); + } catch(e2){ + logLine('err','camera open retry failed: '+(e2.name||'Error')+' — '+e2.message); + if (e2.name === 'NotAllowedError'){ + const ua = navigator.userAgent || ''; + const ff = /Firefox\//.test(ua); + showNotice( + ff + ? 'Camera blocked by your browser. Click the lock icon in the URL bar → "Connection secure" → "More information" → Permissions, then change "Use the camera" to Allow. Reload and try again.' + : 'Camera blocked. Click the lock or camera icon in the URL bar and allow camera access for this site, then try again.', + 'warn' + ); + } + fsm.send('FAILED', { error: e2.message }); fsm.send('DONE'); return; + } } else { - /* User-facing notice on the common "no prompt, instant fail" case. - * NotAllowedError fired without a prompt = the browser has a saved - * Block for this origin's camera permission. Tell the user how to - * unstick it — Firefox doesn't surface this in the URL bar after - * the block is saved; only by visiting site permissions does the - * user discover what's wrong. Ticket 0002. */ - if (e.name === 'NotAllowedError'){ - const ua = navigator.userAgent || ''; - const ff = /Firefox\//.test(ua); - showNotice( - ff - ? 'Camera blocked by your browser. Click the lock icon in the URL bar → "Connection secure" → "More information" → Permissions, then change "Use the camera" to Allow. Reload and try again.' - : 'Camera blocked. Click the lock or camera icon in the URL bar and allow camera access for this site, then try again.', - 'warn' - ); - } else if (e.name === 'NotFoundError'){ + if (e.name === 'NotFoundError'){ showNotice('No camera detected. Connect a camera and try again.', 'warn'); } else if (e.name === 'NotReadableError'){ showNotice('Camera is in use by another application. Close the other app and try again.', 'warn'); @@ -8145,8 +8161,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');