From 2892a8f6efe55508f9b13d28dc447ce638cd4d78 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Mon, 8 Jun 2026 08:02:49 -0400 Subject: [PATCH] zebra-spaces: camera permission probe telemetry (firefox NotAllowedError diagnosis) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ticket 0002 fox 2026-06-08: even with the saved permission removed AND a bare {video:true} retry, Firefox-on-Fedora returns NotAllowedError instantly with no prompt. Webcamtests.com works on the same Firefox session — so the defect is specific to our origin's runtime state, not the browser or hardware. Need to distinguish (a) Permissions API state=denied vs prompt vs granted (b) hidden document vs active (c) user activation inactive vs active. Each picks a different fix branch — (a) saved block or about:config pref, (b) bfcache / non-active document, (c) gesture propagation lost between click and getUserMedia. Add a permission probe in the catch (runs AFTER getUserMedia returned so it doesn't burn transient activation needed for a retry). Logs: camera permission probe: state= vis= activation= state = navigator.permissions.query({name:'camera'}).state vis = document.visibilityState act = navigator.userActivation isActive + hasBeenActive Next click on blanka's firefox surfaces all three at once. --- web/zebra-spaces.html | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 6975f49..c47fa1d 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -4618,6 +4618,30 @@ async function sfuPublishCamera(){ 'camera open cancelled: '+(e.name||'Error')+' — '+e.message+ ' (constraints: 1280x720@30 16:9'+(exact?' deviceId='+String(exact).slice(0,8):'')+')' ); + /* Diagnostic snapshot — runs AFTER getUserMedia returned, so the + * await doesn't break transient activation for any subsequent + * retry. Captures (1) Permissions API state for camera (what + * Firefox itself says about the permission, separate from + * getUserMedia's return), (2) document visibility (Firefox auto- + * denies for hidden documents), (3) user-activation state (gesture + * propagation). Lets us tell apart "saved deny" / "global pref + * block" / "no gesture" / "hidden doc" without another roundtrip. + * Fox ticket 0002. */ + let permState = 'n/a', visState = 'n/a', activation = 'n/a'; + try { + if (navigator.permissions && navigator.permissions.query){ + const p = await navigator.permissions.query({ name: 'camera' }); + permState = p.state; + } + } catch(_){} + try { visState = document.visibilityState || 'n/a'; } catch(_){} + try { + if (navigator.userActivation){ + activation = (navigator.userActivation.isActive ? 'active' : 'inactive') + + '/hasBeenActive=' + (navigator.userActivation.hasBeenActive ? 1 : 0); + } + } catch(_){} + logLine('', 'camera permission probe: state='+permState+' vis='+visState+' activation='+activation); /* OverconstrainedError + a saved deviceId is the canonical "saved * device gone or renamed" case — retry once without deviceId so the * user gets default camera rather than a permanent fail. */ @@ -8161,8 +8185,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');