zebra-report: graceful mic re-acquire on BT/USB device swap — mirror

This commit is contained in:
russell@unturf.com 2026-06-02 10:45:55 -04:00
parent 9a3bb7f64b
commit d34eb61205
No known key found for this signature in database

View file

@ -1622,8 +1622,47 @@ async function getMic(){
micStream = await navigator.mediaDevices.getUserMedia({ audio: micConstraints(), video:false });
const t = micStream.getAudioTracks()[0];
tagTrack(t); await enforceMicConstraints(t);
watchMicTrack(t);
return micStream;
}
/* If the underlying mic device disappears (BT disconnect, USB unplug, OS
* audio swap), the track fires 'ended' or stops emitting samples but the
* MediaStream object stays alive — so existing RTP senders keep pointing
* at a dead track and the host goes silent until they leave + rejoin.
* Watch the track and re-acquire on the fly. */
let micReacquireInFlight = false;
function watchMicTrack(track){
if (!track) return;
track.addEventListener('ended', () => {
logLine('err', 'mic track ended — re-acquiring');
reacquireMic().catch(e => logLine('err','mic re-acquire failed: '+e.message));
});
}
async function reacquireMic(){
if (micReacquireInFlight) return;
micReacquireInFlight = true;
try {
const ns = await navigator.mediaDevices.getUserMedia({ audio: micConstraints(), video:false });
const nt = ns.getAudioTracks()[0];
tagTrack(nt); nt.enabled = !muted;
await enforceMicConstraints(nt);
watchMicTrack(nt);
/* hot-swap onto every existing sender — no renegotiation needed
* since the codec / SDP didn't change, just the track behind it */
async function swap(pc){
const sender = pc.getSenders().find(s=>s.track && s.track.kind==='audio') || pc.getSenders()[0];
if (sender){ try { await sender.replaceTrack(nt); } catch(_){} }
}
for (const [, pc] of peers) await swap(pc);
if (sfuPubPC) await swap(sfuPubPC);
if (micStream) micStream.getTracks().forEach(t=>t.stop());
micStream = ns;
if (myUUID){ stopMeter(myUUID); startMeter(myUUID, micStream); }
logLine('', 'mic re-acquired');
} finally {
micReacquireInFlight = false;
}
}
async function setSenderBitrate(sender){
if (!sender) return;
try {
@ -1681,6 +1720,7 @@ async function applyMicMode(){
const nt = ns.getAudioTracks()[0];
tagTrack(nt); nt.enabled = !muted;
await enforceMicConstraints(nt);
watchMicTrack(nt);
async function swap(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); }
@ -2552,7 +2592,19 @@ $('music-mode').addEventListener('change', async (e) => {
if (micStream){ try { await applyMicMode(); } catch(err){ logLine('err','mic mode switch failed: '+err.message); } }
});
if (navigator.mediaDevices && navigator.mediaDevices.addEventListener){
navigator.mediaDevices.addEventListener('devicechange', refreshMicList);
navigator.mediaDevices.addEventListener('devicechange', () => {
refreshMicList();
/* belt-and-suspenders for browsers that don't fire track.onended on
* device disappearance (Firefox/BT swap can leave the track in
* 'live' state but emitting silence). If our current track has
* gone dead since the last devicechange, re-acquire. */
if (micStream){
const t = micStream.getAudioTracks()[0];
if (t && (t.readyState === 'ended' || t.muted)){
reacquireMic().catch(e => logLine('err','mic re-acquire on devicechange: '+e.message));
}
}
});
}
/* initial population so the dropdown lists the user's inputs before they
* enter a space (matches what the camera-select does). Labels are blank
@ -2656,8 +2708,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">ad6a80d1c54036ff0d7782df2b98c4db</span><br>
sha256 <span class="stamp-sha">77b28cfbf2690bdf0b12697c141ab460d0c95301344ce0245936a1c498aa35a9</span><br>
md5 <span class="stamp-md5">181a0e0d4e4da7c4a40e2e5e576954fd</span><br>
sha256 <span class="stamp-sha">29024b972f5dede7d5fd2b924170aaf9aa097afc9bac4402e86b01f9d543a4ed</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>