zebra-spaces: persistent per-peer meter runner (survives re-renders + wires local mic)
Two real defects in the meter wiring: - the local mic was never fed to its meter — only remote streams were - renderRoom replaces every member's meter DOM element, so the old meterFor tick loop (which captured the OLD fill via closure) exited on the next frame and the bar froze forever Replace with a per-uuid meterCtl Map that holds the analyser + buffer once and re-reads members.get(uuid)._meterEl fresh every animation frame. Cleanup is automatic when the uuid drops from members (peer- left, boot) or explicitly via stopMeter (dropMic, applyMicMode). applyMicMode re-wires the local meter against the freshly acquired stream since the old MediaStreamSource dies with the old track.
This commit is contained in:
parent
cd4a0ff6f5
commit
7e54e1b7ef
1 changed files with 28 additions and 13 deletions
|
|
@ -499,22 +499,37 @@ async function applyMicMode(){
|
|||
}
|
||||
if (micStream) micStream.getTracks().forEach(t=>t.stop());
|
||||
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)();
|
||||
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;
|
||||
src.connect(an);
|
||||
const buf = new Uint8Array(an.fftSize);
|
||||
const fill = typeof elId==='string' ? $(elId) : elId;
|
||||
meterCtl.set(uuid, { an, buf });
|
||||
(function tick(){
|
||||
if (!fill || !document.contains(fill)) return; /* meter removed with member row */
|
||||
an.getByteTimeDomainData(buf);
|
||||
let peak=0; for (let i=0;i<buf.length;i++){ const v=Math.abs(buf[i]-128)/128; if(v>peak)peak=v; }
|
||||
fill.style.width = Math.min(100, Math.round(peak*180))+'%';
|
||||
const c = meterCtl.get(uuid);
|
||||
if (!c) return;
|
||||
if (!members.has(uuid)){ meterCtl.delete(uuid); return; }
|
||||
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);
|
||||
})();
|
||||
}
|
||||
function stopMeter(uuid){ meterCtl.delete(uuid); }
|
||||
|
||||
/* ==================================================================
|
||||
* room state mirror (server is source of truth, we mirror locally for
|
||||
|
|
@ -766,10 +781,12 @@ async function ensureMicAndUI(){
|
|||
try {
|
||||
await getMic(); await refreshMicList();
|
||||
$('btn-mute').disabled = false;
|
||||
if (myUUID) startMeter(myUUID, micStream);
|
||||
} catch(e){ logLine('err','mic blocked: '+e.message); }
|
||||
updateRoleUI();
|
||||
}
|
||||
function dropMic(){
|
||||
if (myUUID) stopMeter(myUUID);
|
||||
if (micStream){ micStream.getTracks().forEach(t=>t.stop()); micStream = null; }
|
||||
$('btn-mute').disabled = true;
|
||||
}
|
||||
|
|
@ -796,10 +813,7 @@ async function connectToPeer(uuid, weOffer){
|
|||
let a = remoteAudio.get(uuid);
|
||||
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]);
|
||||
try {
|
||||
const m = members.get(uuid);
|
||||
if (m && m._meterEl) meterFor(a.srcObject, m._meterEl);
|
||||
} catch(_){}
|
||||
startMeter(uuid, a.srcObject);
|
||||
};
|
||||
pc.onicecandidate = (ev) => { /* using waitForIceGathering pattern, candidates ignored */ };
|
||||
pc.onconnectionstatechange = () => {
|
||||
|
|
@ -818,6 +832,7 @@ async function connectToPeer(uuid, weOffer){
|
|||
}
|
||||
}
|
||||
function tearPeer(uuid){
|
||||
stopMeter(uuid);
|
||||
const pc = peers.get(uuid);
|
||||
if (pc){ try { pc.close(); } catch(_){} peers.delete(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">
|
||||
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> · built <span class="stamp-date">2026-05-31</span><br>
|
||||
md5 <span class="stamp-md5">c5bb52fca1002251e888aa7effa2cf91</span><br>
|
||||
sha256 <span class="stamp-sha">d1082ceeb95d3c616a63d522922e919b60cc7bfef9bb5bc49921008b75064310</span><br>
|
||||
md5 <span class="stamp-md5">cd35adde9997d99b27d4ee03f54863cd</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">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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue