diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index 9fd2b84..2bebcb9 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/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