zebra-spaces: wake AudioContext render thread inside entry gesture via silent oscillator

Phone telemetry post-deploy showed attachListenerStreamViaAudioContext
logging ctxState=suspended despite resume() being called at entry-
click time. Firefox Android requires more than just resume() to keep
the audio render thread alive — the context goes back to suspended
the moment the gesture window closes if nothing is actively playing
through destination.

Fix: inside primeAudioOnGesture, after resume(), play a brief silent
oscillator (50ms, gain=0) through audioCtx.destination. That forces
the render thread to ACTUALLY START rather than just queueing-
pending. After this the context stays running for the session and
every MediaStreamAudioSource attached to destination plays through.

Added 'audioCtx primed state=...' log so we can confirm the state
flipped to 'running' in the entry click.
This commit is contained in:
Russell Ballestrini 2026-06-04 11:49:59 -04:00
parent 64094ecf31
commit 0cba22e135
No known key found for this signature in database

View file

@ -5175,16 +5175,30 @@ function primeAudioOnGesture(){
if (p && p.then) p.then(() => {
setTimeout(() => { try { a.remove(); } catch(_){} }, 500);
}).catch(()=>{ try { a.remove(); } catch(_){} });
/* Create audioCtx if absent + resume so the listener path
* (attachListenerStreamViaAudioContext) has a granted-and-running
* AudioContext by the time the first remote MediaStream arrives.
* Listeners never grab a mic so without this, audioCtx would be
* null forever and the Web Audio listener route couldn't bootstrap.
* Speakers also benefit — meter creation later won't have to
* resume-from-suspended (which can fail outside a gesture). */
/* Create audioCtx + actually start the render thread inside the
* gesture. Just calling resume() isn't enough on Firefox Android —
* telemetry fox 2026-06-04 caught attachListenerStreamViaAudioContext
* logging ctxState=suspended even though resume() was called at
* entry-click time. resume() outside a subsequent gesture silently
* fails on Android.
*
* Play a brief silent oscillator through audioCtx.destination
* inside this gesture — that forces the audio rendering thread to
* ACTUALLY START rather than queueing-pending. After this the
* context stays running for the session and every
* MediaStreamAudioSource attached to destination plays through. */
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume().catch(()=>{});
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
g.gain.value = 0; /* silent — only here to wake the render thread */
osc.connect(g);
g.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.05);
osc.onended = () => { try { osc.disconnect(); g.disconnect(); } catch(_){} };
logLine('', 'audioCtx primed state='+audioCtx.state);
} catch(e){ logLine('err', 'audioCtx prime: '+e.message); }
/* pre-bless a pool of audio elements for unmuted autoplay. Each one
* gets play() called inside this click handler — the gesture grants
@ -5342,8 +5356,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">49f9782cba53987585b1e8e4bb9cd6bd</span><br>
sha256 <span class="stamp-sha">b89d91a064e4b657ebe7f7b4bbcc85d935f7be141679369f88913c8d07a41e80</span><br>
md5 <span class="stamp-md5">8bfa8fa66fb1493ee39b068ab635b95d</span><br>
sha256 <span class="stamp-sha">101001e05d546c74994310129ce846c59598d7d2ef1ded43531612532db4a70e</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>