import { useMemo } from 'react'; import { usePlaybackStore } from '../store/usePlaybackStore'; import { useOptionalPlaybackSync } from '../components/PlaybackSyncProvider'; /** * The transport every control set should call. * * While another device holds the audio, a press has to travel to that device * instead of starting a second stream here. Controls that reach into the * playback store directly work on the desktop and silently do nothing useful * from a phone, so there is one place that makes the choice. */ export interface Transport { play: () => void; pause: () => void; toggle: () => void; next: () => void; prev: () => void; seek: (seconds: number) => void; /** True when the presses are being forwarded rather than played here. */ remote: boolean; /** * Whether the audio is playing, wherever it is. A watching device holds its * own store paused, so a control that reads `isPlaying` from the store draws a * Play button while the desktop plays — and then sends `play` when the * listener presses it, which is why pausing from a phone never worked. Every * control reads this instead. */ playing: boolean; } export function useTransport(): Transport { const sync = useOptionalPlaybackSync(); const hasRemoteOwner = sync?.hasRemoteOwner ?? false; const remotePlaying = sync?.remotePlaying ?? false; const sendCommand = sync?.sendCommand; const localPlaying = usePlaybackStore((state) => state.isPlaying); const playing = hasRemoteOwner ? remotePlaying : localPlaying; return useMemo(() => { const local = () => usePlaybackStore.getState(); const away = hasRemoteOwner && sendCommand !== undefined; // The owning device may hold the session without listening on it — a killed // tab keeps it until the sweep. The backend answers that with a 409, and a // rejected press is not worth an unhandled rejection. const send = (command: Parameters>[0]) => void Promise.resolve(sendCommand!(command)).catch(() => undefined); return { play: () => (away ? send({ type: 'play' }) : local().play()), pause: () => (away ? send({ type: 'pause' }) : local().pause()), toggle: () => { if (away) send({ type: playing ? 'pause' : 'play' }); else if (playing) local().pause(); else local().play(); }, next: () => (away ? send({ type: 'next' }) : local().next()), prev: () => (away ? send({ type: 'prev' }) : local().prev()), seek: (seconds: number) => away ? send({ type: 'seek', position: seconds }) : local().setPosition(seconds), remote: away, playing, }; }, [hasRemoteOwner, playing, sendCommand]); }