chat.html: surface the media path (direct P2P vs relayed via TURN)

After connect, read the selected ICE candidate pair from getStats() and show it
in the connect panel: a host/srflx pair => "DIRECT peer-to-peer — nobody between
you"; a relay candidate => "RELAYED through TURN (proxy.uncloseai.com sees
encrypted audio)". Lets users know whether any server sits in the media path.
This commit is contained in:
Russell Ballestrini 2026-05-28 12:56:51 -04:00
parent c287328bb9
commit 6002732581
No known key found for this signature in database

View file

@ -263,6 +263,10 @@
<span id="rtc-status" class="note">peer link: idle</span>
<button id="btn-reset-rtc" style="margin-left:auto">reset</button>
</div>
<div class="row" style="margin-top:0.3rem">
<div class="dot" id="dot-path"></div>
<span id="path-status" class="status-line">path: not connected</span>
</div>
<p class="note" style="margin-top:0.5rem">
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');
}