Browser-native jitterBufferTarget didn't help on the music stream —
Firefox Android holds it at 0.06–0.21s on a high-bitrate stereo Opus
receiver while honoring 4s on voice and video receivers on the same
PC. Per-codec implementation gap in the receiver-side jitter buffer.
This adds a userland buffer in Web Audio. The listener path already
ran through AudioContext (source → gain → destination); now an
AudioWorkletNode sits between source and gain, queues incoming
128-sample blocks until targetSamples (4 × sampleRate) have arrived,
then emits with a constant delay. Bounded at maxSamples (6 ×
sampleRate) so clock drift can't grow the queue unbounded. If the
queue fully drains, the buffer re-arms — a hiccup doesn't lock us
silent.
Worklet code lives inline as a Blob URL (single-file app: no
separate JS file shipped). loadJitterWorklet is fire-and-forget on
first attach; existing direct-connected streams get swapped through
the buffer the moment the worklet module finishes loading. Fallback
on worklet creation failure: existing source → gain path stays live.
Speaker / cohost / host paths untouched — they need conversational
latency, can't sit on a 4s cushion.
Listener role test plan: rejoin, watch the new "jitter-buffer
installed" log line, observe that the listener is now 4s behind the
host's speech. Wiggle the host (X11) — listeners should hear no
disruption while the buffer is full.