zebra-spaces: jitterBufferTarget enforces 4s buffer (hint isn't honored on FF Android)
Fox 2026-06-04 — definitive observation: "the phone as a listener doesn't seem to be 4 secs behind ever." playoutDelayHint is a HINT the browser is free to ignore; Firefox Android apparently does. The phone's actual buffer was near-zero — so every host-side encoder stall propagated audibly to listeners with no cushion. Three changes: 1. Set RTCRtpReceiver.jitterBufferTarget = 4000 (ms) alongside playoutDelayHint. jitterBufferTarget is NOT a hint — it's a target the receiver must aim for. Chromium 113+ (May 2023), Firefox 124+ (2024). Older browsers silently ignore the assignment (try/catch). 2. Applied at all THREE attach sites: - SFU video receiver (screen/camera/game) - SFU mic receiver - mesh peer mic receiver 3. Add `jbuf=Xs` to telemetry, computed from inbound-rtp jitterBufferDelay / jitterBufferEmittedCount. This is the ACTUAL average buffer depth — we can now see whether the receiver is holding ~4s or 0.05s. If jbuf stays small after this deploy, the browser is ignoring the target too and we need a different approach (AudioWorklet manual buffering, or move to HTTP-pull DJ path for listeners). Existing playoutDelayHint setting kept for older browsers that honor it but don't yet support jitterBufferTarget.
This commit is contained in:
parent
e3f91b1e55
commit
1e4f0fa23d
1 changed files with 35 additions and 5 deletions
|
|
@ -2955,6 +2955,7 @@ function handleRemoteSfuTrack(ev){
|
|||
* is more vital than video — video adapts to audio's delay, never
|
||||
* the other way around. */
|
||||
try { if (ev.receiver) ev.receiver.playoutDelayHint = RECV_PLAYOUT_DELAY_SEC; } catch(_){}
|
||||
try { if (ev.receiver) ev.receiver.jitterBufferTarget = RECV_PLAYOUT_DELAY_SEC * 1000; } catch(_){}
|
||||
}
|
||||
/* MSID-supplant safety: the SFU re-uses the same streamID
|
||||
* (`shortPub-kind`) when a publisher supplants themselves. WebRTC
|
||||
|
|
@ -3003,8 +3004,17 @@ function handleRemoteSfuTrack(ev){
|
|||
}
|
||||
/* mic audio — see RECV_PLAYOUT_DELAY_SEC for the buffer-vs-latency
|
||||
* tradeoff rationale. Bumped 0.7 → 2.0 after fox 2026-06-03 chasing
|
||||
* persistent chop that survived every SDP-side dial-back. */
|
||||
* persistent chop that survived every SDP-side dial-back.
|
||||
*
|
||||
* playoutDelayHint is a HINT the browser is free to ignore. Fox
|
||||
* 2026-06-04: "the phone as a listener doesn't seem to be 4 secs
|
||||
* behind ever" — Firefox Android wasn't honoring the hint, so the
|
||||
* jitter buffer stayed near-zero and any encoder stall on the host
|
||||
* was instantly audible. jitterBufferTarget (Chromium 113+, FF 124+)
|
||||
* is NOT a hint — it sets a target the receiver MUST aim for.
|
||||
* Setting both for cross-browser coverage. */
|
||||
try { if (ev.receiver) ev.receiver.playoutDelayHint = RECV_PLAYOUT_DELAY_SEC; } catch(_){}
|
||||
try { if (ev.receiver) ev.receiver.jitterBufferTarget = RECV_PLAYOUT_DELAY_SEC * 1000; } catch(_){}
|
||||
/* cache by full pubkey (already resolved above) so it survives the
|
||||
* member's session uuid changing across leave/rejoin */
|
||||
sfuStreamsByPubHex.set(pubHex, ev.streams[0]);
|
||||
|
|
@ -3900,21 +3910,38 @@ async function dumpTelemetry(){
|
|||
/* lastPacketReceivedTimestamp pins the EXACT real-time
|
||||
* moment audio RTP stopped — pkt-delta only shows it after
|
||||
* the next tick. Express as 'ago' so a frozen flow stands
|
||||
* out at a glance ('lp=0.1s' = fine, 'lp=12s' = dead). */
|
||||
* out at a glance ('lp=0.1s' = fine, 'lp=12s' = dead).
|
||||
*
|
||||
* jbuf is the ACTUAL average jitter-buffer depth in seconds.
|
||||
* jitterBufferDelay accumulates "total seconds of buffer
|
||||
* delay applied to emitted samples" and jitterBufferEmittedCount
|
||||
* counts the emitted samples — ratio is the average. Fox
|
||||
* 2026-06-04: playoutDelayHint is a hint, not a contract;
|
||||
* we need to SEE whether the receiver actually holds 4s. If
|
||||
* jbuf << 4s the receiver is ignoring the hint and any
|
||||
* upstream stall is instantly audible. */
|
||||
const lp = r.lastPacketReceivedTimestamp
|
||||
? ((nowMs - r.lastPacketReceivedTimestamp) / 1000).toFixed(1) + 's'
|
||||
: '?';
|
||||
const jbuf = (r.jitterBufferEmittedCount > 0)
|
||||
? (r.jitterBufferDelay / r.jitterBufferEmittedCount).toFixed(2) + 's'
|
||||
: '?';
|
||||
parts.push('aud.recv pkt=' + (r.packetsReceived|0) + ' lost=' + (r.packetsLost|0) +
|
||||
' bytes=' + (r.bytesReceived|0) + ' jitter=' + (r.jitter || 0).toFixed(4) +
|
||||
' level=' + (r.audioLevel || 0).toFixed(3) +
|
||||
' jbuf=' + jbuf +
|
||||
' lp=' + lp);
|
||||
}
|
||||
if (r.type === 'inbound-rtp' && r.kind === 'video'){
|
||||
const lp = r.lastPacketReceivedTimestamp
|
||||
? ((nowMs - r.lastPacketReceivedTimestamp) / 1000).toFixed(1) + 's'
|
||||
: '?';
|
||||
const jbuf = (r.jitterBufferEmittedCount > 0)
|
||||
? (r.jitterBufferDelay / r.jitterBufferEmittedCount).toFixed(2) + 's'
|
||||
: '?';
|
||||
parts.push('vid.recv pkt=' + (r.packetsReceived|0) + ' lost=' + (r.packetsLost|0) +
|
||||
' frames=' + (r.framesDecoded|0) +
|
||||
' jbuf=' + jbuf +
|
||||
' lp=' + lp);
|
||||
}
|
||||
});
|
||||
|
|
@ -4667,8 +4694,11 @@ async function connectToPeer(uuid, weOffer){
|
|||
applySinkTo(a);
|
||||
}
|
||||
a.srcObject = ev.streams[0] || new MediaStream([ev.track]);
|
||||
/* mesh path matches the SFU path — same RECV_PLAYOUT_DELAY_SEC. */
|
||||
/* mesh path matches the SFU path — same RECV_PLAYOUT_DELAY_SEC.
|
||||
* jitterBufferTarget enforces (not hints) the buffer depth — see
|
||||
* sfu mic-receiver site for rationale. */
|
||||
try { ev.receiver.playoutDelayHint = RECV_PLAYOUT_DELAY_SEC; } catch(_){}
|
||||
try { ev.receiver.jitterBufferTarget = RECV_PLAYOUT_DELAY_SEC * 1000; } catch(_){}
|
||||
stopMeter(uuid); startMeter(uuid, a.srcObject);
|
||||
};
|
||||
pc.onicecandidate = (ev) => { /* using waitForIceGathering pattern, candidates ignored */ };
|
||||
|
|
@ -5950,8 +5980,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> · built <span class="stamp-date">2026-06-04</span><br>
|
||||
md5 <span class="stamp-md5">e3c7120739d2e4ef52d4f95860be5eaf</span><br>
|
||||
sha256 <span class="stamp-sha">b4416e44b5dca523fe5c9f32160d8f4d219c585b02d90e84e8d8129646ae27c6</span><br>
|
||||
md5 <span class="stamp-md5">25a1c73043470ab0e7e990747635161c</span><br>
|
||||
sha256 <span class="stamp-sha">a91fefd9648e7b76a8aaca85f73cd9a7fb2eb9bc2ca0b27303a233ef43cdb57c</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue