zebra-spaces: parse SFU streamID with short-pubkey + dash — Firefox compat

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
This commit is contained in:
Russell Ballestrini 2026-06-02 10:52:55 -04:00
parent bd770230f9
commit 6beb138490
No known key found for this signature in database

View file

@ -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');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">181a0e0d4e4da7c4a40e2e5e576954fd</span><br>
sha256 <span class="stamp-sha">29024b972f5dede7d5fd2b924170aaf9aa097afc9bac4402e86b01f9d543a4ed</span><br>
md5 <span class="stamp-md5">dee696071acb10104b0a0c68024e9a68</span><br>
sha256 <span class="stamp-sha">7fc20fb17b274f1bf5f667101e705530057cf859984a0dd386d83198ee65439e</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>