diff --git a/components/CurrentlyPlayingBar.tsx b/components/CurrentlyPlayingBar.tsx index 80fcc423..2e84aec1 100644 --- a/components/CurrentlyPlayingBar.tsx +++ b/components/CurrentlyPlayingBar.tsx @@ -25,7 +25,7 @@ export const CurrentlyPlayingBar: React.FC = () => { pauseVideo, playVideo, setCurrentlyPlayingState, - stopVideo, + stopPlayback, setIsPlaying, isPlaying, videoRef, @@ -260,7 +260,7 @@ export const CurrentlyPlayingBar: React.FC = () => { { - setCurrentlyPlayingState(null); + stopPlayback(); }} className="aspect-square rounded flex flex-col items-center justify-center p-2" > diff --git a/components/settings/WebsocketsText.tsx b/components/settings/WebsocketsText.tsx deleted file mode 100644 index 342a26b0..00000000 --- a/components/settings/WebsocketsText.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { apiAtom } from "@/providers/JellyfinProvider"; -import { useAtom } from "jotai"; -import React, { useEffect, useState } from "react"; -import { View, Text } from "react-native"; -import { currentlyPlayingItemAtom, playingAtom } from "../CurrentlyPlayingBar"; - -export const WebSocketsTest = () => { - const [ws, setWs] = useState(null); - - const [api] = useAtom(apiAtom); - - - useEffect(() => { - if (!api || !api.accessToken || !api.basePath) return; - - // Set up WebSocket connection - const newWebSocket = new WebSocket( - `wss://${api?.basePath - .replace("https://", "") - .replace("http://", "")}/socket?api_key=${api?.accessToken}&deviceId=${ - api?.deviceInfo.id - }` - ); - - newWebSocket.onopen = () => { - console.log("WebSocket connection established"); - // You can also send data once the connection is open - newWebSocket.send( - JSON.stringify({ type: "greeting", payload: "Hello from client!" }) - ); - }; - - newWebSocket.onmessage = (e) => {}; - - newWebSocket.onerror = (e) => { - console.error("WebSocket error:", e); - }; - - newWebSocket.onclose = (e) => { - console.log("WebSocket connection closed:", e.reason); - }; - - setWs(newWebSocket); - - // Clean up function - return () => { - newWebSocket.close(); - }; - }, [api]); - - return ( - - WebSocket Demo - - ); -}; diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index b885035b..3a749d03 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -73,67 +73,6 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const [api, setApi] = useAtom(apiAtom); const [user, setUser] = useAtom(userAtom); - const [ws, setWs] = useAtom(wsAtom); - - useEffect(() => { - if (!deviceId || !api) return; - - const url = `wss://${api?.basePath - .replace("https://", "") - .replace("http://", "")}/socket?api_key=${ - api?.accessToken - }&deviceId=${deviceId}`; - - console.log("WS", url); - - const newWebSocket = new WebSocket(url); - - let keepAliveInterval: NodeJS.Timeout | null = null; - - newWebSocket.onopen = () => { - setIsConnected(true); - // Start sending "KeepAlive" message every 30 seconds - keepAliveInterval = setInterval(() => { - if (newWebSocket.readyState === WebSocket.OPEN) { - newWebSocket.send(JSON.stringify({ MessageType: "KeepAlive" })); - console.log("KeepAlive message sent"); - } - }, 30000); - }; - - newWebSocket.onmessage = (e) => { - const json = JSON.parse(e.data); - const command = json?.Data?.Command; - - // On PlayPause - if (command === "PlayPause") { - console.log("Command ~ PlayPause"); - } else if (command === "Stop") { - console.log("Command ~ Stop"); - } - }; - - newWebSocket.onerror = (e) => { - console.error("WebSocket error:", e); - setIsConnected(false); - }; - - newWebSocket.onclose = (e) => { - console.log("WebSocket connection closed:", e.reason); - if (keepAliveInterval) { - clearInterval(keepAliveInterval); - } - }; - - setWs(newWebSocket); - - return () => { - if (keepAliveInterval) { - clearInterval(keepAliveInterval); - } - newWebSocket.close(); - }; - }, [api, deviceId]); const discoverServers = async (url: string): Promise => { const servers = await jellyfin?.discovery.getRecommendedServerCandidates( diff --git a/providers/PlaybackProvider.tsx b/providers/PlaybackProvider.tsx index 1311d755..b5171b1b 100644 --- a/providers/PlaybackProvider.tsx +++ b/providers/PlaybackProvider.tsx @@ -4,6 +4,7 @@ import React, { ReactNode, useCallback, useContext, + useEffect, useRef, useState, } from "react"; @@ -19,6 +20,7 @@ import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; import { useAtom } from "jotai"; import { OnProgressData, type VideoRef } from "react-native-video"; import { apiAtom, userAtom } from "./JellyfinProvider"; +import { getDeviceId } from "@/utils/device"; type CurrentlyPlayingState = { url: string; @@ -34,7 +36,7 @@ interface PlaybackContextType { progressTicks: number | null; playVideo: () => void; pauseVideo: () => void; - stopVideo: () => void; + stopPlayback: () => void; presentFullscreenPlayer: () => void; dismissFullscreenPlayer: () => void; setIsFullscreen: (isFullscreen: boolean) => void; @@ -63,6 +65,10 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ const [currentlyPlaying, setCurrentlyPlaying] = useState(null); + // WS + const [ws, setWs] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const { data: sessionData } = useQuery({ queryKey: ["sessionData", currentlyPlaying?.item.Id, user?.Id, api], queryFn: async () => { @@ -76,6 +82,11 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ enabled: !!currentlyPlaying?.item.Id && !!api && !!user?.Id, }); + const { data: deviceId } = useQuery({ + queryKey: ["deviceId", api], + queryFn: getDeviceId, + }); + const setCurrentlyPlayingState = (state: CurrentlyPlayingState | null) => { if (state) { setCurrentlyPlaying(state); @@ -120,14 +131,15 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ }); }, [sessionData?.PlaySessionId, currentlyPlaying?.item.Id, progressTicks]); - const stopVideo = useCallback(() => { - reportPlaybackStopped({ + const stopPlayback = useCallback(async () => { + await reportPlaybackStopped({ api, itemId: currentlyPlaying?.item?.Id, sessionId: sessionData?.PlaySessionId, positionTicks: progressTicks ? progressTicks : 0, }); - }, [currentlyPlaying?.item?.Id, sessionData?.PlaySessionId, progressTicks]); + setCurrentlyPlayingState(null); + }, [currentlyPlaying, sessionData, progressTicks]); const onProgress = useCallback( ({ currentTime }: OnProgressData) => { @@ -154,6 +166,73 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ setIsFullscreen(false); }, []); + useEffect(() => { + if (!deviceId || !api) return; + + const url = `wss://${api?.basePath + .replace("https://", "") + .replace("http://", "")}/socket?api_key=${ + api?.accessToken + }&deviceId=${deviceId}`; + + console.log("WS", url); + + const newWebSocket = new WebSocket(url); + + let keepAliveInterval: NodeJS.Timeout | null = null; + + newWebSocket.onopen = () => { + setIsConnected(true); + // Start sending "KeepAlive" message every 30 seconds + keepAliveInterval = setInterval(() => { + if (newWebSocket.readyState === WebSocket.OPEN) { + newWebSocket.send(JSON.stringify({ MessageType: "KeepAlive" })); + console.log("KeepAlive message sent"); + } + }, 30000); + }; + + newWebSocket.onerror = (e) => { + console.error("WebSocket error:", e); + setIsConnected(false); + }; + + newWebSocket.onclose = (e) => { + console.log("WebSocket connection closed:", e.reason); + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + } + }; + + setWs(newWebSocket); + + return () => { + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + } + newWebSocket.close(); + }; + }, [api, deviceId]); + + useEffect(() => { + if (!ws) return; + + ws.onmessage = (e) => { + const json = JSON.parse(e.data); + const command = json?.Data?.Command; + + // On PlayPause + if (command === "PlayPause") { + console.log("Command ~ PlayPause"); + if (isPlaying) pauseVideo(); + else playVideo(); + } else if (command === "Stop") { + console.log("Command ~ Stop"); + stopPlayback(); + } + }; + }, [ws, stopPlayback, playVideo, pauseVideo]); + return ( = ({ playVideo, setCurrentlyPlayingState, pauseVideo, - stopVideo, + stopPlayback, presentFullscreenPlayer, dismissFullscreenPlayer, }} diff --git a/utils/device.ts b/utils/device.ts new file mode 100644 index 00000000..3968b02f --- /dev/null +++ b/utils/device.ts @@ -0,0 +1,19 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import uuid from "react-native-uuid"; + +export const getOrSetDeviceId = async () => { + let deviceId = await AsyncStorage.getItem("deviceId"); + + if (!deviceId) { + deviceId = uuid.v4() as string; + await AsyncStorage.setItem("deviceId", deviceId); + } + + return deviceId; +}; + +export const getDeviceId = async () => { + let deviceId = await AsyncStorage.getItem("deviceId"); + + return deviceId || null; +}; diff --git a/utils/jellyfin/playstate/reportPlaybackProgress.ts b/utils/jellyfin/playstate/reportPlaybackProgress.ts index ae4d8117..6e89a962 100644 --- a/utils/jellyfin/playstate/reportPlaybackProgress.ts +++ b/utils/jellyfin/playstate/reportPlaybackProgress.ts @@ -24,7 +24,12 @@ export const reportPlaybackProgress = async ({ IsPaused = false, }: ReportPlaybackProgressParams): Promise => { if (!api || !sessionId || !itemId || !positionTicks) { - console.error("Missing required parameter"); + console.error( + "Missing required parameter", + sessionId, + itemId, + positionTicks + ); return; }