From 5b823a8efdebc25187766f0bf4922ce4e4ed1c12 Mon Sep 17 00:00:00 2001 From: Uruk Date: Fri, 22 May 2026 02:17:30 +0200 Subject: [PATCH] feat(player): register native-video PlaybackController --- app/(auth)/player/direct-player.tsx | 47 +++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 8d755a484..1683a2d9c 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -44,9 +44,7 @@ import { import { useDownload } from "@/providers/DownloadProvider"; import { DownloadedItem } from "@/providers/Downloads/types"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; - import { OfflineModeProvider } from "@/providers/OfflineModeProvider"; - import { useSettings } from "@/utils/atoms/settings"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; @@ -55,6 +53,10 @@ import { getMpvSubtitleId, } from "@/utils/jellyfin/subtitleUtils"; import { writeToLog } from "@/utils/log"; +import { + type PlaybackController, + useRegisterPlaybackController, +} from "@/utils/playback/playbackController"; import { generateDeviceProfile } from "@/utils/profiles/native"; import { msToTicks, ticksToSeconds } from "@/utils/time"; @@ -773,6 +775,47 @@ export default function page() { return (await videoRef.current?.getTechnicalInfo?.()) ?? {}; }, []); + // App-wide remote control: wrap the player's existing handlers so remote + // commands (e.g. dashboard, WebSocket) route to whatever is playing. + const playbackController = useMemo( + () => ({ + // togglePlay flips play/pause and reports progress to the server. + playPause: () => { + void togglePlay(); + }, + pause: () => { + pause(); + }, + unpause: () => { + play(); + }, + stop: () => { + stop(); + }, + // PlaybackController seeks in ms; the player's seek already expects ms. + seek: (positionMs: number) => { + seek(positionMs); + }, + // The player screen has no episode-loading path of its own — episode + // navigation is handled inside via router replacement — so + // next/previous are honest no-ops here. + next: () => {}, + previous: () => {}, + // Volume is device-level (react-native-volume-manager); the slider sends + // 0-1 while setVolumeCb expects 0-100. + setVolume: (level: number) => { + void setVolumeCb(level * 100); + }, + toggleMute: () => { + void toggleMuteCb(); + }, + }), + [togglePlay, pause, play, stop, seek, setVolumeCb, toggleMuteCb], + ); + + // Active for the whole lifetime of the player screen; cleared on unmount. + useRegisterPlaybackController(playbackController, true); + // Determine play method based on stream URL and media source const playMethod = useMemo< "DirectPlay" | "DirectStream" | "Transcode" | undefined