zebra-spaces: rebuild sub PC on remote-driven 'closed' (not just 'failed')

When the SFU closes the listener's sub PC server-side (e.g. via the
wedge-recovery in renegotiateLocked), the browser's
pc.connectionState transitions to 'closed', not 'failed'. The
existing handler only triggered a rebuild on 'failed' and
explicitly returned on 'closed' (under the assumption that 'closed'
== self-teardown). That assumption holds for sfuUnsubscribe (which
nulls sfuSubPC BEFORE pc.close()), but NOT for remote-driven
closes — sfuSubPC === pc is still true, the guard sees
'unexpected close', and we should re-subscribe.

Without this branch the host's sub stayed at sub=none indefinitely
after any server-driven close — every subsequent addPubToSub for
the host went into the void. Fox 2026-06-04 lost cohost camera +
screen share via this exact path after the wedge-recovery falsely
fired on his fresh subscribe (SFU fix 8c5fd65 prevents the false
positive going forward, this fix ensures the page recovers on any
legitimate server close).

Both 'failed' and 'closed' now rebuild via sfuUnsubscribe +
sfuSubscribe, gated on sfuSubPC === pc to catch only remote-driven
state changes.
This commit is contained in:
Russell Ballestrini 2026-06-04 13:27:59 -04:00
parent 545ba93557
commit 09a451697a
No known key found for this signature in database

View file

@ -2911,15 +2911,23 @@ async function sfuSubscribe(){
* a hard refresh handles the WS side; this handles the SFU side. */
pc.onconnectionstatechange = () => {
/* 'failed' is terminal ICE failure (we should rebuild).
* 'closed' is OUR OWN sfuUnsubscribe() — never rebuild on that
* (Chrome fires the state change synchronously before sfuSubPC is
* null'd, which used to trigger a subscribe→close→subscribe loop
* every time anyone deliberately tore down the sub).
* 'closed' coming from our OWN sfuUnsubscribe() must NOT rebuild
* (subscribe→close→subscribe loop). sfuUnsubscribe nulls
* sfuSubPC BEFORE pc.close(), so the `sfuSubPC === pc` guard
* catches self-teardown — the guard fails and we early-return.
* 'closed' coming from the SFU (server-side close — e.g. our
* own wedge-recovery in renegotiateLocked) DOES need a rebuild
* because sfuSubPC === pc is still true (we didn't tear down).
* Without this branch the host's sub stays at sub=none after
* any server-driven close, every subsequent track add goes
* nowhere. Fox 2026-06-04: "lost cohost camera / screen
* share" was downstream of this missing rebuild.
* 'disconnected' is transient — let WebRTC try to recover before
* we yank the rug. */
if (pc.connectionState !== 'failed') return;
* we yank the rug. */
const s = pc.connectionState;
if (s !== 'failed' && s !== 'closed') return;
if (sfuSubPC !== pc) return;
logLine('err', 'sfu sub PC failed — rebuilding');
logLine('err', 'sfu sub PC '+s+' (remote-driven) — rebuilding');
sfuUnsubscribe().then(() => {
if (wantConnected && roomID) sfuSubscribe().catch(e => logLine('err','sfu re-subscribe: '+e.message));
});
@ -5568,8 +5576,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-04</span><br>
md5 <span class="stamp-md5">3a81cec9d5083e29337b765f990cbc04</span><br>
sha256 <span class="stamp-sha">eddd7edb4e785e606f1afe03460c56796864a3bc78a0b8f9a5e9357e886883e3</span><br>
md5 <span class="stamp-md5">3063dce0ff1ca278da701ab8ab0d625c</span><br>
sha256 <span class="stamp-sha">f49ab5aca111dea8bd89e6d7a9627ea14fbb27215b867b4c6b1267b66b1d0975</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>