diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index 349cf82..19e3bf3 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/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');