diff --git a/web/zebra-audio.html b/web/zebra-audio.html
index bce0500..15e90f5 100644
--- a/web/zebra-audio.html
+++ b/web/zebra-audio.html
@@ -225,21 +225,27 @@ async function ensurePC(){
async function reportPath(){
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)||{};
- if (loc.candidateType==='relay'||rem.candidateType==='relay')
- setPath('path: RELAYED via TURN (a server forwards your encrypted audio)', 'err');
- else
- setPath('path: DIRECT peer-to-peer — nobody between you ('+(loc.candidateType||'?')+'/'+(rem.candidateType||'?')+')', 'ok');
+ const stats = await pc.getStats(); let sel=null;
+ /* the authoritative active pair, per the transport */
+ stats.forEach(r=>{ if(r.type==='transport'&&r.selectedCandidatePairId) sel=stats.get(r.selectedCandidatePairId); });
+ if (!sel) stats.forEach(r=>{ if(r.type==='candidate-pair'&&r.state==='succeeded'&&(r.nominated||r.selected)) sel=r; });
+ if (!sel){ setPath('path: connecting…', null); return; }
+ const loc=stats.get(sel.localCandidateId)||{}, rem=stats.get(sel.remoteCandidateId)||{};
+ const la=(loc.address||loc.ip||'?')+':'+(loc.port||'?'), ra=(rem.address||rem.ip||'?')+':'+(rem.port||'?');
+ const relayed = loc.candidateType==='relay'||rem.candidateType==='relay';
+ /* show the real endpoints so it's verifiable: host = the two devices' IPs;
+ * relay = the TURN server's IP (142.93.73.64) appears on at least one side. */
+ setPath('path: '+(relayed?'RELAYED via TURN':'DIRECT P2P')+' — you '+la
+ +' ↔ peer '+ra+' ('+(loc.candidateType||'?')+'/'+(rem.candidateType||'?')+')', relayed?'err':'ok');
+ logLine('', 'active pair: '+la+' ['+(loc.candidateType||'?')+'] ↔ '+ra+' ['+(rem.candidateType||'?')+']');
} catch(_){}
}
function relaySend(obj){ if (relayWS && relayWS.readyState===1) relayWS.send(JSON.stringify(obj)); }
async function relaySendSDP(kind, desc){ relaySend({ type:'sdp', kind, data: b64(await aesEncrypt(relayKey, JSON.stringify(desc))) }); }
+function freshPC(){ if (pc){ try{pc.close();}catch(_){} pc=null; } }
async function makeOffer(){
+ freshPC(); /* always (re)negotiate on a clean connection */
await ensurePC();
await pc.setLocalDescription(await pc.createOffer());
await waitForIceGathering(pc);
@@ -249,6 +255,7 @@ async function makeOffer(){
async function onSDP(kind, json){
const desc = JSON.parse(json);
if (kind==='offer'){
+ freshPC(); /* every incoming offer = a fresh connection */
await ensurePC();
await pc.setRemoteDescription(desc);
await pc.setLocalDescription(await pc.createAnswer());
@@ -277,9 +284,22 @@ async function startCall(){
relayWS.onerror = () => setStatus('rendezvous error','err');
relayWS.onmessage = async (ev) => {
let m; try { m=JSON.parse(ev.data); } catch(_){ return; }
- if (m.type==='welcome'){ relayRole=m.role; logLine('','role: '+m.role+' ('+m.peers+' present)'); if (relayRole==='offerer'&&m.peers>=2) await makeOffer(); }
- else if (m.type==='peer-joined'){ logLine('','partner joined'); if (relayRole==='offerer') await makeOffer(); }
- else if (m.type==='peer-left'){ logLine('','partner left'); setStatus('partner left', null); }
+ /* present-member-offers: whoever is already in the room offers when the
+ * other (re)joins; the joiner waits. Works for the first call and for any
+ * reconnect, regardless of who left. */
+ if (m.type==='welcome'){
+ logLine('','joined room ('+m.peers+' present)');
+ setStatus(m.peers>=2 ? 'partner here — connecting' : 'waiting for partner', null);
+ }
+ else if (m.type==='peer-joined'){ logLine('','partner joined — calling'); await makeOffer(); }
+ else if (m.type==='peer-left'){
+ logLine('','partner left — staying in room, will reconnect when they return');
+ freshPC();
+ $('remote-audio').srcObject=null; $('meter-rem').style.width='0%';
+ $('dot-call').className='dot warn'; setPath('path: —', null);
+ $('btn-mute').disabled=true; muted=false; $('btn-mute').textContent='mute'; $('btn-mute').className='';
+ setStatus('partner left — waiting for them to return', null);
+ }
else if (m.type==='sdp'){ try { await onSDP(m.kind, await aesDecrypt(relayKey, unb64(m.data))); } catch(e){ logLine('err','sdp failed (codes must match): '+e.message); } }
else if (m.type==='error'){ setStatus('rendezvous: '+m.message,'err'); }
};