From 39b74ef622622cac671d25cba56d21819f535907 Mon Sep 17 00:00:00 2001
From: "russell@unturf.com"
Date: Mon, 1 Jun 2026 19:45:30 -0400
Subject: [PATCH] zebra-report: ship local preview tile for screen-share
publisher
Mirror zebra-spaces fix + refreshed page-integrity stamps from
git.unturf.com/engineering/unturf/zebra-report d9852e6.
---
zebra-report/host-your-own.html | 6 +++---
zebra-report/how-it-works.html | 6 +++---
zebra-report/index.html | 6 +++---
zebra-report/zebra-audio.html | 6 +++---
zebra-report/zebra-spaces.html | 29 +++++++++++++++++++++--------
5 files changed, 33 insertions(+), 20 deletions(-)
diff --git a/zebra-report/host-your-own.html b/zebra-report/host-your-own.html
index 50abfa0..13b0cc3 100644
--- a/zebra-report/host-your-own.html
+++ b/zebra-report/host-your-own.html
@@ -440,9 +440,9 @@ handle /zebra-spaces-sfu* { reverse_proxy localhost:8092 { flush_interval -1
diff --git a/zebra-report/how-it-works.html b/zebra-report/how-it-works.html
index b9f2a23..aa5d50b 100644
--- a/zebra-report/how-it-works.html
+++ b/zebra-report/how-it-works.html
@@ -274,9 +274,9 @@
diff --git a/zebra-report/index.html b/zebra-report/index.html
index 0354a22..0285ab6 100644
--- a/zebra-report/index.html
+++ b/zebra-report/index.html
@@ -1652,9 +1652,9 @@ logLine('sys', 'chat content lives in encrypted SRTP audio. no IP packets carry
})();
diff --git a/zebra-report/zebra-audio.html b/zebra-report/zebra-audio.html
index b7f52fb..a4f2c0e 100644
--- a/zebra-report/zebra-audio.html
+++ b/zebra-report/zebra-audio.html
@@ -659,9 +659,9 @@ else wirePuppet();
diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index 4c65260..eb5d5da 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/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');