From 836a036cfb5d3c3f5210211d0c61b3282419622a Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Thu, 4 Jun 2026 14:37:03 -0400 Subject: [PATCH] =?UTF-8?q?zebra-spaces:=20publisher-side=20outbound-rtp?= =?UTF-8?q?=20telemetry=20=E2=80=94=20mic/cam/screen/game?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds aud.send/cam.send/scr.send/game.send lines to the 5s telemetry tick: - pkt sent, bytes sent, framesEncoded (video) - gap = now − lastPacketSentTimestamp (wire stall detector) - rtt + rlost + rjit from remote-inbound-rtp (the SFU's view of us) Until now we only had inbound stats on the listener path. A publisher stall — e.g. X11 compositor blocking Firefox during a window wiggle — was invisible at the wire; we could only infer it from downstream listener loss/silence. Fox 2026-06-04: "wiggling a terminal window causes 1-5s cutout on all mesh devices and listeners (4s buffer should absorb that)." Hypothesis is a publisher-side audio-thread stall; this lets us confirm by watching gap >> 0.02s on aud.send during a wiggle. No new IPC, no new dispatch — same getStats() call shape already in use for inbound, just iterating the publisher PCs (sfuPubPC, sfuCameraPC, sfuScreenPC, sfuGamePC) and picking outbound-rtp + remote-inbound-rtp pairs. --- web/zebra-spaces.html | 45 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 9c2eaac..59112b3 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -3904,6 +3904,47 @@ async function dumpTelemetry(){ }); } catch(e){ parts.push('stats.err=' + e.message); } } + /* publisher-side stats — what we are actually emitting on the wire. + * Catches publisher stalls (X11 compositor blocking the audio thread + * during a window wiggle is a real one). gap = now - lastPacketSent + * — a gap > frame-interval is a wire stall. rtt is the most recent + * round-trip from remote-inbound-rtp (the SFU's view of our sender). */ + const pubPCs = [ + ['mic.send', sfuPubPC], + ['cam.send', sfuCameraPC], + ['scr.send', sfuScreenPC], + ['game.send', sfuGamePC], + ]; + for (const [label, pc] of pubPCs){ + if (!pc || typeof pc.getStats !== 'function') continue; + try { + const stats = await pc.getStats(null); + const nowMs = Date.now(); + let out = null, remoteIn = null; + stats.forEach(r => { + if (r.type === 'outbound-rtp' && (r.kind === 'audio' || r.kind === 'video')){ + if (!out || (r.packetsSent|0) > (out.packetsSent|0)) out = r; + } + if (r.type === 'remote-inbound-rtp') { + if (!remoteIn || (r.packetsLost|0) > (remoteIn.packetsLost|0)) remoteIn = r; + } + }); + if (!out) continue; + const gap = out.lastPacketSentTimestamp + ? ((nowMs - out.lastPacketSentTimestamp) / 1000).toFixed(1) + 's' + : '?'; + let s = label + ' pkt=' + (out.packetsSent|0) + + ' bytes=' + (out.bytesSent|0) + + ' gap=' + gap; + if (out.kind === 'video') s += ' frames=' + (out.framesEncoded|0); + if (remoteIn) { + s += ' rtt=' + ((remoteIn.roundTripTime||0).toFixed(3)) + + ' rlost=' + (remoteIn.packetsLost|0) + + ' rjit=' + ((remoteIn.jitter||0).toFixed(4)); + } + parts.push(s); + } catch(_){} + } /* every