From 3041a1391adc10dff93ea51e9bf30842918dd94b Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Tue, 2 Jun 2026 15:34:56 -0400 Subject: [PATCH] zebra-spaces: don't let an old camera/screen/game track's removal evict the new tile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On publisher republish (cohost hard refresh → same pubkey + kind → SFU supplants), the OLD track sees mute → ended on the subscriber side and its watchVideoTrackForRemoval closure called removeXTile(pubHex). But by then the NEW track had already populated the same tile entry — so the old watcher tore down the live tile. Net effect: other peers saw the cohost's camera flash on and disappear (the local preview kept rendering because that path is independent of SFU subscribe). Each removeFn now checks the streams store still maps pubHex to the stream it was registered against; if not (the new track replaced it), the old watcher is a no-op. --- web/zebra-spaces.html | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index a916b6b..d98bb51 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -2204,25 +2204,33 @@ async function sfuSubscribe(){ logLine('', 'sfu ontrack: kind=' + kind + ' pub=' + pubHex + ' track=' + ev.track.kind + ' mute=' + ev.track.muted + ' state=' + ev.track.readyState); } + /* Stream-identity guard on removeFn: when a publisher supplants + * themselves (typical: hard refresh — new SFU peer_id same pubkey + + * kind), the OLD track's mute → ended → removeFn would tear down the + * tile that the NEW track just installed. Only remove if the stream + * we registered against is still the one in the store for this pub. */ if (kind === 'screen'){ - screenStreams.set(pubHex, ev.streams[0]); - renderScreenTile(pubHex, ev.streams[0]); - watchVideoTrackForRemoval(ev.track, () => removeScreenTile(pubHex)); + const s = ev.streams[0]; + screenStreams.set(pubHex, s); + renderScreenTile(pubHex, s); + watchVideoTrackForRemoval(ev.track, () => { if (screenStreams.get(pubHex) === s) removeScreenTile(pubHex); }); return; } if (kind === 'camera'){ - cameraStreams.set(pubHex, ev.streams[0]); - renderCameraTile(pubHex, ev.streams[0]); - watchVideoTrackForRemoval(ev.track, () => removeCameraTile(pubHex)); + const s = ev.streams[0]; + cameraStreams.set(pubHex, s); + renderCameraTile(pubHex, s); + watchVideoTrackForRemoval(ev.track, () => { if (cameraStreams.get(pubHex) === s) removeCameraTile(pubHex); }); return; } if (kind === 'game'){ /* a publisher is sharing their gameplay (Region-Capture cropped * iframe). Route to its own TILE_KIND so it coexists with a * normal screen-share from the same person. */ - gameStreams.set(pubHex, ev.streams[0]); - renderVideoTile('gameshare', pubHex, ev.streams[0]); - watchVideoTrackForRemoval(ev.track, () => removeVideoTile('gameshare', pubHex)); + const s = ev.streams[0]; + gameStreams.set(pubHex, s); + renderVideoTile('gameshare', pubHex, s); + watchVideoTrackForRemoval(ev.track, () => { if (gameStreams.get(pubHex) === s) removeVideoTile('gameshare', pubHex); }); return; } if (kind !== 'mic'){ @@ -3595,8 +3603,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');