diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 8cf4b9a..f4fb041 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -1785,6 +1785,23 @@ class JitterBufferProcessor extends AudioWorkletProcessor {
this.started = false;
this.emptyStreak = 0;
this.dropped = 0;
+ /* role-change retarget — JS posts {cmd:'retarget', targetSeconds}
+ * when the user is promoted/demoted; we recompute the sample
+ * targets and shrink the queue if the new max is smaller. */
+ this.port.onmessage = (e) => {
+ if (!e.data || e.data.cmd !== 'retarget') return;
+ const t = +e.data.targetSeconds;
+ if (!isFinite(t) || t <= 0) return;
+ this.targetSeconds = t;
+ this.maxSeconds = t * 1.5;
+ this.targetSamples = Math.round(this.targetSeconds * sampleRate);
+ this.maxSamples = Math.round(this.maxSeconds * sampleRate);
+ while (this.buffered > this.maxSamples && this.queue.length > 0){
+ const drop = this.queue.shift();
+ this.buffered -= drop[0].length;
+ this.dropped += drop[0].length;
+ }
+ };
}
process(inputs, outputs){
const inBlk = inputs[0];
@@ -1855,27 +1872,35 @@ function loadJitterWorklet(ctx){
}
function installJitterBuffer(uuid, node){
if (!node || node.jbuf || !workletReady) return;
+ const target = node.targetSeconds || RECV_PLAYOUT_DELAY_SEC;
try {
const jbuf = new AudioWorkletNode(audioCtx, 'jitter-buffer', {
processorOptions: {
- targetSeconds: RECV_PLAYOUT_DELAY_SEC,
- maxSeconds: RECV_PLAYOUT_DELAY_SEC * 1.5,
+ targetSeconds: target,
+ maxSeconds: target * 1.5,
},
outputChannelCount: [2],
});
try { node.src.disconnect(node.gain); } catch(_){}
node.src.connect(jbuf).connect(node.gain);
node.jbuf = jbuf;
- logLine('', 'jitter-buffer installed uuid='+uuid.slice(0,4)+' target='+RECV_PLAYOUT_DELAY_SEC+'s');
+ logLine('', 'jitter-buffer installed uuid='+uuid.slice(0,4)+' target='+target+'s');
} catch (e) {
logLine('err', 'jitter-buffer install '+uuid.slice(0,4)+': '+e.message);
}
}
-function attachListenerStreamViaAudioContext(uuid, stream){
+/* Shared audio attach path. Every role routes through here now so the
+ * worklet can apply role-appropriate buffer depth uniformly. Listener
+ * gets a fat 4s cushion (lean-back, latency doesn't matter); speakers
+ * / cohosts / hosts get ~0.5s (small enough for conversation, big
+ * enough to smooth ordinary jitter). The OS Media-Session hook is
+ * applied only when role==='listener' — speakers don't need lock-
+ * screen transport controls. */
+function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){
if (!audioCtx){
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
- catch(e){ logLine('err','listener audioCtx create: '+e.message); return false; }
+ catch(e){ logLine('err','audioCtx create: '+e.message); return false; }
}
if (audioCtx.state === 'suspended'){
audioCtx.resume().catch(()=>{});
@@ -1889,18 +1914,24 @@ function attachListenerStreamViaAudioContext(uuid, stream){
}
let src;
try { src = audioCtx.createMediaStreamSource(stream); }
- catch(e){ logLine('err','listener createMediaStreamSource '+uuid.slice(0,4)+': '+e.message); return false; }
+ catch(e){ logLine('err','createMediaStreamSource '+uuid.slice(0,4)+': '+e.message); return false; }
const gain = audioCtx.createGain();
gain.gain.value = 1.0;
src.connect(gain);
gain.connect(audioCtx.destination);
- const node = { src, gain, stream };
+ const node = { src, gain, stream, targetSeconds };
listenerAudioNodes.set(uuid, node);
/* fire-and-forget worklet load on first use; once ready, every
* existing stream is swapped through the buffer (see loadJitterWorklet) */
loadJitterWorklet(audioCtx);
if (workletReady) installJitterBuffer(uuid, node);
- logLine('', 'listener audio via AudioContext '+uuid.slice(0,4)+' ctxState='+audioCtx.state);
+ logLine('', 'audio via AudioContext '+uuid.slice(0,4)+' target='+targetSeconds+'s ctxState='+audioCtx.state);
+ return true;
+}
+
+function attachListenerStreamViaAudioContext(uuid, stream){
+ const ok = attachAudioStreamViaWorklet(uuid, stream, RECV_PLAYOUT_DELAY_SEC);
+ if (!ok) return false;
/* Media Session API — tell the OS this tab is playing media. On
* Android Firefox + iOS Safari, this:
* - keeps the tab in media-priority mode (less aggressive JS
@@ -1939,19 +1970,31 @@ function detachListenerStream(uuid){
listenerAudioNodes.delete(uuid);
}
function attachSfuTrack(uuid, stream){
- /* Listener role: route audio through AudioContext (Firefox Android
- * autoplay survives this path;