From 26057ed196d3e651ccdf771cf45d6dbd4883b4ad Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 25 Aug 2024 16:59:53 +0200 Subject: [PATCH] feat: more commands --- components/CurrentlyPlayingBar.tsx | 4 + providers/PlaybackProvider.tsx | 101 ++++++++++++++++--------- utils/jellyfin/session/capabilities.ts | 18 ++++- 3 files changed, 82 insertions(+), 41 deletions(-) diff --git a/components/CurrentlyPlayingBar.tsx b/components/CurrentlyPlayingBar.tsx index a3276692..de8aa32b 100644 --- a/components/CurrentlyPlayingBar.tsx +++ b/components/CurrentlyPlayingBar.tsx @@ -26,6 +26,7 @@ export const CurrentlyPlayingBar: React.FC = () => { playVideo, setCurrentlyPlayingState, stopPlayback, + setVolume, setIsPlaying, isPlaying, videoRef, @@ -202,6 +203,9 @@ export const CurrentlyPlayingBar: React.FC = () => { setIsPlaying(false); } }} + onVolumeChange={(e) => { + setVolume(e.volume); + }} progressUpdateInterval={2000} onError={(e) => { console.log(e); diff --git a/providers/PlaybackProvider.tsx b/providers/PlaybackProvider.tsx index fd07f42a..39c1ebce 100644 --- a/providers/PlaybackProvider.tsx +++ b/providers/PlaybackProvider.tsx @@ -10,6 +10,7 @@ import React, { } from "react"; import { useSettings } from "@/utils/atoms/settings"; +import { getDeviceId } from "@/utils/device"; import { reportPlaybackProgress } from "@/utils/jellyfin/playstate/reportPlaybackProgress"; import { reportPlaybackStopped } from "@/utils/jellyfin/playstate/reportPlaybackStopped"; import { @@ -17,12 +18,12 @@ import { PlaybackInfoResponse, } from "@jellyfin/sdk/lib/generated-client/models"; import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; +import * as Linking from "expo-linking"; import { useAtom } from "jotai"; +import { Alert, Platform } from "react-native"; import { OnProgressData, type VideoRef } from "react-native-video"; import { apiAtom, userAtom } from "./JellyfinProvider"; -import { getDeviceId } from "@/utils/device"; -import * as Linking from "expo-linking"; -import { Platform } from "react-native"; +import { postCapabilities } from "@/utils/jellyfin/session/capabilities"; type CurrentlyPlayingState = { url: string; @@ -44,6 +45,7 @@ interface PlaybackContextType { setIsFullscreen: (isFullscreen: boolean) => void; setIsPlaying: (isPlaying: boolean) => void; onProgress: (data: OnProgressData) => void; + setVolume: (volume: number) => void; setCurrentlyPlayingState: ( currentlyPlaying: CurrentlyPlayingState | null ) => void; @@ -61,9 +63,13 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ const [settings] = useSettings(); + const previousVolume = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [progressTicks, setProgressTicks] = useState(0); + const [volume, _setVolume] = useState(null); + const [session, setSession] = useState(null); const [currentlyPlaying, setCurrentlyPlaying] = useState(null); @@ -71,18 +77,14 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ const [ws, setWs] = useState(null); const [isConnected, setIsConnected] = useState(false); - const { data: sessionData } = useQuery({ - queryKey: ["sessionData", currentlyPlaying?.item.Id, user?.Id, api], - queryFn: async () => { - if (!currentlyPlaying?.item.Id) return null; - const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({ - itemId: currentlyPlaying?.item.Id, - userId: user?.Id, - }); - return playbackData.data; + const setVolume = useCallback( + (newVolume: number) => { + previousVolume.current = volume; + _setVolume(newVolume); + videoRef.current?.setVolume(newVolume); }, - enabled: !!currentlyPlaying?.item.Id && !!api && !!user?.Id, - }); + [_setVolume] + ); const { data: deviceId } = useQuery({ queryKey: ["deviceId", api], @@ -90,15 +92,29 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ }); const setCurrentlyPlayingState = useCallback( - (state: CurrentlyPlayingState | null) => { - const vlcLink = "vlc://" + state?.url; - console.log(vlcLink, settings?.openInVLC, Platform.OS === "ios"); - if (vlcLink && settings?.openInVLC) { - Linking.openURL("vlc://" + state?.url || ""); - return; - } + async (state: CurrentlyPlayingState | null) => { + if (!api) return; - if (state) { + if (state && state.item.Id && user?.Id) { + const vlcLink = "vlc://" + state?.url; + if (vlcLink && settings?.openInVLC) { + console.log(vlcLink, settings?.openInVLC, Platform.OS === "ios"); + Linking.openURL("vlc://" + state?.url || ""); + return; + } + + const res = await getMediaInfoApi(api).getPlaybackInfo({ + itemId: state.item.Id, + userId: user.Id, + }); + + await postCapabilities({ + api, + itemId: state.item.Id, + sessionId: res.data.PlaySessionId, + }); + + setSession(res.data); setCurrentlyPlaying(state); setIsPlaying(true); @@ -113,7 +129,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ setIsPlaying(false); } }, - [settings] + [settings, user, api] ); // Define control methods @@ -124,15 +140,10 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ api, itemId: currentlyPlaying?.item.Id, positionTicks: progressTicks ? progressTicks : 0, - sessionId: sessionData?.PlaySessionId, + sessionId: session?.PlaySessionId, IsPaused: true, }); - }, [ - api, - currentlyPlaying?.item.Id, - sessionData?.PlaySessionId, - progressTicks, - ]); + }, [api, currentlyPlaying?.item.Id, session?.PlaySessionId, progressTicks]); const pauseVideo = useCallback(() => { videoRef.current?.pause(); @@ -141,20 +152,20 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ api, itemId: currentlyPlaying?.item.Id, positionTicks: progressTicks ? progressTicks : 0, - sessionId: sessionData?.PlaySessionId, + sessionId: session?.PlaySessionId, IsPaused: false, }); - }, [sessionData?.PlaySessionId, currentlyPlaying?.item.Id, progressTicks]); + }, [session?.PlaySessionId, currentlyPlaying?.item.Id, progressTicks]); const stopPlayback = useCallback(async () => { await reportPlaybackStopped({ api, itemId: currentlyPlaying?.item?.Id, - sessionId: sessionData?.PlaySessionId, + sessionId: session?.PlaySessionId, positionTicks: progressTicks ? progressTicks : 0, }); setCurrentlyPlayingState(null); - }, [currentlyPlaying, sessionData, progressTicks]); + }, [currentlyPlaying, session, progressTicks]); const onProgress = useCallback( ({ currentTime }: OnProgressData) => { @@ -164,11 +175,11 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ api, itemId: currentlyPlaying?.item.Id, positionTicks: ticks, - sessionId: sessionData?.PlaySessionId, + sessionId: session?.PlaySessionId, IsPaused: !isPlaying, }); }, - [sessionData?.PlaySessionId, currentlyPlaying?.item.Id, isPlaying] + [session?.PlaySessionId, currentlyPlaying?.item.Id, isPlaying] ); const presentFullscreenPlayer = useCallback(() => { @@ -236,6 +247,8 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ const json = JSON.parse(e.data); const command = json?.Data?.Command; + console.log("[WS] ~ ", json); + // On PlayPause if (command === "PlayPause") { console.log("Command ~ PlayPause"); @@ -244,6 +257,19 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ } else if (command === "Stop") { console.log("Command ~ Stop"); stopPlayback(); + } else if (command === "Mute") { + console.log("Command ~ Mute"); + setVolume(0); + } else if (command === "Unmute") { + console.log("Command ~ Unmute"); + setVolume(previousVolume.current || 20); + } else if (command === "SetVolume") { + console.log("Command ~ SetVolume"); + } else if (json?.Data?.Name === "DisplayMessage") { + console.log("Command ~ DisplayMessage"); + const title = json?.Data?.Arguments?.Header; + const body = json?.Data?.Arguments?.Text; + Alert.alert(title, body); } }; }, [ws, stopPlayback, playVideo, pauseVideo]); @@ -253,12 +279,13 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ value={{ onProgress, progressTicks, + setVolume, setIsPlaying, setIsFullscreen, isFullscreen, isPlaying, currentlyPlaying, - sessionData, + sessionData: session, videoRef, playVideo, setCurrentlyPlayingState, diff --git a/utils/jellyfin/session/capabilities.ts b/utils/jellyfin/session/capabilities.ts index 7149adef..d76fbeb7 100644 --- a/utils/jellyfin/session/capabilities.ts +++ b/utils/jellyfin/session/capabilities.ts @@ -4,7 +4,7 @@ import { SessionApiPostCapabilitiesRequest, } from "@jellyfin/sdk/lib/generated-client/api/session-api"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; -import { AxiosError } from "axios"; +import { AxiosError, AxiosResponse } from "axios"; import { getAuthHeaders } from "../jellyfin"; interface PostCapabilitiesParams { @@ -23,17 +23,26 @@ export const postCapabilities = async ({ api, itemId, sessionId, -}: PostCapabilitiesParams): Promise => { +}: PostCapabilitiesParams): Promise => { if (!api || !itemId || !sessionId) { throw new Error("Missing required parameters"); } try { - const r = await api.axiosInstance.post( + const d = api.axiosInstance.post( api.basePath + "/Sessions/Capabilities/Full", { playableMediaTypes: ["Audio", "Video", "Audio"], - supportedCommands: ["PlayState", "Play"], + supportedCommands: [ + "PlayState", + "Play", + "ToggleFullscreen", + "DisplayMessage", + "Mute", + "Unmute", + "SetVolume", + "ToggleMute", + ], supportsMediaControl: true, id: sessionId, }, @@ -41,6 +50,7 @@ export const postCapabilities = async ({ headers: getAuthHeaders(api), } ); + return d; } catch (error: any | AxiosError) { console.log("Failed to mark as not played", error); throw new Error("Failed to mark as not played");