From ae963751cf83bc3ec87f4619747bf1c0f9092134 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 15 Oct 2024 08:01:12 +0200 Subject: [PATCH] wip --- app/(auth)/play-offline-video.tsx | 2 +- app/(auth)/play-video.tsx | 15 +- app/(auth)/vlc-player.tsx | 53 +- components/video-player/Controls.tsx | 255 ++------- components/video-player/VlcControls.tsx | 691 ++++++++++++++++++++++++ hooks/useCreditSkipper.ts | 11 +- hooks/useIntroSkipper.ts | 11 +- 7 files changed, 790 insertions(+), 248 deletions(-) create mode 100644 components/video-player/VlcControls.tsx diff --git a/app/(auth)/play-offline-video.tsx b/app/(auth)/play-offline-video.tsx index ea6029f4..fb54a37d 100644 --- a/app/(auth)/play-offline-video.tsx +++ b/app/(auth)/play-offline-video.tsx @@ -1,4 +1,4 @@ -import { Controls } from "@/components/video-player/Controls"; +import { Controls } from "@/components/video-player/VlcControls"; import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar"; import { useOrientation } from "@/hooks/useOrientation"; import { useOrientationSettings } from "@/hooks/useOrientationSettings"; diff --git a/app/(auth)/play-video.tsx b/app/(auth)/play-video.tsx index e1201d7e..0ae91048 100644 --- a/app/(auth)/play-video.tsx +++ b/app/(auth)/play-video.tsx @@ -27,7 +27,8 @@ import Video, { } from "react-native-video"; export default function page() { - const { playSettings, playUrl, playSessionId } = usePlaySettings(); + const { playSettings, playUrl, playSessionId, mediaSource } = + usePlaySettings(); const api = useAtomValue(apiAtom); const [settings] = useSettings(); const videoRef = useRef(null); @@ -46,7 +47,14 @@ export default function page() { const isSeeking = useSharedValue(false); const cacheProgress = useSharedValue(0); - if (!playSettings || !playUrl || !api || !videoSource || !playSettings.item) + if ( + !playSettings || + !playUrl || + !api || + !videoSource || + !playSettings.item || + !mediaSource + ) return null; const togglePlay = useCallback( @@ -272,8 +280,9 @@ export default function page() { (null); const poster = usePoster(playSettings, api); const videoSource = useVideoSource(playSettings, api, poster, playUrl); @@ -287,22 +282,34 @@ export default function page() { playerRef={videoRef} /> */} - + {videoRef.current && ( + + )} ); } diff --git a/components/video-player/Controls.tsx b/components/video-player/Controls.tsx index 72d70e96..264f0c75 100644 --- a/components/video-player/Controls.tsx +++ b/components/video-player/Controls.tsx @@ -2,32 +2,17 @@ import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes"; import { useCreditSkipper } from "@/hooks/useCreditSkipper"; import { useIntroSkipper } from "@/hooks/useIntroSkipper"; import { useTrickplay } from "@/hooks/useTrickplay"; -import { - TrackInfo, - VlcPlayerViewRef, -} from "@/modules/vlc-player/src/VlcPlayer.types"; import { usePlaySettings } from "@/providers/PlaySettingsProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { writeToLog } from "@/utils/log"; -import { formatTimeString, secondsToMs, ticksToMs } from "@/utils/time"; +import { formatTimeString, ticksToSeconds } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; -import { - BaseItemDto, - MediaSourceInfo, - type MediaStream, -} from "@jellyfin/sdk/lib/generated-client"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import { useRouter } from "expo-router"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { - Alert, Dimensions, Platform, Pressable, @@ -35,22 +20,22 @@ import { View, } from "react-native"; import { Slider } from "react-native-awesome-slider"; -import { +import Animated, { runOnJS, SharedValue, useAnimatedReaction, + useAnimatedStyle, useSharedValue, + withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import * as DropdownMenu from "zeego/dropdown-menu"; +import { VideoRef } from "react-native-video"; import { Text } from "../common/Text"; import { Loader } from "../Loader"; -import { useAtomValue } from "jotai"; -import { apiAtom } from "@/providers/JellyfinProvider"; interface Props { item: BaseItemDto; - videoRef: React.MutableRefObject; + videoRef: React.MutableRefObject; isPlaying: boolean; isSeeking: SharedValue; cacheProgress: SharedValue; @@ -62,9 +47,6 @@ interface Props { enableTrickplay?: boolean; togglePlay: (ticks: number) => void; setShowControls: (shown: boolean) => void; - offline?: boolean; - isVideoLoaded?: boolean; - mediaSource: MediaSourceInfo; } export const Controls: React.FC = ({ @@ -80,21 +62,19 @@ export const Controls: React.FC = ({ setShowControls, ignoreSafeAreas, setIgnoreSafeAreas, - mediaSource, - isVideoLoaded, - offline = false, + enableTrickplay = true, }) => { const [settings] = useSettings(); const router = useRouter(); const insets = useSafeAreaInsets(); - const { setPlaySettings, playSettings } = usePlaySettings(); - const api = useAtomValue(apiAtom); + const { setPlaySettings } = usePlaySettings(); + const windowDimensions = Dimensions.get("window"); const { previousItem, nextItem } = useAdjacentItems({ item }); const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay( item, - !offline + enableTrickplay ); const [currentTime, setCurrentTime] = useState(0); // Seconds @@ -104,18 +84,23 @@ export const Controls: React.FC = ({ const max = useSharedValue(item.RunTimeTicks || 0); const wasPlayingRef = useRef(false); - const lastProgressRef = useRef(0); + + const seek = (ticks: number) => { + videoRef.current?.seek(ticks); + }; const { showSkipButton, skipIntro } = useIntroSkipper( - offline ? undefined : item.Id, + item.Id, currentTime, - videoRef + seek, + () => videoRef.current?.resume() ); const { showSkipCreditButton, skipCredit } = useCreditSkipper( - offline ? undefined : item.Id, + item.Id, currentTime, - videoRef + seek, + () => videoRef.current?.resume() ); const goToPreviousItem = useCallback(() => { @@ -154,8 +139,8 @@ export const Controls: React.FC = ({ const updateTimes = useCallback( (currentProgress: number, maxValue: number) => { - const current = currentProgress; - const remaining = maxValue - currentProgress; + const current = ticksToSeconds(currentProgress); + const remaining = ticksToSeconds(maxValue - currentProgress); setCurrentTime(current); setRemainingTime(remaining); @@ -185,19 +170,18 @@ export const Controls: React.FC = ({ useEffect(() => { if (item) { - progress.value = ticksToMs(item?.UserData?.PlaybackPositionTicks); - max.value = ticksToMs(item.RunTimeTicks || 0); + progress.value = item?.UserData?.PlaybackPositionTicks || 0; + max.value = item.RunTimeTicks || 0; } }, [item]); const toggleControls = () => setShowControls(!showControls); - const handleSliderComplete = useCallback(async (value: number) => { - isSeeking.value = false; + const handleSliderComplete = useCallback((value: number) => { progress.value = value; - - await videoRef.current?.seekTo(Math.max(0, Math.floor(value))); - if (wasPlayingRef.current === true) videoRef.current?.play(); + isSeeking.value = false; + videoRef.current?.seek(Math.max(0, Math.floor(value / 10000000))); + if (wasPlayingRef.current === true) videoRef.current?.resume(); }, []); const handleSliderChange = (value: number) => { @@ -206,24 +190,22 @@ export const Controls: React.FC = ({ const handleSliderStart = useCallback(() => { if (showControls === false) return; - wasPlayingRef.current = isPlaying; - lastProgressRef.current = progress.value; - videoRef.current?.pause(); isSeeking.value = true; }, [showControls, isPlaying]); const handleSkipBackward = useCallback(async () => { + console.log("handleSkipBackward"); if (!settings?.rewindSkipTime) return; wasPlayingRef.current = isPlaying; try { - const curr = progress.value; + const curr = await videoRef.current?.getCurrentPosition(); if (curr !== undefined) { - await videoRef.current?.seekTo( - Math.max(0, curr - secondsToMs(settings.rewindSkipTime)) - ); - if (wasPlayingRef.current === true) videoRef.current?.play(); + videoRef.current?.seek(Math.max(0, curr - settings.rewindSkipTime)); + setTimeout(() => { + if (wasPlayingRef.current === true) videoRef.current?.resume(); + }, 10); } } catch (error) { writeToLog("ERROR", "Error seeking video backwards", error); @@ -231,14 +213,16 @@ export const Controls: React.FC = ({ }, [settings, isPlaying]); const handleSkipForward = useCallback(async () => { + console.log("handleSkipForward"); if (!settings?.forwardSkipTime) return; wasPlayingRef.current = isPlaying; try { - const curr = progress.value; + const curr = await videoRef.current?.getCurrentPosition(); if (curr !== undefined) { - const newTime = curr + secondsToMs(settings.forwardSkipTime); - await videoRef.current?.seekTo(Math.max(0, newTime)); - if (wasPlayingRef.current === true) videoRef.current?.play(); + videoRef.current?.seek(Math.max(0, curr + settings.forwardSkipTime)); + setTimeout(() => { + if (wasPlayingRef.current === true) videoRef.current?.resume(); + }, 10); } } catch (error) { writeToLog("ERROR", "Error seeking video forwards", error); @@ -249,75 +233,6 @@ export const Controls: React.FC = ({ setIgnoreSafeAreas((prev) => !prev); }, []); - const [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState< - MediaStream | undefined - >(undefined); - - const [audioTracks, setAudioTracks] = useState(null); - const [subtitleTracks, setSubtitleTracks] = useState( - null - ); - - useEffect(() => { - const fetchTracks = async () => { - if (videoRef.current) { - const audio = await videoRef.current.getAudioTracks(); - const subtitles = await videoRef.current.getSubtitleTracks(); - setAudioTracks(audio); - setSubtitleTracks(subtitles); - } - }; - - fetchTracks(); - }, [videoRef, isVideoLoaded]); - - type EmbeddedSubtitle = { - name: string; - index: number; - isExternal: false; - }; - - type ExternalSubtitle = { - name: string; - index: number; - isExternal: true; - deliveryUrl: string; - }; - - const allSubtitleTracks = useMemo(() => { - const embeddedSubs = - subtitleTracks?.map((s) => ({ - name: s.name, - index: s.index, - isExternal: false, - deliveryUrl: undefined, - })) || []; - - const externalSubs = - mediaSource?.MediaStreams?.filter( - (stream) => stream.Type === "Subtitle" && stream.IsExternal - ).map((s) => ({ - name: s.DisplayTitle!, - index: s.Index!, - isExternal: true, - deliveryUrl: s.DeliveryUrl, - })) || []; - - // Create a Set of embedded subtitle names for quick lookup - const embeddedSubNames = new Set(embeddedSubs.map((sub) => sub.name)); - - // Filter out external subs that have the same name as embedded subs - const uniqueExternalSubs = externalSubs.filter( - (sub) => !embeddedSubNames.has(sub.name) - ); - - // Combine embedded and unique external subs - return [...embeddedSubs, ...uniqueExternalSubs] as ( - | EmbeddedSubtitle - | ExternalSubtitle - )[]; - }, [item, isVideoLoaded, subtitleTracks, mediaSource]); - return ( = ({ }, ]} > - {/* */} - - - - - - - - - - Subtitle tracks - - - Subtitle - - - {/* { - videoRef.current?.setSubtitleTrack(-1); - }} - > - - - None - - */} - {allSubtitleTracks.length > 0 - ? allSubtitleTracks?.map((sub, idx: number) => ( - { - if (sub.isExternal) { - videoRef.current?.setSubtitleURL( - api?.basePath + sub.deliveryUrl - ); - return; - } - - videoRef.current?.setSubtitleTrack(sub.index); - }} - > - - - {sub.name} - - - )) - : null} - - - - - - = ({ }, ]} pointerEvents={showControls ? "auto" : "none"} - className={`flex flex-row items-center space-x-2 z-10 p-4 `} + className={`flex flex-row items-center space-x-2 z-10 p-4`} > = ({ /> { - await videoRef.current?.stop(); + onPress={() => { router.back(); }} className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" diff --git a/components/video-player/VlcControls.tsx b/components/video-player/VlcControls.tsx new file mode 100644 index 00000000..c8604abb --- /dev/null +++ b/components/video-player/VlcControls.tsx @@ -0,0 +1,691 @@ +import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes"; +import { useCreditSkipper } from "@/hooks/useCreditSkipper"; +import { useIntroSkipper } from "@/hooks/useIntroSkipper"; +import { useTrickplay } from "@/hooks/useTrickplay"; +import { + TrackInfo, + VlcPlayerViewRef, +} from "@/modules/vlc-player/src/VlcPlayer.types"; +import { usePlaySettings } from "@/providers/PlaySettingsProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; +import { writeToLog } from "@/utils/log"; +import { formatTimeString, secondsToMs, ticksToMs } from "@/utils/time"; +import { Ionicons } from "@expo/vector-icons"; +import { + BaseItemDto, + MediaSourceInfo, + type MediaStream, +} from "@jellyfin/sdk/lib/generated-client"; +import { Image } from "expo-image"; +import { useRouter } from "expo-router"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + Alert, + Dimensions, + Platform, + Pressable, + TouchableOpacity, + View, +} from "react-native"; +import { Slider } from "react-native-awesome-slider"; +import { + runOnJS, + SharedValue, + useAnimatedReaction, + useSharedValue, +} from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import * as DropdownMenu from "zeego/dropdown-menu"; +import { Text } from "../common/Text"; +import { Loader } from "../Loader"; +import { useAtomValue } from "jotai"; +import { apiAtom } from "@/providers/JellyfinProvider"; + +interface Props { + item: BaseItemDto; + videoRef: React.MutableRefObject; + isPlaying: boolean; + isSeeking: SharedValue; + cacheProgress: SharedValue; + progress: SharedValue; + isBuffering: boolean; + showControls: boolean; + ignoreSafeAreas?: boolean; + setIgnoreSafeAreas: React.Dispatch>; + enableTrickplay?: boolean; + togglePlay: (ticks: number) => void; + setShowControls: (shown: boolean) => void; + offline?: boolean; + isVideoLoaded?: boolean; + mediaSource: MediaSourceInfo; + seek: (ticks: number) => void; + play: (() => Promise) | (() => void); + pause: () => void; + getAudioTracks?: () => Promise; + getSubtitleTracks?: () => Promise; + setSubtitleURL?: (url: string) => void; + setSubtitleTrack?: (index: number) => void; + stop?: () => Promise; +} + +export const VlcControls: React.FC = ({ + item, + seek, + play, + pause, + togglePlay, + isPlaying, + isSeeking, + progress, + isBuffering, + cacheProgress, + showControls, + setShowControls, + ignoreSafeAreas, + setIgnoreSafeAreas, + mediaSource, + isVideoLoaded, + getAudioTracks, + getSubtitleTracks, + setSubtitleURL, + setSubtitleTrack, + stop, + offline = false, +}) => { + const [settings] = useSettings(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const { setPlaySettings, playSettings } = usePlaySettings(); + const api = useAtomValue(apiAtom); + const windowDimensions = Dimensions.get("window"); + + const { previousItem, nextItem } = useAdjacentItems({ item }); + const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay( + item, + !offline + ); + + const [currentTime, setCurrentTime] = useState(0); // Seconds + const [remainingTime, setRemainingTime] = useState(0); // Seconds + + const min = useSharedValue(0); + const max = useSharedValue(item.RunTimeTicks || 0); + + const wasPlayingRef = useRef(false); + const lastProgressRef = useRef(0); + + const { showSkipButton, skipIntro } = useIntroSkipper( + offline ? undefined : item.Id, + currentTime, + seek, + play + ); + + const { showSkipCreditButton, skipCredit } = useCreditSkipper( + offline ? undefined : item.Id, + currentTime, + seek, + play + ); + + const goToPreviousItem = useCallback(() => { + if (!previousItem || !settings) return; + + const { bitrate, mediaSource, audioIndex, subtitleIndex } = + getDefaultPlaySettings(previousItem, settings); + + setPlaySettings({ + item: previousItem, + bitrate, + mediaSource, + audioIndex, + subtitleIndex, + }); + + router.replace("/play-video"); + }, [previousItem, settings]); + + const goToNextItem = useCallback(() => { + if (!nextItem || !settings) return; + + const { bitrate, mediaSource, audioIndex, subtitleIndex } = + getDefaultPlaySettings(nextItem, settings); + + setPlaySettings({ + item: nextItem, + bitrate, + mediaSource, + audioIndex, + subtitleIndex, + }); + + router.replace("/play-video"); + }, [nextItem, settings]); + + const updateTimes = useCallback( + (currentProgress: number, maxValue: number) => { + const current = currentProgress; + const remaining = maxValue - currentProgress; + + setCurrentTime(current); + setRemainingTime(remaining); + + if (currentProgress === maxValue) { + setShowControls(true); + // Automatically play the next item if it exists + goToNextItem(); + } + }, + [goToNextItem] + ); + + useAnimatedReaction( + () => ({ + progress: progress.value, + max: max.value, + isSeeking: isSeeking.value, + }), + (result) => { + if (result.isSeeking === false) { + runOnJS(updateTimes)(result.progress, result.max); + } + }, + [updateTimes] + ); + + useEffect(() => { + if (item) { + progress.value = ticksToMs(item?.UserData?.PlaybackPositionTicks); + max.value = ticksToMs(item.RunTimeTicks || 0); + } + }, [item]); + + const toggleControls = () => setShowControls(!showControls); + + const handleSliderComplete = useCallback(async (value: number) => { + isSeeking.value = false; + progress.value = value; + + await seek(Math.max(0, Math.floor(value))); + if (wasPlayingRef.current === true) play(); + }, []); + + const handleSliderChange = (value: number) => { + calculateTrickplayUrl(value); + }; + + const handleSliderStart = useCallback(() => { + if (showControls === false) return; + + wasPlayingRef.current = isPlaying; + lastProgressRef.current = progress.value; + + pause(); + isSeeking.value = true; + }, [showControls, isPlaying]); + + const handleSkipBackward = useCallback(async () => { + if (!settings?.rewindSkipTime) return; + wasPlayingRef.current = isPlaying; + try { + const curr = progress.value; + if (curr !== undefined) { + await seek(Math.max(0, curr - secondsToMs(settings.rewindSkipTime))); + if (wasPlayingRef.current === true) play(); + } + } catch (error) { + writeToLog("ERROR", "Error seeking video backwards", error); + } + }, [settings, isPlaying]); + + const handleSkipForward = useCallback(async () => { + if (!settings?.forwardSkipTime) return; + wasPlayingRef.current = isPlaying; + try { + const curr = progress.value; + if (curr !== undefined) { + const newTime = curr + secondsToMs(settings.forwardSkipTime); + await seek(Math.max(0, newTime)); + if (wasPlayingRef.current === true) play(); + } + } catch (error) { + writeToLog("ERROR", "Error seeking video forwards", error); + } + }, [settings, isPlaying]); + + const toggleIgnoreSafeAreas = useCallback(() => { + setIgnoreSafeAreas((prev) => !prev); + }, []); + + const [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState< + MediaStream | undefined + >(undefined); + + const [audioTracks, setAudioTracks] = useState(null); + const [subtitleTracks, setSubtitleTracks] = useState( + null + ); + + useEffect(() => { + const fetchTracks = async () => { + if (getAudioTracks && getSubtitleTracks) { + const audio = await getAudioTracks(); + const subtitles = await getSubtitleTracks(); + setAudioTracks(audio); + setSubtitleTracks(subtitles); + } + }; + + fetchTracks(); + }, [isVideoLoaded, getAudioTracks, getSubtitleTracks]); + + type EmbeddedSubtitle = { + name: string; + index: number; + isExternal: false; + }; + + type ExternalSubtitle = { + name: string; + index: number; + isExternal: true; + deliveryUrl: string; + }; + + const allSubtitleTracks = useMemo(() => { + const embeddedSubs = + subtitleTracks?.map((s) => ({ + name: s.name, + index: s.index, + isExternal: false, + deliveryUrl: undefined, + })) || []; + + const externalSubs = + mediaSource?.MediaStreams?.filter( + (stream) => stream.Type === "Subtitle" && stream.IsExternal + ).map((s) => ({ + name: s.DisplayTitle!, + index: s.Index!, + isExternal: true, + deliveryUrl: s.DeliveryUrl, + })) || []; + + // Create a Set of embedded subtitle names for quick lookup + const embeddedSubNames = new Set(embeddedSubs.map((sub) => sub.name)); + + // Filter out external subs that have the same name as embedded subs + const uniqueExternalSubs = externalSubs.filter( + (sub) => !embeddedSubNames.has(sub.name) + ); + + // Combine embedded and unique external subs + return [...embeddedSubs, ...uniqueExternalSubs] as ( + | EmbeddedSubtitle + | ExternalSubtitle + )[]; + }, [item, isVideoLoaded, subtitleTracks, mediaSource]); + + return ( + + {/* */} + + {setSubtitleURL && setSubtitleTrack && ( + + + + + + + + + Subtitle tracks + + + Subtitle + + + {/* { + videoRef.current?.setSubtitleTrack(-1); + }} + > + + + None + + */} + {allSubtitleTracks.length > 0 + ? allSubtitleTracks?.map((sub, idx: number) => ( + { + if (sub.isExternal) { + setSubtitleURL(api?.basePath + sub.deliveryUrl); + return; + } + + setSubtitleTrack(sub.index); + }} + > + + + {sub.name} + + + )) + : null} + + + + + + )} + + + + Skip Intro + + + + + + Skip Credits + + + + { + toggleControls(); + }} + > + + + + + + + + + + + + { + if (stop) await stop(); + router.back(); + }} + className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" + > + + + + + + + {item?.Name} + {item?.Type === "Episode" && ( + {item.SeriesName} + )} + {item?.Type === "Movie" && ( + {item?.ProductionYear} + )} + {item?.Type === "Audio" && ( + {item?.Album} + )} + + + + + + + + + + { + togglePlay(progress.value); + }} + > + + + + + + + + + + + { + if (!trickPlayUrl || !trickplayInfo) { + return null; + } + const { x, y, url } = trickPlayUrl; + + const tileWidth = 150; + const tileHeight = 150 / trickplayInfo.aspectRatio!; + return ( + + + + ); + }} + sliderHeight={10} + thumbWidth={0} + progress={progress} + minimumValue={min} + maximumValue={max} + /> + + + {formatTimeString(currentTime)} + + + -{formatTimeString(remainingTime)} + + + + + + + ); +}; diff --git a/hooks/useCreditSkipper.ts b/hooks/useCreditSkipper.ts index ba574a78..4b9345de 100644 --- a/hooks/useCreditSkipper.ts +++ b/hooks/useCreditSkipper.ts @@ -21,7 +21,8 @@ interface CreditTimestamps { export const useCreditSkipper = ( itemId: string | undefined, currentTime: number, - videoRef: React.RefObject + seek: (time: number) => void, + play: () => void ) => { const [api] = useAtom(apiAtom); const [showSkipCreditButton, setShowSkipCreditButton] = useState(false); @@ -60,16 +61,16 @@ export const useCreditSkipper = ( }, [creditTimestamps, currentTime]); const skipCredit = useCallback(() => { - if (!creditTimestamps || !videoRef.current) return; + if (!creditTimestamps) return; try { - videoRef.current.seek(creditTimestamps.Credits.End); + seek(creditTimestamps.Credits.End); setTimeout(() => { - videoRef.current?.resume(); + play(); }, 200); } catch (error) { writeToLog("ERROR", "Error skipping intro", error); } - }, [creditTimestamps, videoRef]); + }, [creditTimestamps]); return { showSkipCreditButton, skipCredit }; }; diff --git a/hooks/useIntroSkipper.ts b/hooks/useIntroSkipper.ts index fa1f154b..c092c3e2 100644 --- a/hooks/useIntroSkipper.ts +++ b/hooks/useIntroSkipper.ts @@ -17,7 +17,8 @@ interface IntroTimestamps { export const useIntroSkipper = ( itemId: string | undefined, currentTime: number, - videoRef: React.RefObject + seek: (ticks: number) => void, + play: () => void ) => { const [api] = useAtom(apiAtom); const [showSkipButton, setShowSkipButton] = useState(false); @@ -57,16 +58,16 @@ export const useIntroSkipper = ( const skipIntro = useCallback(() => { console.log("skipIntro"); - if (!introTimestamps || !videoRef.current) return; + if (!introTimestamps) return; try { - videoRef.current.seek(introTimestamps.IntroEnd); + seek(introTimestamps.IntroEnd); setTimeout(() => { - videoRef.current?.resume(); + play(); }, 200); } catch (error) { writeToLog("ERROR", "Error skipping intro", error); } - }, [introTimestamps, videoRef]); + }, [introTimestamps]); return { showSkipButton, skipIntro }; };