diff --git a/zebra-report/zebra-audio.html b/zebra-report/zebra-audio.html index 5d3e2d7..d1c0a33 100644 --- a/zebra-report/zebra-audio.html +++ b/zebra-report/zebra-audio.html @@ -88,6 +88,9 @@
path: — +
+ +

both partners type the same code and hit call. audio is end-to-end encrypted (DTLS-SRTP), peer-to-peer when the network allows, relayed through @@ -173,14 +176,44 @@ async function refreshTurnCred(){ } /* ---- audio ---- */ -let micStream = null, audioCtx = null; +let micStream = null, audioCtx = null, musicMode = false; +/* voice mode = echo-cancel + noise-suppress + auto-gain (clean speech). + * music mode = all of that OFF so music/audio passes through un-mangled. */ +function micConstraints(){ + return musicMode + ? { echoCancellation:false, noiseSuppression:false, autoGainControl:false } + : { echoCancellation:true, noiseSuppression:true, autoGainControl:true }; +} +function tagTrack(t){ if (t) t.contentHint = musicMode ? 'music' : 'speech'; } +async function setSenderBitrate(sender){ + if (!sender) return; + try { + const p = sender.getParameters(); + if (!p.encodings || !p.encodings.length) p.encodings = [{}]; + p.encodings[0].maxBitrate = musicMode ? 160000 : 40000; /* bits/s */ + await sender.setParameters(p); + } catch(_){} +} async function getMic(){ if (micStream) return micStream; - micStream = await navigator.mediaDevices.getUserMedia({ - audio: { echoCancellation:true, noiseSuppression:true, autoGainControl:true }, video:false - }); + micStream = await navigator.mediaDevices.getUserMedia({ audio: micConstraints(), video:false }); + tagTrack(micStream.getAudioTracks()[0]); return micStream; } +/* switch voice<->music mid-call: re-acquire the mic and hot-swap the track on + * the live connection (no renegotiation), preserving mute state */ +async function applyMicMode(){ + const ns = await navigator.mediaDevices.getUserMedia({ audio: micConstraints(), video:false }); + const nt = ns.getAudioTracks()[0]; + tagTrack(nt); nt.enabled = !muted; + if (pc){ + const sender = pc.getSenders().find(s=>s.track && s.track.kind==='audio') || pc.getSenders()[0]; + if (sender){ try { await sender.replaceTrack(nt); } catch(_){} setSenderBitrate(sender); } + } + if (micStream) micStream.getTracks().forEach(t=>t.stop()); + micStream = ns; + meterFor(micStream, 'meter-mic'); +} function meterFor(stream, fillId){ if (!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)(); const src = audioCtx.createMediaStreamSource(stream); @@ -214,7 +247,8 @@ function setPath(msg, cls){ const e=$('path-status'); e.textContent=msg; e.class async function ensurePC(){ if (pc) return pc; pc = new RTCPeerConnection(rtcConfig); - for (const tr of micStream.getTracks()) pc.addTrack(tr, micStream); + for (const tr of micStream.getTracks()) { tagTrack(tr); pc.addTrack(tr, micStream); } + setSenderBitrate(pc.getSenders().find(s=>s.track && s.track.kind==='audio')); pc.ontrack = (ev) => { const stream = ev.streams[0] || new MediaStream([ev.track]); $('remote-audio').srcObject = stream; @@ -353,6 +387,11 @@ $('btn-call').addEventListener('click', startCall); $('btn-hangup').addEventListener('click', hangup); $('btn-mute').addEventListener('click', toggleMute); $('rdv-code').addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); startCall(); } }); +$('music-mode').addEventListener('change', async (e)=>{ + musicMode = e.target.checked; + logLine('', 'mic mode: '+(musicMode?'MUSIC — raw, no voice isolation':'VOICE — echo/noise cancel')); + if (micStream){ try { await applyMicMode(); } catch(err){ logLine('err','mic mode switch failed: '+err.message); } } +}); logLine('', 'ready — type a rendezvous code and call. mic stays muted to the room until connected.'); })();