import { AudioTrackSelector } from "@/components/AudioTrackSelector"; import { Bitrate, BitrateSelector } from "@/components/BitrateSelector"; import { DownloadItem } from "@/components/DownloadItem"; import { OverviewText } from "@/components/OverviewText"; import { ParallaxScrollView } from "@/components/ParallaxPage"; import { PlayButton } from "@/components/PlayButton"; import { PlayedStatus } from "@/components/PlayedStatus"; import { SimilarItems } from "@/components/SimilarItems"; import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector"; import { ItemImage } from "@/components/common/ItemImage"; import { CastAndCrew } from "@/components/series/CastAndCrew"; import { CurrentSeries } from "@/components/series/CurrentSeries"; import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel"; import { useImageColors } from "@/hooks/useImageColors"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { chromecastProfile } from "@/utils/profiles/chromecast"; import iosFmp4 from "@/utils/profiles/iosFmp4"; import native from "@/utils/profiles/native"; import old from "@/utils/profiles/old"; import { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useNavigation } from "expo-router"; import * as ScreenOrientation from "expo-screen-orientation"; import { useAtom } from "jotai"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { View } from "react-native"; import { useCastDevice } from "react-native-google-cast"; import Animated from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Chromecast } from "./Chromecast"; import { ItemHeader } from "./ItemHeader"; import { MediaSourceSelector } from "./MediaSourceSelector"; import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( ({ item }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const castDevice = useCastDevice(); const navigation = useNavigation(); const [settings] = useSettings(); const [selectedMediaSource, setSelectedMediaSource] = useState(null); const [selectedAudioStream, setSelectedAudioStream] = useState(-1); const [selectedSubtitleStream, setSelectedSubtitleStream] = useState(-1); const [maxBitrate, setMaxBitrate] = useState({ key: "Max", value: undefined, }); const [loadingLogo, setLoadingLogo] = useState(true); const [orientation, setOrientation] = useState( ScreenOrientation.Orientation.PORTRAIT_UP ); useEffect(() => { const subscription = ScreenOrientation.addOrientationChangeListener( (event) => { setOrientation(event.orientationInfo.orientation); } ); ScreenOrientation.getOrientationAsync().then((initialOrientation) => { setOrientation(initialOrientation); }); return () => { ScreenOrientation.removeOrientationChangeListener(subscription); }; }, []); const headerHeightRef = useRef(400); useImageColors({ item }); useEffect(() => { navigation.setOptions({ headerRight: () => item && ( {item.Type !== "Program" && ( <> )} ), }); }, [item]); useEffect(() => { if (orientation !== ScreenOrientation.Orientation.PORTRAIT_UP) { headerHeightRef.current = 230; return; } if (item.Type === "Episode") headerHeightRef.current = 400; else if (item.Type === "Movie") headerHeightRef.current = 500; else headerHeightRef.current = 400; }, [item, orientation]); const { data: sessionData } = useQuery({ queryKey: ["sessionData", item.Id], queryFn: async () => { if (!api || !user?.Id || !item.Id) { return null; } const playbackData = await getMediaInfoApi(api!).getPlaybackInfo( { itemId: item.Id, userId: user?.Id, }, { method: "POST", } ); return playbackData.data; }, enabled: !!item.Id && !!api && !!user?.Id, staleTime: 0, }); const { data: playbackUrl } = useQuery({ queryKey: [ "playbackUrl", item.Id, maxBitrate, castDevice?.deviceId, selectedMediaSource?.Id, selectedAudioStream, selectedSubtitleStream, settings, sessionData?.PlaySessionId, ], queryFn: async () => { if (!api || !user?.Id) { return null; } if ( item.Type !== "Program" && (!sessionData || !selectedMediaSource?.Id) ) { return null; } let deviceProfile: any = iosFmp4; if (castDevice?.deviceId) { deviceProfile = chromecastProfile; } else if (settings?.deviceProfile === "Native") { deviceProfile = native; } else if (settings?.deviceProfile === "Old") { deviceProfile = old; } console.log("playbackUrl..."); const url = await getStreamUrl({ api, userId: user.Id, item, startTimeTicks: item.UserData?.PlaybackPositionTicks || 0, maxStreamingBitrate: maxBitrate.value, sessionData, deviceProfile, audioStreamIndex: selectedAudioStream, subtitleStreamIndex: selectedSubtitleStream, forceDirectPlay: settings?.forceDirectPlay, height: maxBitrate.height, mediaSourceId: selectedMediaSource?.Id, }); console.info("Stream URL:", url); return url; }, enabled: !!api && !!user?.Id && !!item.Id, staleTime: 0, }); const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]); const loading = useMemo(() => { return Boolean(logoUrl && loadingLogo); }, [loadingLogo, logoUrl]); const insets = useSafeAreaInsets(); return ( } logo={ <> {logoUrl ? ( setLoadingLogo(false)} onError={() => setLoadingLogo(false)} /> ) : null} } > {item.Type !== "Program" && ( setMaxBitrate(val)} selected={maxBitrate} /> {selectedMediaSource && ( <> )} )} {item.Type === "Episode" && ( )} {item.Type !== "Program" && ( <> {item.People && item.People.length > 0 && ( {item.People.slice(0, 3).map((person) => ( ))} )} {item.Type === "Episode" && ( )} )} ); } );