From 6beb138490cab0690d89d7b255c48618d9c67789 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Tue, 2 Jun 2026 10:52:55 -0400 Subject: [PATCH] =?UTF-8?q?zebra-spaces:=20parse=20SFU=20streamID=20with?= =?UTF-8?q?=20short-pubkey=20+=20dash=20=E2=80=94=20Firefox=20compat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with proxy.unturf.com#9575418. Firefox enforces RFC 7941 msid strictly: 1*64 token-chars, no ':'. Our streamID was pubkey + ':kind' = 71 chars with an invalid separator, so Firefox silently dropped every track and the listener saw only the game iframe. Client parse now: - format SHORT16HEX or SHORT16HEX-screen / SHORT16HEX-camera - 16-char prefix resolved back to the full pubhex via member roster - own-publish echo check matches by prefix (myKeys.pubHex.startsWith) - screens / cameras keep using full pubhex as the map key so existing identity-keyed code (renderScreenTile, removeCameraTile, screenStreams map, etc.) doesn't have to change --- web/zebra-spaces.html | 88 +++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 349cf82..19e3bf3 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -1423,53 +1423,59 @@ async function sfuSubscribe(){ pc.ontrack = (ev) => { const sid = ev.streams[0] ? ev.streams[0].id : ''; if (!sid) return; - /* streamID format: PUBKEY (mic) | PUBKEY:screen | PUBKEY:camera. - * Route video kinds to the tile renderer; mics to the audio path. */ - const colon = sid.indexOf(':'); - if (colon > 0){ - const pubHex = sid.slice(0, colon); - const kind = sid.slice(colon + 1); - /* skip echo of our own publish — we already render a local preview */ - if (myKeys && pubHex === myKeys.pubHex) return; - /* video kinds: when the publisher unshares, the SFU stops the - * transceiver and the corresponding remote track fires 'ended'. - * Wire that to the tile remover so the listener's UI matches the - * publisher's state instead of holding a frozen last frame. */ - if (kind === 'screen'){ - screenStreams.set(pubHex, ev.streams[0]); - renderScreenTile(pubHex, ev.streams[0]); - ev.track.addEventListener('ended', () => removeScreenTile(pubHex)); - return; - } - if (kind === 'camera'){ - cameraStreams.set(pubHex, ev.streams[0]); - renderCameraTile(pubHex, ev.streams[0]); - ev.track.addEventListener('ended', () => removeCameraTile(pubHex)); - return; - } + /* streamID format (RFC 7941 compliant — Firefox enforces 1*64 token- + * chars and rejects ':'): SHORT16HEX (mic) | SHORT16HEX-screen | + * SHORT16HEX-camera. Resolve the 16-char prefix back to a member's + * full pubkey via lookup so the rest of the code keeps using full + * pubhex as identity. */ + const dash = sid.indexOf('-'); + let pubHex16, kind; + if (dash > 0){ + pubHex16 = sid.slice(0, dash); + kind = sid.slice(dash + 1); + } else { + pubHex16 = sid; + kind = 'mic'; + } + /* skip echo of our own publish — match by prefix */ + if (myKeys && myKeys.pubHex.startsWith(pubHex16)) return; + /* resolve short prefix → full pubhex via member roster */ + let pubHex = pubHex16; + for (const [, mm] of members){ + try { + if (mm.pubkey){ + const fh = hex(unb64(mm.pubkey)); + if (fh.startsWith(pubHex16)){ pubHex = fh; break; } + } + } catch(_){} + } + if (kind === 'screen'){ + screenStreams.set(pubHex, ev.streams[0]); + renderScreenTile(pubHex, ev.streams[0]); + ev.track.addEventListener('ended', () => removeScreenTile(pubHex)); + return; + } + if (kind === 'camera'){ + cameraStreams.set(pubHex, ev.streams[0]); + renderCameraTile(pubHex, ev.streams[0]); + ev.track.addEventListener('ended', () => removeCameraTile(pubHex)); + return; + } + if (kind !== 'mic'){ logLine('', 'sfu: unknown kind '+kind+' from '+shortHex(pubHex)); return; } - /* mic audio — 400ms jitter-buffer target. Mobile/Wi-Fi peak inter- - * arrival is 3-5x the smoothed jitter reported by getStats, so a - * smoothed 50ms means 150-250ms peaks. 400ms absorbs that and feels - * fine for music broadcasts; conversation gains ~quarter-second of - * delay which is well below noticeable. */ + /* mic audio — 400ms jitter-buffer target absorbs Wi-Fi peak jitter */ try { ev.receiver.playoutDelayHint = 0.4; } catch(_){} - const pubHex = sid; - /* never play our own mic back to ourselves — the SFU echoes our - * publish to every sub including our own (we always subscribe now) */ - if (myKeys && pubHex === myKeys.pubHex) return; - /* cache stream by publisher pubkey so it survives the member's session - * uuid changing across leave/rejoin — see flushSfuStreams */ + /* 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]); for (const [uuid, mm] of members){ try { if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){ - /* if we're a speaker and already have a mesh peer with this - * member, mesh carries their audio with lower latency — skip - * the duplicate SFU mic. Screens + cameras still come through - * because they're handled above. */ + /* speakers get their peers' audio via mesh (lower latency) — + * skip the duplicate SFU mic. screens + cameras still came + * through above. */ if (canSpeak(myRole) && peers.has(uuid)) return; attachSfuTrack(uuid, ev.streams[0]); return; @@ -2708,8 +2714,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');