zebra-report: deploy — handleRemoteSfuTrack refactor (no behavior change)
This commit is contained in:
parent
b3192df9fe
commit
33f0fbec55
1 changed files with 105 additions and 96 deletions
|
|
@ -2415,103 +2415,112 @@ async function sfuUnpublish(){
|
|||
}
|
||||
}
|
||||
|
||||
/* handleRemoteSfuTrack — receive-side mesh state machine. Called once
|
||||
* per ontrack on the SFU sub PC. Extracted into a named function so
|
||||
* test/multi-peer-mesh.test.js can drive it directly with synthetic
|
||||
* events and verify the mesh invariants without needing a real
|
||||
* RTCPeerConnection or SFU. The signature mirrors a real RTCTrackEvent:
|
||||
* .streams[0] (MediaStream), .track (MediaStreamTrack), .receiver
|
||||
* (RTCRtpReceiver — optional, only used for playoutDelayHint). */
|
||||
function handleRemoteSfuTrack(ev){
|
||||
const sid = ev.streams[0] ? ev.streams[0].id : '';
|
||||
if (!sid) 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' || kind === 'camera' || kind === 'game'){
|
||||
logLine('', 'sfu ontrack: kind=' + kind + ' pub=' + pubHex +
|
||||
' track=' + ev.track.kind + ' mute=' + ev.track.muted + ' state=' + ev.track.readyState);
|
||||
}
|
||||
/* MSID-supplant safety: the SFU re-uses the same streamID
|
||||
* (`shortPub-kind`) when a publisher supplants themselves. WebRTC
|
||||
* merges the new track into the EXISTING MediaStream — ev.streams[0]
|
||||
* is literally the same instance as before, containing both the
|
||||
* dead old track AND the new live one. Setting srcObject to that
|
||||
* stream doesn't switch the playing track; the video element keeps
|
||||
* showing the (now-ended) old track's last frame and reports muted.
|
||||
* Construct a fresh MediaStream containing only the new track so
|
||||
* the video element binds to the new RTP flow cleanly.
|
||||
*
|
||||
* Stream-identity guard on removeFn: when the OLD track's mute →
|
||||
* ended → removeFn would tear down the tile that the NEW track
|
||||
* just installed. Only remove if the stream we registered against
|
||||
* is still the one in the store for this pub. */
|
||||
if (kind === 'screen'){
|
||||
const s = new MediaStream([ev.track]);
|
||||
screenStreams.set(pubHex, s);
|
||||
renderScreenTile(pubHex, s);
|
||||
watchVideoTrackForRemoval(ev.track, () => { if (screenStreams.get(pubHex) === s) removeScreenTile(pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_SCREEN_MS);
|
||||
return;
|
||||
}
|
||||
if (kind === 'camera'){
|
||||
const s = new MediaStream([ev.track]);
|
||||
cameraStreams.set(pubHex, s);
|
||||
renderCameraTile(pubHex, s);
|
||||
watchVideoTrackForRemoval(ev.track, () => { if (cameraStreams.get(pubHex) === s) removeCameraTile(pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_MS);
|
||||
return;
|
||||
}
|
||||
if (kind === 'game'){
|
||||
/* a publisher is sharing their gameplay (Region-Capture cropped
|
||||
* iframe). Route to its own TILE_KIND so it coexists with a
|
||||
* normal screen-share from the same person. */
|
||||
const s = new MediaStream([ev.track]);
|
||||
gameStreams.set(pubHex, s);
|
||||
renderVideoTile('gameshare', pubHex, s);
|
||||
watchVideoTrackForRemoval(ev.track, () => { if (gameStreams.get(pubHex) === s) removeVideoTile('gameshare', pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_SCREEN_MS);
|
||||
return;
|
||||
}
|
||||
if (kind !== 'mic'){
|
||||
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(_){}
|
||||
/* 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){
|
||||
/* 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;
|
||||
}
|
||||
} catch(_){}
|
||||
}
|
||||
/* no matching member yet — flushSfuStreams will attach on peer-joined */
|
||||
}
|
||||
|
||||
async function sfuSubscribe(){
|
||||
if (sfuSubPC || !roomID) return;
|
||||
const pc = new RTCPeerConnection(rtcConfig);
|
||||
pc.ontrack = (ev) => {
|
||||
const sid = ev.streams[0] ? ev.streams[0].id : '';
|
||||
if (!sid) 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' || kind === 'camera' || kind === 'game'){
|
||||
logLine('', 'sfu ontrack: kind=' + kind + ' pub=' + pubHex +
|
||||
' track=' + ev.track.kind + ' mute=' + ev.track.muted + ' state=' + ev.track.readyState);
|
||||
}
|
||||
/* MSID-supplant safety: the SFU re-uses the same streamID
|
||||
* (`shortPub-kind`) when a publisher supplants themselves. WebRTC
|
||||
* merges the new track into the EXISTING MediaStream — ev.streams[0]
|
||||
* is literally the same instance as before, containing both the
|
||||
* dead old track AND the new live one. Setting srcObject to that
|
||||
* stream doesn't switch the playing track; the video element keeps
|
||||
* showing the (now-ended) old track's last frame and reports muted.
|
||||
* Construct a fresh MediaStream containing only the new track so
|
||||
* the video element binds to the new RTP flow cleanly.
|
||||
*
|
||||
* Stream-identity guard on removeFn: when the OLD track's mute →
|
||||
* ended → removeFn would tear down the tile that the NEW track
|
||||
* just installed. Only remove if the stream we registered against
|
||||
* is still the one in the store for this pub. */
|
||||
if (kind === 'screen'){
|
||||
const s = new MediaStream([ev.track]);
|
||||
screenStreams.set(pubHex, s);
|
||||
renderScreenTile(pubHex, s);
|
||||
watchVideoTrackForRemoval(ev.track, () => { if (screenStreams.get(pubHex) === s) removeScreenTile(pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_SCREEN_MS);
|
||||
return;
|
||||
}
|
||||
if (kind === 'camera'){
|
||||
const s = new MediaStream([ev.track]);
|
||||
cameraStreams.set(pubHex, s);
|
||||
renderCameraTile(pubHex, s);
|
||||
watchVideoTrackForRemoval(ev.track, () => { if (cameraStreams.get(pubHex) === s) removeCameraTile(pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_MS);
|
||||
return;
|
||||
}
|
||||
if (kind === 'game'){
|
||||
/* a publisher is sharing their gameplay (Region-Capture cropped
|
||||
* iframe). Route to its own TILE_KIND so it coexists with a
|
||||
* normal screen-share from the same person. */
|
||||
const s = new MediaStream([ev.track]);
|
||||
gameStreams.set(pubHex, s);
|
||||
renderVideoTile('gameshare', pubHex, s);
|
||||
watchVideoTrackForRemoval(ev.track, () => { if (gameStreams.get(pubHex) === s) removeVideoTile('gameshare', pubHex); }, VIDEO_REMOVE_MUTE_WINDOW_SCREEN_MS);
|
||||
return;
|
||||
}
|
||||
if (kind !== 'mic'){
|
||||
logLine('', 'sfu: unknown kind '+kind+' from '+pubHex);
|
||||
return;
|
||||
}
|
||||
/* mic audio — 400ms jitter-buffer target absorbs Wi-Fi peak jitter */
|
||||
try { ev.receiver.playoutDelayHint = 0.4; } 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]);
|
||||
for (const [uuid, mm] of members){
|
||||
try {
|
||||
if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){
|
||||
/* 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;
|
||||
}
|
||||
} catch(_){}
|
||||
}
|
||||
/* no matching member yet — flushSfuStreams will attach on peer-joined */
|
||||
};
|
||||
pc.ontrack = handleRemoteSfuTrack;
|
||||
/* server-initiated offer: POST /subscribe (empty body) — SFU answers with
|
||||
* an SDP offer containing one m-line per current publisher. We answer it
|
||||
* and POST the answer back, which completes the initial handshake. */
|
||||
|
|
@ -3889,8 +3898,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> · built <span class="stamp-date">2026-06-03</span><br>
|
||||
md5 <span class="stamp-md5">e5d7d386ae1ed988094498aa9e352e29</span><br>
|
||||
sha256 <span class="stamp-sha">260d3562b39b72d508f00041f6e3dc9772a2fc747fc3e585c302c89c79140d9d</span><br>
|
||||
md5 <span class="stamp-md5">60524e74b32d941769b3ea31eb9d067c</span><br>
|
||||
sha256 <span class="stamp-sha">c16114b2737d13d6bb8699e3daa59d6702092fa789f1066241b51b1fea9c1dc6</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue