This commit is contained in:
Fredrik Burmester
2024-10-06 13:03:16 +02:00
parent cc242a971f
commit 0233862fc1
13 changed files with 941 additions and 391 deletions

View File

@@ -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;
}

View File

@@ -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>