zebra-report: stream-toggle reliability fixes

Three coupled fixes to the DJ-stream toggle path:
  - await setSinkId() before setting .src so it doesn't race + abort
    the in-flight media load
  - drop the 'stalled' handler that was ping-ponging WebRTC ↔ stream
    on every transient mobile-cellular buffer pause
  - make startStream idempotent on the same URL so a peer-joined
    auto-enrollment race can't reset .src and self-abort

Plus preload="auto" and playsInline on the dynamic <audio> for
mobile compat. Fixes "no music on phone stream stalled" and the
"autoplay blocked: aborted at user's request" log spam from host
self-monitor clicks.
This commit is contained in:
russell@unturf.com 2026-06-03 20:06:45 -04:00
parent 94b981da29
commit e3363d9cf1
No known key found for this signature in database

View file

@ -4050,23 +4050,47 @@ const streamAudio = new Map(); // uuid -> <audio> pulling stream
function streamUrlFor(pubHex){
return SFU_BASE + '/stream?room=' + encodeURIComponent(roomID) + '&pub=' + pubHex;
}
function startStream(uuid, pubHex){
async function startStream(uuid, pubHex){
/* 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. */
const wantUrl = streamUrlFor(pubHex);
let a = streamAudio.get(uuid);
/* Idempotent: a second toggle-on for the same pubHex while we're
* already loading/playing is a no-op. The previous behaviour reset
* .src which aborted the in-flight load — Firefox surfaces this as
* 'fetching process aborted at user's request' on the play()
* promise, which we then mistakenly logged as autoplay-blocked. */
if (a && a.src === wantUrl && !a.error) {
return;
}
if (!a){
a = document.createElement('audio');
a.autoplay = true; a.controls = false;
/* preload=auto: encourage the browser to start buffering immediately
* instead of waiting for play() — helps slow mobile links fill
* the initial buffer before the user-gesture window expires.
* playsInline: tell Safari/iOS not to escalate audio playback into
* a fullscreen player. No effect on Firefox/Chrome but cheap. */
a.preload = 'auto'; a.playsInline = true;
document.body.appendChild(a);
streamAudio.set(uuid, a);
applySinkTo(a);
/* setSinkId is async and can re-init the media pipeline. If we
* call it AFTER setting src + play(), the re-init aborts the load
* with the abort error fox saw. Await it FIRST, before any src
* assignment. Safe to await on browsers that don't support it —
* applySinkTo early-returns. */
await applySinkTo(a);
}
a.muted = false;
a.src = streamUrlFor(pubHex);
a.src = wantUrl;
/* mute WebRTC the moment the HTTP stream actually plays a frame,
* not before. Removed on stop / error so WebRTC takes over again. */
* not before. Removed on error / autoplay reject so WebRTC takes
* over again. NOTE: 'stalled' is NOT treated as a failure — it
* fires constantly during normal startup on mobile cellular and
* each fire was causing the audio path to flip-flop. Real failure
* shows up as 'error' or as a rejected play() promise. */
const muteWebRtcOnPlay = () => {
const w = remoteAudio.get(uuid);
if (w) try { w.muted = true; } catch(_){}
@ -4078,7 +4102,6 @@ function startStream(uuid, pubHex){
};
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){
@ -4756,9 +4779,9 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
</script>
<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">14406e1c86dd563732f048cef3cf4cbb</span><br>
sha256 <span class="stamp-sha">9b7b9f6b416b367475d3c1bf181acafbb684589ed18188fcb14b29927320c940</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">a0d7d8646e0dcec5b7563cc20ee69ade</span><br>
sha256 <span class="stamp-sha">712548a89b0e0ebfa299b91a360ef0a75128e5f35d17b2ffd7519bf0ff4423fa</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>