zebra-report: deploy audioCtx sinkId routing (fixes fedora chrome silence)

Pulls in zebra-report 467146a: every audioCtx now routes to the
user-picked output sink via applySinkToAudioCtx(). Fixes the
fedora chrome silent-speaker telemetry where RTP arrived, worklet
started, ctxState=running, but no audio reached the user — because
audioCtx.destination was always emitting to system default while
the speaker dropdown set <audio> sinks only. fox 2026-06-06.

Audio path now respects the picker on Chrome ≥110 and Firefox ≥116.
Older browsers continue to fall through to default.
This commit is contained in:
russell@unturf.com 2026-06-06 19:27:46 -04:00
parent e45db971aa
commit 0f36e02e42
No known key found for this signature in database

View file

@ -3272,7 +3272,14 @@ function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){
return false; return false;
} }
if (!audioCtx){ if (!audioCtx){
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
/* Route to picked output sink on first creation — the prime-on-
* gesture path already does this, but attach can also be the
* first user of audioCtx on auto-rejoin / desktop-resume paths
* where no gesture fired. fox 2026-06-06 blanka-chrome silence. */
applySinkToAudioCtx();
}
catch(e){ logLine('err','audioCtx create: '+e.message); return false; } catch(e){ logLine('err','audioCtx create: '+e.message); return false; }
} }
if (audioCtx.state === 'suspended'){ if (audioCtx.state === 'suspended'){
@ -3357,7 +3364,8 @@ function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){
if (transcribeEnabled){ if (transcribeEnabled){
startCaptureForUuid(uuid).catch(e => logLine('err', 'transcribe '+uuid.slice(0,4)+': '+e.message)); startCaptureForUuid(uuid).catch(e => logLine('err', 'transcribe '+uuid.slice(0,4)+': '+e.message));
} }
logLine('', 'audio via AudioContext '+uuid.slice(0,4)+' target='+targetSeconds+'s ctxState='+audioCtx.state); logLine('', 'audio via AudioContext '+uuid.slice(0,4)+' target='+targetSeconds+'s ctxState='+audioCtx.state
+ ' sink=' + (audioCtx.sinkId === undefined ? 'n/a' : (audioCtx.sinkId || 'default')));
return true; return true;
} }
@ -5109,10 +5117,36 @@ async function applySinkTo(el){
try { await el.setSinkId(speakerDeviceId || ''); } try { await el.setSinkId(speakerDeviceId || ''); }
catch(e){ logLine('err','setSinkId rejected: '+e.message); } catch(e){ logLine('err','setSinkId rejected: '+e.message); }
} }
/* Re-route every live remote-audio element to the newly picked sink. */ /* AudioContext-level sink routing (Chrome ≥110, Firefox ≥116). Every
* listener / speaker audio path now runs through the worklet feeding
* audioCtx.destination — if the user picked a non-default speaker in
* the UI but we only apply it to <audio> elements, the worklet still
* routes to the system default sink and the user hears nothing on
* their chosen device.
*
* Fox 2026-06-06 (telemetry from blanka-chrome speaker session): RTP
* arriving at 50pps, "jitter-buffer started", ctxState=running, yet
* "fedora chrome no audio from any speakers". The audio was being
* decoded and emitted to audioCtx.destination — the wrong sink.
*
* Best-effort: setSinkId on AudioContext is unsupported on some
* browsers / older versions; swallow errors so the worklet still
* delivers to default in that case. */
async function applySinkToAudioCtx(){
if (!audioCtx || typeof audioCtx.setSinkId !== 'function') return;
try {
await audioCtx.setSinkId(speakerDeviceId || '');
logLine('', 'audioCtx sink → '+(speakerDeviceId || 'default'));
} catch(e){
logLine('err', 'audioCtx setSinkId rejected: '+e.message);
}
}
/* Re-route every live remote-audio element AND the AudioContext to
* the newly picked sink. */
async function applySinkToAll(){ async function applySinkToAll(){
if (!spkSupported) return; if (!spkSupported) return;
for (const [, a] of remoteAudio){ await applySinkTo(a); } for (const [, a] of remoteAudio){ await applySinkTo(a); }
await applySinkToAudioCtx();
} }
function tagTrack(t){ if (t) t.contentHint = musicMode ? 'music' : 'speech'; } function tagTrack(t){ if (t) t.contentHint = musicMode ? 'music' : 'speech'; }
/* Firefox sometimes ignores the EC/NS/AGC constraints at getUserMedia time /* Firefox sometimes ignores the EC/NS/AGC constraints at getUserMedia time
@ -7753,6 +7787,17 @@ function primeAudioOnGesture(){
osc.stop(audioCtx.currentTime + 0.05); osc.stop(audioCtx.currentTime + 0.05);
osc.onended = () => { try { osc.disconnect(); g.disconnect(); } catch(_){} }; osc.onended = () => { try { osc.disconnect(); g.disconnect(); } catch(_){} };
logLine('', 'audioCtx primed state='+audioCtx.state); logLine('', 'audioCtx primed state='+audioCtx.state);
/* Route audioCtx.destination to the user-picked output sink. The
* <audio>-element setSinkId path already runs in the fan-out
* below, but the worklet/listener path runs through this
* AudioContext — without this call every speaker-monitor + every
* listener heard audio on the SYSTEM DEFAULT sink even though
* they'd picked a specific device in the dropdown. fox 2026-06-06:
* blanka-chrome speaker telemetry showed RTP arriving, worklet
* started, ctxState=running, but "no audio from any speakers"
* — audio was being routed to default instead of her selected
* monitor. */
applySinkToAudioCtx();
} catch(e){ logLine('err', 'audioCtx prime: '+e.message); } } catch(e){ logLine('err', 'audioCtx prime: '+e.message); }
/* pre-bless a pool of audio elements for unmuted autoplay. Each one /* pre-bless a pool of audio elements for unmuted autoplay. Each one
* gets play() called inside this click handler — the gesture grants * gets play() called inside this click handler — the gesture grants
@ -7964,8 +8009,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"> <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-06</span><br> <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-06</span><br>
md5 <span class="stamp-md5">bd769bd46730955722b172d51f93e673</span><br> md5 <span class="stamp-md5">43f585be4e478d110d4250eff68b9d68</span><br>
sha256 <span class="stamp-sha">a8f6964a1f1e0c87131b1df57a635659fdbbb215a29c7b0032997eb8f6bc5cfc</span><br> sha256 <span class="stamp-sha">6677c5b69c23976a7c9d35104fdc324fe69fa80d24120e1f32aaab23ebdd0606</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">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> <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> </footer>