zebra-report: zebra-spaces meter survives re-renders, local mic also metered

This commit is contained in:
russell@unturf.com 2026-05-31 12:10:59 -04:00
parent a5a947861d
commit b9938113ad
No known key found for this signature in database

View file

@ -499,22 +499,37 @@ async function applyMicMode(){
} }
if (micStream) micStream.getTracks().forEach(t=>t.stop()); if (micStream) micStream.getTracks().forEach(t=>t.stop());
micStream = ns; micStream = ns;
/* old analyser is now dead — rewire local meter against the fresh stream */
if (myUUID){ stopMeter(myUUID); startMeter(myUUID, micStream); }
} }
function meterFor(stream, elId){ /* per-peer meter: one analyser node + one rAF loop, keyed by uuid. The tick
* reads members.get(uuid)._meterEl fresh each frame so renderRoom can replace
* the DOM element without killing the meter. Cleanup happens when the uuid
* leaves the room (members loses the key) or stopMeter is called. */
const meterCtl = new Map(); /* uuid -> {an, buf} */
function startMeter(uuid, stream){
if (!stream || meterCtl.has(uuid)) return;
if (!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)(); if (!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)();
const src = audioCtx.createMediaStreamSource(stream); let src;
try { src = audioCtx.createMediaStreamSource(stream); }
catch(e){ logLine('err','meter for '+shortHex(uuid)+': '+e.message); return; }
const an = audioCtx.createAnalyser(); an.fftSize = 512; const an = audioCtx.createAnalyser(); an.fftSize = 512;
src.connect(an); src.connect(an);
const buf = new Uint8Array(an.fftSize); const buf = new Uint8Array(an.fftSize);
const fill = typeof elId==='string' ? $(elId) : elId; meterCtl.set(uuid, { an, buf });
(function tick(){ (function tick(){
if (!fill || !document.contains(fill)) return; /* meter removed with member row */ const c = meterCtl.get(uuid);
an.getByteTimeDomainData(buf); if (!c) return;
let peak=0; for (let i=0;i<buf.length;i++){ const v=Math.abs(buf[i]-128)/128; if(v>peak)peak=v; } if (!members.has(uuid)){ meterCtl.delete(uuid); return; }
fill.style.width = Math.min(100, Math.round(peak*180))+'%'; c.an.getByteTimeDomainData(c.buf);
let peak=0;
for (let i=0;i<c.buf.length;i++){ const v=Math.abs(c.buf[i]-128)/128; if(v>peak)peak=v; }
const m = members.get(uuid);
if (m && m._meterEl) m._meterEl.style.width = Math.min(100, Math.round(peak*180))+'%';
requestAnimationFrame(tick); requestAnimationFrame(tick);
})(); })();
} }
function stopMeter(uuid){ meterCtl.delete(uuid); }
/* ================================================================== /* ==================================================================
* room state mirror (server is source of truth, we mirror locally for * room state mirror (server is source of truth, we mirror locally for
@ -766,10 +781,12 @@ async function ensureMicAndUI(){
try { try {
await getMic(); await refreshMicList(); await getMic(); await refreshMicList();
$('btn-mute').disabled = false; $('btn-mute').disabled = false;
if (myUUID) startMeter(myUUID, micStream);
} catch(e){ logLine('err','mic blocked: '+e.message); } } catch(e){ logLine('err','mic blocked: '+e.message); }
updateRoleUI(); updateRoleUI();
} }
function dropMic(){ function dropMic(){
if (myUUID) stopMeter(myUUID);
if (micStream){ micStream.getTracks().forEach(t=>t.stop()); micStream = null; } if (micStream){ micStream.getTracks().forEach(t=>t.stop()); micStream = null; }
$('btn-mute').disabled = true; $('btn-mute').disabled = true;
} }
@ -796,10 +813,7 @@ async function connectToPeer(uuid, weOffer){
let a = remoteAudio.get(uuid); let a = remoteAudio.get(uuid);
if (!a){ a = document.createElement('audio'); a.autoplay = true; document.body.appendChild(a); remoteAudio.set(uuid, a); } if (!a){ a = document.createElement('audio'); a.autoplay = true; document.body.appendChild(a); remoteAudio.set(uuid, a); }
a.srcObject = ev.streams[0] || new MediaStream([ev.track]); a.srcObject = ev.streams[0] || new MediaStream([ev.track]);
try { startMeter(uuid, a.srcObject);
const m = members.get(uuid);
if (m && m._meterEl) meterFor(a.srcObject, m._meterEl);
} catch(_){}
}; };
pc.onicecandidate = (ev) => { /* using waitForIceGathering pattern, candidates ignored */ }; pc.onicecandidate = (ev) => { /* using waitForIceGathering pattern, candidates ignored */ };
pc.onconnectionstatechange = () => { pc.onconnectionstatechange = () => {
@ -818,6 +832,7 @@ async function connectToPeer(uuid, weOffer){
} }
} }
function tearPeer(uuid){ function tearPeer(uuid){
stopMeter(uuid);
const pc = peers.get(uuid); const pc = peers.get(uuid);
if (pc){ try { pc.close(); } catch(_){} peers.delete(uuid); } if (pc){ try { pc.close(); } catch(_){} peers.delete(uuid); }
const a = remoteAudio.get(uuid); const a = remoteAudio.get(uuid);
@ -1021,8 +1036,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"> <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-05-31</span><br> <span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-05-31</span><br>
md5 <span class="stamp-md5">c5bb52fca1002251e888aa7effa2cf91</span><br> md5 <span class="stamp-md5">cd35adde9997d99b27d4ee03f54863cd</span><br>
sha256 <span class="stamp-sha">d1082ceeb95d3c616a63d522922e919b60cc7bfef9bb5bc49921008b75064310</span><br> sha256 <span class="stamp-sha">818aefd367f7031750589fa3c8e870440f6dcd0df2f91793e118060169364767</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">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> <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> </footer>