zebra-spaces: idempotent attach + drop stale stream cache (kill host double-audio)
Fox 2026-06-05 telemetry on host: "host is hearing two" — peer-joined u=64b0 (fxhp-phone rejoin with new uuid, same pubkey) fired THREE attach calls within 1 second for the SAME uuid: Call 1: pool=1 → fallback <audio> path, "tracks=0" Call 2: pool=1 → AudioContext success Call 3: pool=2 → AudioContext success (after mesh stream swap) Three overlapping audio chains = "two streams" perceived. Root causes: 1. flushSfuStreams used the cached sfuStreamsByPubHex entry, which still pointed at the OLD phone session's stream (uuid=91ab had left 5s earlier). Old stream's audio tracks were ended. attach received a zero-live-track stream and fell through to <audio> fallback. 2. Multiple paths (peer-joined → flushSfuStreams, sub PC ontrack, mid-spotlight supplant) each tried to attach the same uuid in the same tick. No idempotency check, so each call tore down and rebuilt. Two fixes: A. attachAudioStreamViaWorklet: reject streams with zero live audio tracks (skip silently); make same-uuid + same-stream calls a no-op. Repeated calls during a peer-joined burst stop racing. B. handleRemoteSfuTrack: listen for the cached track's 'ended' event and drop the sfuStreamsByPubHex entry. Next flushSfuStreams won't hand a dead stream to attach. Combined: the publisher's rejoin cycle now produces exactly ONE attach chain — the one fed by the live ontrack — and ignores stale cache hits + duplicate attach calls.
This commit is contained in:
parent
2e74b92385
commit
f479878fff
1 changed files with 39 additions and 3 deletions
|
|
@ -3180,6 +3180,20 @@ function applyLipSyncForPub(pubHex){
|
|||
* applied only when role==='listener' — speakers don't need lock-
|
||||
* screen transport controls. */
|
||||
function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){
|
||||
/* Reject streams that have no live audio tracks. flushSfuStreams hands
|
||||
* us whatever sfuStreamsByPubHex cached for the publisher's pubkey —
|
||||
* after a publisher leaves and rejoins with the SAME pubkey, the old
|
||||
* cached stream's tracks are ended even though the cache entry still
|
||||
* exists. Without this guard, attach creates a silent MediaStreamSource
|
||||
* → fallback to <audio> path → double-audio when the real ontrack
|
||||
* arrives moments later. Fox 2026-06-05 telemetry: "host hearing two"
|
||||
* after peer-joined u=64b0 fired three attaches in 1s. */
|
||||
const liveAudio = (stream && stream.getAudioTracks ? stream.getAudioTracks() : [])
|
||||
.filter(t => t.readyState === 'live');
|
||||
if (liveAudio.length === 0){
|
||||
logLine('', 'audio attach skip uuid='+uuid.slice(0,4)+' — no live audio tracks');
|
||||
return false;
|
||||
}
|
||||
if (!audioCtx){
|
||||
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
|
||||
catch(e){ logLine('err','audioCtx create: '+e.message); return false; }
|
||||
|
|
@ -3189,6 +3203,15 @@ 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. */
|
||||
if (existing && existing.stream === stream){
|
||||
logLine('', 'audio attach no-op uuid='+uuid.slice(0,4)+' — same stream already attached');
|
||||
return true;
|
||||
}
|
||||
if (existing){
|
||||
try { existing.src.disconnect(); } catch(_){}
|
||||
try { if (existing.jbuf) existing.jbuf.disconnect(); } catch(_){}
|
||||
|
|
@ -4644,8 +4667,21 @@ function handleRemoteSfuTrack(ev){
|
|||
try { if (ev.receiver) ev.receiver.playoutDelayHint = playoutDelayForRole(myRole); } catch(_){}
|
||||
try { if (ev.receiver) ev.receiver.jitterBufferTarget = playoutDelayForRole(myRole) * 1000; } catch(_){}
|
||||
/* cache by full pubkey (already resolved above) so it survives the
|
||||
* member's session uuid changing across leave/rejoin */
|
||||
* member's session uuid changing across leave/rejoin. When the cached
|
||||
* track ends (publisher leaves), drop the cache entry — flushSfuStreams
|
||||
* was handing the dead stream to attachAudioStreamViaWorklet on the
|
||||
* NEXT peer-joined with matching pubkey, racing the live ontrack.
|
||||
* Fox 2026-06-05 telemetry: peer-joined u=64b0 fired "audio attach
|
||||
* fresh=1 tracks=0" before the real attach. */
|
||||
sfuStreamsByPubHex.set(pubHex, ev.streams[0]);
|
||||
try {
|
||||
ev.track.addEventListener('ended', () => {
|
||||
if (sfuStreamsByPubHex.get(pubHex) === ev.streams[0]){
|
||||
sfuStreamsByPubHex.delete(pubHex);
|
||||
logLine('', 'sfu cache drop pub='+pubHex.slice(0,4)+' — track ended');
|
||||
}
|
||||
}, { once: true });
|
||||
} catch(_){}
|
||||
for (const [uuid, mm] of members){
|
||||
try {
|
||||
if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){
|
||||
|
|
@ -7786,8 +7822,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-06-05</span><br>
|
||||
md5 <span class="stamp-md5">00e6a52b14259a52ff7edda1c7617906</span><br>
|
||||
sha256 <span class="stamp-sha">98d5e1f1fe606f4fe3261ea116278d0a584440bdd91a9a01472b07c07e71d78e</span><br>
|
||||
md5 <span class="stamp-md5">69650ff2e837141b237588617887f238</span><br>
|
||||
sha256 <span class="stamp-sha">a99232ece059b34d8e726ed246ada3b54c5a9c75d1f1a2daadb1f9cdac72950f</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