zebra-spaces: auto-rebuild publish PCs on ICE failure (mic/screen/camera)
Repro from fox: a listener joined a room while host's publish PCs were silently in 'failed' state — saw no audio, no camera, no screen for ~60s until host left and rejoined, which created fresh publish PCs. SFU log confirmed: host's mic/screen/camera publish PCs all hit ICE failure within the first minute. SFU reaped them on OnConnectionStateChange(failed). When the listener subscribed there were zero publishers in the room — they got an SDP with only the inactive placeholder m-line and never recovered until host re-published. Page had a 'failed' rebuild path only for sfuSubPC (the subscribe side). The three publish PCs were unwatched — once Pion-on-server killed them they were dead but the page UI kept saying 'sfu: publishing as ...' with nothing actually broadcasting. New watchPublishPC() helper installs the same rebuild pattern on sfuPubPC, sfuScreenPC, sfuCameraPC: on 'failed' the dead PC is closed, the slot is nulled, and the publish entrypoint runs again. The 'wasOurs' guard prevents the rebuild from firing if the user explicitly unpublished (which also fires onconnectionstatechange). sfuGamePC gets a softer treatment — its source is a user-picked iframe via Region Capture, so auto-restart isn't safe. The page just logs the failure clearly + tears down so the share button comes back.
This commit is contained in:
parent
3a6b80e3f4
commit
8631c106d2
1 changed files with 43 additions and 2 deletions
|
|
@ -1959,9 +1959,35 @@ async function sfuPublish(){
|
|||
const ans = await res.json();
|
||||
await pc.setRemoteDescription({ type:'answer', sdp: ans.sdp });
|
||||
sfuPubPC = pc; sfuPubPeerID = ans.peer_id;
|
||||
watchPublishPC(pc, 'mic', sfuPublish, () => sfuPubPC === pc, () => { sfuPubPC = null; sfuPubPeerID = null; });
|
||||
logLine('', 'sfu: publishing as '+sfuPubPeerID);
|
||||
}
|
||||
|
||||
/* Publish-side ICE failure recovery. The SFU reaps a failed publisher
|
||||
* (OnConnectionStateChange in the Go side calls h.removePublisher), so
|
||||
* any listener that arrives during the failure window gets an SDP with
|
||||
* zero m-lines and never sees audio/video until the host re-publishes.
|
||||
* Bug observed: fxhp's mic/screen/camera all failed ICE; a listener
|
||||
* who joined while they were down got nothing until fxhp left + rejoined.
|
||||
*
|
||||
* The page now auto-rebuilds: on 'failed' on any of the publish PCs we
|
||||
* tear down the dead PC and call the publish entrypoint again. wasOurs
|
||||
* guards against the racey case where the user explicitly stopped
|
||||
* sharing between the 'failed' fire and our reaction (e.g. closing a
|
||||
* screen-share). cleanup runs synchronously before the rebuild so the
|
||||
* idempotent guard at the top of sfuPublishX (`if (sfuXPC) return`)
|
||||
* doesn't bail us out of the recovery. */
|
||||
function watchPublishPC(pc, label, rebuildFn, wasOurs, cleanup){
|
||||
pc.onconnectionstatechange = () => {
|
||||
if (pc.connectionState !== 'failed') return;
|
||||
if (!wasOurs()) return;
|
||||
logLine('err', 'sfu '+label+' publish PC failed — rebuilding');
|
||||
try { pc.close(); } catch(_){}
|
||||
cleanup();
|
||||
rebuildFn().catch(e => logLine('err','sfu '+label+' re-publish: '+e.message));
|
||||
};
|
||||
}
|
||||
|
||||
/* ----- screen share ----- */
|
||||
async function sfuPublishScreen(){
|
||||
if (sfuScreenPC || !myKeys || !roomID) return;
|
||||
|
|
@ -2027,6 +2053,9 @@ async function sfuPublishScreen(){
|
|||
if (s.track.kind === 'video') setSenderMaxBitrate(s, 6000000);
|
||||
if (s.track.kind === 'audio') setSenderMaxBitrate(s, 256000);
|
||||
}
|
||||
watchPublishPC(pc, 'screen', sfuPublishScreen,
|
||||
() => sfuScreenPC === pc,
|
||||
() => { sfuScreenPC = null; sfuScreenPeerID = null; sfuScreenStream = null; });
|
||||
logLine('', 'sfu: sharing screen as '+sfuScreenPeerID);
|
||||
/* render a muted local preview so the publisher sees what they're
|
||||
* sharing — SFU does not echo the publisher's own stream back */
|
||||
|
|
@ -2106,6 +2135,15 @@ async function sfuPublishGame(iframe){
|
|||
if (s.track.kind === 'video') setSenderMaxBitrate(s, 4000000);
|
||||
if (s.track.kind === 'audio') setSenderMaxBitrate(s, 256000);
|
||||
}
|
||||
/* game-share doesn't auto-rebuild on 'failed' — its capture source is
|
||||
* a user-selected iframe via Region Capture; the user would have to
|
||||
* pick it again anyway. Just surface the failure clearly and tear
|
||||
* down the dead PC so the share button comes back. */
|
||||
pc.onconnectionstatechange = () => {
|
||||
if (pc.connectionState !== 'failed' || sfuGamePC !== pc) return;
|
||||
logLine('err', 'sfu game-share PC failed — stop and re-share to recover');
|
||||
sfuUnpublishGame().catch(()=>{});
|
||||
};
|
||||
logLine('', 'sfu: sharing gameplay as '+sfuGamePeerID);
|
||||
}
|
||||
async function sfuUnpublishGame(){
|
||||
|
|
@ -2156,6 +2194,9 @@ async function sfuPublishCamera(){
|
|||
for (const s of pc.getSenders()){
|
||||
if (s.track && s.track.kind === 'video') setSenderMaxBitrate(s, 1500000);
|
||||
}
|
||||
watchPublishPC(pc, 'camera', sfuPublishCamera,
|
||||
() => sfuCameraPC === pc,
|
||||
() => { sfuCameraPC = null; sfuCameraPeerID = null; sfuCameraStream = null; });
|
||||
logLine('', 'sfu: camera on as '+sfuCameraPeerID);
|
||||
renderCameraTile(myKeys.pubHex, stream, { local: true });
|
||||
$('btn-camera-share').classList.add('hidden');
|
||||
|
|
@ -3617,8 +3658,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-02</span><br>
|
||||
md5 <span class="stamp-md5">88a1041dc605ad408b4bfd87fa8d441e</span><br>
|
||||
sha256 <span class="stamp-sha">24867165e436c9d417bdb3709bac7ddf1f374a0ffbf95b84742d00423e645dc7</span><br>
|
||||
md5 <span class="stamp-md5">b3db2fd79fc51b75927a92257dee6295</span><br>
|
||||
sha256 <span class="stamp-sha">08421e83202e6042104b1b06b7f5075eab58c844f5b8109d9a00f5d295cd1cde</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