mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-01 11:38:26 +01:00
wip
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user