fix(orbit): event-driven host push + guest seekbar lock (#537)

* fix(orbit): event-driven host push on play/pause flips

Without this, the worst-case delay between "host hits pause" and "guest
stops" is two full polling windows (host's 2.5 s push tick + guest's
2.5 s read tick, plus network) — long enough for the guest to noticeably
run past the host. Subscribing to playerStore.isPlaying changes adds at
most one extra remote write per flip; non-flip state ticks still ride
the existing 2.5 s timer. The listener filters on isPlaying so the
per-second currentTime ticks don't trigger spurious pushes.

* fix(orbit): lock seekbar for guests — sync follows the host

Guests could drag/click/wheel the seekbar, which would jump the local
player and then snap back at the next host poll (2.5 s of inconsistent
UX) — or push the guest into a diverged state where Catch Up was the
only way back. The seekbar is host-controlled in Orbit; the guest input
path now reflects that.

- App.tsx exposes `data-orbit-role="host"|"guest"` on the root element
  alongside the existing `data-orbit-active` marker.
- WaveformSeek's container gains a `.waveform-seek-container` class so
  CSS can target it.
- Guest rule: `pointer-events: none` on children blocks click / drag /
  wheel / hover; the parent keeps `cursor: not-allowed` + reduced opacity
  so the disabled state is visually unambiguous.

Hosts and non-orbit users see no change.

* docs(changelog): credit PR #537 (orbit sync latency + guest seekbar)
This commit is contained in:
Frank Stellmacher
2026-05-11 13:34:02 +02:00
committed by GitHub
parent 64b33e6941
commit 123fbcc802
5 changed files with 39 additions and 2 deletions
+20 -1
View File
@@ -225,7 +225,26 @@ export function useOrbitHost(): void {
void pushState();
const id = window.setInterval(() => { void pushState(); }, STATE_TICK_MS);
return () => window.clearInterval(id);
// Event-driven push on play/pause flips. Without this the worst-case
// delay between "host hits pause" and "guest stops" is two full polling
// windows (host tick + guest tick = up to 5 s) — long enough for the
// guest to noticeably run ahead. Subscribing here adds at most one
// extra write per flip; non-flip state ticks still ride the 2.5 s
// timer. Listener filters on isPlaying so currentTime ticks don't
// trigger spurious pushes.
let prevIsPlaying = usePlayerStore.getState().isPlaying;
const unsubPlayPause = usePlayerStore.subscribe((state) => {
if (state.isPlaying === prevIsPlaying) return;
prevIsPlaying = state.isPlaying;
pushOrbitEvent('host:event-push', `isPlaying flip → ${state.isPlaying}`);
void pushState();
});
return () => {
window.clearInterval(id);
unsubPlayPause();
};
}, [active, sessionPlaylistId]);
useEffect(() => {