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');