zebra-spaces: swap fresh <audio> in listener click — Firefox Android fix

CLIENT_LOG made the failure mode unambiguous: after the play button
tap, the WebRTC audio elements report paused=false rs=4 muted=false
srcObject=true — the browser believes it is playing — but Firefox
Android emits zero output. Same shape as the camera autoplay-block
we fixed earlier with swapFreshVideoElement: an element created
muted=true and later unmuted is internally pinned to silent.

Fix mirrors that pattern. Inside the listener's btn-mute click
(real user gesture), every <audio> in remoteAudio / streamAudio
is replaced with a freshly-built unmuted element, the same
srcObject (for WebRTC) or src URL (for DJ stream) is reattached,
and play() fires inside the gesture context. The fresh element
has never been muted so the internal routing is clean.
This commit is contained in:
Russell Ballestrini 2026-06-03 21:26:33 -04:00
parent c8fc119ea7
commit 1de3f2c1bc
No known key found for this signature in database

View file

@ -4661,17 +4661,39 @@ function applyMuteState(){
if (mm){ mm.muted = muted; renderRoom(); } if (mm){ mm.muted = muted; renderRoom(); }
} }
$('btn-mute').addEventListener('click', () => { $('btn-mute').addEventListener('click', () => {
/* listener path: toggle output mute. The click itself is the
* user gesture mobile browsers wait for — even if listenerOutput-
* Muted was already false, applyAudioMute() will re-call play()
* inside this gesture and unblock any audio element that's been
* waiting. */
if (myRole === 'listener'){ if (myRole === 'listener'){
listenerOutputMuted = !listenerOutputMuted; listenerOutputMuted = !listenerOutputMuted;
logLine('', 'play-button: listenerOutputMuted=' + listenerOutputMuted + logLine('', 'play-button: listenerOutputMuted=' + listenerOutputMuted +
' remoteAudio=' + remoteAudio.size + ' remoteAudio=' + remoteAudio.size +
' streamAudio=' + streamAudio.size + ' streamAudio=' + streamAudio.size +
' audioPath=' + audioPath.size); ' audioPath=' + audioPath.size);
/* Firefox Android internalisation defect: an <audio> created with
* muted=true and later unmuted reports paused=false rs=4 srcObject=true
* but emits zero output. Same shape as the video supplant defect we
* fixed earlier with swapFreshVideoElement. Inside the click gesture,
* swap every audio element with a fresh one (created UNMUTED) and
* re-attach the source. play() fires inside the gesture so autoplay
* is allowed. */
if (!listenerOutputMuted){
for (const [uuid, oldA] of [...remoteAudio]){
const stream = oldA.srcObject;
const fresh = swapFreshAudioElement(oldA);
fresh.muted = false;
applySinkTo(fresh);
fresh.srcObject = stream;
remoteAudio.set(uuid, fresh);
try { const p = fresh.play(); if (p && p.catch) p.catch(e => logLine('err','swap rtc '+uuid+': '+e.message)); } catch(_){}
}
for (const [uuid, oldA] of [...streamAudio]){
const src = oldA.src;
const fresh = swapFreshAudioElement(oldA);
fresh.muted = false;
applySinkTo(fresh);
if (src) fresh.src = src;
streamAudio.set(uuid, fresh);
try { const p = fresh.play(); if (p && p.catch) p.catch(e => logLine('err','swap dj '+uuid+': '+e.message)); } catch(_){}
}
}
applyAudioMute(); applyAudioMute();
const btn = $('btn-mute'); const btn = $('btn-mute');
btn.textContent = listenerOutputMuted ? 'play' : 'mute'; btn.textContent = listenerOutputMuted ? 'play' : 'mute';
@ -4684,6 +4706,23 @@ $('btn-mute').addEventListener('click', () => {
applyMuteState(); applyMuteState();
sendMicState(); sendMicState();
}); });
/* Audio-element analogue of swapFreshVideoElement — replace an <audio>
* with a freshly-built one in the same DOM slot. Used to dodge the
* Firefox-Android internal routing defect where an element created
* muted=true silently stays silent after .muted=false. */
function swapFreshAudioElement(oldA){
if (!oldA) return oldA;
const fresh = document.createElement('audio');
fresh.autoplay = true; fresh.playsInline = true; fresh.preload = 'auto';
if (oldA.parentNode){
try { oldA.srcObject = null; oldA.removeAttribute('src'); oldA.load(); } catch(_){}
oldA.parentNode.replaceChild(fresh, oldA);
} else {
document.body.appendChild(fresh);
}
return fresh;
}
$('mic-select').addEventListener('change', async (e) => { $('mic-select').addEventListener('change', async (e) => {
micDeviceId = e.target.value; micDeviceId = e.target.value;
try { localStorage.setItem(MIC_DEV_KEY, micDeviceId); } catch(_){} try { localStorage.setItem(MIC_DEV_KEY, micDeviceId); } catch(_){}
@ -4932,8 +4971,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"> <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-04</span><br> <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-04</span><br>
md5 <span class="stamp-md5">71493d3cbf0438797dcf8c1b4948202f</span><br> md5 <span class="stamp-md5">eac4e2b4b8c40c4adbdd03c004eb8cc4</span><br>
sha256 <span class="stamp-sha">2a12f6f390f0592daa5ee6ad23f8a661d558ece7a57cce51567ddc34adc89a7d</span><br> sha256 <span class="stamp-sha">d2ca231a1a04199c5e5cc01215e6a82ab4949119d5ac40bd5150a7805fd3cdf1</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">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> <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> </footer>