zebra-report: UA-gated audio NACK — Chrome↔Chrome reactive, Firefox honest
RTCRtpSender.getCapabilities probe decides whether to advertise NACK in our Opus offers. Chromium publishes it (responds to retransmits), Firefox/Safari omit (can't respond). Receivers honor only what the sender promised — mixed-browser rooms stay clean, same-browser rooms get the full reactive recovery.
This commit is contained in:
parent
01bdd802f2
commit
959d2dc65c
1 changed files with 40 additions and 10 deletions
|
|
@ -2981,14 +2981,26 @@ function preferStereoOpus(sdp, maxAvgBps, opts){
|
|||
return 'a=fmtp:' + pt + ' ' + parts.join(';');
|
||||
});
|
||||
if (!opusPT) return sdp;
|
||||
/* Audio NACK was tried and rolled back: Chrome receivers will request
|
||||
* retransmits if the sender advertises rtcp-fb:nack on the Opus PT, but
|
||||
* Firefox senders never reply (Mozilla never shipped the responder
|
||||
* side). Chrome's jitter buffer waits for recovery that never arrives
|
||||
* and then skips — audibly choppy in mixed Firefox→Chrome rooms.
|
||||
* useinbandfec + 700ms playoutDelayHint already cover the loss case
|
||||
* without protocol churn. Do NOT re-add audio NACK without verifying
|
||||
* both sides actually implement it for the negotiated PT. */
|
||||
/* Audio NACK is asymmetric — the SENDER has to respond to retransmit
|
||||
* requests. Chrome implements both sides; Firefox implements neither
|
||||
* for audio (Mozilla never shipped it). Blanket-advertising NACK in
|
||||
* Firefox-published offers made Chrome receivers wait for retransmits
|
||||
* that never arrived and skip audibly.
|
||||
*
|
||||
* Gate on the actual local capability via RTCRtpSender.getCapabilities:
|
||||
* - Chrome: opus rtcpFeedback includes nack → we advertise → Chrome
|
||||
* receivers get reactive recovery, Firefox receivers ignore.
|
||||
* - Firefox: opus rtcpFeedback has no nack → we don't advertise →
|
||||
* Chrome receivers won't NACK us → no choppy.
|
||||
* Each browser only offers what it can back up. No LCD across the
|
||||
* room, no per-peer signaling needed — the SDP itself is honest. */
|
||||
if (senderSupportsAudioNack()){
|
||||
const nackLine = 'a=rtcp-fb:' + opusPT + ' nack';
|
||||
if (sdp.indexOf(nackLine) === -1){
|
||||
sdp = sdp.replace(new RegExp('(a=rtpmap:' + opusPT + ' opus[^\\r\\n]*\\r?\\n)'),
|
||||
'$1' + nackLine + '\r\n');
|
||||
}
|
||||
}
|
||||
/* Tolerate larger frames from peers (up to 120ms). Bigger encode
|
||||
* windows give Opus more context per packet → cleaner music at the
|
||||
* same bitrate. We can't force OUR encoder's frame size from JS but
|
||||
|
|
@ -2998,6 +3010,24 @@ function preferStereoOpus(sdp, maxAvgBps, opts){
|
|||
}
|
||||
return sdp;
|
||||
}
|
||||
/* Probe the local browser's audio sender capabilities — does Opus
|
||||
* advertise nack feedback? Cached after first call: capabilities are
|
||||
* static per UA. Returns true on Chromium (Chrome / Edge / Brave) and
|
||||
* false on Firefox / Safari / older browsers. */
|
||||
let _senderNackCached = null;
|
||||
function senderSupportsAudioNack(){
|
||||
if (_senderNackCached !== null) return _senderNackCached;
|
||||
try {
|
||||
const caps = (window.RTCRtpSender && RTCRtpSender.getCapabilities)
|
||||
? RTCRtpSender.getCapabilities('audio') : null;
|
||||
const opus = caps && caps.codecs && caps.codecs.find(c =>
|
||||
(c.mimeType || '').toLowerCase() === 'audio/opus');
|
||||
_senderNackCached = !!(opus && opus.rtcpFeedback &&
|
||||
opus.rtcpFeedback.some(fb => fb.type === 'nack'));
|
||||
} catch(_){ _senderNackCached = false; }
|
||||
logLine('', 'audio NACK as sender: '+(_senderNackCached ? 'on' : 'off'));
|
||||
return _senderNackCached;
|
||||
}
|
||||
async function applyMicMode(){
|
||||
/* re-acquire mic with new constraints, hot-swap onto every live sender
|
||||
* (mesh peers + the SFU publish PC) */
|
||||
|
|
@ -4417,8 +4447,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-03</span><br>
|
||||
md5 <span class="stamp-md5">daa6375c3e46c8a13c287745467f105f</span><br>
|
||||
sha256 <span class="stamp-sha">534f2a0afe7da5319071f94519988c2f752a8f9919398d5a728582d540b07e46</span><br>
|
||||
md5 <span class="stamp-md5">47863bc1f78b52473afd762515165065</span><br>
|
||||
sha256 <span class="stamp-sha">cb05099c3ac7ce007a656e6b3c940d462a7cac9748ba0f4d56642c52684feded</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