zebra-spaces: always-SFU receive audio + 0.7s worklet cushion

SFU is the only receive-audio path. Mesh PCs carry our outbound
mic; their inbound audio is ignored. handleRemoteSfuTrack always
attaches the SFU stream regardless of mesh peer state. mesh
pc.ontrack is a no-op breadcrumb.

SPEAKER_PLAYOUT_DELAY_SEC bumped 0.5s → 0.7s so the worklet
cushion absorbs the 200ms host-wiggle that mesh's lower-latency
path used to absorb. DD_BASE_TARGET_SEC tracks at 0.7s as the
Double Dragon adaptive floor.

Tests pin the new contract:
  - SPEAKER_PLAYOUT_DELAY_SEC === 0.7
  - SFU attaches across every mesh peer state (connected,
    failed, connecting, disconnected, missing)
  - in-place setWorkletStream still works (renegotiation path)
This commit is contained in:
Russell Ballestrini 2026-06-07 19:33:58 -04:00
parent 4cedd85dc6
commit 1984bb1d41
No known key found for this signature in database
2 changed files with 34 additions and 204 deletions

View file

@ -383,8 +383,8 @@ console.log('listener audio attach:');
test('shipped: RECV_PLAYOUT_DELAY_SEC is 4s (fox-approved for cellular music; do not dial back without sign-off)', () => {
eq(RECV, 4.0, 'RECV_PLAYOUT_DELAY_SEC');
});
test('shipped: SPEAKER_PLAYOUT_DELAY_SEC is 0.5s (conversational latency for speakers/cohost/host)', () => {
eq(SPEAKER, 0.5, 'SPEAKER_PLAYOUT_DELAY_SEC');
test('shipped: SPEAKER_PLAYOUT_DELAY_SEC is 0.7s (worklet cushion on SFU-only receive path absorbs 200ms host wiggle)', () => {
eq(SPEAKER, 0.7, 'SPEAKER_PLAYOUT_DELAY_SEC');
});
/* -------- unit: attachAudioStreamViaWorklet -------- */
@ -953,87 +953,38 @@ test('speaker: SFU ontrack arrives, mesh PC NOT connected → attach proceeds (m
b.api.handleRemoteSfuTrack(fakeOntrack(PUB_A, 'mic', stream));
truthy(b.api.listenerAudioNodes.has(UUID_A), 'chain built via SFU');
const node = b.api.listenerAudioNodes.get(UUID_A);
eq(node.targetSeconds, SPEAKER, '0.5s target for speaker');
eq(node.targetSeconds, SPEAKER, '0.7s target for speaker');
truthy(reachable(node.src, b.ctx.destination), 'reaches destination');
});
test('speaker: SFU ontrack with mesh peer state=connected → SFU attach SKIPPED (no double-chain with mesh)', () => {
/* Pins the canSpeak()+meshState branch at handleRemoteSfuTrack
* ~line 4760. If this guard regresses, every speaker with a mesh
* peer ends up with two chains for the same publisher: one via
* SFU, one via mesh. fox: "fxhp-phone hearing double as speaker."
* Closing Firefox cleared it because the rebuilt PC didn't race
* the SFU ontrack as harshly. */
const b = makeBrowser('speaker');
addMember(b, UUID_A, PUB_A);
b.api.peers.set(UUID_A, { connectionState: 'connected' });
const stream = makeStream();
b.api.handleRemoteSfuTrack(fakeOntrack(PUB_A, 'mic', stream));
falsy(b.api.listenerAudioNodes.has(UUID_A), 'no SFU chain — mesh owns this peer');
/* but cache is still primed so a later mesh-fail can recover */
truthy(b.api.sfuStreamsByPubHex.size > 0, 'SFU stream cached for fallback');
test('speaker: SFU ontrack ALWAYS attaches regardless of mesh peer state — mesh inbound audio is ignored', () => {
/* SFU is the only receive-audio path. Mesh peer state (connected,
* failed, new, missing) must never gate the SFU attach. */
for (const meshState of ['connected', 'failed', 'connecting', 'disconnected']){
const b = makeBrowser('speaker');
addMember(b, UUID_A, PUB_A);
b.api.peers.set(UUID_A, { connectionState: meshState });
const stream = makeStream();
b.api.handleRemoteSfuTrack(fakeOntrack(PUB_A, 'mic', stream));
truthy(b.api.listenerAudioNodes.has(UUID_A), 'SFU attached (mesh state='+meshState+')');
}
});
test('speaker: SFU ontrack with mesh peer state=failed → attach PROCEEDS (mesh is dead, SFU must take over)', () => {
/* The "promoted but nobody hears them" regression a stale 'failed'
* mesh entry must not block SFU. */
test('speaker: setWorkletStream still supports in-place source swap (used internally by attachAudioStreamViaWorklet for renegotiation, not by mesh anymore)', () => {
const b = makeBrowser('speaker');
addMember(b, UUID_A, PUB_A);
b.api.peers.set(UUID_A, { connectionState: 'failed' });
const stream = makeStream();
b.api.handleRemoteSfuTrack(fakeOntrack(PUB_A, 'mic', stream));
truthy(b.api.listenerAudioNodes.has(UUID_A), 'SFU took over from failed mesh');
});
test('speaker: SFU first then mesh — setWorkletStream swaps in place, single chain', () => {
/* This is the common mesh path: SFU subscribe brings audio first
* (because subscribe runs immediately), then mesh PC connects a
* second or two later and ontrack fires, swapping the source. */
const b = makeBrowser('speaker');
addMember(b, UUID_A, PUB_A);
const sfuStream = makeStream();
b.api.attachSfuTrack(UUID_A, sfuStream);
b.api.attachSfuTrack(UUID_A, makeStream());
const node = b.api.listenerAudioNodes.get(UUID_A);
const dest = b.ctx.destination;
truthy(node, 'chain built via SFU');
/* simulate mesh ontrack: swap source in place exact code path the
* connectToPeer ontrack handler uses (web/zebra-spaces.html:6596) */
const meshStream = makeStream();
const swapOk = b.api.setWorkletStream(UUID_A, meshStream);
eq(swapOk, true, 'mesh swap succeeded');
const newStream = makeStream();
const swapOk = b.api.setWorkletStream(UUID_A, newStream);
eq(swapOk, true, 'in-place swap succeeded');
const after = b.api.listenerAudioNodes.get(UUID_A);
eq(after, node, 'same node — no rebuild');
eq(after.gain, node.gain, 'same gain — no rewire to destination');
eq(after.stream, meshStream, 'tracks mesh stream now');
truthy(reachable(after.src, dest), 'mesh src reaches destination');
});
test('speaker: mesh ontrack BEFORE SFU ontrack → mesh attaches via fallback, SFU then skips because mesh.connectionState=connected', () => {
/* Less common but possible: mesh connects faster than SFU subscribe
* negotiation (e.g. STUN binding cached). Mesh's ontrack runs
* setWorkletStream which fails (no existing chain), falls through
* to attachAudioStreamViaWorklet to build one. Then SFU ontrack
* fires; canSpeak+mesh.connected skip. */
const b = makeBrowser('speaker');
addMember(b, UUID_A, PUB_A);
/* fake mesh ontrack effect: chain doesn't exist yet, setWorkletStream
* returns false, fallback builds chain at SPEAKER target */
const meshStream = makeStream();
const swapOk = b.api.setWorkletStream(UUID_A, meshStream);
eq(swapOk, false, 'setWorkletStream false — no chain yet');
b.api.attachAudioStreamViaWorklet(UUID_A, meshStream, SPEAKER);
truthy(b.api.listenerAudioNodes.has(UUID_A), 'chain built from mesh fallback');
/* now SFU ontrack arrives with mesh marked connected */
b.api.peers.set(UUID_A, { connectionState: 'connected' });
const sfuStream = makeStream();
b.api.handleRemoteSfuTrack(fakeOntrack(PUB_A, 'mic', sfuStream));
/* still only one chain — the one mesh built. SFU should have skipped. */
eq(b.api.listenerAudioNodes.size, 1, 'one chain total');
eq(b.api.listenerAudioNodes.get(UUID_A).stream, meshStream, 'still pointing at mesh stream');
truthy(reachable(after.src, dest), 'new src reaches destination');
});
test('speaker: rapid mesh re-ontracks for same publisher → single chain, no duplicate gains or sources stacked on destination', () => {