From e3f91b1e55bb0fc4eb63f7c6e511f2c74035e888 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Thu, 4 Jun 2026 14:53:39 -0400 Subject: [PATCH] zebra-spaces: split outbound-rtp telemetry per kind (.aud + .vid) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous loop picked the max-packets outbound-rtp per PC, which always collapsed onto the video track on screen/camera PCs — hiding what screen-audio was doing during the wiggle test entirely. Now iterates ALL outbound-rtp entries on every publisher PC and emits one line per kind: scr.send.aud, scr.send.vid, cam.send.aud (when the camera mic is published), cam.send.vid, mic.send.aud, game.send.*. remote-inbound-rtp is paired back by ssrc. Lets us answer the key diagnostic question: when Firefox's getDisplayMedia video capture stalls under X11 wiggle, does the audio track from the same MediaStream stall in lockstep (Firefox couples audio + video producers internally) or stay flowing (decoupled)? If decoupled, an app-layer fix (route audio + video to separate RTCPeerConnections) would work. If coupled, the fix has to be either OS-level (PulseAudio loopback to mic) or upstream in Firefox. --- web/zebra-spaces.html | 55 +++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 9fd2b84..2bebcb9 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -3921,10 +3921,13 @@ 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). */ + * One line per kind so screen-audio shows up independently of + * screen-video (earlier collapse hid scr.send.aud entirely under + * scr.send.vid). Critical for diagnosing whether Firefox + * getDisplayMedia couples its audio + video source producers under + * X11 contention. 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], @@ -3936,29 +3939,35 @@ async function dumpTelemetry(){ try { const stats = await pc.getStats(null); const nowMs = Date.now(); - let out = null, remoteIn = null; + /* keyed by ssrc so audio + video each get a slot; remote-inbound + * pairs back via .ssrc on remote-inbound-rtp → ssrc on outbound. */ + const outsBySSRC = new Map(); + const remoteInsBySSRC = new Map(); stats.forEach(r => { if (r.type === 'outbound-rtp' && (r.kind === 'audio' || r.kind === 'video')){ - if (!out || (r.packetsSent|0) > (out.packetsSent|0)) out = r; + outsBySSRC.set(r.ssrc, r); } - if (r.type === 'remote-inbound-rtp') { - if (!remoteIn || (r.packetsLost|0) > (remoteIn.packetsLost|0)) remoteIn = r; + if (r.type === 'remote-inbound-rtp' && typeof r.ssrc !== 'undefined') { + remoteInsBySSRC.set(r.ssrc, 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)); + for (const out of outsBySSRC.values()){ + const gap = out.lastPacketSentTimestamp + ? ((nowMs - out.lastPacketSentTimestamp) / 1000).toFixed(1) + 's' + : '?'; + const kindLabel = out.kind === 'audio' ? '.aud' : '.vid'; + let s = label + kindLabel + ' pkt=' + (out.packetsSent|0) + + ' bytes=' + (out.bytesSent|0) + + ' gap=' + gap; + if (out.kind === 'video') s += ' frames=' + (out.framesEncoded|0); + const remoteIn = remoteInsBySSRC.get(out.ssrc); + if (remoteIn) { + s += ' rtt=' + ((remoteIn.roundTripTime||0).toFixed(3)) + + ' rlost=' + (remoteIn.packetsLost|0) + + ' rjit=' + ((remoteIn.jitter||0).toFixed(4)); + } + parts.push(s); } - parts.push(s); } catch(_){} } /* every