diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html index 45570cc..bd174e4 100644 --- a/zebra-report/zebra-spaces.html +++ b/zebra-report/zebra-spaces.html @@ -2547,8 +2547,8 @@ async function sfuPublishScreen(){ /* render a muted local preview so the publisher sees what they're * sharing — SFU does not echo the publisher's own stream back */ renderScreenTile(myKeys.pubHex, stream, { local: true }); - $('btn-screen-share').classList.add('hidden'); - $('btn-screen-stop').classList.remove('hidden'); + /* share / stop button visibility is driven by applyPublishStateUI + * via the publishSpec observer (pubs.screen state). */ } /* sfuRebuildScreenPC — silent recovery path used by the screen * publisher's onconnectionstatechange when the PC fails BUT the @@ -2626,8 +2626,8 @@ async function sfuUnpublishScreen(){ try { await fetch(SFU_BASE + '/unpublish?room=' + encodeURIComponent(roomID) + '&peer=' + pid, { method:'POST' }); } catch(_){} } fsm.send('DONE'); - $('btn-screen-share').classList.remove('hidden'); - $('btn-screen-stop').classList.add('hidden'); + /* share / stop visibility is driven by applyPublishStateUI from + * the publishSpec 'off' state. */ logLine('', 'screen share stopped'); } @@ -2766,8 +2766,8 @@ async function sfuPublishCamera(){ () => { sfuCameraPC = null; sfuCameraPeerID = null; sfuCameraStream = null; fsm.send('STOP'); fsm.send('DONE'); }); logLine('', 'sfu: camera on as '+sfuCameraPeerID); renderCameraTile(myKeys.pubHex, stream, { local: true }); - $('btn-camera-share').classList.add('hidden'); - $('btn-camera-stop').classList.remove('hidden'); + /* share / stop visibility is driven by applyPublishStateUI via + * the publishSpec observer (pubs.camera state). */ try { sessionStorage.setItem(ACTIVE_CAM_KEY, '1'); } catch(_){} } async function sfuUnpublishCamera(){ @@ -2782,8 +2782,8 @@ async function sfuUnpublishCamera(){ try { await fetch(SFU_BASE + '/unpublish?room=' + encodeURIComponent(roomID) + '&peer=' + pid, { method:'POST' }); } catch(_){} } fsm.send('DONE'); - $('btn-camera-share').classList.remove('hidden'); - $('btn-camera-stop').classList.add('hidden'); + /* share / stop visibility is driven by applyPublishStateUI from + * the publishSpec 'off' state. */ logLine('', 'camera off'); try { sessionStorage.removeItem(ACTIVE_CAM_KEY); } catch(_){} } @@ -3690,9 +3690,7 @@ roomMachines.call.observe(({ state, prev, ctx }) => { /* Publish-FSM trace observers — one per kind (mic / screen / camera). * Logs every transition so the page log shows the publish lifecycle - * synchronously with the imperative sfuPublish* / sfuUnpublish* flow. - * Future side-effect migrations can hang off these observers (the - * imperative code path will shrink as transitions take over). */ + * synchronously with the imperative sfuPublish* / sfuUnpublish* flow. */ for (const kind of ['mic', 'screen', 'camera']){ const m = roomMachines.pubs[kind]; if (!m) continue; @@ -3702,6 +3700,32 @@ for (const kind of ['mic', 'screen', 'camera']){ }); } +/* Share-button visibility observer — derives btn-{kind}-share / + * btn-{kind}-stop visibility from the publishSpec state. Single + * source of truth: the buttons reflect the FSM, not whoever last + * called sfuPublishX. Fox 2026-06-04 directive — every system as a + * state machine, scattered classList writes converge into one + * observer per FSM. */ +function applyPublishStateUI(kind, state){ + const share = $('btn-'+kind+'-share'); + const stop = $('btn-'+kind+'-stop'); + if (!share || !stop) return; + /* sharing = anywhere between START and STOP. The 'stopping' state + * is transient and we treat it as "still showing the stop button" + * so the UI doesn't flicker during teardown. */ + const isSharing = state === 'acquiring' || state === 'negotiating' || state === 'live' || state === 'stopping'; + share.classList.toggle('hidden', isSharing); + stop.classList.toggle('hidden', !isSharing); +} +for (const kind of ['screen', 'camera']){ + const m = roomMachines.pubs[kind]; + if (!m) continue; + m.observe(({ state, prev }) => { + if (prev === null || state === prev) return; + try { applyPublishStateUI(kind, state); } catch(e){ logLine('err','pub.'+kind+' UI: '+e.message); } + }); +} + let ws = null, wantConnected = false, sigKey = null, sigReconnect = null; function send(obj){ if (ws && ws.readyState===1) ws.send(JSON.stringify(obj)); } @@ -5768,8 +5792,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');