mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-21 00:04:42 +01:00
wip
This commit is contained in:
@@ -1,104 +1,191 @@
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { settingsAtom, useSettings } from "@/utils/atoms/settings";
|
||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useRouter } from "expo-router";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import Video, { VideoRef } from "react-native-video";
|
||||
|
||||
type PlaybackType = {
|
||||
item: BaseItemDto;
|
||||
mediaSourceId: string;
|
||||
subtitleIndex: number;
|
||||
audioIndex: number;
|
||||
url: string;
|
||||
quality: any;
|
||||
};
|
||||
|
||||
export const playInfoAtom = atom<PlaybackType | null>(null);
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Video, { OnProgressData, VideoRef } from "react-native-video";
|
||||
import settings from "./(tabs)/(home)/settings";
|
||||
import iosFmp4 from "@/utils/profiles/iosFmp4";
|
||||
import native from "@/utils/profiles/native";
|
||||
import old from "@/utils/profiles/old";
|
||||
import {
|
||||
PlaybackType,
|
||||
usePlaySettings,
|
||||
} from "@/providers/PlaySettingsProvider";
|
||||
import { StatusBar, View } from "react-native";
|
||||
import React from "react";
|
||||
import { Controls } from "@/components/video-player/Controls";
|
||||
import { reportPlaybackProgress } from "@/utils/jellyfin/playstate/reportPlaybackProgress";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import { secondsToTicks } from "@/utils/secondsToTicks";
|
||||
|
||||
export default function page() {
|
||||
const [playInfo, setPlayInfo] = useAtom(playInfoAtom);
|
||||
const { playSettings, setPlaySettings, playUrl, reportStopPlayback } =
|
||||
usePlaySettings();
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const [settings] = useSettings();
|
||||
const router = useRouter();
|
||||
const videoRef = useRef<VideoRef | null>(null);
|
||||
const poster = usePoster(playInfo, api);
|
||||
const videoSource = useVideoSource(playInfo, api, poster);
|
||||
const poster = usePoster(playSettings, api);
|
||||
const videoSource = useVideoSource(playSettings, api, poster, playUrl);
|
||||
|
||||
const [ignoreSafeArea, setIgnoreSafeArea] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isBuffering, setIsBuffering] = useState(true);
|
||||
|
||||
if (!playInfo || !api || !videoSource) return null;
|
||||
const progress = useSharedValue(0);
|
||||
const isSeeking = useSharedValue(false);
|
||||
const cacheProgress = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("play-video ~", playUrl);
|
||||
});
|
||||
|
||||
if (!playSettings || !playUrl || !api || !videoSource || !playSettings.item)
|
||||
return null;
|
||||
|
||||
const togglePlay = useCallback(
|
||||
(ticks: number) => {
|
||||
if (isPlaying) {
|
||||
videoRef.current?.pause();
|
||||
reportPlaybackProgress({
|
||||
api,
|
||||
itemId: playSettings?.item?.Id,
|
||||
positionTicks: ticks,
|
||||
sessionId: undefined,
|
||||
IsPaused: true,
|
||||
});
|
||||
} else {
|
||||
videoRef.current?.resume();
|
||||
reportPlaybackProgress({
|
||||
api,
|
||||
itemId: playSettings?.item?.Id,
|
||||
positionTicks: ticks,
|
||||
sessionId: undefined,
|
||||
IsPaused: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isPlaying, api, playSettings?.item?.Id, videoRef]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) {
|
||||
togglePlay(playSettings.item?.UserData?.PlaybackPositionTicks || 0);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
const onProgress = useCallback(
|
||||
(data: OnProgressData) => {
|
||||
if (isSeeking.value === true) return;
|
||||
progress.value = secondsToTicks(data.currentTime);
|
||||
cacheProgress.value = secondsToTicks(data.playableDuration);
|
||||
setIsBuffering(data.playableDuration === 0);
|
||||
|
||||
if (!playSettings?.item?.Id || data.currentTime === 0) return;
|
||||
const ticks = data.currentTime * 10000000;
|
||||
reportPlaybackProgress({
|
||||
api,
|
||||
itemId: playSettings?.item.Id,
|
||||
positionTicks: ticks,
|
||||
sessionId: undefined,
|
||||
IsPaused: !isPlaying,
|
||||
});
|
||||
},
|
||||
[playSettings?.item.Id, isPlaying, api]
|
||||
);
|
||||
|
||||
return (
|
||||
<Video
|
||||
ref={videoRef}
|
||||
source={videoSource}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
resizeMode={ignoreSafeArea ? "cover" : "contain"}
|
||||
onProgress={() => {}}
|
||||
onLoad={(data) => {}}
|
||||
onError={() => {}}
|
||||
playWhenInactive={true}
|
||||
allowsExternalPlayback={true}
|
||||
playInBackground={true}
|
||||
pictureInPicture={true}
|
||||
showNotificationControls={true}
|
||||
ignoreSilentSwitch="ignore"
|
||||
fullscreen={false}
|
||||
/>
|
||||
<View className="relative h-screen w-screen flex flex-col items-center justify-center">
|
||||
<StatusBar hidden />
|
||||
<Video
|
||||
ref={videoRef}
|
||||
source={videoSource}
|
||||
paused={!isPlaying}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
resizeMode={ignoreSafeArea ? "cover" : "contain"}
|
||||
onProgress={onProgress}
|
||||
onLoad={(data) => {}}
|
||||
onError={() => {}}
|
||||
playWhenInactive={true}
|
||||
allowsExternalPlayback={true}
|
||||
playInBackground={true}
|
||||
pictureInPicture={true}
|
||||
showNotificationControls={true}
|
||||
ignoreSilentSwitch="ignore"
|
||||
fullscreen={false}
|
||||
onPlaybackStateChanged={(state) => setIsPlaying(state.isPlaying)}
|
||||
/>
|
||||
<Controls
|
||||
item={playSettings.item}
|
||||
videoRef={videoRef}
|
||||
togglePlay={togglePlay}
|
||||
isPlaying={isPlaying}
|
||||
isSeeking={isSeeking}
|
||||
progress={progress}
|
||||
cacheProgress={cacheProgress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePoster(
|
||||
playInfo: PlaybackType | null,
|
||||
playSettings: PlaybackType | null,
|
||||
api: Api | null
|
||||
): string | undefined {
|
||||
const poster = useMemo(() => {
|
||||
if (!playInfo?.item || !api) return undefined;
|
||||
return playInfo.item.Type === "Audio"
|
||||
? `${api.basePath}/Items/${playInfo.item.AlbumId}/Images/Primary?tag=${playInfo.item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200`
|
||||
if (!playSettings?.item || !api) return undefined;
|
||||
return playSettings.item.Type === "Audio"
|
||||
? `${api.basePath}/Items/${playSettings.item.AlbumId}/Images/Primary?tag=${playSettings.item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200`
|
||||
: getBackdropUrl({
|
||||
api,
|
||||
item: playInfo.item,
|
||||
item: playSettings.item,
|
||||
quality: 70,
|
||||
width: 200,
|
||||
});
|
||||
}, [playInfo?.item, api]);
|
||||
}, [playSettings?.item, api]);
|
||||
|
||||
return poster ?? undefined;
|
||||
}
|
||||
|
||||
export function useVideoSource(
|
||||
playInfo: PlaybackType | null,
|
||||
playSettings: PlaybackType | null,
|
||||
api: Api | null,
|
||||
poster: string | undefined
|
||||
poster: string | undefined,
|
||||
playUrl?: string | null
|
||||
) {
|
||||
const videoSource = useMemo(() => {
|
||||
if (!playInfo || !api) return null;
|
||||
if (!playSettings || !api || !playUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startPosition = playInfo.item?.UserData?.PlaybackPositionTicks
|
||||
? Math.round(playInfo.item.UserData.PlaybackPositionTicks / 10000)
|
||||
const startPosition = playSettings.item?.UserData?.PlaybackPositionTicks
|
||||
? Math.round(playSettings.item.UserData.PlaybackPositionTicks / 10000)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
uri: playInfo.url,
|
||||
uri: playUrl,
|
||||
isNetwork: true,
|
||||
startPosition,
|
||||
headers: getAuthHeaders(api),
|
||||
metadata: {
|
||||
artist: playInfo.item?.AlbumArtist ?? undefined,
|
||||
title: playInfo.item?.Name || "Unknown",
|
||||
description: playInfo.item?.Overview ?? undefined,
|
||||
artist: playSettings.item?.AlbumArtist ?? undefined,
|
||||
title: playSettings.item?.Name || "Unknown",
|
||||
description: playSettings.item?.Overview ?? undefined,
|
||||
imageUri: poster,
|
||||
subtitle: playInfo.item?.Album ?? undefined,
|
||||
subtitle: playSettings.item?.Album ?? undefined,
|
||||
},
|
||||
};
|
||||
}, [playInfo, api, poster]);
|
||||
}, [playSettings, api, poster]);
|
||||
|
||||
return videoSource;
|
||||
}
|
||||
|
||||
115
app/_layout.tsx
115
app/_layout.tsx
@@ -6,6 +6,7 @@ import {
|
||||
} from "@/providers/JellyfinProvider";
|
||||
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
||||
import { PlaybackProvider } from "@/providers/PlaybackProvider";
|
||||
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
||||
import { orientationAtom } from "@/utils/atoms/orientation";
|
||||
import { Settings, useSettings } from "@/utils/atoms/settings";
|
||||
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
|
||||
@@ -317,62 +318,68 @@ function Layout() {
|
||||
<ActionSheetProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<JellyfinProvider>
|
||||
<PlaybackProvider>
|
||||
<StatusBar style="light" backgroundColor="#000" />
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<Stack
|
||||
initialRouteName="/home"
|
||||
screenOptions={{
|
||||
autoHideHomeIndicator: true,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="(auth)/(tabs)"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "",
|
||||
<PlaySettingsProvider>
|
||||
<PlaybackProvider>
|
||||
<StatusBar style="light" backgroundColor="#000" />
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<Stack initialRouteName="/home">
|
||||
<Stack.Screen
|
||||
name="(auth)/(tabs)"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/play"
|
||||
options={{
|
||||
headerShown: false,
|
||||
autoHideHomeIndicator: true,
|
||||
title: "",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/play-video"
|
||||
options={{
|
||||
headerShown: false,
|
||||
autoHideHomeIndicator: true,
|
||||
title: "",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/play-music"
|
||||
options={{
|
||||
headerShown: false,
|
||||
autoHideHomeIndicator: true,
|
||||
title: "",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="login"
|
||||
options={{ headerShown: false, title: "Login" }}
|
||||
/>
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<Toaster
|
||||
duration={4000}
|
||||
toastOptions={{
|
||||
style: {
|
||||
backgroundColor: "#262626",
|
||||
borderColor: "#363639",
|
||||
borderWidth: 1,
|
||||
},
|
||||
titleStyle: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
closeButton
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/play"
|
||||
options={{
|
||||
headerShown: false,
|
||||
autoHideHomeIndicator: true,
|
||||
title: "",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/play-music"
|
||||
options={{
|
||||
headerShown: false,
|
||||
autoHideHomeIndicator: true,
|
||||
title: "",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="login"
|
||||
options={{ headerShown: false, title: "Login" }}
|
||||
/>
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<Toaster
|
||||
duration={4000}
|
||||
toastOptions={{
|
||||
style: {
|
||||
backgroundColor: "#262626",
|
||||
borderColor: "#363639",
|
||||
borderWidth: 1,
|
||||
},
|
||||
titleStyle: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
closeButton
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</PlaybackProvider>
|
||||
</ThemeProvider>
|
||||
</PlaybackProvider>
|
||||
</PlaySettingsProvider>
|
||||
</JellyfinProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</ActionSheetProvider>
|
||||
|
||||
Reference in New Issue
Block a user