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:
parent
4cedd85dc6
commit
1984bb1d41
2 changed files with 34 additions and 204 deletions
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue