mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
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:
committed by
GitHub
parent
64b33e6941
commit
123fbcc802
@@ -197,8 +197,12 @@ function AppShell() {
|
||||
&& (orbitPhase === 'active' || orbitPhase === 'joining' || orbitPhase === 'starting');
|
||||
if (inOrbit) {
|
||||
document.documentElement.setAttribute('data-orbit-active', 'true');
|
||||
// Also expose the role so CSS can target host-vs-guest UI states
|
||||
// (e.g. guest seekbar is read-only — sync follows the host).
|
||||
document.documentElement.setAttribute('data-orbit-role', orbitRole as string);
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-orbit-active');
|
||||
document.documentElement.removeAttribute('data-orbit-role');
|
||||
}
|
||||
}, [orbitRole, orbitPhase]);
|
||||
|
||||
|
||||
@@ -1350,7 +1350,7 @@ export default function WaveformSeek({ trackId }: Props) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
<div className="waveform-seek-container" style={{ position: 'relative', width: '100%' }}>
|
||||
{hoverPct !== null && duration > 0 && (
|
||||
<span
|
||||
className="player-volume-pct"
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1915,6 +1915,18 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Guest in an Orbit session: seekbar is read-only — sync follows the host.
|
||||
`pointer-events: none` blocks click / drag / wheel / hover; the dimmed
|
||||
look + not-allowed cursor (set on the parent so it shows when hovered
|
||||
over the now-inert child) signal "this control is not yours". */
|
||||
[data-orbit-role="guest"] .waveform-seek-container {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
[data-orbit-role="guest"] .waveform-seek-container > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.playlist-suggestions .track-row:hover .playlist-suggestion-preview-btn,
|
||||
.playlist-suggestion-preview-btn:hover,
|
||||
.playlist-suggestion-preview-btn.is-previewing {
|
||||
|
||||
Reference in New Issue
Block a user