import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { chromecastProfile, iosProfile, iOSProfile_2, } from "@/utils/device-profiles"; import { getStreamUrl, getUserItemData, reportPlaybackProgress, reportPlaybackStopped, } from "@/utils/jellyfin"; import { runtimeTicksToMinutes } from "@/utils/time"; import { Feather, Ionicons } from "@expo/vector-icons"; import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAtom } from "jotai"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { TouchableOpacity, View } from "react-native"; import { useCastDevice, useRemoteMediaClient } from "react-native-google-cast"; import Video, { OnBufferData, OnPlaybackStateChangedData, OnProgressData, OnVideoErrorData, VideoRef, } from "react-native-video"; import * as DropdownMenu from "zeego/dropdown-menu"; import { Button } from "./Button"; import { Text } from "./common/Text"; import iosFmp4 from "../utils/profiles/iosFmp4"; import ios12 from "../utils/profiles/ios12"; type VideoPlayerProps = { itemId: string; onChangePlaybackURL: (url: string | null) => void; }; const BITRATES = [ { key: "Max", value: undefined, }, { key: "4 Mb/s", value: 4000000, }, { key: "2 Mb/s", value: 2000000, }, { key: "500 Kb/s", value: 500000, }, ]; export const VideoPlayer: React.FC = ({ itemId, onChangePlaybackURL, }) => { const videoRef = useRef(null); const [maxBitrate, setMaxbitrate] = useState(undefined); const [paused, setPaused] = useState(true); const [progress, setProgress] = useState(0); const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const castDevice = useCastDevice(); const client = useRemoteMediaClient(); const queryClient = useQueryClient(); const { data: item } = useQuery({ queryKey: ["item", itemId], queryFn: async () => await getUserItemData({ api, userId: user?.Id, itemId, }), enabled: !!itemId && !!api, staleTime: 60, }); const { data: sessionData } = useQuery({ queryKey: ["sessionData", itemId], queryFn: async () => { const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({ itemId, userId: user?.Id, }); return playbackData.data; }, enabled: !!itemId && !!api && !!user?.Id, staleTime: 0, }); const { data: playbackURL } = useQuery({ queryKey: ["playbackUrl", itemId, maxBitrate, castDevice], queryFn: async () => { if (!api || !user?.Id || !sessionData) return null; const url = await getStreamUrl({ api, userId: user.Id, item, startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0, maxStreamingBitrate: maxBitrate, sessionData, deviceProfile: castDevice?.deviceId ? chromecastProfile : ios12, }); console.log("Transcode URL:", url); onChangePlaybackURL(url); return url; }, enabled: !!sessionData, staleTime: 0, }); const onProgress = useCallback( ({ currentTime, playableDuration, seekableDuration }: OnProgressData) => { if (!currentTime || !sessionData?.PlaySessionId) return; if (paused) return; const newProgress = currentTime * 10000000; setProgress(newProgress); reportPlaybackProgress({ api, itemId: itemId, positionTicks: newProgress, sessionId: sessionData.PlaySessionId, }); }, [sessionData?.PlaySessionId, item, api, paused] ); const onSeek = ({ currentTime, seekTime, }: { currentTime: number; seekTime: number; }) => { // console.log("Seek to time: ", seekTime); }; const onError = (error: OnVideoErrorData) => { console.log("Video Error: ", JSON.stringify(error.error)); }; const onBuffer = (error: OnBufferData) => { console.log("Video buffering: ", error.isBuffering); }; const play = () => { if (videoRef.current) { videoRef.current.resume(); setPaused(false); } }; const startPosition = useMemo(() => { return Math.round((item?.UserData?.PlaybackPositionTicks || 0) / 10000); }, [item]); const [hidePlayer, setHidePlayer] = useState(true); const enableVideo = useMemo(() => { return ( playbackURL !== undefined && item !== undefined && item !== null && startPosition !== undefined && sessionData !== undefined ); }, [playbackURL, item, startPosition, sessionData]); const cast = useCallback(() => { if (client === null) { console.log("no client "); return; } if (!playbackURL) { console.log("no playback url"); return; } if (!item) { console.log("no item"); return; } client.loadMedia({ mediaInfo: { contentUrl: playbackURL, contentType: "video/mp4", metadata: { type: item?.Type === "Episode" ? "tvShow" : "movie", title: item?.Name || "", subtitle: item?.Overview || "", }, streamDuration: Math.floor((item?.RunTimeTicks || 0) / 10000), }, startTime: Math.floor( (item?.UserData?.PlaybackPositionTicks || 0) / 10000 ), }); }, [item, client, playbackURL]); useEffect(() => { if (videoRef.current) { videoRef.current.pause(); } }, []); const chromecastReady = useMemo(() => { return castDevice?.deviceId && item; }, [castDevice, item]); return ( {enableVideo === true ? ( ); };