From 6ec6c6daa08f560a6dfa7b3ab3dd21d1a76cffab Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Fri, 6 Dec 2024 17:41:42 +1100 Subject: [PATCH] Added feature to prefetch trick-play images on video start rather than downloading it while scrubbing --- components/video-player/controls/Controls.tsx | 14 +++++-- hooks/useTrickplay.ts | 39 ++++++++++++++++++- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 0e8c5451..483d4517 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -113,10 +113,12 @@ export const Controls: React.FC = ({ const insets = useSafeAreaInsets(); const { previousItem, nextItem } = useAdjacentItems({ item }); - const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay( - item, - !offline && enableTrickplay - ); + const { + trickPlayUrl, + calculateTrickplayUrl, + trickplayInfo, + prefetchAllTrickplayImages, + } = useTrickplay(item, !offline && enableTrickplay); const [currentTime, setCurrentTime] = useState(0); const [remainingTime, setRemainingTime] = useState(0); @@ -239,6 +241,10 @@ export const Controls: React.FC = ({ } }, [item, isVlc]); + useEffect(() => { + prefetchAllTrickplayImages(); + }, []); + const toggleControls = () => setShowControls(!showControls); const handleSliderComplete = useCallback( diff --git a/hooks/useTrickplay.ts b/hooks/useTrickplay.ts index 960aee62..9bb3630f 100644 --- a/hooks/useTrickplay.ts +++ b/hooks/useTrickplay.ts @@ -1,8 +1,7 @@ -// hooks/useTrickplay.ts - import { apiAtom } from "@/providers/JellyfinProvider"; import { ticksToMs } from "@/utils/time"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useCallback, useMemo, useRef, useState } from "react"; @@ -111,9 +110,45 @@ export const useTrickplay = (item: BaseItemDto, enabled = true) => { [trickplayInfo, item, api, enabled] ); + const prefetchAllTrickplayImages = useCallback(() => { + if (!api || !enabled || !trickplayInfo || !item.Id || !item.RunTimeTicks) { + return; + } + + const { data, resolution } = trickplayInfo; + const { Interval, TileWidth, TileHeight, Width, Height } = data; + + if ( + !Interval || + !TileWidth || + !TileHeight || + !resolution || + !Width || + !Height + ) { + throw new Error("Invalid trickplay data"); + } + + // Calculate tiles per sheet + const tilesPerRow = TileWidth; + const tilesPerColumn = TileHeight; + const tilesPerSheet = tilesPerRow * tilesPerColumn; + const totalTiles = Math.ceil(ticksToMs(item.RunTimeTicks) / Interval); + const totalIndexes = Math.ceil(totalTiles / tilesPerSheet); + + // Prefetch all trickplay images + for (let index = 0; index < totalIndexes; index++) { + const url = `${api.basePath}/Videos/${item.Id}/Trickplay/${resolution}/${index}.jpg?api_key=${api.accessToken}`; + Image.prefetch(url); + } + }, [trickplayInfo, item, api, enabled]); + return { trickPlayUrl: enabled ? trickPlayUrl : null, calculateTrickplayUrl: enabled ? calculateTrickplayUrl : () => null, + prefetchAllTrickplayImages: enabled + ? prefetchAllTrickplayImages + : () => null, trickplayInfo: enabled ? trickplayInfo : null, }; };