zebra-report: deploy — 120s window for screen/game, 15s stays for camera

This commit is contained in:
russell@unturf.com 2026-06-03 10:33:30 -04:00
parent 27a5319d81
commit 80a38e14c1
No known key found for this signature in database

View file

@ -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');
<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-03</span><br>
md5 <span class="stamp-md5">202400eb71552609f46039301779f8ed</span><br>
sha256 <span class="stamp-sha">684fb8eb60e8a4a30f7a9781f51a3dba088401b874262a84f735f332269f5758</span><br>
md5 <span class="stamp-md5">e7d4557587a42469e48e836866e4cc56</span><br>
sha256 <span class="stamp-sha">027a8b232ffdcb7333dc18a1bf8f14ae68b582e3c7e9d9824288821ab80886dc</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>