zebra-spaces: in-place source swap on re-attach (single chain per uuid, kill overlap)

Fox 2026-06-05: "fxhp-android-firefox is also hearing two, one
perfectly synced, other is not" + "host hearing two."

Diagnosis from signal-server telemetry: every peer-joined cycle
produced 3 "audio attach uuid=..." events for the SAME uuid within
1 second. Each call tore down the existing src+jbuf+gain chain and
built a new one. During the brief window between disconnect() and
the next chain's destination wiring, both old and new gains were
audible — and when the rebuild raced multiple paths
(flushSfuStreams, sub PC ontrack, mesh ontrack, mid-spotlight
supplant), two complete chains stayed live in parallel. The
listener heard them as one synced + one offset.

Root cause is the rebuild itself, not the count: the SFU forwards
stale audio transceivers across publisher rejoins, so every ontrack
delivers a NEW MediaStream object even when the publisher is the
same. Reference-equality "same stream" check from the prior commit
couldn't catch this.

Fix: when a chain already exists for the uuid, REUSE the existing
worklet+gain+destination wiring and swap only the MediaStreamSource
via setWorkletStream — the same in-place swap path mesh ontrack
already uses. Single chain per uuid for its entire lifetime. No
overlap window, no parallel chains, no audible double-audio
regardless of how many ontracks arrive.

The teardown+rebuild branch is kept as a fallback for the
setWorkletStream-fails case (no audioCtx, no jbuf yet, etc.) — the
common path now never tears down.
This commit is contained in:
Russell Ballestrini 2026-06-05 19:46:12 -04:00
parent f479878fff
commit 144dd15a53
No known key found for this signature in database

View file

@ -3204,15 +3204,32 @@ function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){
logLine('', 'audio attach uuid='+uuid.slice(0,4)+' target='+targetSeconds+'s (current pool='+listenerAudioNodes.size+')'); logLine('', 'audio attach uuid='+uuid.slice(0,4)+' target='+targetSeconds+'s (current pool='+listenerAudioNodes.size+')');
const existing = listenerAudioNodes.get(uuid); const existing = listenerAudioNodes.get(uuid);
/* IDEMPOTENT: if we already have a chain for this uuid AND it points at /* IDEMPOTENT: if we already have a chain for this uuid AND it points at
* THIS exact stream object, treat as no-op. Multiple paths (peer-joined * THIS exact stream object, treat as no-op. */
* → flushSfuStreams, sub PC ontrack, mid-spotlight supplant) can each
* race to attach the same stream within the same tick — without this,
* each rebuilds the chain and two of them overlap audibly. */
if (existing && existing.stream === stream){ if (existing && existing.stream === stream){
logLine('', 'audio attach no-op uuid='+uuid.slice(0,4)+' — same stream already attached'); logLine('', 'audio attach no-op uuid='+uuid.slice(0,4)+' — same stream already attached');
return true; return true;
} }
/* IN-PLACE SOURCE SWAP for different stream on same uuid. The SFU
* forwards stale audio transceivers across publisher rejoins — each
* ontrack delivers a NEW MediaStream object for the same publisher.
* Tearing down the chain and rebuilding leaves the old gain briefly
* connected to destination while the new chain is being wired — and
* when multiple paths race (peer-joined → flushSfuStreams, sub PC
* ontrack, mesh ontrack, mid-spotlight supplant), the rebuilt chains
* overlap audibly. fxhp-android-firefox 2026-06-05: "hearing two,
* one perfectly synced other is not." That's exactly two
* destination-connected gains playing the same source out of phase.
*
* Instead reuse the existing worklet+gain+destination chain and swap
* only the MediaStreamSource — the path setWorkletStream already uses
* for mesh ontrack. Single chain per uuid forever. */
if (existing){ if (existing){
if (setWorkletStream(uuid, stream)){
logLine('', 'audio attach in-place swap uuid='+uuid.slice(0,4));
return true;
}
/* setWorkletStream failed (no audioCtx? no jbuf yet?). Fall
* through to the legacy teardown+rebuild as last resort. */
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.capture) existing.capture.disconnect(); } catch(_){}
@ -7822,8 +7839,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">69650ff2e837141b237588617887f238</span><br> md5 <span class="stamp-md5">bebca66d15d021b0f695814f8b0330f3</span><br>
sha256 <span class="stamp-sha">a99232ece059b34d8e726ed246ada3b54c5a9c75d1f1a2daadb1f9cdac72950f</span><br> sha256 <span class="stamp-sha">1489ff6f3224001b234fdffec8c616127b3ccf4812e410e188e892ce2dec1875</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>