zebra-spaces: local preview tile so publisher sees their own screen share

The SFU does not echo a publisher's stream back, so anyone sharing alone
in a room had no visual confirmation — game iframe stayed up, no tile
appeared. Render a muted local preview keyed by myKeys.pubHex on publish,
remove on unpublish. Subscribe path skips own pubHex to defend against
SFUs that do echo publishers.

Also refreshes integrity stamps on the other web pages (date drift).
This commit is contained in:
Russell Ballestrini 2026-06-01 19:45:15 -04:00
parent e8ba836b9c
commit d9852e661e
No known key found for this signature in database
5 changed files with 33 additions and 20 deletions

View file

@ -1652,9 +1652,9 @@ logLine('sys', 'chat content lives in encrypted SRTP audio. no IP packets carry
})();
</script>
<footer style="max-width:820px;margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span style="color:#777">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-05-31</span><br>
md5 <span class="stamp-md5">cd40b6b56af82a7e46a937e7f3b7692a</span><br>
sha256 <span class="stamp-sha">b0cd349f8abe8e898a95446ae7147769849916269c06c5e5a14862e596a9448a</span><br>
<span style="color:#777">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-01</span><br>
md5 <span class="stamp-md5">f1d9169e022a422b9b2f1fa166b1da3d</span><br>
sha256 <span class="stamp-sha">e8af1dceb904c649a681670056c1c664c352f0e0102ffdf202d007dbb50e0182</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 it against these hashes; point it 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>

View file

