diff --git a/web/chat.html b/web/chat.html index 1299a78..b923696 100644 --- a/web/chat.html +++ b/web/chat.html @@ -263,6 +263,10 @@ peer link: idle +
+
+ path: not connected +

both peers type the same code; SDP is exchanged automatically via the relay (encrypted with the code). chat content rides the audio carrier. @@ -1171,8 +1175,10 @@ function ensurePC() { dotRtc.className = 'dot ok'; logLine('sys', 'webrtc connected'); announceHello().catch(() => {}); + setTimeout(reportTransport, 1200); } else if (s === 'failed' || s === 'closed' || s === 'disconnected') { dotRtc.className = 'dot'; + setPath('path: not connected', null); logLine('err', `webrtc ${s} — click "reset connection" then retry`); } }; @@ -1182,6 +1188,34 @@ function ensurePC() { return pc; } +function setPath(msg, kind) { + const el = $('path-status'); if (el) { el.textContent = msg; el.className = 'status-line' + (kind ? ' ' + kind : ''); } + const dot = $('dot-path'); if (dot) dot.className = 'dot' + (kind === 'ok' ? ' ok' : kind === 'err' ? ' warn' : ''); +} +/* Read the live ICE candidate pair so users can see whether anything sits + * between them: a direct host/srflx pair = peer-to-peer, nobody in the audio + * path; a 'relay' candidate = the TURN server is forwarding their (encrypted) + * audio. */ +async function reportTransport() { + if (!pc) return; + try { + const stats = await pc.getStats(); + let pair = null; + stats.forEach(r => { if (r.type === 'candidate-pair' && r.nominated && r.state === 'succeeded') pair = r; }); + if (!pair) stats.forEach(r => { if (!pair && r.type === 'candidate-pair' && r.state === 'succeeded') pair = r; }); + if (!pair) { setPath('path: connecting…', null); return; } + const loc = stats.get(pair.localCandidateId) || {}, rem = stats.get(pair.remoteCandidateId) || {}; + const relayed = loc.candidateType === 'relay' || rem.candidateType === 'relay'; + if (relayed) { + setPath('path: RELAYED through TURN (proxy.uncloseai.com sees encrypted audio)', 'err'); + logLine('sys', 'transport: relayed via TURN — a server is in the path (audio stays encrypted)'); + } else { + setPath(`path: DIRECT peer-to-peer — nobody between you (${loc.candidateType || '?'}/${rem.candidateType || '?'})`, 'ok'); + logLine('sys', `transport: direct peer-to-peer (${loc.candidateType || '?'}/${rem.candidateType || '?'})`); + } + } catch (_) {} +} + function resetConnection() { if (typeof disconnectRelay === 'function') disconnectRelay(); if (pc) { @@ -1195,6 +1229,7 @@ function resetConnection() { rxDecoders.clear(); rtcStatus.textContent = 'peer link: idle'; dotRtc.className = 'dot warn'; + setPath('path: not connected', null); logLine('sys', 'connection reset — ready to retry'); }