zebra-spaces: kill audio chops + clear tile when publisher unshares
Audio robustness: - useinbandfec=1 + usedtx=1 in publisher's Opus fmtp (was only fec; dtx drops silence so the budget goes to audible content + cuts congestion) - ev.receiver.playoutDelayHint = 0.1 (100ms jitter buffer) on every incoming audio receiver — SFU subscribe + mesh peer. Absorbs Wi-Fi micro-bursts without perceptible conversation lag. Tile cleanup on unshare: - when SFU stops a screen/camera transceiver after the publisher unpublishes, the remote track fires 'ended'. Listeners now wire track.onended → removeScreenTile/removeCameraTile so the tile disappears instead of freezing on the last frame. Paired with SFU 60a620f which advertises nack + transport-cc feedback so browsers send the RTCP we depend on for both retransmit and congestion control.
This commit is contained in:
parent
7ddedc9a09
commit
353037bc5d
1 changed files with 24 additions and 3 deletions
|
|
@ -928,19 +928,29 @@ async function sfuSubscribe(){
|
|||
const kind = sid.slice(colon + 1);
|
||||
/* skip echo of our own publish — we already render a local preview */
|
||||
if (myKeys && pubHex === myKeys.pubHex) return;
|
||||
/* video kinds: when the publisher unshares, the SFU stops the
|
||||
* transceiver and the corresponding remote track fires 'ended'.
|
||||
* Wire that to the tile remover so the listener's UI matches the
|
||||
* publisher's state instead of holding a frozen last frame. */
|
||||
if (kind === 'screen'){
|
||||
screenStreams.set(pubHex, ev.streams[0]);
|
||||
renderScreenTile(pubHex, ev.streams[0]);
|
||||
ev.track.addEventListener('ended', () => removeScreenTile(pubHex));
|
||||
return;
|
||||
}
|
||||
if (kind === 'camera'){
|
||||
cameraStreams.set(pubHex, ev.streams[0]);
|
||||
renderCameraTile(pubHex, ev.streams[0]);
|
||||
ev.track.addEventListener('ended', () => removeCameraTile(pubHex));
|
||||
return;
|
||||
}
|
||||
logLine('', 'sfu: unknown kind '+kind+' from '+shortHex(pubHex));
|
||||
return;
|
||||
}
|
||||
/* mic audio — set a small jitter-buffer hint so brief network jitter
|
||||
* doesn't cause audible drops. 100ms is enough to absorb typical
|
||||
* Wi-Fi micro-bursts without making conversation feel laggy. */
|
||||
try { ev.receiver.playoutDelayHint = 0.1; } catch(_){}
|
||||
const pubHex = sid;
|
||||
/* cache stream by publisher pubkey so it survives the member's session
|
||||
* uuid changing across leave/rejoin — see flushSfuStreams */
|
||||
|
|
@ -1113,7 +1123,15 @@ async function setSenderMaxBitrate(sender, bps){
|
|||
* has to be raised explicitly for music to actually use the headroom. */
|
||||
function preferStereoOpus(sdp, maxAvgBps){
|
||||
return sdp.replace(/a=fmtp:(\d+) ([^\r\n]*minptime=10[^\r\n]*)/g, (m, pt, fmtp) => {
|
||||
const want = { 'stereo': '1', 'sprop-stereo': '1', 'maxaveragebitrate': String(maxAvgBps) };
|
||||
/* useinbandfec=1: forward error correction so a single dropped
|
||||
* packet doesn't audibly chop — Opus reconstructs from FEC.
|
||||
* usedtx=1: discontinuous transmission elides silence so the
|
||||
* bandwidth budget goes to actual audio (cuts congestion). */
|
||||
const want = {
|
||||
'stereo': '1', 'sprop-stereo': '1',
|
||||
'maxaveragebitrate': String(maxAvgBps),
|
||||
'useinbandfec': '1', 'usedtx': '1',
|
||||
};
|
||||
const parts = fmtp.split(';').map(s => s.trim()).filter(Boolean);
|
||||
const seen = new Set();
|
||||
for (let i = 0; i < parts.length; i++){
|
||||
|
|
@ -1526,6 +1544,9 @@ async function connectToPeer(uuid, weOffer){
|
|||
let a = remoteAudio.get(uuid);
|
||||
if (!a){ a = document.createElement('audio'); a.autoplay = true; document.body.appendChild(a); remoteAudio.set(uuid, a); }
|
||||
a.srcObject = ev.streams[0] || new MediaStream([ev.track]);
|
||||
/* 100ms jitter buffer absorbs typical Wi-Fi micro-bursts without
|
||||
* adding perceptible conversation lag */
|
||||
try { ev.receiver.playoutDelayHint = 0.1; } catch(_){}
|
||||
stopMeter(uuid); startMeter(uuid, a.srcObject);
|
||||
};
|
||||
pc.onicecandidate = (ev) => { /* using waitForIceGathering pattern, candidates ignored */ };
|
||||
|
|
@ -1878,8 +1899,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-02</span><br>
|
||||
md5 <span class="stamp-md5">d91602a3c30333c3c88c8fb9d1ce83d6</span><br>
|
||||
sha256 <span class="stamp-sha">d57f2da4b2958764af64527fdbc7f41a8420c216975f1ec9d55f259d5832c6ed</span><br>
|
||||
md5 <span class="stamp-md5">39992d2506da348cfff78c04a999e409</span><br>
|
||||
sha256 <span class="stamp-sha">0208f19ac63acfd243a6e745aa045fd1d56a206bbff9ca3f058e6c2466aed525</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