zebra-spaces: re-apply sink on every attach + per-tick ctx.sink telemetry (kill blanka-chrome sink drift)

After caa0548 (fixes A + B for ticket 0001), blanka-chrome was still
silent. Fresh telemetry pinned a third failure mode:

  21:31:31  audio via AudioContext 47e5 sink=c38572ec…   ← good
  …leave + rejoin…
  21:31:33  audio via AudioContext 47e5 sink=default     ← drifted

No `audioCtx sink → c38572ec…` log line between the two attaches.
applySinkToAudioCtx was only invoked inside the `if (!audioCtx)` branch
in attachAudioStreamViaWorklet — a persistent audioCtx whose .sinkId
getter returned '' (system default) after a leave/rejoin never got
its sink re-applied, so every subsequent attach emitted to the system
default speaker instead of the device the user picked in the dropdown.

Fix: when audioCtx already exists AND speakerDeviceId is set AND
audioCtx.sinkId !== speakerDeviceId, call applySinkToAudioCtx() to
restore routing. Idempotent (same-sink call is a noop).

Telemetry: every 5s tick now includes `ctx.sink=…` and `want=…` when
the active sink doesn't match the picked one, so drift is visible
without grepping for the rare attach event.

Test: attach re-applies sink when audioCtx persists across leave/rejoin
and the picked sink drifted. Sandbox now exposes setSpeakerDeviceId so
the test can drive the picked-device path through the real
attachAudioStreamViaWorklet branch.

Ticket 0001 updated with the second-pass telemetry and fix (C).
This commit is contained in:
Russell Ballestrini 2026-06-07 17:42:12 -04:00
parent b9e254172b
commit 4cedd85dc6
No known key found for this signature in database
3 changed files with 115 additions and 6 deletions

View file

@ -3281,6 +3281,19 @@ function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){
applySinkToAudioCtx();
}
catch(e){ logLine('err','audioCtx create: '+e.message); return false; }
} else if (speakerDeviceId
&& typeof audioCtx.setSinkId === 'function'
&& audioCtx.sinkId !== speakerDeviceId){
/* audioCtx survived a leave→re-enter but its sinkId drifted back
* to '' (system default). Re-apply the picked sink so the worklet
* keeps emitting to the device the user picked in the dropdown.
* Fox 2026-06-07 blanka-chrome telemetry: first attach
* `sink=c38572ec…`, a leave/rejoin later, next attach
* `sink=default` and no `audioCtx sink → …` success line —
* applySinkToAudioCtx only ran inside the `!audioCtx` branch above,
* so a persistent audioCtx with reset sinkId stayed default
* forever. Idempotent: same-sink call is a noop. */
applySinkToAudioCtx();
}
if (audioCtx.state === 'suspended'){
audioCtx.resume().catch(()=>{});
@ -5830,6 +5843,23 @@ async function dumpTelemetry(){
/* reset per-tick whisper counters */
_whisperTick.sent = 0; _whisperTick.dropped = 0;
_whisperTick.emitted = 0; _whisperTick.totalLatencyMs = 0;
/* audioCtx sink visibility on EVERY tick — the one-shot 'sink=…' in
* the attach log only captures the moment of attach; if the sink
* drifts later (Chrome's audioCtx.sinkId getter can revert to '' on
* device-changes, leave/rejoin, suspend/resume), this surfaces it
* every 5s instead of needing to grep for the rare attach event.
* Fox 2026-06-07 blanka-chrome: per-attach sink looked fine but the
* silent-listener state persisted because the GETTER value drifted
* between attaches. */
if (audioCtx){
const cs = (audioCtx.sinkId === undefined) ? 'n/a'
: (audioCtx.sinkId || 'default');
const want = speakerDeviceId || 'default';
parts.push('ctx.sink=' + (cs.length > 12 ? cs.slice(0,8) : cs)
+ (cs !== want && audioCtx.sinkId !== undefined
? ' want=' + (want.length > 12 ? want.slice(0,8) : want)
: ''));
}
/* receiver stats from sfuSubPC (the main listener path) */
if (sfuSubPC && typeof sfuSubPC.getStats === 'function'){
try {
@ -8183,8 +8213,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-07</span><br>
md5 <span class="stamp-md5">bb5c7dea19fcb19f88c479d1cb26f24f</span><br>
sha256 <span class="stamp-sha">03e08e3499cf18fac3aec4b5dbeb808e6b3b6bceb5222ca560026c6d65864d38</span><br>
md5 <span class="stamp-md5">1e77ac2d270d5b6b8697257a6d7f43cc</span><br>
sha256 <span class="stamp-sha">6b76bd2486a0b366599cc8e22c1e11d51072e3bf09df3afed6f0208e5af8b0a9</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>