mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
Co-authored-by: lostb1t <coding-mosses0z@icloud.com> Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com> Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com> Co-authored-by: Gauvino <uruknarb20@gmail.com> Co-authored-by: storm1er <le.storm1er@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Chris <182387676+whoopsi-daisy@users.noreply.github.com> Co-authored-by: arch-fan <55891793+arch-fan@users.noreply.github.com> Co-authored-by: Alex Kim <alexkim@Alexs-MacBook-Pro.local>
176 lines
5.4 KiB
TypeScript
176 lines
5.4 KiB
TypeScript
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
|
import { Image } from "expo-image";
|
|
import { useGlobalSearchParams } from "expo-router";
|
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
import { useDownload } from "@/providers/DownloadProvider";
|
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
|
import { store } from "@/utils/store";
|
|
import { ticksToMs } from "@/utils/time";
|
|
|
|
interface TrickplayUrl {
|
|
x: number;
|
|
y: number;
|
|
url: string;
|
|
}
|
|
|
|
/** Hook to handle trickplay logic for a given item. */
|
|
export const useTrickplay = (item: BaseItemDto) => {
|
|
const [trickPlayUrl, setTrickPlayUrl] = useState<TrickplayUrl | null>(null);
|
|
const { getDownloadedItemById } = useDownload();
|
|
const lastCalculationTime = useRef(0);
|
|
const throttleDelay = 200;
|
|
const isOffline = useGlobalSearchParams().offline === "true";
|
|
const trickplayInfo = useMemo(() => getTrickplayInfo(item), [item]);
|
|
|
|
/** Generates the trickplay URL for the given item and sheet index.
|
|
* We change between offline and online trickplay URLs depending on the state of the app. */
|
|
const getTrickplayUrl = useCallback(
|
|
(item: BaseItemDto, sheetIndex: number) => {
|
|
// If we are offline, we can use the downloaded item's trickplay data path
|
|
const downloadedItem = getDownloadedItemById(item.Id!);
|
|
if (isOffline && downloadedItem?.trickPlayData?.path) {
|
|
return `${downloadedItem.trickPlayData.path}${sheetIndex}.jpg`;
|
|
}
|
|
return generateTrickplayUrl(item, sheetIndex);
|
|
},
|
|
[trickplayInfo],
|
|
);
|
|
|
|
/** Calculates the trickplay URL for the current progress. */
|
|
const calculateTrickplayUrl = useCallback(
|
|
(progress: number) => {
|
|
const now = Date.now();
|
|
if (
|
|
!trickplayInfo ||
|
|
!item.Id ||
|
|
now - lastCalculationTime.current < throttleDelay
|
|
)
|
|
return;
|
|
lastCalculationTime.current = now;
|
|
const { sheetIndex, x, y } = calculateTrickplayTile(
|
|
progress,
|
|
trickplayInfo,
|
|
);
|
|
const url = getTrickplayUrl(item, sheetIndex);
|
|
if (url) setTrickPlayUrl({ x, y, url });
|
|
},
|
|
[trickplayInfo, item, throttleDelay, getTrickplayUrl],
|
|
);
|
|
|
|
/** Prefetches all the trickplay images for the item. */
|
|
const prefetchAllTrickplayImages = useCallback(() => {
|
|
if (!trickplayInfo || !item.Id) return;
|
|
for (let index = 0; index < trickplayInfo.totalImageSheets; index++) {
|
|
const url = getTrickplayUrl(item, index);
|
|
if (url) Image.prefetch(url);
|
|
}
|
|
}, [trickplayInfo, item, getTrickplayUrl]);
|
|
|
|
return {
|
|
trickPlayUrl,
|
|
calculateTrickplayUrl,
|
|
prefetchAllTrickplayImages,
|
|
trickplayInfo,
|
|
};
|
|
};
|
|
|
|
export interface TrickplayData {
|
|
Interval?: number;
|
|
TileWidth?: number;
|
|
TileHeight?: number;
|
|
Height?: number;
|
|
Width?: number;
|
|
ThumbnailCount?: number;
|
|
}
|
|
|
|
export interface TrickplayInfo {
|
|
resolution: string;
|
|
aspectRatio: number;
|
|
data: TrickplayData;
|
|
totalImageSheets: number;
|
|
}
|
|
|
|
/** Generates a trickplay URL based on the item, resolution, and sheet index. */
|
|
export const generateTrickplayUrl = (item: BaseItemDto, sheetIndex: number) => {
|
|
const api = store.get(apiAtom);
|
|
const resolution = getTrickplayInfo(item)?.resolution;
|
|
if (!resolution || !api) return null;
|
|
return `${api.basePath}/Videos/${item.Id}/Trickplay/${resolution}/${sheetIndex}.jpg?api_key=${api.accessToken}`;
|
|
};
|
|
|
|
/**
|
|
* Parses the trickplay metadata from a BaseItemDto.
|
|
* @param item The Jellyfin media item.
|
|
* @returns Parsed trickplay information or null if not available.
|
|
*/
|
|
export const getTrickplayInfo = (item: BaseItemDto): TrickplayInfo | null => {
|
|
if (!item.Id || !item.Trickplay) return null;
|
|
|
|
const mediaSourceId = item.Id;
|
|
const trickplayDataForSource = item.Trickplay[mediaSourceId];
|
|
|
|
if (!trickplayDataForSource) {
|
|
return null;
|
|
}
|
|
|
|
const firstResolution = Object.keys(trickplayDataForSource)[0];
|
|
if (!firstResolution) {
|
|
return null;
|
|
}
|
|
|
|
const data = trickplayDataForSource[firstResolution];
|
|
const { Interval, TileWidth, TileHeight, Width, Height } = data;
|
|
|
|
if (
|
|
!Interval ||
|
|
!TileWidth ||
|
|
!TileHeight ||
|
|
!Width ||
|
|
!Height ||
|
|
!item.RunTimeTicks
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const tilesPerSheet = TileWidth * TileHeight;
|
|
const totalTiles = Math.ceil(ticksToMs(item.RunTimeTicks) / Interval);
|
|
const totalImageSheets = Math.ceil(totalTiles / tilesPerSheet);
|
|
|
|
return {
|
|
resolution: firstResolution,
|
|
aspectRatio: Width / Height,
|
|
data,
|
|
totalImageSheets,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Calculates the specific image sheet and tile offset for a given time.
|
|
* @param progressTicks The current playback time in ticks.
|
|
* @param trickplayInfo The parsed trickplay information object.
|
|
* @returns An object with the image sheet index, and the X/Y coordinates for the tile.
|
|
*/
|
|
const calculateTrickplayTile = (
|
|
progressTicks: number,
|
|
trickplayInfo: TrickplayInfo,
|
|
) => {
|
|
const { data } = trickplayInfo;
|
|
const { Interval, TileWidth, TileHeight } = data;
|
|
|
|
if (!Interval || !TileWidth || !TileHeight) {
|
|
throw new Error("Invalid trickplay data provided to calculateTile");
|
|
}
|
|
|
|
const currentTimeMs = Math.max(0, ticksToMs(progressTicks));
|
|
const currentTile = Math.floor(currentTimeMs / Interval);
|
|
|
|
const tilesPerSheet = TileWidth * TileHeight;
|
|
const sheetIndex = Math.floor(currentTile / tilesPerSheet);
|
|
const tileIndexInSheet = currentTile % tilesPerSheet;
|
|
|
|
const x = tileIndexInSheet % TileWidth;
|
|
const y = Math.floor(tileIndexInSheet / TileWidth);
|
|
|
|
return { sheetIndex, x, y };
|
|
};
|