fix ice candidates
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 31s

This commit is contained in:
developer committed 2026-09-23 19:16:56 +00:00
1 parent 87f181504c
commit 9b8c2a9599
1 file changed
+32 -8
+32 -8
View File
@@ -625,12 +625,19 @@ function handleSignal(msg) {
toast(`Joined ${msg.room}`, 'ok'); toast(`Joined ${msg.room}`, 'ok');
break; break;
} }
case 'joined': case 'joined': {
S.peers.set(msg.peer.id, makePeerRecord(msg.peer)); // 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'); playRetroCue('join');
renderAll(); renderAll();
toast(`${msg.peer.name} joined`, 'ok'); toast(`${msg.peer.name} joined`, 'ok');
break; break;
}
case 'left': { case 'left': {
const peer = S.peers.get(msg.id); const peer = S.peers.get(msg.id);
if (peer) { if (peer) {
@@ -662,8 +669,13 @@ function handleSignal(msg) {
break; break;
} }
case 'candidate': { case 'candidate': {
const peer = S.peers.get(msg.from); // A candidate can legitimately arrive before its description is
if (!peer || !peer.pc) return; // candidates only after description path created pc // 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) => { onCandidate(peer, msg.candidate).catch((e) => {
console.error('candidate failed', e); console.error('candidate failed', e);
captureRumIssue('WebRTC ICE candidate failed', 'WARNING', { peerId: peer.id, error: e?.message || String(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 // 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 // scratch — tear down our half so a fresh offer lands on a clean
// RTCPeerConnection instead of an already-established (and now // 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); const peer = S.peers.get(msg.from);
if (peer) recreatePeerConnection(peer, { announce: false }); if (peer) recreatePeerConnection(peer, { announce: false, remote: true });
break; break;
} }
default: default:
@@ -864,7 +878,7 @@ const RESET_MAX_ATTEMPTS = 3;
// failure first, whichever side detects it also tells the other to rebuild via // 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 // a 'reset' signal — otherwise the other side keeps its old, now-mismatched
// transport and the fresh offer we send will fail against it too. // transport and the fresh offer we send will fail against it too.
function recreatePeerConnection(peer, { announce }) { function recreatePeerConnection(peer, { announce, remote = false }) {
if (announce) { if (announce) {
peer.resetAttempts++; peer.resetAttempts++;
if (peer.resetAttempts > RESET_MAX_ATTEMPTS) { if (peer.resetAttempts > RESET_MAX_ATTEMPTS) {
@@ -876,6 +890,16 @@ function recreatePeerConnection(peer, { announce }) {
return; return;
} }
send({ type: 'reset', to: peer.id }); 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.pc) { try { peer.pc.close(); } catch {} }
if (peer.qualityTimer) { clearInterval(peer.qualityTimer); peer.qualityTimer = null; } if (peer.qualityTimer) { clearInterval(peer.qualityTimer); peer.qualityTimer = null; }
@@ -915,7 +939,7 @@ async function onDescription(peer, desc) {
async function onCandidate(peer, candidate) { async function onCandidate(peer, candidate) {
if (peer.ignoreOffer) return; 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); peer.pendingCandidates.push(candidate);
return; return;
} }