zebra-spaces: state-based autoplay-block detection — catch pa=1 ct=0 case

Telemetry from fxhp-phone proved the bug isn't play()-promise-rejects.
Firefox Android: receiver decodes audio (aud.recv level=0.449 in the
stats) but the <audio> element stays pa=1 ct=0.00 forever. play()
returns a resolved promise on the pre-blessed pool element, but
assigning the WebRTC srcObject silently breaks the autoplay grant
without firing an error — flagAudioBlocked's promise-reject hook
never gets called.

Add a state-based trigger inside the existing 5s telemetry tick: if
any audio element has rs>=2 (HAVE_CURRENT_DATA) but is paused at
ct=0, call flagAudioBlocked() to surface the tap-anywhere notice.
Listener role only — speakers have other audio paths that shouldn't
be disturbed by a global click handler.

Re-uses the existing tap-to-resume scaffolding from b8fd50c (single
document-level click listener that fires activateListenerAudio()
inside the gesture and self-removes). One tap on the phone screen
should now recover all paused audio.
This commit is contained in:
Russell Ballestrini 2026-06-04 11:30:56 -04:00
parent 4ebf353e2f
commit 305c60f23b
No known key found for this signature in database

View file

@ -3426,7 +3426,10 @@ async function dumpTelemetry(){
/* every <audio> element in the room — both the WebRTC remoteAudio
* pool (rtc[]) and the DJ HTTP-pull streamAudio (stream[]). */
let i = 0;
let pausedZeroCt = 0, totalAudio = 0;
for (const [uuid, a] of remoteAudio){
totalAudio++;
if (a.paused && a.currentTime === 0 && a.readyState >= 2) pausedZeroCt++;
parts.push('rtc[' + uuid.slice(0,4) + '] rs=' + a.readyState + ' ns=' + a.networkState +
' pa=' + (a.paused?1:0) + ' mu=' + (a.muted?1:0) + ' ct=' + a.currentTime.toFixed(2) +
' err=' + (a.error?a.error.code:'_'));
@ -3434,6 +3437,8 @@ async function dumpTelemetry(){
}
i = 0;
for (const [uuid, a] of streamAudio){
totalAudio++;
if (a.paused && a.currentTime === 0 && a.readyState >= 2) pausedZeroCt++;
parts.push('stream[' + uuid.slice(0,4) + '] rs=' + a.readyState + ' ns=' + a.networkState +
' pa=' + (a.paused?1:0) + ' mu=' + (a.muted?1:0) + ' ct=' + a.currentTime.toFixed(2) +
' src=' + (a.src ? (a.src.length>30 ? '…'+a.src.slice(-30) : a.src) : 'none') +
@ -3441,6 +3446,19 @@ async function dumpTelemetry(){
if (++i > 4) { parts.push('stream…+' + (streamAudio.size - i) + ' more'); break; }
}
logLine('', '· ' + parts.join(' '));
/* State-based autoplay-block detection: if any audio element has data
* (rs>=2 HAVE_CURRENT_DATA) but stays paused at ct=0, play() either
* silently failed or the pre-blessing wore off when srcObject was
* swapped. The Firefox Android phone hits this consistently: receiver
* decodes audio (level>0 in aud.recv stats) but the <audio> stays
* pa=1 ct=0.00 forever. play()-promise-reject doesn't fire in that
* shape, so a state-based trigger is the only reliable detector.
* Only fire on listener role — speakers/cohosts/hosts get the same
* pre-blessing path but also have an active mic that requires its
* own gesture chain we don't want to disturb. */
if (pausedZeroCt > 0 && totalAudio > 0 && myRole === 'listener'){
flagAudioBlocked('telemetry: '+pausedZeroCt+'/'+totalAudio+' audio paused at ct=0');
}
}
function startTelemetryLoop(){
if (telemetryTimer) return;
@ -5253,8 +5271,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">6e89267a0fe4c92fb46b50f0315a4223</span><br>
sha256 <span class="stamp-sha">e28bb2a3b1a846b590515b0221d9cdb1e5e50e4b4c6a884c68891e7607a613c6</span><br>
md5 <span class="stamp-md5">c84aa4f6e7bc464e3712a8e07b7f1052</span><br>
sha256 <span class="stamp-sha">520f5e0765afe3a6345f84236d56c53c5ec751f83992ffe9e902268e5d0702cd</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>