zebra-report: deploy silent wedge-recovery for stuck audio elements
This commit is contained in:
parent
aaf7402912
commit
10d705beb3
1 changed files with 52 additions and 52 deletions
|
|
@ -1667,14 +1667,16 @@ 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);
|
||||
flagAudioBlocked('rtc autoplay '+uuid.slice(0,4));
|
||||
});
|
||||
if (p && p.catch) p.catch(e => logLine('err','rtc autoplay '+uuid.slice(0,4)+': '+e.message));
|
||||
} catch(e){
|
||||
logLine('err','rtc play threw '+uuid.slice(0,4)+': '+e.message);
|
||||
flagAudioBlocked('rtc play threw '+uuid.slice(0,4));
|
||||
}
|
||||
/* If play() returns a resolved promise but the element stays paused
|
||||
* at ct=0 (Firefox Android srcObject-swap kills the pre-blessed
|
||||
* engagement silently), the telemetry tick's wedge-recovery will
|
||||
* detect that within 5s and lease a fresh pool element. No user-
|
||||
* visible notice — fox: tap-anywhere from 6c9d1b8 was rejected for
|
||||
* adding noise, automatic silent recovery is the right shape. */
|
||||
stopMeter(uuid); startMeter(uuid, stream);
|
||||
logLine('', 'sfu: receiving '+((members.get(uuid)||{}).handle || uuid));
|
||||
}
|
||||
|
|
@ -3424,12 +3426,17 @@ async function dumpTelemetry(){
|
|||
} catch(e){ parts.push('stats.err=' + e.message); }
|
||||
}
|
||||
/* every <audio> element in the room — both the WebRTC remoteAudio
|
||||
* pool (rtc[]) and the DJ HTTP-pull streamAudio (stream[]). */
|
||||
* pool (rtc[]) and the DJ HTTP-pull streamAudio (stream[]). Capture
|
||||
* wedged elements (rs>=2 HAVE_CURRENT_DATA but paused at ct=0) so
|
||||
* the recovery path below can re-lease fresh pool elements for
|
||||
* them. This mimics what host hard-refresh achieves: fresh
|
||||
* attachSfuTrack on a fresh element from the pre-blessed pool. */
|
||||
const wedgedRtc = [];
|
||||
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++;
|
||||
if (a.paused && a.currentTime === 0 && a.readyState >= 2 && a.srcObject){
|
||||
wedgedRtc.push([uuid, a]);
|
||||
}
|
||||
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:'_'));
|
||||
|
|
@ -3437,8 +3444,6 @@ 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') +
|
||||
|
|
@ -3446,18 +3451,34 @@ 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');
|
||||
/* Auto-recovery for wedged WebRTC audio elements. The Firefox
|
||||
* Android phone consistently gets stuck with pa=1 ct=0 even though
|
||||
* the receiver's aud.recv stat shows level>0 (real audio decoding).
|
||||
* Reproduced by fox 2026-06-04: "hard refresh on host fixed the
|
||||
* phone" — because the host's republish triggers a fresh ontrack
|
||||
* on the phone, which leases a NEW pool element with intact
|
||||
* autoplay blessing. We mirror that mechanism here: when an element
|
||||
* is wedged, lease a fresh pool element, rebind the same stream,
|
||||
* play(), swap into remoteAudio. The old wedged element gets
|
||||
* removed from the DOM. tap-anywhere was tried in 6c9d1b8 and
|
||||
* reverted in 24bfc81 — fox: don't add user-visible noise. This
|
||||
* is a silent automatic recovery instead. */
|
||||
for (const [uuid, oldEl] of wedgedRtc){
|
||||
const stream = oldEl.srcObject;
|
||||
if (!stream) continue;
|
||||
try { oldEl.pause(); } catch(_){}
|
||||
try { oldEl.srcObject = null; } catch(_){}
|
||||
try { oldEl.remove(); } catch(_){}
|
||||
const fresh = leaseAudioElement();
|
||||
remoteAudio.set(uuid, fresh);
|
||||
applySinkTo(fresh);
|
||||
fresh.addEventListener('playing', () => logLine('', 'rtc recover '+uuid.slice(0,4)+' playing'), { once: true });
|
||||
fresh.srcObject = stream;
|
||||
try {
|
||||
const p = fresh.play();
|
||||
if (p && p.catch) p.catch(e => logLine('err','rtc recover '+uuid.slice(0,4)+' play rejected: '+e.message));
|
||||
} catch(e){ logLine('err','rtc recover '+uuid.slice(0,4)+' play threw: '+e.message); }
|
||||
logLine('', 'rtc wedge-recovery '+uuid.slice(0,4)+' — fresh pool element bound');
|
||||
}
|
||||
}
|
||||
function startTelemetryLoop(){
|
||||
|
|
@ -4315,33 +4336,12 @@ function activateListenerAudio(){
|
|||
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);
|
||||
}
|
||||
/* activateListenerAudio above remains as a manual recovery primitive
|
||||
* (callable from a future UI button if needed), but the page no
|
||||
* longer arms a global tap-anywhere listener — fox 2026-06-04: tap-
|
||||
* anywhere from 6c9d1b8 was rejected for adding user-visible noise.
|
||||
* Silent automatic recovery via the telemetry tick's wedge-detect +
|
||||
* pool re-lease is the shape that ships. */
|
||||
function applyAudioMute(){
|
||||
/* legacy hook still called by attachSfuTrack / startStream; for the
|
||||
* listener-tap-to-play flow we now do the work in activateListener-
|
||||
|
|
@ -5271,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> · built <span class="stamp-date">2026-06-04</span><br>
|
||||
md5 <span class="stamp-md5">c84aa4f6e7bc464e3712a8e07b7f1052</span><br>
|
||||
sha256 <span class="stamp-sha">520f5e0765afe3a6345f84236d56c53c5ec751f83992ffe9e902268e5d0702cd</span><br>
|
||||
md5 <span class="stamp-md5">10ede11a8d0ae2961b6c9736a66ece97</span><br>
|
||||
sha256 <span class="stamp-sha">ab67e30e56518083e4ecb975f20c63f41a32c8c9b2003f3e3fa7dc0f09a31b79</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