zebra-spaces: graceful mic re-acquire on BT/USB swap — no more leave+rejoin
When the host's mic device disappears (BT disconnect, USB unplug, OS audio swap to laptop speakers), the active MediaStreamTrack ends but the senders on every PC keep pointing at the dead track. Host goes silent until a full leave+rejoin tears everything down and re-publishes. Fix: - watchMicTrack(track) listens for 'ended' on every mic track we hand out (initial getMic + applyMicMode re-acquire) - reacquireMic() gets a fresh getUserMedia with the same constraints, then sender.replaceTrack on every live mesh peer + sfuPubPC. No renegotiation — codec / SDP stays the same, just the track swaps under the existing transceiver. Single in-flight guard so a burst of devicechange events doesn't race. - devicechange listener is now an active probe: if the current track has gone readyState='ended' or muted=true since the last event, trigger reacquireMic. Firefox in particular doesn't always fire 'ended' on BT swap — the track stays 'live' but emits silence.
This commit is contained in:
parent
cdcc15365a
commit
bd770230f9
1 changed files with 55 additions and 3 deletions
|
|
@ -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> · 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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue