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');
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;
}