diff --git a/app.json b/app.json index 27f16b7d..438c220f 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Streamyfin", "slug": "streamyfin", - "version": "0.0.5", + "version": "0.0.6", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "streamyfin", @@ -39,6 +39,11 @@ "expo-router", "expo-font", "react-native-compressor", + [ + "react-native-google-cast", + { + } + ], [ "react-native-video", { @@ -50,7 +55,8 @@ "useExoplayerDash": false } } - ] + ], + ["expo-build-properties", { "ios": { "deploymentTarget": "14.0" } }] ], "experiments": { "typedRoutes": true diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index e9630ff1..2b4e4232 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -19,6 +19,7 @@ export default function TabLayout() { options={{ headerShown: true, headerStyle: { backgroundColor: "black" }, + headerShadowVisible: false, title: "Home", tabBarIcon: ({ color, focused }) => ( ( diff --git a/app/(auth)/(tabs)/index.tsx b/app/(auth)/(tabs)/index.tsx index 994b783f..b447b7e2 100644 --- a/app/(auth)/(tabs)/index.tsx +++ b/app/(auth)/(tabs)/index.tsx @@ -3,12 +3,13 @@ import { Text } from "@/components/common/Text"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; import { ItemCardText } from "@/components/ItemCardText"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { nextUp } from "@/utils/jellyfin"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi, getSuggestionsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useRouter } from "expo-router"; import { useAtom } from "jotai"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { ActivityIndicator, RefreshControl, @@ -38,6 +39,21 @@ export default function index() { staleTime: 60, }); + const { data: _nextUpData } = useQuery({ + queryKey: ["nextUp-all", user?.Id], + queryFn: async () => + await nextUp({ + userId: user?.Id, + api, + }), + enabled: !!api && !!user?.Id, + staleTime: 0, + }); + + const nextUpData = useMemo(() => { + return _nextUpData?.filter((i) => !data?.find((d) => d.Id === i.Id)); + }, [_nextUpData]); + const { data: collections } = useQuery({ queryKey: ["collections", user?.Id], queryFn: async () => { @@ -142,6 +158,22 @@ export default function index() { )} /> + Next Up + + data={nextUpData} + renderItem={(item, index) => ( + router.push(`/items/${item.Id}/page`)} + className="flex flex-col w-48" + > + + + + + + )} + /> Collections data={collections} diff --git a/app/(auth)/(tabs)/search.tsx b/app/(auth)/(tabs)/search.tsx index d750ffb5..28852112 100644 --- a/app/(auth)/(tabs)/search.tsx +++ b/app/(auth)/(tabs)/search.tsx @@ -6,7 +6,7 @@ import { ItemCardText } from "@/components/ItemCardText"; import MoviePoster from "@/components/MoviePoster"; import Poster from "@/components/Poster"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { getUserItemData } from "@/utils/jellyfin"; +import { getPrimaryImage, getUserItemData } from "@/utils/jellyfin"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getSearchApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; @@ -113,7 +113,11 @@ export default function search() { onPress={() => router.push(`/series/${item.Id}/page`)} className="flex flex-col w-32" > - + {item.Name} {item.ProductionYear} diff --git a/app/(auth)/items/[id]/ParallaxPage.tsx b/app/(auth)/items/[id]/ParallaxPage.tsx deleted file mode 100644 index a238e14f..00000000 --- a/app/(auth)/items/[id]/ParallaxPage.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import type { PropsWithChildren, ReactElement } from "react"; -import { - NativeScrollEvent, - NativeSyntheticEvent, - StyleSheet, - useColorScheme, -} from "react-native"; -import Animated, { - interpolate, - useAnimatedRef, - useAnimatedStyle, - useScrollViewOffset, -} from "react-native-reanimated"; - -import { ThemedView } from "@/components/ThemedView"; - -const HEADER_HEIGHT = 250; - -type Props = PropsWithChildren<{ - headerImage: ReactElement; - onScroll?: (event: NativeSyntheticEvent) => void; -}>; - -export const ParallaxScrollView: React.FC = ({ - children, - headerImage, - onScroll, -}: Props) => { - const scrollRef = useAnimatedRef(); - const scrollOffset = useScrollViewOffset(scrollRef); - - const headerAnimatedStyle = useAnimatedStyle(() => { - return { - transform: [ - { - translateY: interpolate( - scrollOffset.value, - [-HEADER_HEIGHT, 0, HEADER_HEIGHT], - [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] - ), - }, - { - scale: interpolate( - scrollOffset.value, - [-HEADER_HEIGHT, 0, HEADER_HEIGHT], - [2, 1, 1] - ), - }, - ], - }; - }); - - return ( - - - - {headerImage} - - {children} - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - header: { - height: 250, - overflow: "hidden", - }, - content: { - flex: 1, - overflow: "hidden", - }, -}); diff --git a/app/(auth)/items/[id]/page.tsx b/app/(auth)/items/[id]/page.tsx index dc6c831e..bb051ab9 100644 --- a/app/(auth)/items/[id]/page.tsx +++ b/app/(auth)/items/[id]/page.tsx @@ -1,4 +1,3 @@ -import { LargePoster } from "@/components/common/LargePoster"; import { Text } from "@/components/common/Text"; import { DownloadItem } from "@/components/DownloadItem"; import { PlayedStatus } from "@/components/PlayedStatus"; @@ -7,22 +6,26 @@ import { CurrentSeries } from "@/components/series/CurrentSeries"; import { SimilarItems } from "@/components/SimilarItems"; import { VideoPlayer } from "@/components/VideoPlayer"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { getBackdrop, getStreamUrl, getUserItemData } from "@/utils/jellyfin"; -import { Ionicons } from "@expo/vector-icons"; -import {} from "@jellyfin/sdk/lib/utils/url"; +import { + getBackdrop, + getLogoImageById, + getPrimaryImage, + getUserItemData, +} from "@/utils/jellyfin"; import { useQuery } from "@tanstack/react-query"; -import { useLocalSearchParams, useNavigation } from "expo-router"; +import { Image } from "expo-image"; +import { router, useLocalSearchParams } from "expo-router"; import { useAtom } from "jotai"; -import { useEffect, useState } from "react"; +import { useMemo } from "react"; import { ActivityIndicator, - NativeScrollEvent, - NativeSyntheticEvent, ScrollView, + TouchableOpacity, View, } from "react-native"; -import { ParallaxScrollView } from "./ParallaxPage"; -import { Image } from "expo-image"; +import { ParallaxScrollView } from "../../../../components/ParallaxPage"; +import { Chromecast } from "@/components/Chromecast"; +import { useRemoteMediaClient } from "react-native-google-cast"; const page: React.FC = () => { const local = useLocalSearchParams(); @@ -43,12 +46,21 @@ const page: React.FC = () => { staleTime: 60, }); - const { data: posterUrl } = useQuery({ - queryKey: ["backdrop", item?.Id], - queryFn: async () => getBackdrop(api, item), - enabled: !!api && !!item?.Id, - staleTime: 60 * 60 * 24 * 7, - }); + const backdropUrl = useMemo( + () => + getBackdrop({ + api, + item, + quality: 90, + width: 1000, + }), + [item] + ); + + const logoUrl = useMemo( + () => (item?.Type === "Movie" ? getLogoImageById({ api, item }) : null), + [item] + ); if (l1) return ( @@ -57,35 +69,71 @@ const page: React.FC = () => { ); - if (!item?.Id || !posterUrl) return null; + if (!item?.Id || !backdropUrl) return null; return ( } + logo={ + <> + {logoUrl ? ( + + ) : null} + + } > {item.Type === "Episode" ? ( <> - {item?.SeriesName} - - {item?.Name} - + + router.push(`/(auth)/series/${item.SeriesId}/page`) + } + > + + {item?.SeriesName} + + + + + {item?.Name} + + + + + + {}}> + + {item?.SeasonName} + + + {"—"} + + {`Episode ${item.IndexNumber}`} + + + - {`S${item?.SeasonName?.replace("Season ", "")}:E${( - item.IndexNumber || 0 - ).toString()}`} - {" - "} {item.ProductionYear} @@ -101,11 +149,9 @@ const page: React.FC = () => { )} - + - - - + {item.Overview} diff --git a/app/(auth)/series/[id]/page.tsx b/app/(auth)/series/[id]/page.tsx index f9862530..792e0339 100644 --- a/app/(auth)/series/[id]/page.tsx +++ b/app/(auth)/series/[id]/page.tsx @@ -1,14 +1,21 @@ import { Text } from "@/components/common/Text"; -import MoviePoster from "@/components/MoviePoster"; +import { ParallaxScrollView } from "@/components/ParallaxPage"; import { NextUp } from "@/components/series/NextUp"; import { SeasonPicker } from "@/components/series/SeasonPicker"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { getPrimaryImageById, getUserItemData, nextUp } from "@/utils/jellyfin"; +import { + getBackdrop, + getLogoImageById, + getPrimaryImage, + getPrimaryImageById, + getUserItemData, +} from "@/utils/jellyfin"; import { useQuery } from "@tanstack/react-query"; +import { Image } from "expo-image"; import { useLocalSearchParams } from "expo-router"; import { useAtom } from "jotai"; -import { useEffect } from "react"; -import { ScrollView, View } from "react-native"; +import { useMemo } from "react"; +import { View } from "react-native"; const page: React.FC = () => { const params = useLocalSearchParams(); @@ -26,25 +33,72 @@ const page: React.FC = () => { itemId: seriesId, }), enabled: !!seriesId && !!api, - staleTime: 60, + staleTime: 0, }); - if (!item) return null; + const backdropUrl = useMemo( + () => + getBackdrop({ + api, + item, + quality: 90, + width: 1000, + }), + [item] + ); + + const logoUrl = useMemo( + () => + getLogoImageById({ + api, + item, + }), + [item] + ); + + if (!item || !backdropUrl) return null; return ( - - - - - - {item?.Name} - {item?.Overview} - + + } + logo={ + <> + {logoUrl ? ( + + ) : null} + + } + > + + + {item?.Name} + {item?.Overview} + + + - - + ); }; diff --git a/app/_layout.tsx b/app/_layout.tsx index e0564d46..3f5c4ab8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -12,6 +12,7 @@ import { TouchableOpacity } from "react-native"; import Feather from "@expo/vector-icons/Feather"; import { StatusBar } from "expo-status-bar"; +import { Ionicons } from "@expo/vector-icons"; // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); @@ -87,10 +88,7 @@ export default function RootLayout() { > = ({ @@ -21,14 +23,18 @@ export const Button: React.FC> = ({ loading = false, color = "purple", iconRight, + iconLeft, children, + justify = "center", }) => { const colorClasses = useMemo(() => { switch (color) { case "purple": return "bg-purple-600 active:bg-purple-700"; case "red": - return "bg-red-500 active:bg-red-600"; + return "bg-red-500"; + case "black": + return "bg-black border border-neutral-900"; } }, [color]); @@ -51,18 +57,24 @@ export const Button: React.FC> = ({ {loading ? ( ) : ( - + + {iconLeft ? iconLeft : } {children} - {iconRight} + {iconRight ? iconRight : } )} diff --git a/components/Chromecast.tsx b/components/Chromecast.tsx new file mode 100644 index 00000000..b1420861 --- /dev/null +++ b/components/Chromecast.tsx @@ -0,0 +1,69 @@ +import { Feather, FontAwesome, Ionicons } from "@expo/vector-icons"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import React, { useEffect } from "react"; +import { TouchableOpacity, View } from "react-native"; +import { + CastButton, + useCastDevice, + useDevices, + useRemoteMediaClient, +} from "react-native-google-cast"; +import GoogleCast from "react-native-google-cast"; +import { Text } from "./common/Text"; + +type Props = { + item?: BaseItemDto | null; + startTimeTicks?: number | null; +}; + +export const Chromecast: React.FC = ({ item, startTimeTicks }) => { + const client = useRemoteMediaClient(); + const castDevice = useCastDevice(); + const devices = useDevices(); + const sessionManager = GoogleCast.getSessionManager(); + const discoveryManager = GoogleCast.getDiscoveryManager(); + + useEffect(() => { + (async () => { + if (!discoveryManager) { + console.log("No discoveryManager client"); + return; + } + + await discoveryManager.startDiscovery(); + + const started = await discoveryManager.isRunning(); + console.log("started", started); + + console.log({ + devices, + castDevice, + sessionManager, + }); + })(); + }, [client, devices, castDevice, sessionManager, discoveryManager]); + + const cast = () => { + if (!client) { + console.log("No chromecast client"); + return; + } + + client.loadMedia({ + mediaInfo: { + contentUrl: + "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/BigBuckBunny.mp4", + 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((startTimeTicks || 0) / 10000), + }); + }; + + return ; +}; diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index b7daa5e6..9841bba5 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -1,13 +1,10 @@ import { apiAtom } from "@/providers/JellyfinProvider"; -import { getBackdrop } from "@/utils/jellyfin"; -import { Ionicons } from "@expo/vector-icons"; +import { getPrimaryImage } from "@/utils/jellyfin"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useAtom } from "jotai"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { View } from "react-native"; -import { Text } from "./common/Text"; import { WatchedIndicator } from "./WatchedIndicator"; type ContinueWatchingPosterProps = { @@ -19,12 +16,16 @@ const ContinueWatchingPoster: React.FC = ({ }) => { const [api] = useAtom(apiAtom); - const { data: url } = useQuery({ - queryKey: ["backdrop", item.Id], - queryFn: async () => getBackdrop(api, item), - enabled: !!api && !!item.Id, - staleTime: 60 * 60 * 24 * 7, - }); + const url = useMemo( + () => + getPrimaryImage({ + api, + item, + quality: 70, + width: 300, + }), + [item] + ); const [progress, setProgress] = useState( item.UserData?.PlayedPercentage || 0 @@ -47,7 +48,7 @@ const ContinueWatchingPoster: React.FC = ({ contentFit="cover" className="w-full h-full" /> - + {!progress && } {progress > 0 && ( <> = ({ }) => { const [api] = useAtom(apiAtom); - const { data: url } = useQuery({ - queryKey: ["backdrop", item.Id], - queryFn: async () => getPrimaryImageById(api, item.Id), - enabled: !!api && !!item.Id, - staleTime: Infinity, - }); + const url = useMemo( + () => + getPrimaryImage({ + api, + item, + }), + [item] + ); const [progress, setProgress] = useState( item.UserData?.PlayedPercentage || 0 diff --git a/components/ParallaxPage.tsx b/components/ParallaxPage.tsx new file mode 100644 index 00000000..0e35ed88 --- /dev/null +++ b/components/ParallaxPage.tsx @@ -0,0 +1,96 @@ +import { Ionicons } from "@expo/vector-icons"; +import { router } from "expo-router"; +import type { PropsWithChildren, ReactElement } from "react"; +import { TouchableOpacity, View } from "react-native"; +import Animated, { + interpolate, + useAnimatedRef, + useAnimatedStyle, + useScrollViewOffset, +} from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +const HEADER_HEIGHT = 400; + +type Props = PropsWithChildren<{ + headerImage: ReactElement; + logo?: ReactElement; +}>; + +export const ParallaxScrollView: React.FC = ({ + children, + headerImage, + logo, +}: Props) => { + const scrollRef = useAnimatedRef(); + const scrollOffset = useScrollViewOffset(scrollRef); + + const headerAnimatedStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateY: interpolate( + scrollOffset.value, + [-HEADER_HEIGHT, 0, HEADER_HEIGHT], + [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] + ), + }, + { + scale: interpolate( + scrollOffset.value, + [-HEADER_HEIGHT, 0, HEADER_HEIGHT], + [2, 1, 1] + ), + }, + ], + }; + }); + + const inset = useSafeAreaInsets(); + + return ( + + + router.back()} + className="absolute left-4 z-50 bg-black rounded-full p-2 border border-neutral-900" + style={{ + top: inset.top, + }} + > + + + + {logo && ( + + {logo} + + )} + + + {headerImage} + + {children} + + + ); +}; diff --git a/components/ParentPoster.tsx b/components/ParentPoster.tsx new file mode 100644 index 00000000..db4c9ec3 --- /dev/null +++ b/components/ParentPoster.tsx @@ -0,0 +1,49 @@ +import { apiAtom } from "@/providers/JellyfinProvider"; +import { useQuery } from "@tanstack/react-query"; +import { Image } from "expo-image"; +import { useAtom } from "jotai"; +import { useMemo } from "react"; +import { View } from "react-native"; + +type PosterProps = { + id?: string; + showProgress?: boolean; +}; + +const ParentPoster: React.FC = ({ id }) => { + const [api] = useAtom(apiAtom); + + const url = useMemo( + () => `${api?.basePath}/Items/${id}/Images/Primary`, + [id] + ); + + if (!url || !id) + return ( + + ); + + return ( + + + + ); +}; + +export default ParentPoster; diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx index d6b73442..69f3663d 100644 --- a/components/PlayedStatus.tsx +++ b/components/PlayedStatus.tsx @@ -58,7 +58,7 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => { onPress={() => { markAsPlayed({ api: api, - itemId: item?.Id, + item: item, userId: user?.Id, }); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); diff --git a/components/Poster.tsx b/components/Poster.tsx index 456b0653..d5b17e27 100644 --- a/components/Poster.tsx +++ b/components/Poster.tsx @@ -1,26 +1,18 @@ -import { apiAtom } from "@/providers/JellyfinProvider"; -import { getPrimaryImageById } from "@/utils/jellyfin"; -import { useQuery } from "@tanstack/react-query"; +import { + BaseItemDto, + BaseItemPerson, +} from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; -import { useAtom } from "jotai"; import { View } from "react-native"; type PosterProps = { - itemId?: string | null; + item?: BaseItemDto | BaseItemPerson | null; + url?: string | null; showProgress?: boolean; }; -const Poster: React.FC = ({ itemId }) => { - const [api] = useAtom(apiAtom); - - const { data: url } = useQuery({ - queryKey: ["backdrop", itemId], - queryFn: async () => getPrimaryImageById(api, itemId), - enabled: !!api && !!itemId, - staleTime: Infinity, - }); - - if (!url || !itemId) +const Poster: React.FC = ({ item, url }) => { + if (!url || !item) return ( = ({ itemId }) => { return ( = ({ 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 () => @@ -81,7 +97,7 @@ export const VideoPlayer: React.FC = ({ itemId }) => { }); const { data: playbackURL } = useQuery({ - queryKey: ["playbackUrl", itemId, maxBitrate], + queryKey: ["playbackUrl", itemId, maxBitrate, castDevice], queryFn: async () => { if (!api || !user?.Id || !sessionData) return null; @@ -92,6 +108,7 @@ export const VideoPlayer: React.FC = ({ itemId }) => { startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0, maxStreamingBitrate: maxBitrate, sessionData, + deviceProfile: castDevice?.deviceId ? chromecastProfile : iosProfile, }); console.log("Transcode URL:", url); @@ -102,21 +119,22 @@ export const VideoPlayer: React.FC = ({ itemId }) => { staleTime: 0, }); - const [progress, setProgress] = useState(0); + const onProgress = useCallback( + ({ currentTime, playableDuration, seekableDuration }: OnProgressData) => { + if (!currentTime || !sessionData?.PlaySessionId) return; + if (paused) return; - const onProgress = ({ - currentTime, - playableDuration, - seekableDuration, - }: OnProgressData) => { - setProgress(currentTime * 10000000); - reportPlaybackProgress({ - api, - itemId: itemId, - positionTicks: currentTime * 10000000, - sessionId: sessionData?.PlaySessionId, - }); - }; + const newProgress = currentTime * 10000000; + setProgress(newProgress); + reportPlaybackProgress({ + api, + itemId: itemId, + positionTicks: newProgress, + sessionId: sessionData.PlaySessionId, + }); + }, + [sessionData?.PlaySessionId, item, api, paused] + ); const onSeek = ({ currentTime, @@ -139,6 +157,7 @@ export const VideoPlayer: React.FC = ({ itemId }) => { const play = () => { if (videoRef.current) { videoRef.current.resume(); + setPaused(false); } }; @@ -146,12 +165,6 @@ export const VideoPlayer: React.FC = ({ itemId }) => { return Math.round((item?.UserData?.PlaybackPositionTicks || 0) / 10000); }, [item]); - useEffect(() => { - if (videoRef.current) { - videoRef.current.pause(); - } - }, []); - const enableVideo = useMemo(() => { return ( playbackURL !== undefined && @@ -162,6 +175,45 @@ export const VideoPlayer: React.FC = ({ itemId }) => { ); }, [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 && @@ -185,6 +237,19 @@ export const VideoPlayer: React.FC = ({ itemId }) => { onProgress={(e) => onProgress(e)} onFullscreenPlayerDidDismiss={() => { videoRef.current?.pause(); + setPaused(true); + + queryClient.invalidateQueries({ + queryKey: ["nextUp", item?.SeriesId], + refetchType: "all", + }); + queryClient.invalidateQueries({ + queryKey: ["episodes"], + refetchType: "all", + }); + + if (progress === 0) return; + reportPlaybackStopped({ api, itemId: item?.Id, @@ -203,6 +268,7 @@ export const VideoPlayer: React.FC = ({ itemId }) => { bufferForPlaybackMs: 1000, backBufferDurationMs: 30 * 1000, }} + ignoreSilentSwitch="ignore" /> ) : null} @@ -247,7 +313,9 @@ export const VideoPlayer: React.FC = ({ itemId }) => {