zebra-spaces: drop screen-resume banner; richer picker hints on share

Reverts 35908cf's resume banner. The browser security model makes
getDisplayMedia ALWAYS show the picker — selection can't be remembered
across reload — so a resume banner adds an extra click without saving
the user from anything. Single 'share screen' button stays the canonical
path.

Add picker hints on getDisplayMedia that Chrome honors (Firefox / Safari
silently ignore — no behavior change):
  surfaceSwitching: 'include'   — show the in-stream switcher widget
  selfBrowserSurface: 'include' — let the user pick this tab if useful
  systemAudio: 'include'        — surface the tab-audio toggle by default

Net result: same gesture flow as before the resume experiment, slightly
friendlier picker on Chrome, no extra UI.
This commit is contained in:
Russell Ballestrini 2026-06-03 12:29:49 -04:00
parent 35908cf898
commit b4e0c3d2d6
No known key found for this signature in database

View file

@ -731,12 +731,6 @@ try {
<select id="camera-select" title="camera device — applies on next start"><option value="">default camera</option></select>
</div>
</section>
<section id="sec-screen-resume" class="hidden">
<div class="notice-banner warn">
<span>you were sharing your screen before this refresh — tap to resume (browsers need a fresh click to grant screen capture)</span>
<button id="btn-screen-resume" class="small invert">resume screen share</button>
</div>
</section>
<section id="sec-listener-actions" class="hidden">
<h2>your hand</h2>
@ -1292,14 +1286,13 @@ 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';
/* screen-share state is deliberately NOT tracked across reload. Even
* with a click banner the user has to re-pick the surface (browser
* security: getDisplayMedia always shows the picker; selection cannot
* be remembered across a reload). A re-pick is the same friction as
* just clicking 'share screen' again, so we keep the single canonical
* button instead of adding a banner that adds zero net clicks. */
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
@ -2127,7 +2120,13 @@ async function sfuPublishScreen(){
stream = await navigator.mediaDevices.getDisplayMedia({
video: { width:{ideal:1920}, height:{ideal:1080}, frameRate:{ideal:30} },
audio: { echoCancellation:false, noiseSuppression:false, autoGainControl:false,
channelCount:2, sampleRate:48000 }
channelCount:2, sampleRate:48000 },
/* picker UX hints. Chrome respects these — biases toward the
* previously-shared surface family and shows tab audio toggle by
* default. Firefox / Safari currently ignore them. */
surfaceSwitching: 'include',
selfBrowserSurface: 'include',
systemAudio: 'include',
});
} catch(e){ logLine('err','screen share cancelled: '+e.message); return; }
sfuScreenStream = stream;
@ -2188,8 +2187,6 @@ 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;
@ -2202,8 +2199,6 @@ 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');
}
@ -3011,20 +3006,8 @@ 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();
@ -3514,17 +3497,6 @@ 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
@ -3943,8 +3915,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-03</span><br>
md5 <span class="stamp-md5">2439f48e2bbd34e36b34209e29d631df</span><br>
sha256 <span class="stamp-sha">afb06260b27c0e1d2a0b76b228c3b7af5138b826fd6b123100d1ccd4d763543b</span><br>
md5 <span class="stamp-md5">66a614bab55cb7b90c288e575177dc4a</span><br>
sha256 <span class="stamp-sha">902f6be9372bc22eaa90e50e140efef9222c80ad30c92d093619c6d59a8c20a5</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>