This commit is contained in:
Fredrik Burmester
2024-10-06 16:33:29 +02:00
parent 862e783de1
commit d6f02bd970
9 changed files with 394 additions and 147 deletions

View File

@@ -1,5 +1,6 @@
import { Controls } from "@/components/video-player/Controls";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useWebSocket } from "@/hooks/useWebsockets";
import { apiAtom } from "@/providers/JellyfinProvider";
import {
PlaybackType,
usePlaySettings,
@@ -7,14 +8,13 @@ import {
import { useSettings } from "@/utils/atoms/settings";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { reportPlaybackProgress } from "@/utils/jellyfin/playstate/reportPlaybackProgress";
import orientationToOrientationLock from "@/utils/OrientationLockConverter";
import { secondsToTicks } from "@/utils/secondsToTicks";
import { Api } from "@jellyfin/sdk";
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
import * as Haptics from "expo-haptics";
import { useRouter } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation";
import { useAtom } from "jotai";
import { useAtomValue } from "jotai";
import React, {
useCallback,
useEffect,
@@ -22,27 +22,23 @@ import React, {
useRef,
useState,
} from "react";
import { Dimensions, Pressable, StatusBar, View } from "react-native";
import { Dimensions, Platform, Pressable, StatusBar, View } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import Video, { OnProgressData, VideoRef } from "react-native-video";
import * as NavigationBar from "expo-navigation-bar";
export default function page() {
const { playSettings, setPlaySettings, playUrl, reportStopPlayback } =
usePlaySettings();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { playSettings, playUrl } = usePlaySettings();
const api = useAtomValue(apiAtom);
const [settings] = useSettings();
const router = useRouter();
const videoRef = useRef<VideoRef | null>(null);
const poster = usePoster(playSettings, api);
const videoSource = useVideoSource(playSettings, api, poster, playUrl);
const windowDimensions = Dimensions.get("window");
const screenDimensions = Dimensions.get("screen");
const [showControls, setShowControls] = useState(true);
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
const [ignoreSafeArea, setIgnoreSafeArea] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
const [isBuffering, setIsBuffering] = useState(true);
const [orientation, setOrientation] = useState(
@@ -57,59 +53,127 @@ export default function page() {
return null;
const togglePlay = useCallback(
(ticks: number) => {
async (ticks: number) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
console.log("togglePlay", ticks);
if (isPlaying) {
setIsPlaying(false);
videoRef.current?.pause();
reportPlaybackProgress({
api,
itemId: playSettings?.item?.Id,
positionTicks: ticks,
sessionId: undefined,
IsPaused: true,
await getPlaystateApi(api).onPlaybackProgress({
itemId: playSettings.item?.Id!,
audioStreamIndex: playSettings.audioIndex
? playSettings.audioIndex
: undefined,
subtitleStreamIndex: playSettings.subtitleIndex
? playSettings.subtitleIndex
: undefined,
mediaSourceId: playSettings.mediaSource?.Id!,
positionTicks: Math.round(ticks),
isPaused: true,
playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream",
});
} else {
setIsPlaying(true);
videoRef.current?.resume();
reportPlaybackProgress({
api,
itemId: playSettings?.item?.Id,
positionTicks: ticks,
sessionId: undefined,
IsPaused: false,
await getPlaystateApi(api).onPlaybackProgress({
itemId: playSettings.item?.Id!,
audioStreamIndex: playSettings.audioIndex
? playSettings.audioIndex
: undefined,
subtitleStreamIndex: playSettings.subtitleIndex
? playSettings.subtitleIndex
: undefined,
mediaSourceId: playSettings.mediaSource?.Id!,
positionTicks: Math.round(ticks),
isPaused: false,
playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream",
});
}
},
[isPlaying, api, playSettings?.item?.Id, videoRef]
[isPlaying, api, playSettings?.item?.Id, videoRef, settings]
);
const play = useCallback(() => {
setIsPlaying(true);
videoRef.current?.resume();
reportPlaybackStart();
}, [videoRef]);
const pause = useCallback(() => {
setIsPlaying(false);
videoRef.current?.pause();
}, [videoRef]);
const stop = useCallback(() => {
setIsPlaying(false);
videoRef.current?.pause();
reportPlaybackStopped();
}, [videoRef]);
const reportPlaybackStopped = async () => {
await getPlaystateApi(api).onPlaybackStopped({
itemId: playSettings?.item?.Id!,
mediaSourceId: playSettings.mediaSource?.Id!,
positionTicks: progress.value,
});
};
const reportPlaybackStart = async () => {
await getPlaystateApi(api).onPlaybackStart({
itemId: playSettings?.item?.Id!,
audioStreamIndex: playSettings.audioIndex
? playSettings.audioIndex
: undefined,
subtitleStreamIndex: playSettings.subtitleIndex
? playSettings.subtitleIndex
: undefined,
mediaSourceId: playSettings.mediaSource?.Id!,
playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream",
});
};
const firstTime = useRef(true);
useEffect(() => {
play();
if (Platform.OS === "android") {
NavigationBar.setVisibilityAsync("hidden");
NavigationBar.setBehaviorAsync("overlay-swipe");
}
return () => {
stop();
if (Platform.OS === "android") {
NavigationBar.setVisibilityAsync("visible");
NavigationBar.setBehaviorAsync("inset-swipe");
}
};
}, []);
const onProgress = useCallback(
(data: OnProgressData) => {
async (data: OnProgressData) => {
if (isSeeking.value === true) return;
const ticks = data.currentTime * 10000000;
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,
await getPlaystateApi(api).onPlaybackProgress({
itemId: playSettings.item.Id,
audioStreamIndex: playSettings.audioIndex
? playSettings.audioIndex
: undefined,
subtitleStreamIndex: playSettings.subtitleIndex
? playSettings.subtitleIndex
: undefined,
mediaSourceId: playSettings.mediaSource?.Id!,
positionTicks: Math.round(ticks),
isPaused: !isPlaying,
playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream",
});
},
[playSettings?.item.Id, isPlaying, api]
@@ -139,6 +203,13 @@ export default function page() {
: false;
}, [orientation]);
const { isConnected } = useWebSocket({
isPlaying: isPlaying,
pauseVideo: pause,
playVideo: play,
stopPlayback: stop,
});
return (
<View
style={{
@@ -163,6 +234,12 @@ export default function page() {
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
onProgress={onProgress}
onError={() => {}}
onLoad={() => {
if (firstTime.current === true) {
play();
firstTime.current = false;
}
}}
playWhenInactive={true}
allowsExternalPlayback={true}
playInBackground={true}

View File

@@ -319,66 +319,64 @@ function Layout() {
<BottomSheetModalProvider>
<JellyfinProvider>
<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",
},
<StatusBar style="light" backgroundColor="#000" />
<ThemeProvider value={DarkTheme}>
<Stack initialRouteName="/home">
<Stack.Screen
name="(auth)/(tabs)"
options={{
headerShown: false,
title: "",
}}
closeButton
/>
</ThemeProvider>
</PlaybackProvider>
<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
/>
</ThemeProvider>
</PlaySettingsProvider>
</JellyfinProvider>
</BottomSheetModalProvider>