diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index f805fcd..6975f49 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/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');