diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 297925f..3ae6ece 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -5877,6 +5877,41 @@ async function dumpTelemetry(){ }); } catch(e){ parts.push('stats.err=' + e.message); } } + /* mesh receiver stats — one line per peer's mesh PC inbound audio. + * Lets us tell whether the worklet is being fed by a live mesh + * decoder vs an orphaned one. For ticket 0001 we needed to answer: + * when handleRemoteSfuTrack hands off to mesh and the SFU receiver + * reports level=0+jbuf=? (expected — orphaned), is the MESH receiver + * actually decoding (level>0+jbuf>0) or also silent? fox 2026-06-07. + * + * Each peer's PC has one audio inbound-rtp; tag with the short uuid + * so a multi-peer room is readable. Failure on one peer's getStats + * doesn't stop the others. */ + for (const [uuid, pc] of peers){ + if (!pc || typeof pc.getStats !== 'function') continue; + if (pc.connectionState !== 'connected') continue; /* skip non-live PCs */ + try { + const stats = await pc.getStats(null); + const nowMs = Date.now(); + stats.forEach(r => { + if (r.type !== 'inbound-rtp' || r.kind !== 'audio') return; + const lp = r.lastPacketReceivedTimestamp + ? ((nowMs - r.lastPacketReceivedTimestamp) / 1000).toFixed(1) + 's' + : '?'; + const jbuf = (r.jitterBufferEmittedCount > 0) + ? (r.jitterBufferDelay / r.jitterBufferEmittedCount).toFixed(2) + 's' + : '?'; + parts.push('mesh.recv u=' + uuid.slice(0,4) + + ' pkt=' + (r.packetsReceived|0) + + ' lost=' + (r.packetsLost|0) + + ' bytes=' + (r.bytesReceived|0) + + ' jitter=' + (r.jitter || 0).toFixed(4) + + ' level=' + (r.audioLevel || 0).toFixed(3) + + ' jbuf=' + jbuf + + ' lp=' + lp); + }); + } catch(_){} + } /* publisher-side stats — what we are actually emitting on the wire. * One line per kind so screen-audio shows up independently of * screen-video (earlier collapse hid scr.send.aud entirely under @@ -8148,8 +8183,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');