import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getStreamUrl, getUserItemData, reportPlaybackProgress, reportPlaybackStopped, } from "@/utils/jellyfin"; import { runtimeTicksToMinutes } from "@/utils/time"; import { 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 { ActivityIndicator, TouchableOpacity, View } from "react-native"; 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 { useCastDevice, useRemoteMediaClient } from "react-native-google-cast"; import GoogleCast from "react-native-google-cast"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { chromecastProfile, iosProfile } from "@/utils/device-profiles"; type VideoPlayerProps = { itemId: string; }; 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 }) => { 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 : iosProfile, }); console.log("Transcode URL:", 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 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(); } }, []); return ( {enableVideo === true && playbackURL !== null && playbackURL !== undefined ? ( ); };