diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index eeff56f..b6aa8f4 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/zebra-spaces.html
@@ -2441,20 +2441,29 @@ 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. */
+ /* MSID-supplant safety: the SFU re-uses the same streamID
+ * (`shortPub-kind`) when a publisher supplants themselves. WebRTC
+ * merges the new track into the EXISTING MediaStream — ev.streams[0]
+ * is literally the same instance as before, containing both the
+ * dead old track AND the new live one. Setting srcObject to that
+ * stream doesn't switch the playing track; the video element keeps
+ * showing the (now-ended) old track's last frame and reports muted.
+ * Construct a fresh MediaStream containing only the new track so
+ * the video element binds to the new RTP flow cleanly.
+ *
+ * Stream-identity guard on removeFn: when 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'){
- const s = ev.streams[0];
+ const s = new MediaStream([ev.track]);
screenStreams.set(pubHex, s);
renderScreenTile(pubHex, s);
watchVideoTrackForRemoval(ev.track, () => { if (screenStreams.get(pubHex) === s) removeScreenTile(pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_SCREEN_MS);
return;
}
if (kind === 'camera'){
- const s = ev.streams[0];
+ const s = new MediaStream([ev.track]);
cameraStreams.set(pubHex, s);
renderCameraTile(pubHex, s);
watchVideoTrackForRemoval(ev.track, () => { if (cameraStreams.get(pubHex) === s) removeCameraTile(pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_MS);
@@ -2464,7 +2473,7 @@ async function sfuSubscribe(){
/* 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. */
- const s = ev.streams[0];
+ const s = new MediaStream([ev.track]);
gameStreams.set(pubHex, s);
renderVideoTile('gameshare', pubHex, s);
watchVideoTrackForRemoval(ev.track, () => { if (gameStreams.get(pubHex) === s) removeVideoTile('gameshare', pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_SCREEN_MS);
@@ -3861,8 +3870,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');