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:
parent
d9198727d9
commit
6c9d1b8ecc
1 changed files with 42 additions and 3 deletions
|
|
@ -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> · 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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue