zebra-spaces: idempotent attach + drop stale stream cache

This commit is contained in:
russell@unturf.com 2026-06-05 19:41:22 -04:00
parent 269ba777ca
commit 59c407f1c7
No known key found for this signature in database

View file

@ -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> &nbsp;·&nbsp; 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>