import { Controls } from "@/components/video-player/Controls"; import { useOrientation } from "@/hooks/useOrientation"; import { useOrientationSettings } from "@/hooks/useOrientationSettings"; import { apiAtom } from "@/providers/JellyfinProvider"; import { PlaybackType, usePlaySettings, } from "@/providers/PlaySettingsProvider"; import { secondsToTicks } from "@/utils/secondsToTicks"; import { Api } from "@jellyfin/sdk"; import * as Haptics from "expo-haptics"; import { useFocusEffect } from "expo-router"; import { useAtomValue } from "jotai"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Pressable, useWindowDimensions, View } from "react-native"; import { SystemBars } from "react-native-edge-to-edge"; import { useSharedValue } from "react-native-reanimated"; import Video, { OnProgressData, VideoRef } from "react-native-video"; const OfflinePlayer = () => { const { playSettings, playUrl } = usePlaySettings(); const api = useAtomValue(apiAtom); const videoRef = useRef(null); const videoSource = useVideoSource(playSettings, api, playUrl); const firstTime = useRef(true); const dimensions = useWindowDimensions(); useOrientation(); useOrientationSettings(); const [showControls, setShowControls] = useState(true); const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [isBuffering, setIsBuffering] = useState(true); const [isReady, setIsReady] = useState(false); useEffect(() => { const timer = setTimeout(() => { setIsReady(true); }, 2000); return () => clearTimeout(timer); }, []); const progress = useSharedValue(0); const isSeeking = useSharedValue(false); const cacheProgress = useSharedValue(0); const [embededTextTracks, setEmbededTextTracks] = useState< { index: number; language?: string | undefined; selected?: boolean | undefined; title?: string | undefined; type: any; }[] >([]); const togglePlay = useCallback(async () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); if (isPlaying) { videoRef.current?.pause(); } else { videoRef.current?.resume(); } }, [isPlaying]); const play = useCallback(() => { setIsPlaying(true); videoRef.current?.resume(); }, [videoRef]); const stop = useCallback(() => { setIsPlaying(false); videoRef.current?.pause(); }, [videoRef]); const pause = useCallback(() => { videoRef.current?.pause(); }, [videoRef]); const seek = useCallback( (seconds: number) => { videoRef.current?.seek(seconds); }, [videoRef] ); useFocusEffect( useCallback(() => { play(); return () => { stop(); }; }, [play, stop]) ); 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); }, []); if (!isReady) return null; if (!playSettings || !playUrl || !api || !videoSource || !playSettings.item) return null; return ( { setShowControls(!showControls); }} className="absolute z-0 h-full w-full" > ); }; 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; } export default OfflinePlayer;