This commit is contained in:
Fredrik Burmester
2024-09-09 08:55:58 +03:00
parent 788b4bcbd2
commit b4eaabce7a
3 changed files with 143 additions and 72 deletions

View File

@@ -24,7 +24,11 @@ export default function page() {
const res = await getSyncPlayApi(api).syncPlayGetGroups(); const res = await getSyncPlayApi(api).syncPlayGetGroups();
return res.data; return res.data;
}, },
refetchInterval: 5000, refetchInterval: 1000,
refetchIntervalInBackground: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
}); });
const createGroupMutation = useMutation({ const createGroupMutation = useMutation({

View File

@@ -31,6 +31,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
videoRef, videoRef,
presentFullscreenPlayer, presentFullscreenPlayer,
onProgress, onProgress,
onBuffer,
} = usePlayback(); } = usePlayback();
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
@@ -182,18 +183,24 @@ export const CurrentlyPlayingBar: React.FC = () => {
enable: true, enable: true,
thread: true, thread: true,
}} }}
onIdle={() => {
console.log("IDLE");
}}
fullscreenAutorotate={true}
onReadyForDisplay={() => {
console.log("READY FOR DISPLAY");
}}
onProgress={(e) => onProgress(e)} onProgress={(e) => onProgress(e)}
subtitleStyle={{ subtitleStyle={{
fontSize: 16, fontSize: 16,
}} }}
onBuffer={(e) => onBuffer(e.isBuffering)}
source={videoSource} source={videoSource}
onRestoreUserInterfaceForPictureInPictureStop={() => { onRestoreUserInterfaceForPictureInPictureStop={() => {
setTimeout(() => { setTimeout(() => {
presentFullscreenPlayer(); presentFullscreenPlayer();
}, 300); }, 300);
}} }}
onFullscreenPlayerDidDismiss={() => {}}
onFullscreenPlayerDidPresent={() => {}}
onPlaybackStateChanged={(e) => { onPlaybackStateChanged={(e) => {
if (e.isPlaying === true) { if (e.isPlaying === true) {
playVideo(false); playVideo(false);

View File

@@ -21,7 +21,7 @@ import {
import { getMediaInfoApi, getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api"; import { getMediaInfoApi, getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
import * as Linking from "expo-linking"; import * as Linking from "expo-linking";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { debounce } from "lodash"; import { debounce, isBuffer } from "lodash";
import { Alert } from "react-native"; import { Alert } from "react-native";
import { OnProgressData, type VideoRef } from "react-native-video"; import { OnProgressData, type VideoRef } from "react-native-video";
import { apiAtom, userAtom } from "./JellyfinProvider"; import { apiAtom, userAtom } from "./JellyfinProvider";
@@ -43,6 +43,8 @@ interface PlaybackContextType {
sessionData: PlaybackInfoResponse | null | undefined; sessionData: PlaybackInfoResponse | null | undefined;
currentlyPlaying: CurrentlyPlayingState | null; currentlyPlaying: CurrentlyPlayingState | null;
videoRef: React.MutableRefObject<VideoRef | null>; videoRef: React.MutableRefObject<VideoRef | null>;
onBuffer: (isBuffering: boolean) => void;
onReady: () => void;
isPlaying: boolean; isPlaying: boolean;
isFullscreen: boolean; isFullscreen: boolean;
progressTicks: number | null; progressTicks: number | null;
@@ -82,6 +84,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
const [progressTicks, setProgressTicks] = useState<number | null>(0); const [progressTicks, setProgressTicks] = useState<number | null>(0);
const [volume, _setVolume] = useState<number | null>(null); const [volume, _setVolume] = useState<number | null>(null);
const [session, setSession] = useState<PlaybackInfoResponse | null>(null); const [session, setSession] = useState<PlaybackInfoResponse | null>(null);
const [syncplayGroup, setSyncplayGroup] = useState<GroupData | null>(null);
const [currentlyPlaying, setCurrentlyPlaying] = const [currentlyPlaying, setCurrentlyPlaying] =
useState<CurrentlyPlayingState | null>(null); useState<CurrentlyPlayingState | null>(null);
@@ -264,6 +267,53 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
[session?.PlaySessionId, currentlyPlaying?.item.Id, isPlaying, api] [session?.PlaySessionId, currentlyPlaying?.item.Id, isPlaying, api]
); );
const onBuffer = useCallback(
(isBuffering: boolean) => {
console.log("Buffering...", "Playing:", isPlaying);
if (
isBuffering &&
syncplayGroup?.GroupId &&
isPlaying === false &&
currentlyPlaying?.item.PlaylistItemId
) {
console.log("Sending syncplay buffering...");
getSyncPlayApi(api!).syncPlayBuffering({
bufferRequestDto: {
IsPlaying: isPlaying,
When: new Date().toISOString(),
PositionTicks: progressTicks ? progressTicks : 0,
PlaylistItemId: currentlyPlaying?.item.PlaylistItemId,
},
});
}
},
[
isPlaying,
syncplayGroup?.GroupId,
currentlyPlaying?.item.PlaylistItemId,
api,
]
);
const onReady = useCallback(() => {
if (syncplayGroup?.GroupId && currentlyPlaying?.item.PlaylistItemId) {
getSyncPlayApi(api!).syncPlayReady({
readyRequestDto: {
When: new Date().toISOString(),
PlaylistItemId: currentlyPlaying?.item.PlaylistItemId,
IsPlaying: isPlaying,
PositionTicks: progressTicks ? progressTicks : 0,
},
});
}
}, [
syncplayGroup?.GroupId,
currentlyPlaying?.item.PlaylistItemId,
progressTicks,
isPlaying,
api,
]);
const onProgress = useCallback( const onProgress = useCallback(
debounce((e: OnProgressData) => { debounce((e: OnProgressData) => {
_onProgress(e); _onProgress(e);
@@ -287,119 +337,127 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!deviceId || !api?.accessToken) return; if (!deviceId || !api?.accessToken || !user?.Id) {
console.info("[WS] Waiting for deviceId, accessToken and userId");
return;
}
const protocol = api?.basePath.includes("https") ? "wss" : "ws"; const protocol = api?.basePath.includes("https") ? "wss" : "ws";
const url = `${protocol}://${api?.basePath const url = `${protocol}://${api?.basePath
.replace("https://", "") .replace("https://", "")
.replace("http://", "")}/socket?api_key=${ .replace("http://", "")}/socket?api_key=${
api?.accessToken api?.accessToken
}&deviceId=${deviceId}`; }&deviceId=${deviceId}`;
const newWebSocket = new WebSocket(url); let ws: WebSocket | null = null;
let keepAliveInterval: NodeJS.Timeout | null = null; let keepAliveInterval: NodeJS.Timeout | null = null;
newWebSocket.onopen = () => { const connect = () => {
setIsConnected(true); ws = new WebSocket(url);
// Start sending "KeepAlive" message every 30 seconds
keepAliveInterval = setInterval(() => { ws.onopen = () => {
if (newWebSocket.readyState === WebSocket.OPEN) { setIsConnected(true);
newWebSocket.send(JSON.stringify({ MessageType: "KeepAlive" })); keepAliveInterval = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log("⬆︎ KeepAlive...");
ws.send(JSON.stringify({ MessageType: "KeepAlive" }));
}
}, 30000);
};
ws.onerror = (e) => {
console.error("WebSocket error:", e);
setIsConnected(false);
};
ws.onclose = () => {
setIsConnected(false);
if (keepAliveInterval) {
clearInterval(keepAliveInterval);
} }
}, 30000); setTimeout(connect, 5000); // Attempt to reconnect after 5 seconds
};
setWs(ws);
}; };
newWebSocket.onerror = (e) => { connect();
console.error("WebSocket error:", e);
setIsConnected(false);
};
newWebSocket.onclose = (e) => {
if (keepAliveInterval) {
clearInterval(keepAliveInterval);
}
};
setWs(newWebSocket);
return () => { return () => {
if (ws) {
ws.close();
}
if (keepAliveInterval) { if (keepAliveInterval) {
clearInterval(keepAliveInterval); clearInterval(keepAliveInterval);
} }
newWebSocket.close();
}; };
}, [api, deviceId, user]); }, [api?.accessToken, deviceId, user]);
useEffect(() => { useEffect(() => {
if (!ws) return; if (!ws || !api) return;
ws.onmessage = (e) => { ws.onmessage = (e) => {
const json = JSON.parse(e.data); const json = JSON.parse(e.data);
const command = json?.Data?.Command; const command = json?.Data?.Command;
if (json.MessageType === "KeepAlive") { if (json.MessageType === "KeepAlive") {
// TODO: ?? console.log("⬇︎ KeepAlive...");
} else if (json.MessageType === "ForceKeepAlive") { } else if (json.MessageType === "ForceKeepAlive") {
// TODO: ?? console.log("⬇︎ ForceKeepAlive...");
} else if (json.MessageType === "SyncPlayCommand") { } else if (json.MessageType === "SyncPlayCommand") {
console.log("SyncPlayCommand ~", command); console.log("SyncPlayCommand ~", command, json.Data);
if (command === "Stop") { switch (command) {
console.log("Command ~ Stop"); case "Stop":
stopPlayback(); console.log("STOP");
stopPlayback();
break;
case "Pause":
console.log("PAUSE");
pauseVideo();
break;
case "Play":
case "Unpause":
console.log("PLAY");
playVideo();
break;
case "Seek":
console.log("SEEK", json.Data.PositionTicks);
seek(json.Data.PositionTicks);
break;
} }
} else if (json.MessageType === "SyncPlayGroupUpdate") { } else if (json.MessageType === "SyncPlayGroupUpdate") {
if (!api) return;
const type = json.Data.Type; const type = json.Data.Type;
if (type === "StateUpdate") { if (type === "StateUpdate") {
const data = json.Data.Data as StateUpdateData; const data = json.Data.Data as StateUpdateData;
console.log("StateUpdate ~", data); console.log("StateUpdate ~", data);
} else if (type === "GroupJoined") { } else if (type === "GroupJoined") {
const data = json.Data.Data as GroupJoinedData; const data = json.Data.Data as GroupData;
setSyncplayGroup(data);
console.log("GroupJoined ~", data); console.log("GroupJoined ~", data);
} else if (type === "GroupLeft") {
console.log("GroupLeft");
setSyncplayGroup(null);
} else if (type === "PlayQueue") { } else if (type === "PlayQueue") {
const data = json.Data.Data as PlayQueueData; const data = json.Data.Data as PlayQueueData;
console.log("PlayQueue ~", { console.log("PlayQueue ~", {
IsPlaying: data.IsPlaying, IsPlaying: data.IsPlaying,
StartPositionTicks: data.StartPositionTicks,
PlaylistLength: data.Playlist?.length,
PlayingItemIndex: data.PlayingItemIndex,
Reason: data.Reason, Reason: data.Reason,
}); });
if (data.Reason === "SetCurrentItem") { if (data.Reason === "SetCurrentItem") {
if ( console.log("SetCurrentItem ~ ", json);
currentlyPlaying?.item.Id === return;
data.Playlist?.[data.PlayingItemIndex].ItemId
) {
console.log("SetCurrentItem ~", json);
seek(data.StartPositionTicks);
if (data.IsPlaying) {
playVideo();
} else {
pauseVideo();
}
getSyncPlayApi(api).syncPlayReady({
readyRequestDto: {
IsPlaying: data.IsPlaying,
PositionTicks: data.StartPositionTicks,
PlaylistItemId: currentlyPlaying?.item.Id,
When: new Date().toISOString(),
},
});
return;
}
} }
const itemId = data.Playlist?.[data.PlayingItemIndex].ItemId; if (data.Reason === "NewPlaylist") {
if (itemId) { const itemId = data.Playlist?.[data.PlayingItemIndex].ItemId;
if (!itemId) {
console.error("No itemId found in PlayQueue");
return;
}
// Set playback item
getUserItemData({ getUserItemData({
api, api,
userId: user?.Id, userId: user?.Id,
@@ -428,14 +486,14 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
item, item,
url, url,
}, },
true !data.IsPlaying
); );
await getSyncPlayApi(api).syncPlayReady({ await getSyncPlayApi(api).syncPlayReady({
readyRequestDto: { readyRequestDto: {
IsPlaying: data.IsPlaying, IsPlaying: data.IsPlaying,
PositionTicks: data.StartPositionTicks, PositionTicks: data.StartPositionTicks,
PlaylistItemId: itemId, PlaylistItemId: data.Playlist[0].PlaylistItemId,
When: new Date().toISOString(), When: new Date().toISOString(),
}, },
}); });
@@ -473,16 +531,18 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
Alert.alert(title, body); Alert.alert(title, body);
} }
}; };
}, [ws, stopPlayback, playVideo, pauseVideo]); }, [ws, stopPlayback, playVideo, pauseVideo, setVolume, api, seek]);
return ( return (
<PlaybackContext.Provider <PlaybackContext.Provider
value={{ value={{
onProgress, onProgress,
onReady,
progressTicks, progressTicks,
setVolume, setVolume,
setIsPlaying, setIsPlaying,
setIsFullscreen, setIsFullscreen,
onBuffer,
isFullscreen, isFullscreen,
isPlaying, isPlaying,
currentlyPlaying, currentlyPlaying,