Compare commits

...

1 Commits

Author SHA1 Message Date
Fredrik Burmester
1c4bc68566 wip 2024-09-19 21:14:03 +02:00
5 changed files with 126 additions and 113 deletions

View File

@@ -82,9 +82,11 @@
"expo-build-properties", "expo-build-properties",
{ {
"ios": { "ios": {
"newArchEnabled": true,
"deploymentTarget": "14.0" "deploymentTarget": "14.0"
}, },
"android": { "android": {
"newArchEnabled": true,
"android": { "android": {
"compileSdkVersion": 34, "compileSdkVersion": 34,
"targetSdkVersion": 34, "targetSdkVersion": 34,
@@ -111,7 +113,8 @@
{ {
"motionPermission": "Allow Streamyfin to access your device motion for landscape video watching." "motionPermission": "Allow Streamyfin to access your device motion for landscape video watching."
} }
] ],
"expo-video"
], ],
"experiments": { "experiments": {
"typedRoutes": true "typedRoutes": true

BIN
bun.lockb

Binary file not shown.

View File

@@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
import { usePlayback } from "@/providers/PlaybackProvider"; import { usePlayback } from "@/providers/PlaybackProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { getAuthHeaders, isBaseItemDto } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
import orientationToOrientationLock from "@/utils/OrientationLockConverter"; import orientationToOrientationLock from "@/utils/OrientationLockConverter";
import { secondsToTicks } from "@/utils/secondsToTicks"; import { secondsToTicks } from "@/utils/secondsToTicks";
@@ -32,6 +32,7 @@ import "react-native-gesture-handler";
import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, { import Animated, {
runOnJS, runOnJS,
useAnimatedReaction,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
withTiming, withTiming,
@@ -41,6 +42,7 @@ import Video, { OnProgressData } from "react-native-video";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { itemRouter } from "./common/TouchableItemRouter"; import { itemRouter } from "./common/TouchableItemRouter";
import { Loader } from "./Loader"; import { Loader } from "./Loader";
import { useVideoPlayer, VideoView } from "expo-video";
async function lockOrientation(orientation: ScreenOrientation.OrientationLock) { async function lockOrientation(orientation: ScreenOrientation.OrientationLock) {
await ScreenOrientation.lockAsync(orientation); await ScreenOrientation.lockAsync(orientation);
@@ -59,10 +61,10 @@ export const FullScreenVideoPlayer: React.FC = () => {
setVolume, setVolume,
setIsPlaying, setIsPlaying,
isPlaying, isPlaying,
videoRef,
onProgress, onProgress,
isBuffering: _isBuffering, isBuffering,
setIsBuffering, setIsBuffering,
player,
} = usePlayback(); } = usePlayback();
const [settings] = useSettings(); const [settings] = useSettings();
@@ -71,6 +73,8 @@ export const FullScreenVideoPlayer: React.FC = () => {
const segments = useSegments(); const segments = useSegments();
const router = useRouter(); const router = useRouter();
const firstLoad = useRef(true);
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } =
useTrickplay(currentlyPlaying); useTrickplay(currentlyPlaying);
const { previousItem, nextItem } = useAdjacentEpisodes({ currentlyPlaying }); const { previousItem, nextItem } = useAdjacentEpisodes({ currentlyPlaying });
@@ -90,6 +94,24 @@ export const FullScreenVideoPlayer: React.FC = () => {
const localIsBuffering = useSharedValue(true); const localIsBuffering = useSharedValue(true);
const cacheProgress = useSharedValue(0); const cacheProgress = useSharedValue(0);
const [isStatusBarHidden, setIsStatusBarHidden] = useState(false); const [isStatusBarHidden, setIsStatusBarHidden] = useState(false);
const [progressState, _setProgressState] = useState(0);
const setProgressState = useCallback(
(value: number) => {
if (sliding.current === true) return;
_setProgressState(value);
},
[sliding.current]
);
useAnimatedReaction(
() => {
return progress.value;
},
(progress) => {
runOnJS(setProgressState)(progress);
}
);
const hideControls = useCallback(() => { const hideControls = useCallback(() => {
"worklet"; "worklet";
@@ -104,8 +126,6 @@ export const FullScreenVideoPlayer: React.FC = () => {
useEffect(() => { useEffect(() => {
const backAction = () => { const backAction = () => {
if (currentlyPlaying) { if (currentlyPlaying) {
// Your custom back action here
console.log("onback");
Alert.alert("Hold on!", "Are you sure you want to exit?", [ Alert.alert("Hold on!", "Are you sure you want to exit?", [
{ {
text: "Cancel", text: "Cancel",
@@ -162,10 +182,62 @@ export const FullScreenVideoPlayer: React.FC = () => {
}; };
}, [currentlyPlaying, api, poster]); }, [currentlyPlaying, api, poster]);
useEffect(() => {
const subscription = player.addListener("playingChange", (isPlaying) => {
setIsPlaying(isPlaying);
});
const subscription2 = player.addListener("statusChange", (status) => {
if (status === "error") {
console.log("player.addListener ~ error");
Alert.alert("Error", "An error occurred while playing the video.");
}
if (status === "readyToPlay") {
console.log("player.addListener ~ readyToPlay");
localIsBuffering.value = false;
setIsBuffering(false);
if (firstLoad.current === true) {
playVideo();
firstLoad.current = false;
}
}
if (status === "loading") {
localIsBuffering.value = true;
setIsBuffering(true);
}
if (status === "idle") {
console.log("player.addListener ~ idle");
}
});
return () => {
subscription.remove();
subscription2.remove();
};
}, [player, setIsBuffering]);
useEffect(() => { useEffect(() => {
max.value = currentlyPlaying?.item.RunTimeTicks || 0; max.value = currentlyPlaying?.item.RunTimeTicks || 0;
}, [currentlyPlaying?.item.RunTimeTicks]); }, [currentlyPlaying?.item.RunTimeTicks]);
useEffect(() => {
if (!player) return;
const interval = setInterval(async () => {
try {
if (sliding.current === true) return;
if (player.playing === true) {
const time = secondsToTicks(player.currentTime);
progress.value = time;
}
} catch (error) {
console.error("Error getting current time:", error);
}
}, 500);
return () => clearInterval(interval);
}, [player, sliding.current]);
useEffect(() => { useEffect(() => {
if (!currentlyPlaying) { if (!currentlyPlaying) {
resetOrientation(); resetOrientation();
@@ -173,14 +245,11 @@ export const FullScreenVideoPlayer: React.FC = () => {
min.value = 0; min.value = 0;
max.value = 0; max.value = 0;
cacheProgress.value = 0; cacheProgress.value = 0;
localIsBuffering.value = false;
sliding.current = false; sliding.current = false;
hideControls(); hideControls();
setStatusBarHidden(false); setStatusBarHidden(false);
// NavigationBar.setVisibilityAsync("visible")
} else { } else {
setStatusBarHidden(true); setStatusBarHidden(true);
// NavigationBar.setVisibilityAsync("hidden")
lockOrientation( lockOrientation(
settings?.defaultVideoOrientation || settings?.defaultVideoOrientation ||
ScreenOrientation.OrientationLock.DEFAULT ScreenOrientation.OrientationLock.DEFAULT
@@ -229,10 +298,9 @@ export const FullScreenVideoPlayer: React.FC = () => {
), ),
})), })),
loader: useAnimatedStyle(() => ({ loader: useAnimatedStyle(() => ({
opacity: withTiming( opacity: withTiming(localIsBuffering.value === true ? 1 : 0, {
localIsBuffering.value === true || progress.value === 0 ? 1 : 0, duration: 300,
{ duration: 300 } }),
),
})), })),
}; };
@@ -303,32 +371,14 @@ export const FullScreenVideoPlayer: React.FC = () => {
}, [opacity.value, hideControls, showControls]); }, [opacity.value, hideControls, showControls]);
const skipIntro = useCallback(async () => { const skipIntro = useCallback(async () => {
if (!introTimestamps || !videoRef.current) return; if (!introTimestamps || !player) return;
try { try {
videoRef.current.seek(introTimestamps.IntroEnd); player.currentTime = introTimestamps.IntroEnd;
} catch (error) { } catch (error) {
writeToLog("ERROR", "Error skipping intro", error); writeToLog("ERROR", "Error skipping intro", error);
} }
}, [introTimestamps]); }, [introTimestamps]);
const handleVideoProgress = useCallback(
(e: OnProgressData) => {
if (e.playableDuration === 0) {
setIsBuffering(true);
localIsBuffering.value = true;
} else {
setIsBuffering(false);
localIsBuffering.value = false;
}
if (sliding.current) return;
onProgress(e);
progress.value = secondsToTicks(e.currentTime);
cacheProgress.value = secondsToTicks(e.playableDuration);
},
[onProgress, setIsBuffering]
);
const handleVideoError = useCallback( const handleVideoError = useCallback(
(e: any) => { (e: any) => {
console.log(e); console.log(e);
@@ -341,27 +391,27 @@ export const FullScreenVideoPlayer: React.FC = () => {
const handleSkipBackward = useCallback(async () => { const handleSkipBackward = useCallback(async () => {
try { try {
const curr = await videoRef.current?.getCurrentPosition(); const curr = player.currentTime;
if (curr !== undefined) { if (curr !== undefined) {
videoRef.current?.seek(Math.max(0, curr - 15)); player.currentTime = Math.max(0, curr - 15);
showControls(); showControls();
} }
} catch (error) { } catch (error) {
writeToLog("ERROR", "Error seeking video backwards", error); writeToLog("ERROR", "Error seeking video backwards", error);
} }
}, [videoRef, showControls]); }, [player, showControls]);
const handleSkipForward = useCallback(async () => { const handleSkipForward = useCallback(async () => {
try { try {
const curr = await videoRef.current?.getCurrentPosition(); const curr = player.currentTime;
if (curr !== undefined) { if (curr !== undefined) {
videoRef.current?.seek(Math.max(0, curr + 15)); player.currentTime = Math.max(0, curr + 15);
showControls(); showControls();
} }
} catch (error) { } catch (error) {
writeToLog("ERROR", "Error seeking video forwards", error); writeToLog("ERROR", "Error seeking video forwards", error);
} }
}, [videoRef, showControls]); }, [player, showControls]);
const handlePlayPause = useCallback(() => { const handlePlayPause = useCallback(() => {
console.log("handlePlayPause"); console.log("handlePlayPause");
@@ -379,19 +429,19 @@ export const FullScreenVideoPlayer: React.FC = () => {
(val: number) => { (val: number) => {
if (opacity.value === 0) return; if (opacity.value === 0) return;
const tick = Math.floor(val); const tick = Math.floor(val);
videoRef.current?.seek(tick / 10000000); player.currentTime = tick / 10000000;
sliding.current = false; sliding.current = false;
}, },
[videoRef] [player]
); );
const handleSliderChange = useCallback( const handleSliderChange = useCallback(
(val: number) => { (val: number) => {
if (opacity.value === 0) return; if (opacity.value === 0) return;
sliding.current = true;
const tick = Math.floor(val); const tick = Math.floor(val);
progress.value = tick; progress.value = tick;
calculateTrickplayUrl(progress); calculateTrickplayUrl(progress);
showControls();
}, },
[progress, calculateTrickplayUrl, showControls] [progress, calculateTrickplayUrl, showControls]
); );
@@ -495,55 +545,16 @@ export const FullScreenVideoPlayer: React.FC = () => {
}} }}
> >
{videoSource && ( {videoSource && (
<Video <VideoView
ref={videoRef}
allowsExternalPlayback
style={{ style={{
width: "100%", width: "100%",
height: "100%", height: "100%",
}} }}
resizeMode={ignoreSafeArea ? "cover" : "contain"} player={player}
playWhenInactive={true} allowsFullscreen
playInBackground={true} nativeControls={false}
showNotificationControls={true} allowsPictureInPicture
ignoreSilentSwitch="ignore" contentFit={ignoreSafeArea ? "cover" : "contain"}
controls={false}
pictureInPicture={true}
onProgress={handleVideoProgress}
subtitleStyle={{
fontSize: 16,
}}
source={videoSource}
onPlaybackStateChanged={(e) => {
if (e.isPlaying === true) {
playVideo(false);
} else if (e.isPlaying === false) {
pauseVideo(false);
}
}}
onBuffer={(e) => {
if (e.isBuffering) {
console.log("Buffering...");
setIsBuffering(true);
localIsBuffering.value = true;
}
}}
onRestoreUserInterfaceForPictureInPictureStop={() => {
showControls();
}}
onVolumeChange={(e) => {
setVolume(e.volume);
}}
fullscreen={false}
onLoadStart={() => {
localIsBuffering.value = true;
}}
onLoad={() => {
localIsBuffering.value = true
}}
progressUpdateInterval={1000}
onError={handleVideoError}
/> />
)} )}
</View> </View>
@@ -778,10 +789,13 @@ export const FullScreenVideoPlayer: React.FC = () => {
/> />
<View className="flex flex-row items-center justify-between mt-0.5"> <View className="flex flex-row items-center justify-between mt-0.5">
<Text className="text-[12px] text-neutral-400"> <Text className="text-[12px] text-neutral-400">
{runtimeTicksToSeconds(progress.value)} {runtimeTicksToSeconds(progressState)}
</Text> </Text>
<Text className="text-[12px] text-neutral-400"> <Text className="text-[12px] text-neutral-400">
-{runtimeTicksToSeconds(max.value - progress.value)} -
{runtimeTicksToSeconds(
(currentlyPlaying?.item.RunTimeTicks || 0) - progressState
)}
</Text> </Text>
</View> </View>
</View> </View>

View File

@@ -50,6 +50,7 @@
"expo-status-bar": "~1.12.1", "expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7", "expo-system-ui": "~3.0.7",
"expo-updates": "~0.25.24", "expo-updates": "~0.25.24",
"expo-video": "^1.2.6",
"expo-web-browser": "~13.0.3", "expo-web-browser": "~13.0.3",
"ffmpeg-kit-react-native": "^6.0.2", "ffmpeg-kit-react-native": "^6.0.2",
"jotai": "^2.9.3", "jotai": "^2.9.3",
@@ -93,5 +94,12 @@
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"typescript": "~5.3.3" "typescript": "~5.3.3"
}, },
"private": true "private": true,
"expo": {
"doctor": {
"reactNativeDirectoryCheck": {
"enabled": true
}
}
}
} }

View File

@@ -29,6 +29,7 @@ import {
parseM3U8ForSubtitles, parseM3U8ForSubtitles,
SubtitleTrack, SubtitleTrack,
} from "@/utils/hls/parseM3U8ForSubtitles"; } from "@/utils/hls/parseM3U8ForSubtitles";
import { useVideoPlayer, VideoPlayer } from "expo-video";
export type CurrentlyPlayingState = { export type CurrentlyPlayingState = {
url: string; url: string;
@@ -40,14 +41,10 @@ interface PlaybackContextType {
currentlyPlaying: CurrentlyPlayingState | null; currentlyPlaying: CurrentlyPlayingState | null;
videoRef: React.MutableRefObject<VideoRef | null>; videoRef: React.MutableRefObject<VideoRef | null>;
isPlaying: boolean; isPlaying: boolean;
isFullscreen: boolean;
progressTicks: number | null; progressTicks: number | null;
playVideo: (triggerRef?: boolean) => void; playVideo: (triggerRef?: boolean) => void;
pauseVideo: (triggerRef?: boolean) => void; pauseVideo: (triggerRef?: boolean) => void;
stopPlayback: () => void; stopPlayback: () => void;
presentFullscreenPlayer: () => void;
dismissFullscreenPlayer: () => void;
setIsFullscreen: (isFullscreen: boolean) => void;
setIsPlaying: (isPlaying: boolean) => void; setIsPlaying: (isPlaying: boolean) => void;
isBuffering: boolean; isBuffering: boolean;
setIsBuffering: (val: boolean) => void; setIsBuffering: (val: boolean) => void;
@@ -60,6 +57,7 @@ interface PlaybackContextType {
currentlyPlaying: CurrentlyPlayingState | null currentlyPlaying: CurrentlyPlayingState | null
) => void; ) => void;
subtitles: SubtitleTrack[]; subtitles: SubtitleTrack[];
player: VideoPlayer;
} }
const PlaybackContext = createContext<PlaybackContextType | null>(null); const PlaybackContext = createContext<PlaybackContextType | null>(null);
@@ -71,13 +69,12 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const videoRef = useRef<VideoRef | null>(null); const videoRef = useRef<VideoRef | null>(null);
const previousVolume = useRef<number | null>(null);
const [settings] = useSettings(); const [settings] = useSettings();
const previousVolume = useRef<number | null>(null);
const [isPlaying, _setIsPlaying] = useState<boolean>(false); const [isPlaying, _setIsPlaying] = useState<boolean>(false);
const [isBuffering, setIsBuffering] = useState<boolean>(false); const [isBuffering, setIsBuffering] = useState<boolean>(true);
const [isFullscreen, setIsFullscreen] = useState<boolean>(false); const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
const [progressTicks, setProgressTicks] = useState<number | null>(0); const [progressTicks, setProgressTicks] = useState<number | null>(0);
const [volume, _setVolume] = useState<number | null>(null); const [volume, _setVolume] = useState<number | null>(null);
@@ -99,6 +96,10 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
[_setVolume] [_setVolume]
); );
const player = useVideoPlayer(currentlyPlaying?.url || null, () => {
if (player) player.play();
});
const { data: deviceId } = useQuery({ const { data: deviceId } = useQuery({
queryKey: ["deviceId", api], queryKey: ["deviceId", api],
queryFn: getDeviceId, queryFn: getDeviceId,
@@ -175,7 +176,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
const playVideo = useCallback( const playVideo = useCallback(
(triggerRef: boolean = true) => { (triggerRef: boolean = true) => {
if (triggerRef === true) { if (triggerRef === true) {
videoRef.current?.resume(); player.play();
} }
_setIsPlaying(true); _setIsPlaying(true);
reportPlaybackProgress({ reportPlaybackProgress({
@@ -192,7 +193,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
const pauseVideo = useCallback( const pauseVideo = useCallback(
(triggerRef: boolean = true) => { (triggerRef: boolean = true) => {
if (triggerRef === true) { if (triggerRef === true) {
videoRef.current?.pause(); player.pause();
} }
_setIsPlaying(false); _setIsPlaying(false);
reportPlaybackProgress({ reportPlaybackProgress({
@@ -253,16 +254,6 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
[_onProgress] [_onProgress]
); );
const presentFullscreenPlayer = useCallback(() => {
videoRef.current?.presentFullscreenPlayer();
setIsFullscreen(true);
}, []);
const dismissFullscreenPlayer = useCallback(() => {
videoRef.current?.dismissFullscreenPlayer();
setIsFullscreen(false);
}, []);
useEffect(() => { useEffect(() => {
if (!deviceId || !api?.accessToken) return; if (!deviceId || !api?.accessToken) return;
@@ -346,14 +337,13 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
return ( return (
<PlaybackContext.Provider <PlaybackContext.Provider
value={{ value={{
player,
onProgress, onProgress,
isBuffering, isBuffering,
setIsBuffering, setIsBuffering,
progressTicks, progressTicks,
setVolume, setVolume,
setIsPlaying, setIsPlaying,
setIsFullscreen,
isFullscreen,
isPlaying, isPlaying,
currentlyPlaying, currentlyPlaying,
sessionData: session, sessionData: session,
@@ -362,8 +352,6 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
setCurrentlyPlayingState, setCurrentlyPlayingState,
pauseVideo, pauseVideo,
stopPlayback, stopPlayback,
presentFullscreenPlayer,
dismissFullscreenPlayer,
startDownloadedFilePlayback, startDownloadedFilePlayback,
subtitles, subtitles,
}} }}