zebra-spaces: dedup duplicate listenerAudioNodes per publisher (kill double audio)

Fox 2026-06-05: "fxhp-android-firefox is playing host audio twice."

Root cause: publisher rejoined with new session uuid before the old
session's peer-left fired. handleRemoteSfuTrack's member iteration
matched both uuids by pubkey and called attachSfuTrack(uuid, stream)
for BOTH. Two listenerAudioNodes entries created (keyed by uuid),
each with its own source → gain → destination chain. Both worklets
were processing the audio; both gains were connected to destination
→ host audio mixed twice.

Fix in attachAudioStreamViaWorklet: after the same-uuid cleanup,
walk listenerAudioNodes and detachListenerStream for ANY other
uuid whose member.pubkey matches the current uuid's pubkey. Same
publisher, different session id, the older entry is stale — kill
its worklet/gain so only one chain plays. Logged so we can see the
dedup firing.

Also: cleanup now also disconnects existing.capture / .captureLpf
(whisper-related nodes) which the previous cleanup missed —
unrelated to the echo but a memory/CPU leak across attach cycles.
This commit is contained in:
Russell Ballestrini 2026-06-05 14:14:23 -04:00
parent c493626b69
commit f9736c27b4
No known key found for this signature in database

View file

@ -2965,9 +2965,35 @@ function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){
if (existing){ if (existing){
try { existing.src.disconnect(); } catch(_){} try { existing.src.disconnect(); } catch(_){}
try { if (existing.jbuf) existing.jbuf.disconnect(); } catch(_){} try { if (existing.jbuf) existing.jbuf.disconnect(); } catch(_){}
try { if (existing.capture) existing.capture.disconnect(); } catch(_){}
try { if (existing.captureLpf) existing.captureLpf.disconnect(); } catch(_){}
try { existing.gain.disconnect(); } catch(_){} try { existing.gain.disconnect(); } catch(_){}
listenerAudioNodes.delete(uuid); listenerAudioNodes.delete(uuid);
} }
/* DEDUP by publisher pubkey: if a different uuid in
* listenerAudioNodes belongs to the SAME publisher (e.g. they
* rejoined with a new session uuid before the old session's
* peer-left fired), detach the older entry. Two worklets+gains
* connected to destination = host audio played twice. Fox
* 2026-06-05: "fxhp-android-firefox is playing host audio
* twice." */
try {
const mm = members.get(uuid);
if (mm && mm.pubkey){
const myPubHex = hex(unb64(mm.pubkey));
for (const [otherUuid] of [...listenerAudioNodes]){
if (otherUuid === uuid) continue;
const otherMm = members.get(otherUuid);
if (!otherMm || !otherMm.pubkey) continue;
try {
if (hex(unb64(otherMm.pubkey)) === myPubHex){
logLine('', 'audio dedup: detaching stale uuid='+otherUuid.slice(0,4)+' for pub='+myPubHex.slice(0,4));
detachListenerStream(otherUuid);
}
} catch(_){}
}
}
} catch(_){}
let src; let src;
try { src = audioCtx.createMediaStreamSource(stream); } try { src = audioCtx.createMediaStreamSource(stream); }
catch(e){ logLine('err','createMediaStreamSource '+uuid.slice(0,4)+': '+e.message); return false; } catch(e){ logLine('err','createMediaStreamSource '+uuid.slice(0,4)+': '+e.message); return false; }
@ -7477,8 +7503,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-06-05</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-06-05</span><br>
md5 <span class="stamp-md5">90ea35ac08110f785509c523db5c720a</span><br> md5 <span class="stamp-md5">f2358b7676357ab41897765c5be62e41</span><br>
sha256 <span class="stamp-sha">73faa630db78a68abfaf5b70e122b7f9c408bf5f69ccb8086025c28540decfc9</span><br> sha256 <span class="stamp-sha">72edcdd634877e58a49eab0a86505fc8c2562e067ef1166d8ea29e8c50ea9661</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>