+ you were sharing your screen before this refresh — tap to resume (browsers need a fresh click to grant screen capture)
+
+
+
your hand
@@ -1286,11 +1292,14 @@ const THEME_KEY = 'zebra-theme-v1';
* hard refresh auto-rejoins. Different tabs can sit in different
* spaces because sessionStorage doesn't bleed across tabs. */
const ACTIVE_CALL_KEY = 'zebra-spaces-active-call-v1';
-/* camera resume only — screen-share recovery isn't possible silently
- * (getDisplayMedia needs a fresh gesture) and offering a separate
- * 'resume screen' button is the same friction as clicking 'share
- * screen' again, so we just don't track screen state across reload. */
const ACTIVE_CAM_KEY = 'zebra-spaces-active-cam-v1';
+/* screen-share resume — getDisplayMedia REQUIRES a fresh user gesture
+ * (Chrome / Firefox block silent triggers after a hard refresh), so
+ * unlike the camera we can't just re-acquire silently on welcome.
+ * Instead we surface a one-click resume banner if the user was sharing
+ * before the refresh — the click IS the gesture and counts toward the
+ * getDisplayMedia call inside the same task. */
+const ACTIVE_SCREEN_KEY = 'zebra-spaces-active-screen-v1';
/* mute state for the current tab — '1' means muted. Survives a hard
* refresh via sessionStorage so the page reconnects in the same state
* the user last set. Cleared on explicit leave. NOT cleared on role
@@ -2179,6 +2188,8 @@ async function sfuPublishScreen(){
renderScreenTile(myKeys.pubHex, stream, { local: true });
$('btn-screen-share').classList.add('hidden');
$('btn-screen-stop').classList.remove('hidden');
+ try { sessionStorage.setItem(ACTIVE_SCREEN_KEY, '1'); } catch(_){}
+ $('sec-screen-resume').classList.add('hidden');
}
async function sfuUnpublishScreen(){
if (!sfuScreenPC && !sfuScreenStream) return;
@@ -2191,6 +2202,8 @@ async function sfuUnpublishScreen(){
}
$('btn-screen-share').classList.remove('hidden');
$('btn-screen-stop').classList.add('hidden');
+ try { sessionStorage.removeItem(ACTIVE_SCREEN_KEY); } catch(_){}
+ $('sec-screen-resume').classList.add('hidden');
logLine('', 'screen share stopped');
}
@@ -2998,8 +3011,20 @@ async function handleSignal(raw){
});
}, 250);
}
+ /* screen — getDisplayMedia requires a fresh user gesture per
+ * browser security policy, so we surface a one-click resume
+ * banner instead. The click IS the gesture. */
+ let wantScreen = false;
+ try { wantScreen = sessionStorage.getItem(ACTIVE_SCREEN_KEY) === '1'; } catch(_){}
+ if (wantScreen){
+ $('sec-screen-resume').classList.remove('hidden');
+ } else {
+ $('sec-screen-resume').classList.add('hidden');
+ }
} else {
try { sessionStorage.removeItem(ACTIVE_CAM_KEY); } catch(_){}
+ try { sessionStorage.removeItem(ACTIVE_SCREEN_KEY); } catch(_){}
+ $('sec-screen-resume').classList.add('hidden');
}
renderShareUrl();
onRoleEntered();
@@ -3489,6 +3514,17 @@ function renderRoom(){
$('btn-raise').addEventListener('click', () => { send({ type:'raise-hand' }); });
$('btn-lower').addEventListener('click', () => { send({ type:'lower-hand' }); });
$('btn-screen-share').addEventListener('click', () => { logLine('', 'click: btn-screen-share'); sfuPublishScreen().catch(e => logLine('err','screen share: '+e.message)); });
+$('btn-screen-resume').addEventListener('click', () => {
+ /* The user gesture from this click is what unlocks getDisplayMedia
+ * after a refresh — chained directly into sfuPublishScreen so the
+ * browser counts it toward the same task. */
+ logLine('', 'click: btn-screen-resume');
+ sfuPublishScreen().catch(e => {
+ logLine('err', 'screen resume: ' + e.message);
+ try { sessionStorage.removeItem(ACTIVE_SCREEN_KEY); } catch(_){}
+ $('sec-screen-resume').classList.add('hidden');
+ });
+});
$('btn-screen-stop').addEventListener('click', () => {
/* click telemetry — the 'screen share stopped' line lower in
* sfuUnpublishScreen only fires AFTER the early-return, so logging
@@ -3907,8 +3943,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');