zebra-spaces: tap-to-resume for Firefox Android autoplay block + kick clears auto-rejoin
Two fixes pulled from today's QA: 1. activateListenerAudio() existed in the page but was orphaned — never called from anywhere after a prior refactor. Wire it back via flagAudioBlocked(): when attachSfuTrack's play() promise rejects (Firefox Android autoplay block past the entry gesture window), set audioPlaybackBlocked=true, show a warn notice "Audio paused by browser. Tap anywhere to resume." and arm a single document-level click/touchstart listener. First tap fires activateListenerAudio() inside the gesture, retries play() on every paused element (rtc + stream), clears the notice. Telemetry that exposed this: phone showed `rtc[3110] rs=4 ns=1 pa=1 ct=0.00` — element had data, network idle, paused, never started playing. No recovery path until the user reloaded. Scoped — the document-click handler is armed only while the flag is true, removed on first tap. Doesn't add noise to working sessions (which was why the prior tap-anywhere was reverted). 2. Kick clears sessionStorage[ACTIVE_CALL_KEY]. The confirm() text says "they can rejoin" — but that means MANUALLY (type the code, click enter), not automatically on a hard-refresh / bfcache restore via auto-rejoin. Fox 2026-06-04: "we have auto-join on the phone that is kicked just rejoins". Closes that loophole; victim can still re-enter manually.
This commit is contained in:
parent
84f591834f
commit
4ebf353e2f
1 changed files with 51 additions and 10 deletions
|
|
@ -1667,9 +1667,13 @@ function attachSfuTrack(uuid, stream){
|
|||
(t0 ? ' tr0={en='+t0.enabled+' mu='+t0.muted+' rs='+t0.readyState+'}' : ''));
|
||||
try {
|
||||
const p = a.play();
|
||||
if (p && p.catch) p.catch(e => logLine('err','rtc autoplay '+uuid.slice(0,4)+': '+e.message));
|
||||
if (p && p.catch) p.catch(e => {
|
||||
logLine('err','rtc autoplay '+uuid.slice(0,4)+': '+e.message);
|
||||
flagAudioBlocked('rtc autoplay '+uuid.slice(0,4));
|
||||
});
|
||||
} catch(e){
|
||||
logLine('err','rtc play threw '+uuid.slice(0,4)+': '+e.message);
|
||||
flagAudioBlocked('rtc play threw '+uuid.slice(0,4));
|
||||
}
|
||||
stopMeter(uuid); startMeter(uuid, stream);
|
||||
logLine('', 'sfu: receiving '+((members.get(uuid)||{}).handle || uuid));
|
||||
|
|
@ -3800,6 +3804,13 @@ async function handleSignal(raw){
|
|||
* down our SFU + mesh PCs too so we actually stop hearing /
|
||||
* broadcasting — closing the WS alone leaves the WebRTC paths up. */
|
||||
wantConnected = false;
|
||||
/* Clear the active-call key so a hard-refresh / bfcache restore
|
||||
* doesn't sneak the kicked user right back in via auto-rejoin.
|
||||
* The confirm() text says "they can rejoin" — but that means
|
||||
* MANUALLY (type the code, click enter), not automatically on
|
||||
* any tab event. Fox 2026-06-04: "we have auto-join on the
|
||||
* phone that is kicked just rejoins". */
|
||||
try { sessionStorage.removeItem(ACTIVE_CALL_KEY); } catch(_){}
|
||||
const noticeVerb = m.action === 'kick' ? 'kicked from'
|
||||
: m.action === 'ban' ? 'banned from'
|
||||
: 'removed from';
|
||||
|
|
@ -4269,19 +4280,49 @@ let selfListenerMode = false;
|
|||
function activateListenerAudio(){
|
||||
/* Fire .play() on every existing <audio> element inside the click
|
||||
* gesture. The user just tapped — Firefox Android grants media
|
||||
* permission for the duration of this synchronous handler. */
|
||||
* permission for the duration of this synchronous handler. Also
|
||||
* extends to streamAudio (DJ HTTP pulls) so a single tap recovers
|
||||
* both WebRTC and HTTP paths. */
|
||||
let played = 0, failed = 0;
|
||||
for (const [uuid, a] of remoteAudio){
|
||||
const tryPlay = (a, label, uuid) => {
|
||||
try { a.muted = false; } catch(_){}
|
||||
try {
|
||||
const p = a.play();
|
||||
if (p && p.then){
|
||||
p.then(()=>played++).catch(e => { failed++; logLine('err','rtc play '+uuid.slice(0,8)+' paused='+a.paused+' rs='+a.readyState+' srcObj='+!!a.srcObject+' err='+e.name+': '+e.message); });
|
||||
p.then(()=>played++).catch(e => { failed++; logLine('err',label+' play '+uuid.slice(0,8)+' paused='+a.paused+' rs='+a.readyState+' err='+e.name+': '+e.message); });
|
||||
} else played++;
|
||||
} catch(e){ failed++; logLine('err','rtc play threw '+uuid.slice(0,8)+': '+e.message); }
|
||||
logLine('', 'rtc '+uuid.slice(0,8)+' paused='+a.paused+' muted='+a.muted+' rs='+a.readyState+' srcObj='+!!a.srcObject+' vol='+a.volume);
|
||||
}
|
||||
logLine('', 'activateListenerAudio played='+played+' failed='+failed+' remoteAudio='+remoteAudio.size);
|
||||
} catch(e){ failed++; logLine('err',label+' play threw '+uuid.slice(0,8)+': '+e.message); }
|
||||
};
|
||||
for (const [uuid, a] of remoteAudio){ tryPlay(a, 'rtc', uuid); }
|
||||
for (const [uuid, a] of streamAudio){ tryPlay(a, 'stream', uuid); }
|
||||
logLine('', 'activateListenerAudio played='+played+' failed='+failed+' rtc='+remoteAudio.size+' stream='+streamAudio.size);
|
||||
}
|
||||
/* Auto-recovery hook for Firefox Android autoplay block: when
|
||||
* attachSfuTrack's play() rejects, set audioPlaybackBlocked=true and
|
||||
* surface a single notice. The next click anywhere on the page calls
|
||||
* activateListenerAudio() inside the gesture, which re-tries play()
|
||||
* on every paused element. One tap, all audio unblocks.
|
||||
*
|
||||
* Scoped — the document-click listener is armed only while the flag
|
||||
* is true. Cleared the moment any audio resumes. Doesn't add noise
|
||||
* to working sessions. */
|
||||
let audioPlaybackBlocked = false;
|
||||
function flagAudioBlocked(why){
|
||||
if (audioPlaybackBlocked) return;
|
||||
audioPlaybackBlocked = true;
|
||||
logLine('err', 'audio blocked: '+why+' — tap anywhere to resume');
|
||||
showNotice('Audio paused by browser. Tap anywhere to resume.', 'warn');
|
||||
const handler = () => {
|
||||
document.removeEventListener('click', handler, true);
|
||||
document.removeEventListener('touchstart', handler, true);
|
||||
activateListenerAudio();
|
||||
/* the play() promises resolve async, but we can clear the flag
|
||||
* optimistically — if any of them re-reject we'll re-flag. */
|
||||
audioPlaybackBlocked = false;
|
||||
hideNotice();
|
||||
};
|
||||
document.addEventListener('click', handler, true);
|
||||
document.addEventListener('touchstart', handler, true);
|
||||
}
|
||||
function applyAudioMute(){
|
||||
/* legacy hook still called by attachSfuTrack / startStream; for the
|
||||
|
|
@ -5212,8 +5253,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">ac81473f271b2df615e659a510a042b0</span><br>
|
||||
sha256 <span class="stamp-sha">85e48d687bf5e4aae15997e165a08769ad99a93213436b97b695feada96b25e6</span><br>
|
||||
md5 <span class="stamp-md5">6e89267a0fe4c92fb46b50f0315a4223</span><br>
|
||||
sha256 <span class="stamp-sha">e28bb2a3b1a846b590515b0221d9cdb1e5e50e4b4c6a884c68891e7607a613c6</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