diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html index 63518e7..f057f04 100644 --- a/zebra-report/zebra-spaces.html +++ b/zebra-report/zebra-spaces.html @@ -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');