diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html index 3eb798b..adf4e6b 100644 --- a/zebra-report/zebra-spaces.html +++ b/zebra-report/zebra-spaces.html @@ -3204,15 +3204,32 @@ function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){ logLine('', 'audio attach uuid='+uuid.slice(0,4)+' target='+targetSeconds+'s (current pool='+listenerAudioNodes.size+')'); const existing = listenerAudioNodes.get(uuid); /* 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 - * → 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. */ + * THIS exact stream object, treat as no-op. */ if (existing && existing.stream === stream){ logLine('', 'audio attach no-op uuid='+uuid.slice(0,4)+' — same stream already attached'); 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 (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 { if (existing.jbuf) existing.jbuf.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');