zebra-report: deploy realtime telemetry

This commit is contained in:
russell@unturf.com 2026-06-04 10:38:14 -04:00
parent 5fc18d2423
commit 382d72b687
No known key found for this signature in database

View file

@ -1640,6 +1640,7 @@ const sfuStreamsByPubHex = new Map(); // pubHex -> MediaStream
function attachSfuTrack(uuid, stream){
let a = remoteAudio.get(uuid);
const fresh = !a;
if (!a){
/* lease from the pre-blessed audio pool so Firefox Android's
* per-element autoplay grant carries over from the entry-button
@ -1650,12 +1651,26 @@ function attachSfuTrack(uuid, stream){
a = leaseAudioElement();
remoteAudio.set(uuid, a);
applySinkTo(a);
/* one-shot breadcrumbs so we can see whether the new path lights
* up at all, vs. the silent-but-state=connected pattern. */
a.addEventListener('playing', () => logLine('', 'rtc playing '+uuid.slice(0,4)+' ct='+a.currentTime.toFixed(2)), { once: true });
a.addEventListener('pause', () => logLine('err','rtc paused '+uuid.slice(0,4)+' ct='+a.currentTime.toFixed(2)));
a.addEventListener('ended', () => logLine('err','rtc ended '+uuid.slice(0,4)));
a.addEventListener('stalled', () => logLine('err','rtc stalled '+uuid.slice(0,4)));
a.addEventListener('error', () => logLine('err','rtc error '+uuid.slice(0,4)+' code='+(a.error?a.error.code:'?')));
}
a.srcObject = stream;
const tracks = stream && stream.getAudioTracks ? stream.getAudioTracks() : [];
const t0 = tracks[0];
logLine('', 'sfu attach '+uuid.slice(0,4)+' fresh='+(fresh?1:0)+
' tracks='+tracks.length+
(t0 ? ' tr0={en='+t0.enabled+' mu='+t0.muted+' rs='+t0.readyState+'}' : ''));
try {
const p = a.play();
if (p && p.catch) p.catch(e => logLine('err','rtc autoplay '+uuid+': '+e.message));
} catch(_){}
if (p && p.catch) p.catch(e => logLine('err','rtc autoplay '+uuid.slice(0,4)+': '+e.message));
} catch(e){
logLine('err','rtc play threw '+uuid.slice(0,4)+': '+e.message);
}
stopMeter(uuid); startMeter(uuid, stream);
logLine('', 'sfu: receiving '+((members.get(uuid)||{}).handle || uuid));
}
@ -3355,6 +3370,88 @@ function stopAlivePings(){
clearInterval(aliveTimer); aliveTimer = null;
}
/* Telemetry tick — fired every TELEMETRY_INTERVAL_MS while in a room.
* Dumps PC states + receiver RTP counters + <audio> element state to
* the page log so we can see in real time whether a "silent" listener
* is starved (no RTP arriving), decoded-but-not-playing (RTP fine but
* <audio> paused/muted), or DJ-mode-stalled (streamAudio not loading).
*
* One line per tick per device gives a timeline that we can scroll
* back through after a regression and pinpoint exactly when the chain
* broke. Verbose by design — fox: "way more telemetry NOW".
*
* Numbers we lean on:
* - sub.aud.pkt : packetsReceived on sub PC's audio receiver
* - sub.aud.bytes : bytesReceived on the same
* - sub.aud.jitter : current jitter in seconds
* - sub.aud.level : audioLevel (0..1, 0 = silence)
* - rtc[id]/stream[id] : <audio> element diagnostics
* - ct = currentTime (advancing = audio playing)
* - rs = readyState (4 = enough data)
* - ns = networkState (1=idle 2=loading 3=stalled)
* - pa = paused, mu = muted, vol = volume, err = MediaError code
*/
const TELEMETRY_INTERVAL_MS = 5000;
let telemetryTimer = null;
async function dumpTelemetry(){
const parts = [];
parts.push('role=' + myRole);
parts.push('sub=' + (sfuSubPC ? sfuSubPC.connectionState + '/' + sfuSubPC.iceConnectionState : 'none'));
parts.push('pub=' + (sfuPubPC ? sfuPubPC.connectionState + '/' + sfuPubPC.iceConnectionState : 'none'));
parts.push('mesh=' + peers.size);
parts.push('sListen=' + (selfListenerMode ? '1' : '0'));
parts.push('streamMode=' + streamMode.size);
parts.push('muted=' + (muted ? '1' : '0'));
/* receiver stats from sfuSubPC (the main listener path) */
if (sfuSubPC && typeof sfuSubPC.getStats === 'function'){
try {
const stats = await sfuSubPC.getStats(null);
stats.forEach(r => {
if (r.type === 'inbound-rtp' && r.kind === 'audio'){
parts.push('aud.recv pkt=' + (r.packetsReceived|0) + ' lost=' + (r.packetsLost|0) +
' bytes=' + (r.bytesReceived|0) + ' jitter=' + (r.jitter || 0).toFixed(4) +
' level=' + (r.audioLevel || 0).toFixed(3));
}
if (r.type === 'inbound-rtp' && r.kind === 'video'){
parts.push('vid.recv pkt=' + (r.packetsReceived|0) + ' lost=' + (r.packetsLost|0) +
' frames=' + (r.framesDecoded|0));
}
});
} catch(e){ parts.push('stats.err=' + e.message); }
}
/* every <audio> element in the room — both the WebRTC remoteAudio
* pool (rtc[]) and the DJ HTTP-pull streamAudio (stream[]). */
let i = 0;
for (const [uuid, a] of remoteAudio){
parts.push('rtc[' + uuid.slice(0,4) + '] rs=' + a.readyState + ' ns=' + a.networkState +
' pa=' + (a.paused?1:0) + ' mu=' + (a.muted?1:0) + ' ct=' + a.currentTime.toFixed(2) +
' err=' + (a.error?a.error.code:'_'));
if (++i > 4) { parts.push('rtc…+' + (remoteAudio.size - i) + ' more'); break; }
}
i = 0;
for (const [uuid, a] of streamAudio){
parts.push('stream[' + uuid.slice(0,4) + '] rs=' + a.readyState + ' ns=' + a.networkState +
' pa=' + (a.paused?1:0) + ' mu=' + (a.muted?1:0) + ' ct=' + a.currentTime.toFixed(2) +
' src=' + (a.src ? (a.src.length>30 ? '…'+a.src.slice(-30) : a.src) : 'none') +
' err=' + (a.error?a.error.code:'_'));
if (++i > 4) { parts.push('stream…+' + (streamAudio.size - i) + ' more'); break; }
}
logLine('', '· ' + parts.join(' '));
}
function startTelemetryLoop(){
if (telemetryTimer) return;
telemetryTimer = setInterval(() => {
dumpTelemetry().catch(e => logLine('err','telemetry: '+e.message));
}, TELEMETRY_INTERVAL_MS);
/* fire one immediately so the user sees state without waiting for
* the first interval to elapse. */
dumpTelemetry().catch(()=>{});
}
function stopTelemetryLoop(){
if (!telemetryTimer) return;
clearInterval(telemetryTimer); telemetryTimer = null;
}
function openSignal(){
ws = new WebSocket(SIGNAL_URL + '?room=' + encodeURIComponent(roomID));
ws.onopen = async () => {
@ -3364,10 +3461,12 @@ function openSignal(){
const sig = await signBytes(sigJoin(roomID, nonce, myKeys.pubB64, myHandle));
send({ type:'join', pubkey: myKeys.pubB64, handle: myHandle, nonce, sig });
startAlivePings();
startTelemetryLoop();
};
ws.onclose = () => {
ws = null;
stopAlivePings();
stopTelemetryLoop();
if (wantConnected){
setStatus('rendezvous dropped — reconnecting…');
if (sigReconnect) clearTimeout(sigReconnect);
@ -5051,8 +5150,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-04</span><br>
md5 <span class="stamp-md5">0136a83ef341ade811b60cfc6466d357</span><br>
sha256 <span class="stamp-sha">0ab8c94acd78b1950385cd1abd93aa9bc7f3614dc08b3feddaa6dd9a8b7a746e</span><br>
md5 <span class="stamp-md5">dbb0b8148bf501d93478f23305ede777</span><br>
sha256 <span class="stamp-sha">e75528626e808db2c85749679e01ac45a3f308666d7fe606c370b5672d56201f</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>