zebra-report: serialise SFU sub renegotiation + skip own mic echo — mirror

This commit is contained in:
russell@unturf.com 2026-06-02 10:20:49 -04:00
parent f858869b3e
commit 6e4d58ffab
No known key found for this signature in database

View file

@ -1254,6 +1254,9 @@ async function sfuSubscribe(){
* delay which is well below noticeable. */
try { ev.receiver.playoutDelayHint = 0.4; } catch(_){}
const pubHex = sid;
/* never play our own mic back to ourselves — the SFU echoes our
* publish to every sub including our own (we always subscribe now) */
if (myKeys && pubHex === myKeys.pubHex) return;
/* cache stream by publisher pubkey so it survives the member's session
* uuid changing across leave/rejoin — see flushSfuStreams */
sfuStreamsByPubHex.set(pubHex, ev.streams[0]);
@ -1296,21 +1299,35 @@ async function sfuSubscribe(){
if (!ackRes.ok){ pc.close(); throw new Error('sfu subscribe-answer http '+ackRes.status); }
sfuSubPC = pc; sfuSubPeerID = offer.peer_id;
/* SSE: server pushes renegotiation offers when publisher set changes.
* We answer each via POST /answer. ping events are keepalive only. */
* We answer each via POST /answer. ping events are keepalive only.
*
* Renegotiation must be SERIALISED. Each offer transitions the PC
* through have-remote-offer → stable, and if we kick off the next
* setRemoteDescription before the previous has applied its answer,
* the second one throws ('failed to set remote offer sdp: Called in
* wrong state'). When a speaker publishes mic + screen + camera in
* quick succession the SFU fires three offers in a row — without
* serialisation we drop the later ones, browser-side track set goes
* out of sync with the SFU, and existing screen/camera tiles can
* stop receiving RTP. Chain through a single promise queue. */
let renegQueue = Promise.resolve();
sfuSubEvents = new EventSource(SFU_BASE + '/events?room=' + encodeURIComponent(roomID) + '&peer=' + sfuSubPeerID);
sfuSubEvents.onmessage = async (ev) => {
sfuSubEvents.onmessage = (ev) => {
let m; try { m = JSON.parse(ev.data); } catch(_){ return; }
if (m.type !== 'offer' || !sfuSubPC) return;
try {
await sfuSubPC.setRemoteDescription({ type:'offer', sdp: m.sdp });
const ans = await sfuSubPC.createAnswer();
await sfuSubPC.setLocalDescription(ans);
await waitForIceGathering(sfuSubPC);
await fetch(SFU_BASE + '/answer?room=' + encodeURIComponent(roomID) + '&peer=' + sfuSubPeerID, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ sdp: sfuSubPC.localDescription.sdp })
});
} catch(e){ logLine('err','sfu renegotiate: '+e.message); }
renegQueue = renegQueue.then(async () => {
if (!sfuSubPC) return;
try {
await sfuSubPC.setRemoteDescription({ type:'offer', sdp: m.sdp });
const ans = await sfuSubPC.createAnswer();
await sfuSubPC.setLocalDescription(ans);
await waitForIceGathering(sfuSubPC);
await fetch(SFU_BASE + '/answer?room=' + encodeURIComponent(roomID) + '&peer=' + sfuSubPeerID, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ sdp: sfuSubPC.localDescription.sdp })
});
} catch(e){ logLine('err','sfu renegotiate: '+e.message); }
});
};
sfuSubEvents.onerror = () => { /* EventSource auto-reconnects */ };
logLine('', 'sfu: subscribed as '+shortHex(sfuSubPeerID));
@ -2415,8 +2432,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> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">e1c393db6371901f9dd2bc5f30195cf1</span><br>
sha256 <span class="stamp-sha">b7c89d66d504eb2765bac6c128f31d07df03bc7f4309e4f6173811212510bb71</span><br>
md5 <span class="stamp-md5">4d956692f61f60657077d2e04b576b71</span><br>
sha256 <span class="stamp-sha">eb02187ba33a4618b347444a41e25d6d2f58ff6c52c7f7ff0e3437a020dbfa25</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>