diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html index 937069e..88fc1f1 100644 --- a/zebra-report/zebra-spaces.html +++ b/zebra-report/zebra-spaces.html @@ -1601,21 +1601,37 @@ function flushSfuStreams(){ * subscriber's pc and renegotiates. Browsers don't reliably fire * 'ended' on remote tracks under this path (Chromium half-fires, * Firefox stays silent). They DO fire 'mute' when RTP stops arriving. - * Watch both: a sustained mute for >MUTE_WINDOW ms = the publisher is + * Watch both: a sustained mute for >windowMs = the publisher is * (probably) gone and we remove the tile. If 'unmute' fires within * the window — transient network blip, NACK retransmission gap, brief - * CPU pressure on publisher, mobile network handoff — we cancel the - * removal. + * CPU pressure on publisher, mobile network handoff, OR (for screen + * shares) a long stretch of static content where the encoder simply + * isn't producing RTP — we cancel the removal. + * + * Window depends on the kind of content (caller passes via windowMs): + * - 15s for cameras: face/motion content keeps RTP flowing + * continuously, so a 15s gap is meaningful. Generous enough to + * weather Wi-Fi stalls and mobile network handoffs. + * - 120s for screen / gameshare: STATIC content (a still desktop, a + * paused video, a code editor with no caret motion) doesn't push + * new RTP for long stretches. The encoder genuinely stops emitting + * packets, the receiver's track goes muted, and the 15s window + * would falsely reap a live tile. 120s lets a static screen + * survive comfortably; a real unshare still resolves through the + * 'ended' path within a frame. * * Window history: - * - 3s: too aggressive — transient blips killed live tiles, the - * 'host shares screen, other speaker loses it' regression. - * - 15s: enough to weather Wi-Fi stalls and mobile network handoffs - * without leaving frozen tiles around for ages. A genuine unshare - * still removes the tile within 15s. */ + * - 3s (all kinds): killed live tiles on any transient mute. + * - 15s (all kinds): cameras ok, but static screens vanished after + * 15s of no motion. + * - 15s camera / 120s screen+game (current). */ const VIDEO_REMOVE_MUTE_WINDOW_MS = 15000; -function watchVideoTrackForRemoval(track, removeFn){ +const VIDEO_REMOVE_MUTE_WINDOW_SCREEN_MS = 120000; +function watchVideoTrackForRemoval(track, removeFn, windowMs){ if (!track) return; + if (typeof windowMs !== 'number' || !isFinite(windowMs) || windowMs <= 0){ + windowMs = VIDEO_REMOVE_MUTE_WINDOW_MS; + } let timer = null, removed = false, hasFlowed = false; const remove = () => { if (removed) return; @@ -1641,10 +1657,10 @@ function watchVideoTrackForRemoval(track, removeFn){ timer = setTimeout(() => { timer = null; if (!removed && track.muted){ - logLine('', 'video track muted >'+(VIDEO_REMOVE_MUTE_WINDOW_MS/1000)+'s — removing tile'); + logLine('', 'video track muted >'+(windowMs/1000)+'s — removing tile'); remove(); } - }, VIDEO_REMOVE_MUTE_WINDOW_MS); + }, windowMs); }; track.addEventListener('ended', remove); track.addEventListener('mute', onMute); @@ -2427,14 +2443,14 @@ async function sfuSubscribe(){ const s = ev.streams[0]; screenStreams.set(pubHex, s); renderScreenTile(pubHex, s); - watchVideoTrackForRemoval(ev.track, () => { if (screenStreams.get(pubHex) === s) removeScreenTile(pubHex); }); + 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]; cameraStreams.set(pubHex, s); renderCameraTile(pubHex, s); - watchVideoTrackForRemoval(ev.track, () => { if (cameraStreams.get(pubHex) === s) removeCameraTile(pubHex); }); + watchVideoTrackForRemoval(ev.track, () => { if (cameraStreams.get(pubHex) === s) removeCameraTile(pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_MS); return; } if (kind === 'game'){ @@ -2444,7 +2460,7 @@ async function sfuSubscribe(){ const s = ev.streams[0]; gameStreams.set(pubHex, s); renderVideoTile('gameshare', pubHex, s); - watchVideoTrackForRemoval(ev.track, () => { if (gameStreams.get(pubHex) === s) removeVideoTile('gameshare', pubHex); }); + watchVideoTrackForRemoval(ev.track, () => { if (gameStreams.get(pubHex) === s) removeVideoTile('gameshare', pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_SCREEN_MS); return; } if (kind !== 'mic'){ @@ -3829,8 +3845,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');