diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html index 10af972..37fc6f5 100644 --- a/zebra-report/zebra-spaces.html +++ b/zebra-report/zebra-spaces.html @@ -1975,6 +1975,7 @@ function resetListenerBufferReady(){ * the new median differs by more than LIP_SYNC_THRESHOLD from the * last applied value. */ const lipSync = new Map(); /* pubHex → state */ +const sfuAudioReceivers = new Map(); /* pubHex → RTCRtpReceiver (SFU audio) — cached so we can restore as the lip-sync source after mesh fail */ const LIP_SYNC_HISTORY = 5; const LIP_SYNC_THRESHOLD = 0.05; /* 50ms — below this is within perception noise */ @@ -2119,21 +2120,44 @@ function detachListenerStream(uuid){ listenerAudioNodes.delete(uuid); } -/* Smooth crossfade for the SFU worklet path. The worklet stays running +/* In-place stream swap on an existing worklet. Both the SFU stream + * and any mesh stream from the same publisher contain the same + * encoded content at slightly different network delays — so when we + * disconnect the old source from the worklet and connect a new one, + * the worklet's queued samples continue emitting seamlessly while + * the new source fills the queue. No buffer drain, no audible gap. + * + * This is how we make a mesh peer's audio benefit from the worklet's + * cushion: route mesh through the same worklet the SFU was using, so + * the 0.5s buffer absorbs the same wiggle-glitches it does for SFU. + * Fox 2026-06-04: "the cohost on fedora chrome has the wiggle issue + * when host is messing with tabs … can we sync the other stream and + * recover the glitches assuming it is still buffering where the low- + * latency version is glitched." + * + * Returns true on successful swap, false if no existing worklet. */ +function setWorkletStream(uuid, newStream){ + const node = listenerAudioNodes.get(uuid); + if (!node || !audioCtx) return false; + let newSrc; + try { newSrc = audioCtx.createMediaStreamSource(newStream); } + catch(e){ logLine('err', 'setWorkletStream src '+uuid.slice(0,4)+': '+e.message); return false; } + try { node.src.disconnect(); } catch(_){} + if (node.jbuf) newSrc.connect(node.jbuf); + else newSrc.connect(node.gain); + node.src = newSrc; + node.stream = newStream; + logLine('', 'worklet source swapped for '+uuid.slice(0,4)); + return true; +} + +/* Smooth crossfade for the worklet path. The worklet stays running * (continues decoding samples at small CPU cost); only its GainNode - * value moves. We use Web Audio's linearRampToValueAtTime for click- - * free transitions — settings audio.volume directly causes audible - * zipper noise on Android. + * value moves. Used by the HTTP /stream toggle to cleanly cut the + * worklet while the HTTP