@ -440,9 +440,9 @@ handle /zebra-spaces-sfu* { reverse_proxy localhost:8092 { flush_interval -1
</p>
<footer style="max-width:820px;margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span style="color:#777">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-05-31</span><br>
md5 <span class="stamp-md5">b112cc063dcc4337a086775c97cdfc59</span><br>
sha256 <span class="stamp-sha">6c4d87467ef58fe408d5a8cf55a34c4d51f13ad3f8f21178977177d6ca2560f5</span><br>
<span style="color:#777">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-01</span><br>
md5 <span class="stamp-md5">2d8b20ca6a7d4caceac7402f434d6bb0</span><br>
sha256 <span class="stamp-sha">70abdd694c78aa485d01d76fdd5e577dadce901c5aef7876ce74dadc597ece11</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 it against these hashes; point it at your own servers with ?signal= and ?turncred=</span>
</footer>

View file

@ -274,9 +274,9 @@
</p>
<footer style="max-width:820px;margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span style="color:#777">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-05-31</span><br>
md5 <span class="stamp-md5">1d9238a8fffdd3a5b8505dc92b8ad25d</span><br>
sha256 <span class="stamp-sha">f34c07111239b7c9b3901ea3af342fdc4df1f43d580bb5b2fa81cdcd19903bd4</span><br>
<span style="color:#777">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-01</span><br>
md5 <span class="stamp-md5">d7b8cf7bb3d0de305227d877211e1c68</span><br>
sha256 <span class="stamp-sha">a173bbc0c29dfd8bd11c94f66e561ba31b9c1fc7534ce2032659cd0a5e43d147</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 it against these hashes; point it 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>

View file

@ -659,9 +659,9 @@ else wirePuppet();
</section>
<footer style="max-width:560px;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-05-31</span><br>
md5 <span class="stamp-md5">533949e030e36803366e2e20b85b4e00</span><br>
sha256 <span class="stamp-sha">eb60ddbf8848f2f7f36248dc77810f862396483db111ce639da8663e3b223387</span><br>
<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-01</span><br>
md5 <span class="stamp-md5">913f10bc546c13c1216bf7605a1b5afe</span><br>
sha256 <span class="stamp-sha">90b1c453c194824f79b77a72d5d52343baa79053894da6d2cf094c40ee4e9d48</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 it against these hashes; point it 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>

View file

@ -608,17 +608,24 @@ function flushSfuStreams(){
/* render one screen-share tile (or update its srcObject if it already
* exists). Removed on peer-left / boot / SFU track removal via
* removeScreenTile. */
function renderScreenTile(pubHex, stream){
/* find the matching member for the title */
function renderScreenTile(pubHex, stream, opts){
const local = !!(opts && opts.local);
/* find the matching member for the title; self isn't in members map */
let label = shortHex(pubHex);
for (const [, mm] of members){
try { if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){ label = mm.handle || label; break; } } catch(_){}
if (local){
label = (myHandle || label) + ' (you)';
} else {
for (const [, mm] of members){
try { if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){ label = mm.handle || label; break; } } catch(_){}
}
}
let entry = screenVideos.get(pubHex);
if (!entry){
const tile = document.createElement('div'); tile.className = 'screen-tile';
const video = document.createElement('video');
video.autoplay = true; video.playsInline = true; video.muted = false;
/* local preview must be muted — playing back our own captured audio
* would feed back through the mic */
video.autoplay = true; video.playsInline = true; video.muted = local;
const meta = document.createElement('div'); meta.className = 'screen-meta';
const who = document.createElement('span'); who.textContent = 'screen: '+label;
const pop = document.createElement('button'); pop.className = 'small';
@ -632,7 +639,7 @@ function renderScreenTile(pubHex, stream){
entry.video.srcObject = stream;
try { const p = entry.video.play(); if (p && p.catch) p.catch(()=>{}); } catch(_){}
$('sec-screens').classList.remove('hidden');
logLine('', 'screen share: receiving '+label);
logLine('', 'screen share: '+(local?'local preview ':'receiving ')+label);
}
function removeScreenTile(pubHex){
const entry = screenVideos.get(pubHex);
@ -694,12 +701,16 @@ async function sfuPublishScreen(){
await pc.setRemoteDescription({ type:'answer', sdp: ans.sdp });
sfuScreenPC = pc; sfuScreenPeerID = ans.peer_id;
logLine('', 'sfu: sharing screen as '+shortHex(sfuScreenPeerID));
/* render a muted local preview so the publisher sees what they're
* sharing — SFU does not echo the publisher's own stream back */
renderScreenTile(myKeys.pubHex, stream, { local: true });
$('btn-screen-share').classList.add('hidden');
$('btn-screen-stop').classList.remove('hidden');
}
async function sfuUnpublishScreen(){
if (!sfuScreenPC && !sfuScreenStream) return;
const pid = sfuScreenPeerID;
if (myKeys) removeScreenTile(myKeys.pubHex);
if (sfuScreenStream){ sfuScreenStream.getTracks().forEach(t=>t.stop()); sfuScreenStream = null; }
if (sfuScreenPC){ try { sfuScreenPC.close(); } catch(_){} sfuScreenPC = null; sfuScreenPeerID = null; }
if (pid && roomID){
@ -730,6 +741,8 @@ async function sfuSubscribe(){
* Route screens to the video tile renderer; mics to the audio path. */
if (sid.endsWith(':screen')){
const pubHex = sid.slice(0, -':screen'.length);
/* skip echo of our own screen — we already render a local preview */
if (myKeys && pubHex === myKeys.pubHex) return;
screenStreams.set(pubHex, ev.streams[0]);
renderScreenTile(pubHex, ev.streams[0]);
return;
@ -1569,8 +1582,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-01</span><br>
md5 <span class="stamp-md5">008a832a8d63dfcdc90c233c788d06ed</span><br>
sha256 <span class="stamp-sha">da27ddaf0020e72ece6bdb1c15c32bcdfc63de5b286e09c1602bffdb4109d137</span><br>
md5 <span class="stamp-md5">3b4349329d1441a1ded19fb38dee687b</span><br>
sha256 <span class="stamp-sha">0e6bf4dceb9230fb54fa401f532b65a8c624dcd6a04adb73eab5455e8fdeafa2</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>