zebra-spaces: defer WebRTC mute until DJ stream actually plays

Fox 2026-06-03: 'the listener phone doesn't hear anything since your last
push.' startStream() was muting the WebRTC <audio> for the speaker BEFORE
calling .play() on the fresh HTTP <audio>. On mobile the fresh <audio>'s
autoplay was often blocked (the entry-button gesture had aged out for
freshly-created elements), so the failure mode was: WebRTC silenced +
HTTP not playing = total silence.

Now WebRTC stays unmuted until the HTTP <audio> fires 'playing'. Any
autoplay/error/stalled path unmutes WebRTC and surfaces the reason in
the log, so we fall back to live WebRTC instead of going dead.

play() promise rejections are also logged now (were silenced) so next
session we can tell exactly which speakers' streams failed to start.
This commit is contained in:
Russell Ballestrini 2026-06-03 19:30:50 -04:00
parent 9fd6b06a3f
commit ebcd2575e5
No known key found for this signature in database

View file

@ -3998,9 +3998,10 @@ function streamUrlFor(pubHex){
return SFU_BASE + '/stream?room=' + encodeURIComponent(roomID) + '&pub=' + pubHex;
}
function startStream(uuid, pubHex){
/* mute the WebRTC mic for this uuid so they don't double-play */
const w = remoteAudio.get(uuid);
if (w) try { w.muted = true; } catch(_){}
/* DON'T mute the WebRTC audio yet — if the fresh <audio>'s autoplay
* is blocked (common on mobile after the entry gesture's grace has
* passed) we'd be left with zero audio. Mute only after 'playing'
* fires so the WebRTC path stays as the live fallback. */
let a = streamAudio.get(uuid);
if (!a){
a = document.createElement('audio');
@ -4011,8 +4012,31 @@ function startStream(uuid, pubHex){
}
a.muted = false;
a.src = streamUrlFor(pubHex);
try { const p = a.play(); if (p && p.catch) p.catch(()=>{}); } catch(_){}
logLine('', 'stream on for '+pubHex.slice(0,12)+' — DJ mode (~2s delay, glitch-free)');
/* mute WebRTC the moment the HTTP stream actually plays a frame,
* not before. Removed on stop / error so WebRTC takes over again. */
const muteWebRtcOnPlay = () => {
const w = remoteAudio.get(uuid);
if (w) try { w.muted = true; } catch(_){}
};
const unmuteWebRtcOnFail = (why) => {
const w = remoteAudio.get(uuid);
if (w) try { w.muted = false; } catch(_){}
logLine('err', 'stream for '+pubHex.slice(0,12)+' '+why+' — staying on live WebRTC');
};
a.addEventListener('playing', muteWebRtcOnPlay, { once: true });
a.addEventListener('error', () => unmuteWebRtcOnFail('error'), { once: true });
a.addEventListener('stalled', () => unmuteWebRtcOnFail('stalled'));
try {
const p = a.play();
if (p && p.then){
p.then(() => logLine('', 'stream on for '+pubHex.slice(0,12)+' — DJ mode (~2s delay, glitch-free)'))
.catch(e => unmuteWebRtcOnFail('autoplay blocked: '+e.message));
} else {
logLine('', 'stream on for '+pubHex.slice(0,12)+' — DJ mode (~2s delay, glitch-free)');
}
} catch(e){
unmuteWebRtcOnFail('play threw: '+e.message);
}
}
function stopStream(uuid){
const a = streamAudio.get(uuid);
@ -4659,8 +4683,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-03</span><br>
md5 <span class="stamp-md5">cebb664d38a0843a3a440fa74fdf2253</span><br>
sha256 <span class="stamp-sha">67ba8ee7c7047d4f7c3a9aca5a398357ef6c24b7d57fd5ca04e3d5f6227f20a7</span><br>
md5 <span class="stamp-md5">3c056398b2b24a618e1ee01a18f9c091</span><br>
sha256 <span class="stamp-sha">f981c4df0ab237ba9dc02cb0dbca728f13a8c06dacf4bf95f0dac17408f7ad57</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>