diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 84ef5c1..a3fb8b2 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -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');