improve WebRTC NAT reliability diagnostics
Build and Publish / Metadata (push) Successful in 3s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 29s

This commit is contained in:
mats committed 2026-09-22 16:31:21 +02:00
1 parent f3c735b808
commit 365415740e
5 files changed
+104 -11

No files matched your search

+78 -1
View File
@@ -96,6 +96,41 @@ function toast(msg, kind = '') {
setTimeout(() => t.remove(), 5000);
}
const ICE_NOTICE_INTERVAL = 15000;
function notifyIceIssue(peer, message, extra = {}) {
const now = Date.now();
const key = `${message}:${extra.url || ''}`;
if (!peer || peer.lastIceNotice?.key !== key || now - peer.lastIceNotice.at > ICE_NOTICE_INTERVAL) {
toast(`${peer?.name || 'Voice'}: ${message}`, 'warn');
if (peer) peer.lastIceNotice = { key, at: now };
}
captureRumIssue(message, 'WARNING', {
peerId: peer?.id,
peerName: peer?.name,
...extra,
});
}
async function selectedIcePath(pc) {
if (!pc?.getStats) return null;
const stats = await pc.getStats();
let pair;
stats.forEach((report) => {
if (report.type === 'candidate-pair' && (report.selected || report.nominated) && report.state === 'succeeded') {
pair = report;
}
});
if (!pair) return null;
const local = stats.get(pair.localCandidateId);
const remote = stats.get(pair.remoteCandidateId);
return {
localType: local?.candidateType,
remoteType: remote?.candidateType,
protocol: pair.protocol,
path: local?.candidateType === 'relay' || remote?.candidateType === 'relay' ? 'TURN relay' : 'direct/STUN',
};
}
function playRetroCue(name) {
const ctx = S.audioCtx;
if (!ctx) return;
@@ -524,6 +559,8 @@ function makePeerRecord(p) {
pendingCandidates: [], micStream: null, screenStreams: new Map(),
gain: null, panner: null, analyser: null, vol: 1, speaking: false,
resetAttempts: 0,
lastIceNotice: null,
icePath: null,
els: {},
};
}
@@ -562,17 +599,40 @@ function ensurePC(peer, initiator) {
if (candidate) send({ type: 'candidate', to: peer.id, candidate });
};
pc.onicecandidateerror = (event) => {
captureRumIssue('WebRTC ICE candidate error', 'WARNING', {
notifyIceIssue(peer, `ICE server unavailable (${event.errorCode || 'unknown'})`, {
peerId: peer.id,
iceConnectionState: pc.iceConnectionState,
iceGatheringState: pc.iceGatheringState,
errorCode: event.errorCode,
errorText: event.errorText,
url: event.url,
});
};
pc.oniceconnectionstatechange = () => {
const state = pc.iceConnectionState;
if (state === 'disconnected') {
notifyIceIssue(peer, 'ICE disconnected; audio may stop', {
iceConnectionState: state,
iceGatheringState: pc.iceGatheringState,
});
} else if (state === 'failed') {
notifyIceIssue(peer, 'ICE failed; trying TURN relay', {
iceConnectionState: state,
iceGatheringState: pc.iceGatheringState,
});
}
};
pc.onconnectionstatechange = () => {
renderTiles();
if (pc.connectionState === 'failed') {
console.warn('peer connection failed, rebuilding it from scratch', peer.id);
notifyIceIssue(peer, 'connection failed; rebuilding voice path', {
connectionState: pc.connectionState,
iceConnectionState: pc.iceConnectionState,
iceGatheringState: pc.iceGatheringState,
signalingState: pc.signalingState,
resetAttempts: peer.resetAttempts,
});
captureRumIssue('WebRTC peer connection failed', 'ERROR', {
peerId: peer.id,
state: pc.connectionState,
@@ -581,6 +641,23 @@ function ensurePC(peer, initiator) {
recreatePeerConnection(peer, { announce: true });
} else if (pc.connectionState === 'connected') {
peer.resetAttempts = 0; // a successful connection clears the failure count
selectedIcePath(pc).then((path) => {
if (!path) return;
peer.icePath = path;
toast(`${peer.name}: ${path.path} connection`, 'ok');
captureRumIssue('WebRTC connection established', 'INFO', {
peerId: peer.id,
peerName: peer.name,
connectionState: pc.connectionState,
iceConnectionState: pc.iceConnectionState,
...path,
});
}).catch((err) => {
captureRumIssue('Could not inspect selected ICE path', 'WARNING', {
peerId: peer.id,
error: err?.message || String(err),
});
});
}
};
pc.ontrack = (ev) => onRemoteTrack(peer, ev);