zebra-spaces: tap-anywhere-to-resume on stream autoplay block (mobile)

Mobile Firefox/Safari refuse audio.play() when the gesture activation
window has expired between the entry-button tap and auto-enrolment
into DJ mode. Previously this surfaced as "autoplay blocked" in the
log and the listener was silently stuck on WebRTC.

Now: when play() rejects, the <audio> element goes into a
pendingAutoplay set and one global pointerdown/touchstart listener
on document retries every queued element. Once they all play the
listener self-uninstalls. Idempotent install — adding more elements
during the pending window just enqueues them.

Listener sees: 'stream for <pubhex> — tap anywhere to start (msg)'
in the log; one tap later, every queued stream resumes and the
log fills with 'stream on for ...' lines.

WebRTC stays unmuted during the wait so the listener still hears
the live (possibly choppy) audio — they're not left in silence
between the autoplay block and their tap.
This commit is contained in:
Russell Ballestrini 2026-06-03 20:16:11 -04:00
parent d9198727d9
commit 6c9d1b8ecc
No known key found for this signature in database

View file

@ -4050,6 +4050,33 @@ const streamAudio = new Map(); // uuid -> <audio> pulling stream
function streamUrlFor(pubHex){
return SFU_BASE + '/stream?room=' + encodeURIComponent(roomID) + '&pub=' + pubHex;
}
/* Tap-to-resume queue: when mobile Firefox/Safari blocks autoplay
* (auto-enrolment runs too far away from the entry-button gesture
* for the activation window to still be alive), we stash the audio
* element here and retry its play() on the first user touch anywhere
* on the page. One global listener handles every queued stream at
* once. Idempotent install. */
const pendingAutoplay = new Set();
let tapResumeInstalled = false;
function installTapResume(){
if (tapResumeInstalled) return;
tapResumeInstalled = true;
const retry = () => {
if (!pendingAutoplay.size) return;
for (const a of [...pendingAutoplay]){
const p = a.play();
if (p && p.then) p.then(() => pendingAutoplay.delete(a)).catch(()=>{});
else pendingAutoplay.delete(a);
}
if (!pendingAutoplay.size){
document.removeEventListener('pointerdown', retry);
document.removeEventListener('touchstart', retry);
tapResumeInstalled = false;
}
};
document.addEventListener('pointerdown', retry, { passive: true });
document.addEventListener('touchstart', retry, { passive: true });
}
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
@ -4106,7 +4133,19 @@ async function startStream(uuid, pubHex){
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));
.catch(e => {
/* mobile Firefox / Safari block autoplay when the gesture
* activation window has expired between entry-click and
* auto-enrolment. Don't permanently fall back to WebRTC —
* queue this element for tap-to-resume so the next tap
* anywhere on the page retries play(). */
pendingAutoplay.add(a);
installTapResume();
logLine('', 'stream for '+pubHex.slice(0,12)+' — tap anywhere to start ('+e.message+')');
/* Keep WebRTC audible as the fallback while waiting for a tap. */
const w = remoteAudio.get(uuid);
if (w) try { w.muted = false; } catch(_){}
});
} else {
logLine('', 'stream on for '+pubHex.slice(0,12)+' — DJ mode (~2s delay, glitch-free)');
}
@ -4780,8 +4819,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-04</span><br>
md5 <span class="stamp-md5">a0d7d8646e0dcec5b7563cc20ee69ade</span><br>
sha256 <span class="stamp-sha">712548a89b0e0ebfa299b91a360ef0a75128e5f35d17b2ffd7519bf0ff4423fa</span><br>
md5 <span class="stamp-md5">988fb9812616b4cbd5c7f1073f3e4d01</span><br>
sha256 <span class="stamp-sha">7b9cc2c6cdab3bba55beb73b6afa413b784743b986294b3ad4e043f31c553e3f</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>