From d9852e661e9f37732b9850446eee492ccd1b36ff Mon Sep 17 00:00:00 2001
From: Russell Ballestrini
Date: Mon, 1 Jun 2026 19:45:15 -0400
Subject: [PATCH] zebra-spaces: local preview tile so publisher sees their own
screen share
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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).
---
web/chat.html | 6 +++---
web/host-your-own.html | 6 +++---
web/how-it-works.html | 6 +++---
web/zebra-audio.html | 6 +++---
web/zebra-spaces.html | 29 +++++++++++++++++++++--------
5 files changed, 33 insertions(+), 20 deletions(-)
diff --git a/web/chat.html b/web/chat.html
index 0354a22..0285ab6 100644
--- a/web/chat.html
+++ b/web/chat.html
@@ -1652,9 +1652,9 @@ logLine('sys', 'chat content lives in encrypted SRTP audio. no IP packets carry
})();
diff --git a/web/host-your-own.html b/web/host-your-own.html
index 50abfa0..13b0cc3 100644
--- a/web/host-your-own.html
+++ b/web/host-your-own.html
@@ -440,9 +440,9 @@ handle /zebra-spaces-sfu* { reverse_proxy localhost:8092 { flush_interval -1
diff --git a/web/how-it-works.html b/web/how-it-works.html
index b9f2a23..aa5d50b 100644
--- a/web/how-it-works.html
+++ b/web/how-it-works.html
@@ -274,9 +274,9 @@
diff --git a/web/zebra-audio.html b/web/zebra-audio.html
index b7f52fb..a4f2c0e 100644
--- a/web/zebra-audio.html
+++ b/web/zebra-audio.html
@@ -659,9 +659,9 @@ else wirePuppet();
diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 4c65260..eb5d5da 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -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');