diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index adf4e6b..a8f7798 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -3392,6 +3392,20 @@ function attachSfuTrack(uuid, stream){ * AudioContext where .play() doesn't. */ if (myRole === 'listener'){ if (attachListenerStreamViaAudioContext(uuid, stream)){ + /* If a stale-stream attempt earlier in this lifecycle left an + * fallback element in remoteAudio[uuid], the SFU's still- + * alive transceiver continues feeding RTP into it. Now that the + * worklet path is the live audio source, tear the element + * down so we don't play the same publisher twice — once via + * (~0.5s native jbuf = "early") and once via worklet (4s + * = "on time"). Fox 2026-06-05: "fxhp-android-firefox is also + * hearing two, one perfectly synced, other is not." */ + const stale = remoteAudio.get(uuid); + if (stale){ + try { stale.srcObject = null; stale.pause(); stale.remove(); } catch(_){} + remoteAudio.delete(uuid); + logLine('', 'detached stale fallback for '+uuid.slice(0,4)); + } stopMeter(uuid); startMeter(uuid, stream); logLine('', 'sfu: receiving '+((members.get(uuid)||{}).handle || uuid)+' (audioctx 4s)'); return; @@ -3401,6 +3415,12 @@ function attachSfuTrack(uuid, stream){ } else { /* speaker / cohost / host: smaller buffer, same path. */ if (attachAudioStreamViaWorklet(uuid, stream, SPEAKER_PLAYOUT_DELAY_SEC)){ + const stale = remoteAudio.get(uuid); + if (stale){ + try { stale.srcObject = null; stale.pause(); stale.remove(); } catch(_){} + remoteAudio.delete(uuid); + logLine('', 'detached stale fallback for '+uuid.slice(0,4)); + } stopMeter(uuid); startMeter(uuid, stream); logLine('', 'sfu: receiving '+((members.get(uuid)||{}).handle || uuid)+' (audioctx '+SPEAKER_PLAYOUT_DELAY_SEC+'s)'); return; @@ -7839,8 +7859,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');