From 9b8c2a959900ccaba8ce1e045dd253bec22a23d1 Mon Sep 17 00:00:00 2001 From: developer Date: Wed, 23 Sep 2026 19:16:56 +0000 Subject: [PATCH] fix ice candidates --- public/app.js | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/public/app.js b/public/app.js index 3881396..ab9a0a0 100644 --- a/public/app.js +++ b/public/app.js @@ -625,12 +625,19 @@ function handleSignal(msg) { toast(`Joined ${msg.room}`, 'ok'); break; } - case 'joined': - S.peers.set(msg.peer.id, makePeerRecord(msg.peer)); + case 'joined': { + // If candidates for this peer arrived before its description (orphan + // record from the candidate handler), carry the buffer over instead of + // dropping it with the old record. + const prev = S.peers.get(msg.peer.id); + const peer = makePeerRecord(msg.peer); + if (prev?.pendingCandidates?.length) peer.pendingCandidates = prev.pendingCandidates; + S.peers.set(msg.peer.id, peer); playRetroCue('join'); renderAll(); toast(`${msg.peer.name} joined`, 'ok'); break; + } case 'left': { const peer = S.peers.get(msg.id); if (peer) { @@ -662,8 +669,13 @@ function handleSignal(msg) { break; } case 'candidate': { - const peer = S.peers.get(msg.from); - if (!peer || !peer.pc) return; // candidates only after description path created pc + // A candidate can legitimately arrive before its description is + // applied: during a join storm the offer may still be inside + // createOffer(), and after a 'reset' the pc is briefly null mid-rebuild. + // Dropping those loses srflx/relay candidates the pair may have no + // alternative for, so buffer them — onDescription() flushes + // peer.pendingCandidates once the remote description lands. + const peer = S.peers.get(msg.from) || createOrphanPeer(msg.from); onCandidate(peer, msg.candidate).catch((e) => { console.error('candidate failed', e); captureRumIssue('WebRTC ICE candidate failed', 'WARNING', { peerId: peer.id, error: e?.message || String(e) }); @@ -674,9 +686,11 @@ function handleSignal(msg) { // The other side's connection to us failed and it's rebuilding from // scratch — tear down our half so a fresh offer lands on a clean // RTCPeerConnection instead of an already-established (and now - // mismatched) DTLS transport. See recreatePeerConnection(). + // mismatched) DTLS transport, and count the failure toward the relay + // fallback so both halves force TURN on the retry. + // See recreatePeerConnection(). const peer = S.peers.get(msg.from); - if (peer) recreatePeerConnection(peer, { announce: false }); + if (peer) recreatePeerConnection(peer, { announce: false, remote: true }); break; } default: @@ -864,7 +878,7 @@ const RESET_MAX_ATTEMPTS = 3; // failure first, whichever side detects it also tells the other to rebuild via // a 'reset' signal — otherwise the other side keeps its old, now-mismatched // transport and the fresh offer we send will fail against it too. -function recreatePeerConnection(peer, { announce }) { +function recreatePeerConnection(peer, { announce, remote = false }) { if (announce) { peer.resetAttempts++; if (peer.resetAttempts > RESET_MAX_ATTEMPTS) { @@ -876,6 +890,16 @@ function recreatePeerConnection(peer, { announce }) { return; } send({ type: 'reset', to: peer.id }); + } else if (remote) { + // The far side detected 'failed' against us and is rebuilding. Count the + // failure here too so our half ALSO recreates with + // iceTransportPolicy:'relay' — before, only the detecting side forced + // relay, so the other side kept retrying the same direct path that just + // died and the pair burned rebuild rounds before converging. + peer.resetAttempts++; + if (peer.resetAttempts === RESET_MAX_ATTEMPTS + 1) { + toast(`Connection to ${peer.name} keeps failing — TURN relay may be down`, 'error'); + } } if (peer.pc) { try { peer.pc.close(); } catch {} } if (peer.qualityTimer) { clearInterval(peer.qualityTimer); peer.qualityTimer = null; } @@ -915,7 +939,7 @@ async function onDescription(peer, desc) { async function onCandidate(peer, candidate) { if (peer.ignoreOffer) return; - if (!peer.pc.remoteDescription) { + if (!peer.pc || !peer.pc.remoteDescription) { // no pc yet: orphan peer mid-join or mid-rebuild peer.pendingCandidates.push(candidate); return; }