diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 090f197..f805fcd 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -4633,6 +4633,26 @@ async function sfuPublishCamera(){
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'){
+ 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');
+ }
fsm.send('FAILED', { error: e.message }); fsm.send('DONE'); return;
}
}
@@ -8125,8 +8145,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');