diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 9d8c370e..b418a139 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -1,6 +1,26 @@ +import { + type BaseItemDto, + type MediaSourceInfo, + PlaybackOrder, + type PlaybackProgressInfo, + PlaybackStartInfo, + RepeatMode, +} from "@jellyfin/sdk/lib/generated-client"; +import { + getPlaystateApi, + getUserLibraryApi, +} from "@jellyfin/sdk/lib/utils/api"; +import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; +import { router, useGlobalSearchParams, useNavigation } from "expo-router"; +import { useAtomValue } from "jotai"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Alert, Platform, View } from "react-native"; +import { useSharedValue } from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { BITRATES } from "@/components/BitrateSelector"; -import { Loader } from "@/components/Loader"; import { Text } from "@/components/common/Text"; +import { Loader } from "@/components/Loader"; import { Controls } from "@/components/video-player/controls/Controls"; import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener"; import { useHaptic } from "@/hooks/useHaptic"; @@ -20,35 +40,10 @@ import { writeToLog } from "@/utils/log"; import { storage } from "@/utils/mmkv"; import generateDeviceProfile from "@/utils/profiles/native"; import { msToTicks, ticksToSeconds } from "@/utils/time"; -import { - type BaseItemDto, - type MediaSourceInfo, - PlaybackOrder, - type PlaybackProgressInfo, - PlaybackStartInfo, - RepeatMode, -} from "@jellyfin/sdk/lib/generated-client"; -import { - getPlaystateApi, - getUserLibraryApi, -} from "@jellyfin/sdk/lib/utils/api"; -import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; -import { useGlobalSearchParams, useNavigation } from "expo-router"; -import { useAtomValue } from "jotai"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { useTranslation } from "react-i18next"; -import { Alert, Platform, View } from "react-native"; -import { useSharedValue } from "react-native-reanimated"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; + const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") - : null; + : { useDownload: () => null }; const IGNORE_SAFE_AREAS_KEY = "video_player_ignore_safe_areas"; @@ -79,10 +74,7 @@ export default function page() { ? null : require("react-native-volume-manager"); - let getDownloadedItem = null; - if (!Platform.isTV) { - getDownloadedItem = downloadProvider.useDownload(); - } + const getDownloadedItem = downloadProvider.useDownload(); const revalidateProgressCache = useInvalidatePlaybackProgressCache(); @@ -105,6 +97,7 @@ export default function page() { mediaSourceId, bitrateValue: bitrateValueStr, offline: offlineStr, + playbackPosition: playbackPositionFromUrl, } = useGlobalSearchParams<{ itemId: string; audioIndex: string; @@ -112,6 +105,8 @@ export default function page() { mediaSourceId: string; bitrateValue: string; offline: string; + /** Playback position in ticks. */ + playbackPosition?: string; }>(); const [settings] = useSettings(); const insets = useSafeAreaInsets(); @@ -133,6 +128,14 @@ export default function page() { isError: false, }); + /** Gets the initial playback position from the URL or the item's user data. */ + const getInitialPlaybackTicks = useCallback((): number => { + if (playbackPositionFromUrl) { + return Number.parseInt(playbackPositionFromUrl, 10); + } + return item?.UserData?.PlaybackPositionTicks ?? 0; + }, [playbackPositionFromUrl, item]); + useEffect(() => { const fetchItemData = async () => { setItemStatus({ isLoading: true, isError: false }); @@ -190,7 +193,7 @@ export default function page() { const res = await getStreamUrl({ api, item, - startTimeTicks: item?.UserData?.PlaybackPositionTicks!, + startTimeTicks: getInitialPlaybackTicks(), userId: user?.Id, audioStreamIndex: audioIndex, maxStreamingBitrate: bitrateValue, @@ -308,8 +311,12 @@ export default function page() { progress.set(currentTime); - if (offline) return; + // Update the playback position in the URL. + router.setParams({ + playbackPosition: msToTicks(currentTime).toString(), + }); + if (offline) return; if (!item?.Id || !stream) return; reportPlaybackProgress(); @@ -349,12 +356,11 @@ export default function page() { progress, ]); + /** Gets the initial playback position in seconds. */ const startPosition = useMemo(() => { if (offline) return 0; - return item?.UserData?.PlaybackPositionTicks - ? ticksToSeconds(item.UserData.PlaybackPositionTicks) - : 0; - }, [item, offline]); + return ticksToSeconds(getInitialPlaybackTicks()); + }, [offline, getInitialPlaybackTicks]); const volumeUpCb = useCallback(async () => { if (Platform.isTV) return; diff --git a/components/video-player/controls/contexts/VideoContext.tsx b/components/video-player/controls/contexts/VideoContext.tsx index a7417c79..ca1344d6 100644 --- a/components/video-player/controls/contexts/VideoContext.tsx +++ b/components/video-player/controls/contexts/VideoContext.tsx @@ -1,15 +1,15 @@ -import type { TrackInfo } from "@/modules/VlcPlayer.types"; -import { VideoPlayer, useSettings } from "@/utils/atoms/settings"; import { router, useLocalSearchParams } from "expo-router"; import type React from "react"; import { - type ReactNode, createContext, + type ReactNode, useContext, useEffect, useMemo, useState, } from "react"; +import type { TrackInfo } from "@/modules/VlcPlayer.types"; +import { useSettings, VideoPlayer } from "@/utils/atoms/settings"; import type { Track } from "../types"; import { useControlContext } from "./ControlContext"; @@ -57,13 +57,14 @@ export const VideoProvider: React.FC = ({ const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; - const { itemId, audioIndex, bitrateValue, subtitleIndex } = + const { itemId, audioIndex, bitrateValue, subtitleIndex, playbackPosition } = useLocalSearchParams<{ itemId: string; audioIndex: string; subtitleIndex: string; mediaSourceId: string; bitrateValue: string; + playbackPosition: string; }>(); const onTextBasedSubtitle = useMemo( @@ -88,6 +89,7 @@ export const VideoProvider: React.FC = ({ subtitleIndex: chosenSubtitleIndex, mediaSourceId: mediaSource?.Id ?? "", bitrateValue: bitrateValue, + playbackPosition: playbackPosition, }).toString(); //@ts-ignore diff --git a/components/video-player/controls/dropdown/DropdownView.tsx b/components/video-player/controls/dropdown/DropdownView.tsx index 3168e942..af3de55b 100644 --- a/components/video-player/controls/dropdown/DropdownView.tsx +++ b/components/video-player/controls/dropdown/DropdownView.tsx @@ -1,9 +1,11 @@ import { Ionicons } from "@expo/vector-icons"; -import React, { useCallback } from "react"; +import { useCallback } from "react"; import { Platform, TouchableOpacity } from "react-native"; + const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import { BITRATES } from "@/components/BitrateSelector"; + import { useLocalSearchParams, useRouter } from "expo-router"; +import { BITRATES } from "@/components/BitrateSelector"; import { useControlContext } from "../contexts/ControlContext"; import { useVideoContext } from "../contexts/VideoContext"; @@ -17,13 +19,15 @@ const DropdownView = () => { ]; const router = useRouter(); - const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{ - itemId: string; - audioIndex: string; - subtitleIndex: string; - mediaSourceId: string; - bitrateValue: string; - }>(); + const { subtitleIndex, audioIndex, bitrateValue, playbackPosition } = + useLocalSearchParams<{ + itemId: string; + audioIndex: string; + subtitleIndex: string; + mediaSourceId: string; + bitrateValue: string; + playbackPosition: string; + }>(); const changeBitrate = useCallback( (bitrate: string) => { @@ -33,11 +37,12 @@ const DropdownView = () => { subtitleIndex: subtitleIndex.toString() ?? "", mediaSourceId: mediaSource?.Id ?? "", bitrateValue: bitrate.toString(), + playbackPosition: playbackPosition, }).toString(); // @ts-expect-error router.replace(`player/direct-player?${queryParams}`); }, - [item, mediaSource, subtitleIndex, audioIndex], + [item, mediaSource, subtitleIndex, audioIndex, playbackPosition], ); return (