From 1920cf6a556eeeaae8fc4af4b562f467ea664219 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Wed, 3 Jun 2026 13:19:38 -0400 Subject: [PATCH] zebra-spaces: bump playoutDelayHint 400ms -> 700ms for cross-Wi-Fi voice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reports of robot-voice artifacts from a peer on a different Wi-Fi — classic symptom of jitter-buffer under-runs (Opus PLC kicks in, generates synthetic samples that sound vocoder-y). 400ms was tight enough that bursty inter-arrival on cross-AP / cellular hand-off networks would exhaust the cushion. 700ms costs barely-audible end-to-end latency (well under the 1s people perceive as 'walkie talkie'), well above typical Wi-Fi peak jitter, and matches both the SFU subscribe path and mesh peer path so the room sounds consistent regardless of how the audio is being delivered. Bitrate / FEC / DTX settings unchanged — adding bandwidth would make a lossy link worse, not better. --- web/zebra-spaces.html | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 6b4fe68..6a64d39 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -2523,8 +2523,12 @@ function handleRemoteSfuTrack(ev){ logLine('', 'sfu: unknown kind '+kind+' from '+pubHex); return; } - /* mic audio — 400ms jitter-buffer target absorbs Wi-Fi peak jitter */ - try { if (ev.receiver) ev.receiver.playoutDelayHint = 0.4; } catch(_){} + /* mic audio — 700ms jitter-buffer target absorbs cross-Wi-Fi peak + * jitter and burst packet loss. 400ms was too tight for users on + * different APs / cellular hand-offs — under-runs were producing + * the 'robot voice' artifact. 700ms costs an inaudible bit of + * end-to-end latency in exchange for clean playback under loss. */ + try { if (ev.receiver) ev.receiver.playoutDelayHint = 0.7; } catch(_){} /* cache by full pubkey (already resolved above) so it survives the * member's session uuid changing across leave/rejoin */ sfuStreamsByPubHex.set(pubHex, ev.streams[0]); @@ -3494,9 +3498,10 @@ async function connectToPeer(uuid, weOffer){ let a = remoteAudio.get(uuid); if (!a){ a = document.createElement('audio'); a.autoplay = true; document.body.appendChild(a); remoteAudio.set(uuid, a); } a.srcObject = ev.streams[0] || new MediaStream([ev.track]); - /* 400ms jitter buffer — Wi-Fi peak inter-arrival is multiples of - * the smoothed jitter, so a generous buffer absorbs the bursts */ - try { ev.receiver.playoutDelayHint = 0.4; } catch(_){} + /* 700ms jitter buffer — matches the SFU path. Cross-Wi-Fi and + * cellular hand-off jitter ate into the old 400ms cushion and + * produced robot-voice artifacts on tighter networks. */ + try { ev.receiver.playoutDelayHint = 0.7; } catch(_){} stopMeter(uuid); startMeter(uuid, a.srcObject); }; pc.onicecandidate = (ev) => { /* using waitForIceGathering pattern, candidates ignored */ }; @@ -4086,8 +4091,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');