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:
parent
b9e254172b
commit
4cedd85dc6
3 changed files with 115 additions and 6 deletions
|
|
@ -252,6 +252,15 @@ function makeBrowser(role){
|
|||
return el;
|
||||
}
|
||||
function applySinkTo(){ return Promise.resolve(); }
|
||||
function applySinkToAudioCtx(){
|
||||
/* sandbox stub for the sink-routing fan-out — real impl awaits
|
||||
* audioCtx.setSinkId(speakerDeviceId). Tests that care about
|
||||
* the setSinkId call inspect ctx._setSinkCalls in the override
|
||||
* below; this stub just hits the same code path. */
|
||||
try { audioCtx.setSinkId(speakerDeviceId || ''); } catch(_){}
|
||||
}
|
||||
let speakerDeviceId = '';
|
||||
function setSpeakerDeviceId(id){ speakerDeviceId = id || ''; }
|
||||
function startMeter(){}
|
||||
function stopMeter(){}
|
||||
function registerLipSyncAudio(){}
|
||||
|
|
@ -320,7 +329,7 @@ function makeBrowser(role){
|
|||
' attachAudioStreamViaWorklet, attachListenerStreamViaAudioContext,\n' +
|
||||
' attachSfuTrack, flushSfuStreams, setWorkletStream, detachListenerStream,\n' +
|
||||
' handleRemoteSfuTrack,\n' +
|
||||
' triggerWorkletReady, setRole,\n' +
|
||||
' triggerWorkletReady, setRole, setSpeakerDeviceId,\n' +
|
||||
' __seedLipSync,\n' +
|
||||
' get audioCtx(){ return audioCtx; },\n' +
|
||||
' get workletReady(){ return workletReady; },\n' +
|
||||
|
|
@ -1076,6 +1085,38 @@ test('audioCtx.setSinkId is called on creation so the worklet routes to the user
|
|||
truthy(typeof b.ctx.setSinkId === 'function', 'setSinkId is wired');
|
||||
});
|
||||
|
||||
test('attach re-applies sink when audioCtx persists across leave/rejoin and the picked sink drifted (kills blanka-chrome silent-after-rejoin)', () => {
|
||||
/* Fox 2026-06-07 blanka-chrome telemetry showed:
|
||||
* - first attach: audio via AudioContext ... sink=c38572ec… (good)
|
||||
* - leave + re-enter the space
|
||||
* - second attach: audio via AudioContext ... sink=default (BAD — sink reverted)
|
||||
* - no `audioCtx sink → c38572ec…` log between them
|
||||
*
|
||||
* Root cause: applySinkToAudioCtx() only ran inside the
|
||||
* `if (!audioCtx)` branch — fresh ctx → apply sink. A persistent
|
||||
* audioCtx that somehow had its sinkId getter return '' (default)
|
||||
* stayed default forever, because attach saw audioCtx already
|
||||
* existed and skipped the apply.
|
||||
*
|
||||
* Pin the fix: when audioCtx exists AND speakerDeviceId is set AND
|
||||
* the ctx's current sinkId !== speakerDeviceId, attach must call
|
||||
* setSinkId again to restore routing. */
|
||||
const b = makeBrowser('listener');
|
||||
addMember(b, UUID_A, PUB_A);
|
||||
const calls = [];
|
||||
b.ctx.setSinkId = function(id){
|
||||
calls.push(id);
|
||||
/* simulate setSinkId behaviour: the getter now reflects the call */
|
||||
b.ctx.sinkId = id;
|
||||
return Promise.resolve();
|
||||
};
|
||||
b.ctx.sinkId = ''; /* default sink */
|
||||
b.api.setSpeakerDeviceId('dev-1234'); /* user picked a specific device */
|
||||
b.api.attachAudioStreamViaWorklet(UUID_A, makeStream(), RECV);
|
||||
truthy(calls.includes('dev-1234'),
|
||||
'attach called setSinkId("dev-1234") to restore the picked sink on the persistent audioCtx');
|
||||
});
|
||||
|
||||
test('audioCtx without setSinkId support (older browser) does not throw — silent fallback to default', () => {
|
||||
/* Firefox <116 and Chrome <110 don't have audioCtx.setSinkId.
|
||||
* applySinkToAudioCtx must check before calling. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue