mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-02 20:18:29 +01:00
75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
/**
|
|
* The canonical playback-control surface. Every player (cast, native video,
|
|
* music) implements this interface and registers itself as the active
|
|
* controller while it is playing, so remote-control commands can be routed to
|
|
* whatever is currently playing.
|
|
*/
|
|
|
|
import { atom, useSetAtom } from "jotai";
|
|
import { useEffect, useRef } from "react";
|
|
|
|
export interface PlaybackController {
|
|
playPause(): void;
|
|
pause(): void;
|
|
unpause(): void;
|
|
stop(): void;
|
|
/** Absolute seek position in milliseconds. */
|
|
seek(positionMs: number): void;
|
|
next(): void;
|
|
previous(): void;
|
|
/** Volume 0-1. */
|
|
setVolume(level: number): void;
|
|
toggleMute(): void;
|
|
}
|
|
|
|
/** The currently-active playback controller, or null when nothing is playing. */
|
|
export const activePlaybackControllerAtom = atom<PlaybackController | null>(
|
|
null,
|
|
);
|
|
|
|
/**
|
|
* Register `controller` as the active playback controller while `active` is
|
|
* true. Cleared on unmount or when `active` becomes false.
|
|
*
|
|
* The registered value is a *stable proxy* whose identity never changes — it
|
|
* forwards each call to whatever `controller` is current (tracked via a ref).
|
|
* This keeps the registration effect's dependencies stable (`active` only), so
|
|
* a `controller` that is recreated every render does NOT re-run the effect and
|
|
* cannot cause a `setState`/render loop.
|
|
*/
|
|
export const useRegisterPlaybackController = (
|
|
controller: PlaybackController | null,
|
|
active: boolean,
|
|
): void => {
|
|
const setController = useSetAtom(activePlaybackControllerAtom);
|
|
|
|
// Always points at the latest controller passed in.
|
|
const controllerRef = useRef(controller);
|
|
controllerRef.current = controller;
|
|
|
|
// Created once; its identity is stable for the component's lifetime.
|
|
const proxyRef = useRef<PlaybackController | null>(null);
|
|
if (proxyRef.current === null) {
|
|
proxyRef.current = {
|
|
playPause: () => controllerRef.current?.playPause(),
|
|
pause: () => controllerRef.current?.pause(),
|
|
unpause: () => controllerRef.current?.unpause(),
|
|
stop: () => controllerRef.current?.stop(),
|
|
seek: (positionMs) => controllerRef.current?.seek(positionMs),
|
|
next: () => controllerRef.current?.next(),
|
|
previous: () => controllerRef.current?.previous(),
|
|
setVolume: (level) => controllerRef.current?.setVolume(level),
|
|
toggleMute: () => controllerRef.current?.toggleMute(),
|
|
};
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!active) return;
|
|
const proxy = proxyRef.current;
|
|
setController(proxy);
|
|
return () => {
|
|
setController((current) => (current === proxy ? null : current));
|
|
};
|
|
}, [active, setController]);
|
|
};
|