This commit is contained in:
Fredrik Burmester
2024-09-16 18:18:08 +02:00
parent 595120229f
commit 402bdec5ab
10 changed files with 368 additions and 134 deletions

View File

@@ -4,6 +4,7 @@ import { useNavigationBarVisibility } from "@/hooks/useNavigationBarVisibility";
import { useTrickplay } from "@/hooks/useTrickplay";
import { apiAtom } from "@/providers/JellyfinProvider";
import { usePlayback } from "@/providers/PlaybackProvider";
import { parseM3U8ForSubtitles } from "@/utils/hls/parseM3U8ForSubtitles";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log";
@@ -34,6 +35,16 @@ import Video from "react-native-video";
import { Text } from "./common/Text";
import { itemRouter } from "./common/TouchableItemRouter";
import { Loader } from "./Loader";
import * as ScreenOrientation from "expo-screen-orientation";
import { useSettings } from "@/utils/atoms/settings";
async function setOrientation(orientation: ScreenOrientation.OrientationLock) {
await ScreenOrientation.lockAsync(orientation);
}
async function resetOrientation() {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT);
}
export const CurrentlyPlayingBar: React.FC = () => {
const {
@@ -45,7 +56,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
setIsPlaying,
isPlaying,
videoRef,
progressTicks,
onProgress,
isBuffering: _isBuffering,
setIsBuffering,
@@ -53,6 +63,8 @@ export const CurrentlyPlayingBar: React.FC = () => {
useNavigationBarVisibility(isPlaying);
const [settings] = useSettings();
const insets = useSafeAreaInsets();
const segments = useSegments();
const router = useRouter();
@@ -69,7 +81,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
const screenHeight = Dimensions.get("window").height;
const screenWidth = Dimensions.get("window").width;
const progress = useSharedValue(progressTicks || 0);
const progress = useSharedValue(0);
const min = useSharedValue(0);
const max = useSharedValue(currentlyPlaying?.item.RunTimeTicks || 0);
const sliding = useRef(false);
@@ -222,6 +234,56 @@ export const CurrentlyPlayingBar: React.FC = () => {
showControls();
}, [currentlyPlaying]);
const { data: subtitleTracks } = useQuery({
queryKey: ["subtitleTracks", currentlyPlaying?.url],
queryFn: async () => {
if (!currentlyPlaying?.url) {
console.log("No item url");
return null;
}
const tracks = await parseM3U8ForSubtitles(currentlyPlaying.url);
console.log("Subtitle tracks", tracks);
return tracks;
},
});
/**
* This should clean up all values if curentlyPlaying sets to null or changes
*/
useEffect(() => {
if (!currentlyPlaying) {
// Reset all local state and shared values
progress.value = 0;
min.value = 0;
max.value = 0;
cacheProgress.value = 0;
localIsBuffering.value = false;
sliding.current = false;
hideControls();
resetOrientation();
} else {
// Initialize or update values based on the new currentlyPlaying item
progress.value =
currentlyPlaying.item?.UserData?.PlaybackPositionTicks || 0;
max.value = currentlyPlaying.item.RunTimeTicks || 0;
showControls();
setOrientation(
settings?.defaultVideoOrientation ||
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT
);
}
// Cleanup function
return () => {
// Cancel any subscriptions or timers if you have any
// clearTimeout(timerId);
// unsubscribe();
};
}, [currentlyPlaying, settings]);
if (!api || !currentlyPlaying) return null;
return (
@@ -232,68 +294,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
backgroundColor: "black",
}}
>
<Animated.View
style={[
{
position: "absolute",
top: insets.top,
right: insets.right + 20,
height: 70,
zIndex: 10,
},
animatedControlsStyle,
]}
>
<View className="flex flex-row items-center h-full">
<TouchableOpacity
onPress={() => {
if (!isVisible) return;
toggleIgnoreSafeArea();
}}
className="aspect-square rounded flex flex-col items-center justify-center p-2"
>
<Ionicons
name={ignoreSafeArea ? "contract-outline" : "expand"}
size={24}
color="white"
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
if (!isVisible) return;
stopPlayback();
}}
className="aspect-square rounded flex flex-col items-center justify-center p-2"
>
<Ionicons name="close" size={24} color="white" />
</TouchableOpacity>
</View>
</Animated.View>
<Animated.View
style={[
{
position: "absolute",
bottom: insets.bottom + 8 * 7,
right: insets.right + 32,
zIndex: 10,
},
animatedIntroSkipperStyle,
]}
>
<View className="flex flex-row items-center h-full">
<TouchableOpacity
onPress={() => {
if (!isVisible) return;
skipIntro();
}}
className="flex flex-col items-center justify-center px-2 py-1.5 bg-purple-600 rounded-full"
>
<Text>Skip intro</Text>
</TouchableOpacity>
</View>
</Animated.View>
<Animated.View style={[videoContainerStyle, animatedVideoContainerStyle]}>
<Pressable
onPress={() => {
@@ -340,6 +340,15 @@ export const CurrentlyPlayingBar: React.FC = () => {
subtitleStyle={{
fontSize: 16,
}}
onTextTracks={(e) => {
console.log("onTextTracks ~", e.textTracks);
}}
onTextTrackDataChanged={(e) => {
console.log("onTextTrackDataChanged ~", e);
}}
onVideoTracks={(e) => {
console.log("onVideoTracks ~", e.videoTracks);
}}
source={videoSource}
onPlaybackStateChanged={(e) => {
if (e.isPlaying === true) {
@@ -362,7 +371,10 @@ export const CurrentlyPlayingBar: React.FC = () => {
setIsPlaying(false);
}}
renderLoader={
<View className="absolute w-screen h-screen flex flex-col items-center justify-center">
<View
pointerEvents="none"
className="absolute w-screen h-screen top-0 left-0 items-center justify-center"
>
<Loader />
</View>
}
@@ -371,6 +383,87 @@ export const CurrentlyPlayingBar: React.FC = () => {
</Pressable>
</Animated.View>
<Animated.View
pointerEvents="none"
style={[
{
position: "absolute" as const,
top: 0,
bottom: 0,
left: ignoreSafeArea ? 0 : insets.left,
right: ignoreSafeArea ? 0 : insets.right,
width: ignoreSafeArea
? screenWidth
: screenWidth - (insets.left + insets.right),
justifyContent: "center",
alignItems: "center",
},
animatedLoaderStyle,
]}
>
<Loader />
</Animated.View>
<Animated.View
style={[
{
position: "absolute",
bottom: insets.bottom + 8 * 7,
right: insets.right + 32,
},
animatedIntroSkipperStyle,
]}
>
<View className="flex flex-row items-center h-full">
<TouchableOpacity
onPress={() => {
if (!isVisible) return;
skipIntro();
}}
className="flex flex-col items-center justify-center px-2 py-1.5 bg-purple-600 rounded-full"
>
<Text>Skip intro</Text>
</TouchableOpacity>
</View>
</Animated.View>
<Animated.View
style={[
{
position: "absolute",
top: insets.top,
right: insets.right + 20,
height: 70,
},
animatedControlsStyle,
]}
>
<View className="flex flex-row items-center h-full">
<TouchableOpacity
onPress={() => {
if (!isVisible) return;
toggleIgnoreSafeArea();
}}
className="aspect-square rounded flex flex-col items-center justify-center p-2"
>
<Ionicons
name={ignoreSafeArea ? "contract-outline" : "expand"}
size={24}
color="white"
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
if (!isVisible) return;
stopPlayback();
}}
className="aspect-square rounded flex flex-col items-center justify-center p-2"
>
<Ionicons name="close" size={24} color="white" />
</TouchableOpacity>
</View>
</Animated.View>
<Animated.View
style={[
{
@@ -401,7 +494,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
</Text>
)}
</View>
<View className="flex flex-row items-center space-x-6 rounded-full py-1.5 pl-4 pr-4 z-10 bg-neutral-800">
<View className="flex flex-row items-center space-x-6 rounded-full py-1.5 pl-4 pr-4 bg-neutral-800">
<View className="flex flex-row items-center space-x-2">
<TouchableOpacity
disabled={!previousItem}
@@ -562,28 +655,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
</View>
</View>
</Animated.View>
<Animated.View
pointerEvents="none"
style={[
{
position: "absolute" as const,
top: 0,
bottom: 0,
left: ignoreSafeArea ? 0 : insets.left,
right: ignoreSafeArea ? 0 : insets.right,
width: ignoreSafeArea
? screenWidth
: screenWidth - (insets.left + insets.right),
justifyContent: "center",
alignItems: "center",
zIndex: 10,
},
animatedLoaderStyle,
]}
>
<Loader />
</Animated.View>
</View>
);
};

View File

@@ -56,7 +56,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
useState<MediaSourceInfo | null>(null);
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
const [selectedSubtitleStream, setSelectedSubtitleStream] =
useState<number>(0);
useState<number>(-1);
const [maxBitrate, setMaxBitrate] = useState<Bitrate>({
key: "Max",
value: undefined,

View File

@@ -2,6 +2,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
DefaultLanguageOption,
DownloadOptions,
ScreenOrientationEnum,
useSettings,
} from "@/utils/atoms/settings";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
@@ -21,6 +22,7 @@ import { Input } from "../common/Input";
import { useState } from "react";
import { Button } from "../Button";
import { MediaToggles } from "./MediaToggles";
import * as ScreenOrientation from "expo-screen-orientation";
interface Props extends ViewProps {}
@@ -56,6 +58,8 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
staleTime: 0,
});
if (!settings) return null;
return (
<View {...props}>
{/* <View>
@@ -88,7 +92,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</Text>
</View>
<Switch
value={settings?.autoRotate}
value={settings.autoRotate}
onValueChange={(value) => updateSettings({ autoRotate: value })}
/>
</View>
@@ -102,7 +106,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</Text>
</View>
<Switch
value={settings?.openFullScreenVideoPlayerByDefault}
value={settings.openFullScreenVideoPlayerByDefault}
onValueChange={(value) =>
updateSettings({ openFullScreenVideoPlayerByDefault: value })
}
@@ -118,7 +122,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</Text>
</View>
<Switch
value={settings?.openInVLC}
value={settings.openInVLC}
onValueChange={(value) => {
updateSettings({ openInVLC: value, forceDirectPlay: value });
}}
@@ -141,13 +145,13 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</TouchableOpacity>
</View>
<Switch
value={settings?.usePopularPlugin}
value={settings.usePopularPlugin}
onValueChange={(value) =>
updateSettings({ usePopularPlugin: value })
}
/>
</View>
{settings?.usePopularPlugin && (
{settings.usePopularPlugin && (
<View className="flex flex-col py-2 bg-neutral-900">
{mediaListCollections?.map((mlc) => (
<View
@@ -158,9 +162,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<Text className="font-semibold">{mlc.Name}</Text>
</View>
<Switch
value={settings?.mediaListCollectionIds?.includes(
mlc.Id!
)}
value={settings.mediaListCollectionIds?.includes(mlc.Id!)}
onValueChange={(value) => {
if (!settings.mediaListCollectionIds) {
updateSettings({
@@ -171,11 +173,11 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
updateSettings({
mediaListCollectionIds:
settings?.mediaListCollectionIds.includes(mlc.Id!)
? settings?.mediaListCollectionIds.filter(
settings.mediaListCollectionIds.includes(mlc.Id!)
? settings.mediaListCollectionIds.filter(
(id) => id !== mlc.Id
)
: [...settings?.mediaListCollectionIds, mlc.Id!],
: [...settings.mediaListCollectionIds, mlc.Id!],
});
}}
/>
@@ -206,7 +208,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</Text>
</View>
<Switch
value={settings?.forceDirectPlay}
value={settings.forceDirectPlay}
onValueChange={(value) =>
updateSettings({ forceDirectPlay: value })
}
@@ -216,7 +218,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<View
className={`
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
${settings?.forceDirectPlay ? "opacity-50 select-none" : ""}
${settings.forceDirectPlay ? "opacity-50 select-none" : ""}
`}
>
<View className="flex flex-col shrink">
@@ -229,7 +231,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text>{settings?.deviceProfile}</Text>
<Text>{settings.deviceProfile}</Text>
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content
@@ -272,8 +274,108 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<View className="flex flex-col">
<View
className={`
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
`}
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
`}
>
<View className="flex flex-col shrink">
<Text className="font-semibold">Video orientation</Text>
<Text className="text-xs opacity-50">
Set the full screen video player orientation.
</Text>
</View>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text>
{ScreenOrientationEnum[settings.defaultVideoOrientation]}
</Text>
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content
loop={true}
side="bottom"
align="start"
alignOffset={0}
avoidCollisions={true}
collisionPadding={8}
sideOffset={8}
>
<DropdownMenu.Label>Orientation</DropdownMenu.Label>
<DropdownMenu.Item
key="1"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.DEFAULT,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.DEFAULT
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="2"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.PORTRAIT_UP,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.PORTRAIT_UP
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="3"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="4"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</View>
<View
className={`
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
`}
>
<View className="flex flex-col shrink">
<Text className="font-semibold">Search engine</Text>
@@ -284,7 +386,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text>{settings?.searchEngine}</Text>
<Text>{settings.searchEngine}</Text>
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content
@@ -318,7 +420,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</DropdownMenu.Content>
</DropdownMenu.Root>
</View>
{settings?.searchEngine === "Marlin" && (
{settings.searchEngine === "Marlin" && (
<View className="flex flex-col bg-neutral-900 px-4 pb-4">
<>
<View className="flex flex-row items-center space-x-2">
@@ -346,7 +448,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</View>
<Text className="text-neutral-500 mt-2">
{settings?.marlinServerUrl}
{settings.marlinServerUrl}
</Text>
</>
</View>