mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-17 18:03:01 +01:00
wip
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { FullScreenMusicPlayer } from "@/components/FullScreenMusicPlayer";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { View, ViewProps } from "react-native";
|
||||
|
||||
@@ -8,7 +7,6 @@ export default function page() {
|
||||
return (
|
||||
<View className="">
|
||||
<StatusBar hidden={false} />
|
||||
<FullScreenMusicPlayer />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
246
app/(auth)/play-offline-video.tsx
Normal file
246
app/(auth)/play-offline-video.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { Controls } from "@/components/video-player/Controls";
|
||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
PlaybackType,
|
||||
usePlaySettings,
|
||||
} from "@/providers/PlaySettingsProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||
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 * as ScreenOrientation from "expo-screen-orientation";
|
||||
import { useAtomValue } from "jotai";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
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";
|
||||
import { useLocalSearchParams, useGlobalSearchParams, Link } from "expo-router";
|
||||
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
|
||||
export default function page() {
|
||||
const { playSettings, playUrl } = usePlaySettings();
|
||||
|
||||
const api = useAtomValue(apiAtom);
|
||||
const [settings] = useSettings();
|
||||
const videoRef = useRef<VideoRef | null>(null);
|
||||
const videoSource = useVideoSource(playSettings, api, playUrl);
|
||||
const firstTime = useRef(true);
|
||||
|
||||
const screenDimensions = Dimensions.get("screen");
|
||||
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isBuffering, setIsBuffering] = useState(true);
|
||||
const [orientation, setOrientation] = useState(
|
||||
ScreenOrientation.OrientationLock.UNKNOWN
|
||||
);
|
||||
|
||||
const progress = useSharedValue(0);
|
||||
const isSeeking = useSharedValue(false);
|
||||
const cacheProgress = useSharedValue(0);
|
||||
|
||||
if (!playSettings || !playUrl || !api || !videoSource || !playSettings.item)
|
||||
return null;
|
||||
|
||||
const togglePlay = useCallback(async () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
if (isPlaying) {
|
||||
setIsPlaying(false);
|
||||
videoRef.current?.pause();
|
||||
} else {
|
||||
setIsPlaying(true);
|
||||
videoRef.current?.resume();
|
||||
}
|
||||
}, [isPlaying, api, playSettings?.item?.Id, videoRef, settings]);
|
||||
|
||||
const play = useCallback(() => {
|
||||
setIsPlaying(true);
|
||||
videoRef.current?.resume();
|
||||
}, [videoRef]);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
setIsPlaying(false);
|
||||
videoRef.current?.pause();
|
||||
}, [videoRef]);
|
||||
|
||||
useEffect(() => {
|
||||
play();
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const orientationSubscription =
|
||||
ScreenOrientation.addOrientationChangeListener((event) => {
|
||||
setOrientation(
|
||||
orientationToOrientationLock(event.orientationInfo.orientation)
|
||||
);
|
||||
});
|
||||
|
||||
ScreenOrientation.getOrientationAsync().then((orientation) => {
|
||||
setOrientation(orientationToOrientationLock(orientation));
|
||||
});
|
||||
|
||||
return () => {
|
||||
orientationSubscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.autoRotate) {
|
||||
// Don't need to do anything
|
||||
} else if (settings?.defaultVideoOrientation) {
|
||||
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
NavigationBar.setVisibilityAsync("hidden");
|
||||
NavigationBar.setBehaviorAsync("overlay-swipe");
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (settings?.autoRotate) {
|
||||
ScreenOrientation.unlockAsync();
|
||||
} else {
|
||||
ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
NavigationBar.setVisibilityAsync("visible");
|
||||
NavigationBar.setBehaviorAsync("inset-swipe");
|
||||
}
|
||||
};
|
||||
}, [settings]);
|
||||
|
||||
const onProgress = useCallback(
|
||||
async (data: OnProgressData) => {
|
||||
if (isSeeking.value === true) return;
|
||||
progress.value = secondsToTicks(data.currentTime);
|
||||
cacheProgress.value = secondsToTicks(data.playableDuration);
|
||||
setIsBuffering(data.playableDuration === 0);
|
||||
},
|
||||
[playSettings?.item.Id, isPlaying, api]
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: screenDimensions.width,
|
||||
height: screenDimensions.height,
|
||||
position: "relative",
|
||||
}}
|
||||
className="flex flex-col items-center justify-center"
|
||||
>
|
||||
<StatusBar hidden />
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setShowControls(!showControls);
|
||||
}}
|
||||
className="absolute z-0 h-full w-full"
|
||||
>
|
||||
<Video
|
||||
ref={videoRef}
|
||||
source={videoSource}
|
||||
paused={!isPlaying}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
|
||||
onProgress={onProgress}
|
||||
onError={() => {}}
|
||||
onLoad={() => {
|
||||
if (firstTime.current === true) {
|
||||
play();
|
||||
firstTime.current = false;
|
||||
}
|
||||
}}
|
||||
playWhenInactive={true}
|
||||
allowsExternalPlayback={true}
|
||||
playInBackground={true}
|
||||
pictureInPicture={true}
|
||||
showNotificationControls={true}
|
||||
ignoreSilentSwitch="ignore"
|
||||
fullscreen={false}
|
||||
onPlaybackStateChanged={(state) => setIsPlaying(state.isPlaying)}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
<Controls
|
||||
item={playSettings.item}
|
||||
videoRef={videoRef}
|
||||
togglePlay={togglePlay}
|
||||
isPlaying={isPlaying}
|
||||
isSeeking={isSeeking}
|
||||
progress={progress}
|
||||
cacheProgress={cacheProgress}
|
||||
isBuffering={isBuffering}
|
||||
showControls={showControls}
|
||||
setShowControls={setShowControls}
|
||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
||||
ignoreSafeAreas={ignoreSafeAreas}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePoster(
|
||||
playSettings: PlaybackType | null,
|
||||
api: Api | null
|
||||
): string | undefined {
|
||||
const poster = useMemo(() => {
|
||||
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: playSettings.item,
|
||||
quality: 70,
|
||||
width: 200,
|
||||
});
|
||||
}, [playSettings?.item, api]);
|
||||
|
||||
return poster ?? undefined;
|
||||
}
|
||||
|
||||
export function useVideoSource(
|
||||
playSettings: PlaybackType | null,
|
||||
api: Api | null,
|
||||
playUrl?: string | null
|
||||
) {
|
||||
const videoSource = useMemo(() => {
|
||||
if (!playSettings || !api || !playUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startPosition = 0;
|
||||
|
||||
return {
|
||||
uri: playUrl,
|
||||
isNetwork: false,
|
||||
startPosition,
|
||||
metadata: {
|
||||
artist: playSettings.item?.AlbumArtist ?? undefined,
|
||||
title: playSettings.item?.Name || "Unknown",
|
||||
description: playSettings.item?.Overview ?? undefined,
|
||||
subtitle: playSettings.item?.Album ?? undefined,
|
||||
},
|
||||
};
|
||||
}, [playSettings, api]);
|
||||
|
||||
return videoSource;
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export default function page() {
|
||||
const videoRef = useRef<VideoRef | null>(null);
|
||||
const poster = usePoster(playSettings, api);
|
||||
const videoSource = useVideoSource(playSettings, api, poster, playUrl);
|
||||
const firstTime = useRef(true);
|
||||
|
||||
const screenDimensions = Dimensions.get("screen");
|
||||
|
||||
@@ -131,25 +132,6 @@ export default function page() {
|
||||
});
|
||||
};
|
||||
|
||||
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(
|
||||
async (data: OnProgressData) => {
|
||||
if (isSeeking.value === true) return;
|
||||
@@ -179,6 +161,13 @@ export default function page() {
|
||||
[playSettings?.item.Id, isPlaying, api]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
play();
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const orientationSubscription =
|
||||
ScreenOrientation.addOrientationChangeListener((event) => {
|
||||
@@ -196,14 +185,35 @@ export default function page() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isLandscape = useMemo(() => {
|
||||
return orientation === ScreenOrientation.OrientationLock.LANDSCAPE_LEFT ||
|
||||
orientation === ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
|
||||
? true
|
||||
: false;
|
||||
}, [orientation]);
|
||||
useEffect(() => {
|
||||
if (settings?.autoRotate) {
|
||||
// Don't need to do anything
|
||||
} else if (settings?.defaultVideoOrientation) {
|
||||
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
|
||||
}
|
||||
|
||||
const { isConnected } = useWebSocket({
|
||||
if (Platform.OS === "android") {
|
||||
NavigationBar.setVisibilityAsync("hidden");
|
||||
NavigationBar.setBehaviorAsync("overlay-swipe");
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (settings?.autoRotate) {
|
||||
ScreenOrientation.unlockAsync();
|
||||
} else {
|
||||
ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
NavigationBar.setVisibilityAsync("visible");
|
||||
NavigationBar.setBehaviorAsync("inset-swipe");
|
||||
}
|
||||
};
|
||||
}, [settings]);
|
||||
|
||||
useWebSocket({
|
||||
isPlaying: isPlaying,
|
||||
pauseVideo: pause,
|
||||
playVideo: play,
|
||||
@@ -262,7 +272,6 @@ export default function page() {
|
||||
isBuffering={isBuffering}
|
||||
showControls={showControls}
|
||||
setShowControls={setShowControls}
|
||||
isLandscape={isLandscape}
|
||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
||||
ignoreSafeAreas={ignoreSafeAreas}
|
||||
/>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { FullScreenVideoPlayer } from "@/components/FullScreenVideoPlayer";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import * as NavigationBar from "expo-navigation-bar";
|
||||
import * as ScreenOrientation from "expo-screen-orientation";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { useEffect } from "react";
|
||||
import { Platform, View, ViewProps } from "react-native";
|
||||
|
||||
interface Props extends ViewProps {}
|
||||
|
||||
export default function page() {
|
||||
const [settings] = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.autoRotate) {
|
||||
// Don't need to do anything
|
||||
} else if (settings?.defaultVideoOrientation) {
|
||||
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
NavigationBar.setVisibilityAsync("hidden");
|
||||
NavigationBar.setBehaviorAsync("overlay-swipe");
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (settings?.autoRotate) {
|
||||
ScreenOrientation.unlockAsync();
|
||||
} else {
|
||||
ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
NavigationBar.setVisibilityAsync("visible");
|
||||
NavigationBar.setBehaviorAsync("inset-swipe");
|
||||
}
|
||||
};
|
||||
}, [settings]);
|
||||
|
||||
return (
|
||||
<View className="">
|
||||
<StatusBar hidden />
|
||||
<FullScreenVideoPlayer />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
JellyfinProvider,
|
||||
} 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";
|
||||
@@ -313,12 +312,12 @@ function Layout() {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<QueryClientProvider client={queryClientRef.current}>
|
||||
<JobQueueProvider>
|
||||
<DownloadProvider>
|
||||
<ActionSheetProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<JellyfinProvider>
|
||||
<PlaySettingsProvider>
|
||||
<ActionSheetProvider>
|
||||
<JobQueueProvider>
|
||||
<JellyfinProvider>
|
||||
<PlaySettingsProvider>
|
||||
<DownloadProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<StatusBar style="light" backgroundColor="#000" />
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<Stack initialRouteName="/home">
|
||||
@@ -330,7 +329,7 @@ function Layout() {
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/play"
|
||||
name="(auth)/play-video"
|
||||
options={{
|
||||
headerShown: false,
|
||||
autoHideHomeIndicator: true,
|
||||
@@ -339,7 +338,7 @@ function Layout() {
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/play-video"
|
||||
name="(auth)/play-offline-video"
|
||||
options={{
|
||||
headerShown: false,
|
||||
autoHideHomeIndicator: true,
|
||||
@@ -377,12 +376,12 @@ function Layout() {
|
||||
closeButton
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</PlaySettingsProvider>
|
||||
</JellyfinProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</ActionSheetProvider>
|
||||
</DownloadProvider>
|
||||
</JobQueueProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</DownloadProvider>
|
||||
</PlaySettingsProvider>
|
||||
</JellyfinProvider>
|
||||
</JobQueueProvider>
|
||||
</ActionSheetProvider>
|
||||
</QueryClientProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user