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:
parent
c8fc119ea7
commit
1de3f2c1bc
1 changed files with 46 additions and 7 deletions
|
|
@ -4661,17 +4661,39 @@ function applyMuteState(){
|
|||
if (mm){ mm.muted = muted; renderRoom(); }
|
||||
}
|
||||
$('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'){
|
||||
listenerOutputMuted = !listenerOutputMuted;
|
||||
logLine('', 'play-button: listenerOutputMuted=' + listenerOutputMuted +
|
||||
' remoteAudio=' + remoteAudio.size +
|
||||
' streamAudio=' + streamAudio.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();
|
||||
const btn = $('btn-mute');
|
||||
btn.textContent = listenerOutputMuted ? 'play' : 'mute';
|
||||
|
|
@ -4684,6 +4706,23 @@ $('btn-mute').addEventListener('click', () => {
|
|||
applyMuteState();
|
||||
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) => {
|
||||
micDeviceId = e.target.value;
|
||||
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">
|
||||
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> · built <span class="stamp-date">2026-06-04</span><br>
|
||||
md5 <span class="stamp-md5">71493d3cbf0438797dcf8c1b4948202f</span><br>
|
||||
sha256 <span class="stamp-sha">2a12f6f390f0592daa5ee6ad23f8a661d558ece7a57cce51567ddc34adc89a7d</span><br>
|
||||
md5 <span class="stamp-md5">eac4e2b4b8c40c4adbdd03c004eb8cc4</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">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