zebra-spaces: don't let an old camera/screen/game track's removal evict the new tile

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.
This commit is contained in:
Russell Ballestrini 2026-06-02 15:34:56 -04:00
parent 438e886627
commit 3041a1391a
No known key found for this signature in database

View file

@ -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');
<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-02</span><br>
md5 <span class="stamp-md5">d4513db08ec37ff2a55a1cf91cf300da</span><br>
sha256 <span class="stamp-sha">da7da34bdf1b955af557df99a2bfd701ea2ab734c00dee9776f250fccddfcf4d</span><br>
md5 <span class="stamp-md5">9874964f15f679f199a10f6b652d6ca4</span><br>
sha256 <span class="stamp-sha">322d72414d2e6cb89de6e8ee961f312fffdea9e539d77650efa6d9ec1844be77</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>