zebra-spaces: serialise SFU subscribe renegotiation + skip own mic echo
Bug fox reported: late joiner sees host's screen + camera initially. The moment they share their own screen/camera, they lose the host's tiles. The host never sees their stuff either. Symptoms point at the renegotiation flow being raced. Root cause: SSE delivers offers via async onmessage handlers. JS is single-threaded but each `await` yields. When a speaker publishes mic + screen + camera in quick succession, the SFU's addPubToSub serialises and fires three SSE offers. The browser handler picks up offer 1 with setRemoteDescription (state → have-remote-offer), then awaits createAnswer. During that await another onmessage fires for offer 2 and tries setRemoteDescription — which throws because the PC is in have-remote-offer state. Offer 2 is dropped, the new tracks for that publish never register on the browser side. SFU still forwards RTP for those tracks but the browser has no receiver, so they vanish. Existing tiles can also stop receiving RTP when the SFU's track set diverges from the browser's transceiver set. Fix: - chain onmessage handlers through a single Promise queue (`renegQueue = renegQueue.then(...)`) so each renegotiation fully completes (SRD → answer → SLD → /answer POST) before the next starts. Browser PC always returns to stable between offers. - ontrack mic-path now also short-circuits when pubHex === own pubHex. Without this, a speaker who subscribes to the SFU would attach their OWN mic to a remote-audio sink and hear themselves.
This commit is contained in:
parent
622db439d7
commit
ae9721e336
1 changed files with 31 additions and 14 deletions
|
|
@ -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> · 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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue