zebra-report: pre-bless audio-element pool on entry click

Mirror of zebra-report 44e05b2.
This commit is contained in:
russell@unturf.com 2026-06-03 22:12:59 -04:00
parent cef405c204
commit 0f70d62148
No known key found for this signature in database

View file

@ -1627,17 +1627,17 @@ const sfuStreamsByPubHex = new Map(); // pubHex -> MediaStream
function attachSfuTrack(uuid, stream){
let a = remoteAudio.get(uuid);
if (!a){
a = document.createElement('audio'); a.autoplay = true;
a.playsInline = true;
document.body.appendChild(a); remoteAudio.set(uuid, a);
/* lease from the pre-blessed audio pool so Firefox Android's
* per-element autoplay grant carries over from the entry-button
* gesture. attachSfuTrack runs after several async hops (WS join,
* SFU subscribe negotiation, ontrack) so the click activation
* window has expired by now — only a pre-played pool element
* still has the engagement bit set. */
a = leaseAudioElement();
remoteAudio.set(uuid, a);
applySinkTo(a);
}
a.srcObject = stream;
/* programmatic play in case autoplay policy needs the nudge after a track
* swap — caller already had a user gesture (entered the space). On Firefox
* Android the play() promise will reject if the gesture grace has aged
* out; the listener's play button re-runs play() inside its own gesture
* to recover. */
try {
const p = a.play();
if (p && p.catch) p.catch(e => logLine('err','rtc autoplay '+uuid+': '+e.message));
@ -4743,23 +4743,65 @@ const SILENCE_WAV = 'data:audio/wav;base64,UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAA
* after a successful first prime. Mobile browsers can suspend the
* audio session on tab background / leave; the next entry click needs
* a fresh play() to re-bless audio for the new session. */
/* Pool of audio elements pre-blessed for autoplay by the user gesture
* (entry button click). Firefox Android grants media autoplay
* permission PER ELEMENT, not per document — so attachSfuTrack creating
* a fresh <audio> later (after the gesture activation has aged out)
* gets its play() rejected with NotAllowedError. We pre-create a pool
* inside the click handler, call play() on each (succeeds inside the
* gesture), and attachSfuTrack consumes from the pool when a real
* stream arrives. The element has retained 'engagement' so setting
* srcObject and play() works without a fresh gesture. */
const AUDIO_POOL_SIZE = 16;
const audioPool = [];
function primeAudioOnGesture(){
/* legacy silent primer kept for AudioContext + chime engagement */
const a = document.createElement('audio');
a.src = SILENCE_WAV; a.muted = false; a.volume = 0;
a.style.display = 'none';
document.body.appendChild(a);
const p = a.play();
if (p && p.then) p.then(() => {
/* keep the element around briefly so the play() actually emits a
* frame; then yank it from the DOM to avoid a growing pile of
* primer elements after many entry clicks. */
setTimeout(() => { try { a.remove(); } catch(_){} }, 500);
}).catch(()=>{ try { a.remove(); } catch(_){} });
/* Also try resuming any existing AudioContext (meter / chime).
* resume() on an already-running context is a no-op, and on a
* suspended one it transitions to running — granting Web Audio
* playback permission alongside <audio>. */
try { if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); } catch(_){}
/* pre-bless a pool of audio elements for unmuted autoplay. Each one
* gets play() called inside this click handler — the gesture grants
* each element individual engagement that survives the click's
* activation window. attachSfuTrack later assigns srcObject onto a
* pool element instead of creating a fresh one. */
for (let i = 0; i < AUDIO_POOL_SIZE; i++){
const el = document.createElement('audio');
el.autoplay = true; el.playsInline = true;
el.style.display = 'none';
document.body.appendChild(el);
/* assign a 1-frame silent source so play() has something to chew
* on. The element retains engagement after this play(); subsequent
* srcObject= assignments inherit it. */
el.src = SILENCE_WAV;
try {
const pp = el.play();
if (pp && pp.catch) pp.catch(()=>{});
} catch(_){}
audioPool.push(el);
}
logLine('', 'audio pool primed: '+audioPool.length+' elements');
}
function leaseAudioElement(){
/* hand out a pre-blessed element from the pool; if the pool is dry,
* fall back to creating a fresh one (will probably fail autoplay
* on Firefox Android listeners but at least logs the rejection). */
if (audioPool.length > 0){
const el = audioPool.shift();
try { el.removeAttribute('src'); el.load(); } catch(_){}
return el;
}
logLine('err', 'audio pool exhausted — autoplay may fail');
const fresh = document.createElement('audio');
fresh.autoplay = true; fresh.playsInline = true;
fresh.style.display = 'none';
document.body.appendChild(fresh);
return fresh;
}
$('btn-enter').addEventListener('click', () => { primeAudioOnGesture(); joinSpace(); });
$('rdv-code').addEventListener('keydown', e=>{
@ -4880,8 +4922,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">627a038a99648b35f87e7ad75f3d057a</span><br>
sha256 <span class="stamp-sha">a3ea7e91b1a8ed05a02414bd86ab4a4cd9968f8ec7f2bf9521f24ccc41783c29</span><br>
md5 <span class="stamp-md5">e8ab005d36f6cf2fb05d8064d8f15a97</span><br>
sha256 <span class="stamp-sha">8688b810622d9696cad31f3f4da127c2e8a7613ab12919b5a679d79887ca2ac3</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>