zebra-audio: advertise mic (mute) state to the partner

Each peer signals its mute state over the rendezvous channel (encrypted with
the room code, so the relay never learns it) on connect and on every toggle.
A "muted" badge now shows on both your own and the partner's level meter, so
both players can see who is muted. Verified both directions headless.
This commit is contained in:
Russell Ballestrini 2026-05-28 14:58:16 -04:00
parent 8dfdd9561c
commit da78dc8612
No known key found for this signature in database

View file

@ -94,10 +94,12 @@
<div class="row">
<span class="note" style="min-width:3.5rem">you</span>
<div class="meter"><div class="meter-fill" id="meter-mic"></div></div>
<span class="note" id="you-state" style="min-width:4.5rem; text-align:right"></span>
</div>
<div class="row">
<span class="note" style="min-width:3.5rem">them</span>
<div class="meter"><div class="meter-fill" id="meter-rem"></div></div>
<span class="note" id="them-state" style="min-width:4.5rem; text-align:right"></span>
</div>
</section>
@ -217,7 +219,7 @@ async function ensurePC(){
const s = pc.connectionState;
setStatus('call: '+s, s==='connected'?'ok':null);
$('dot-call').className = 'dot ' + (s==='connected'?'ok':'warn');
if (s==='connected'){ $('btn-mute').disabled=false; $('btn-hangup').disabled=false; setTimeout(reportPath,1200); }
if (s==='connected'){ $('btn-mute').disabled=false; $('btn-hangup').disabled=false; setTimeout(reportPath,1200); setMicLabels(); sendMicState(); }
else if (s==='failed'||s==='disconnected'||s==='closed'){ setPath('path: —', null); }
};
return pc;
@ -298,9 +300,11 @@ async function startCall(){
$('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='';
setPartnerMic(false); setMicLabels();
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==='mic'){ try { const s=JSON.parse(await aesDecrypt(relayKey, unb64(m.data))); setPartnerMic(!!s.muted); } catch(_){} }
else if (m.type==='error'){ setStatus('rendezvous: '+m.message,'err'); }
};
}
@ -311,15 +315,29 @@ function hangup(){
$('btn-mute').disabled=true; $('btn-hangup').disabled=true; $('btn-call').disabled=false;
$('dot-call').className='dot warn'; setStatus('idle', null); setPath('path: —', null);
$('meter-rem').style.width='0%';
muted=false; partnerMuted=false; $('btn-mute').textContent='mute'; $('btn-mute').className=''; setMicLabels();
logLine('', 'call ended');
}
let muted=false;
let muted=false, partnerMuted=false;
function setMicLabels(){
$('you-state').textContent = muted ? 'muted' : '';
$('them-state').textContent = partnerMuted ? 'muted' : '';
}
function setPartnerMic(m){ partnerMuted=m; setMicLabels(); logLine('', 'partner '+(m?'muted':'unmuted')); }
/* advertise our mic state to the partner over the rendezvous channel, encrypted
* with the room code so the relay never learns it */
async function sendMicState(){
if (!relayWS || relayWS.readyState!==1 || !relayKey) return;
try { relayWS.send(JSON.stringify({ type:'mic', data: b64(await aesEncrypt(relayKey, JSON.stringify({muted}))) })); } catch(_){}
}
function toggleMute(){
if (!micStream) return;
muted=!muted;
micStream.getAudioTracks().forEach(t=>t.enabled=!muted);
$('btn-mute').textContent = muted?'unmute':'mute';
$('btn-mute').className = muted?'invert':'';
setMicLabels();
sendMicState();
}
$('btn-call').addEventListener('click', startCall);