From eefd1d9d13079d01b96c3fd1b3e4b9000c32cdc1 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 13 Oct 2024 16:07:03 +0200 Subject: [PATCH] fix: local playback --- app/(auth)/play-offline-video.tsx | 206 +++++++++++++-------- components/video-player/Controls.tsx | 13 +- hooks/useAdjacentEpisodes.ts | 9 +- modules/vlc-player/ios/VlcPlayerView.swift | 18 +- 4 files changed, 149 insertions(+), 97 deletions(-) diff --git a/app/(auth)/play-offline-video.tsx b/app/(auth)/play-offline-video.tsx index 63d9f092..872bd91c 100644 --- a/app/(auth)/play-offline-video.tsx +++ b/app/(auth)/play-offline-video.tsx @@ -2,43 +2,44 @@ import { Controls } from "@/components/video-player/Controls"; import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar"; import { useOrientation } from "@/hooks/useOrientation"; import { useOrientationSettings } from "@/hooks/useOrientationSettings"; +import { VlcPlayerView } from "@/modules/vlc-player"; +import { + PlaybackStatePayload, + ProgressUpdatePayload, + VlcPlayerViewRef, +} from "@/modules/vlc-player/src/VlcPlayer.types"; import { apiAtom } from "@/providers/JellyfinProvider"; import { PlaybackType, usePlaySettings, } from "@/providers/PlaySettingsProvider"; import { useSettings } from "@/utils/atoms/settings"; -import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; -import orientationToOrientationLock from "@/utils/OrientationLockConverter"; -import { secondsToTicks } from "@/utils/secondsToTicks"; +import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; +import { ticksToSeconds } from "@/utils/time"; import { Api } from "@jellyfin/sdk"; import * as Haptics from "expo-haptics"; -import * as NavigationBar from "expo-navigation-bar"; import { useFocusEffect } from "expo-router"; -import * as ScreenOrientation from "expo-screen-orientation"; import { useAtomValue } from "jotai"; import React, { useCallback, useEffect, - useLayoutEffect, useMemo, useRef, useState, } from "react"; -import { Dimensions, Platform, Pressable, StatusBar, View } from "react-native"; +import { Dimensions, Pressable, StatusBar, View } from "react-native"; import { useSharedValue } from "react-native-reanimated"; -import Video, { OnProgressData, VideoRef } from "react-native-video"; +import { SelectedTrackType } from "react-native-video"; export default function page() { const { playSettings, playUrl } = usePlaySettings(); - const api = useAtomValue(apiAtom); - const videoRef = useRef(null); - const videoSource = useVideoSource(playSettings, api, playUrl); - const firstTime = useRef(true); + const [settings] = useSettings(); + const videoRef = useRef(null); const screenDimensions = Dimensions.get("screen"); + const [isPlaybackStopped, setIsPlaybackStopped] = useState(false); const [showControls, setShowControls] = useState(true); const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false); const [isPlaying, setIsPlaying] = useState(false); @@ -48,25 +49,53 @@ export default function page() { const isSeeking = useSharedValue(false); const cacheProgress = useSharedValue(0); - const togglePlay = useCallback(async () => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - if (isPlaying) { - videoRef.current?.pause(); - } else { - videoRef.current?.resume(); - } - }, [isPlaying]); + const [playbackState, setPlaybackState] = useState< + PlaybackStatePayload["nativeEvent"] | null + >(null); + + if (!playSettings || !playUrl || !api || !playSettings.item) return null; + + const togglePlay = useCallback( + async (ticks: number) => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + if (isPlaying) { + videoRef.current?.pause(); + } else { + videoRef.current?.play(); + } + }, + [isPlaying, api, playSettings?.item?.Id, videoRef, settings] + ); const play = useCallback(() => { - setIsPlaying(true); - videoRef.current?.resume(); + videoRef.current?.play(); + }, [videoRef]); + + const pause = useCallback(() => { + videoRef.current?.pause(); }, [videoRef]); const stop = useCallback(() => { - setIsPlaying(false); - videoRef.current?.pause(); + setIsPlaybackStopped(true); + videoRef.current?.stop(); }, [videoRef]); + const onProgress = useCallback( + async (data: ProgressUpdatePayload) => { + if (isSeeking.value === true) return; + if (isPlaybackStopped === true) return; + + const { currentTime, duration, isBuffering, isPlaying } = + data.nativeEvent; + + progress.value = currentTime; + + // cacheProgress.value = secondsToTicks(data.playableDuration); + // setIsBuffering(data.playableDuration === 0); + }, + [playSettings?.item.Id, isPlaying, api, isPlaybackStopped] + ); + useFocusEffect( useCallback(() => { play(); @@ -77,19 +106,72 @@ export default function page() { }, [play, stop]) ); - const { orientation } = useOrientation(); + useOrientation(); useOrientationSettings(); useAndroidNavigationBar(); - const onProgress = useCallback(async (data: OnProgressData) => { - if (isSeeking.value === true) return; - progress.value = secondsToTicks(data.currentTime); - cacheProgress.value = secondsToTicks(data.playableDuration); - setIsBuffering(data.playableDuration === 0); + const selectedSubtitleTrack = useMemo(() => { + const a = playSettings?.mediaSource?.MediaStreams?.find( + (s) => s.Index === playSettings.subtitleIndex + ); + console.log(a); + return a; + }, [playSettings]); + + const [hlsSubTracks, setHlsSubTracks] = useState< + { + index: number; + language?: string | undefined; + selected?: boolean | undefined; + title?: string | undefined; + type: any; + }[] + >([]); + + const selectedTextTrack = useMemo(() => { + for (let st of hlsSubTracks) { + if (st.title === selectedSubtitleTrack?.DisplayTitle) { + return { + type: SelectedTrackType.TITLE, + value: selectedSubtitleTrack?.DisplayTitle ?? "", + }; + } + } + return undefined; + }, [hlsSubTracks]); + + const onPlaybackStateChanged = (e: PlaybackStatePayload) => { + const { target, state, isBuffering, isPlaying } = e.nativeEvent; + + if (state === "Playing") { + setIsPlaying(true); + return; + } + + if (state === "Paused") { + setIsPlaying(false); + return; + } + + if (isPlaying) { + setIsPlaying(true); + setIsBuffering(false); + } else if (isBuffering) { + setIsBuffering(true); + } + + setPlaybackState(e.nativeEvent); + }; + + useEffect(() => { + return () => { + stop(); + }; }, []); - if (!playSettings || !playUrl || !api || !videoSource || !playSettings.item) - return null; + useEffect(() => { + console.log(playUrl); + }, [playUrl]); return ( - ); } - -export function useVideoSource( - playSettings: PlaybackType | null, - api: Api | null, - playUrl?: string | null -) { - const videoSource = useMemo(() => { - if (!playSettings || !api || !playUrl) { - return null; - } - - const startPosition = 0; - - return { - uri: playUrl, - isNetwork: false, - startPosition, - metadata: { - artist: playSettings.item?.AlbumArtist ?? undefined, - title: playSettings.item?.Name || "Unknown", - description: playSettings.item?.Overview ?? undefined, - subtitle: playSettings.item?.Album ?? undefined, - }, - }; - }, [playSettings, api]); - - return videoSource; -} diff --git a/components/video-player/Controls.tsx b/components/video-player/Controls.tsx index 512f6768..a2484045 100644 --- a/components/video-player/Controls.tsx +++ b/components/video-player/Controls.tsx @@ -49,6 +49,7 @@ interface Props { enableTrickplay?: boolean; togglePlay: (ticks: number) => void; setShowControls: (shown: boolean) => void; + offline?: boolean; } export const Controls: React.FC = ({ @@ -64,7 +65,7 @@ export const Controls: React.FC = ({ setShowControls, ignoreSafeAreas, setIgnoreSafeAreas, - enableTrickplay = true, + offline = false, }) => { const [settings] = useSettings(); const router = useRouter(); @@ -111,10 +112,12 @@ export const Controls: React.FC = ({ } }, [showControls, isBuffering]); - const { previousItem, nextItem } = useAdjacentItems({ item }); + const { previousItem, nextItem } = useAdjacentItems({ + item: offline ? undefined : item, + }); const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay( item, - enableTrickplay + !offline ); const [currentTime, setCurrentTime] = useState(0); // Seconds @@ -127,13 +130,13 @@ export const Controls: React.FC = ({ const lastProgressRef = useRef(0); const { showSkipButton, skipIntro } = useIntroSkipper( - item.Id, + offline ? undefined : item.Id, currentTime, videoRef ); const { showSkipCreditButton, skipCredit } = useCreditSkipper( - item.Id, + offline ? undefined : item.Id, currentTime, videoRef ); diff --git a/hooks/useAdjacentEpisodes.ts b/hooks/useAdjacentEpisodes.ts index 90b50b21..26fa777f 100644 --- a/hooks/useAdjacentEpisodes.ts +++ b/hooks/useAdjacentEpisodes.ts @@ -18,7 +18,6 @@ export const useAdjacentItems = ({ item }: AdjacentEpisodesProps) => { const parentId = item?.AlbumId || item?.ParentId; const indexNumber = item?.IndexNumber; - console.log("Getting previous item for " + indexNumber); if ( !api || !parentId || @@ -26,15 +25,11 @@ export const useAdjacentItems = ({ item }: AdjacentEpisodesProps) => { indexNumber === null || indexNumber - 1 < 1 ) { - console.log("No previous item", { - itemIndex: indexNumber, - itemId: item?.Id, - parentId: parentId, - indexNumber: indexNumber, - }); return null; } + console.log("Getting previous item for " + indexNumber); + const newIndexNumber = indexNumber - 2; const res = await getItemsApi(api).getItems({ diff --git a/modules/vlc-player/ios/VlcPlayerView.swift b/modules/vlc-player/ios/VlcPlayerView.swift index 28a33935..7892a255 100644 --- a/modules/vlc-player/ios/VlcPlayerView.swift +++ b/modules/vlc-player/ios/VlcPlayerView.swift @@ -110,9 +110,20 @@ class VlcPlayerView: ExpoView { let media: VLCMedia if isNetwork { + print("Loading network file: \(uri)") media = VLCMedia(url: URL(string: uri)!) } else { - media = VLCMedia(path: uri) + print("Loading local file: \(uri)") + if uri.starts(with: "file://") { + if let url = URL(string: uri) { + media = VLCMedia(url: url) + } else { + print("Error: Invalid local file URL") + return + } + } else { + media = VLCMedia(path: uri) + } } media.delegate = self @@ -454,9 +465,6 @@ extension VlcPlayerView: VLCMediaPlayerDelegate { stateInfo["state"] = "Buffering" } - print("VLC Player State Changed: \(currentState.description)") - print("VLC Player State Changed: \(player.isPlaying)") - // switch currentState { // case .opening: // stateInfo["state"] = "Opening" @@ -477,6 +485,8 @@ extension VlcPlayerView: VLCMediaPlayerDelegate { // stateInfo["state"] = "Unknown" // } + print("State changed: \(stateInfo)") + self.lastReportedState = currentState self.onVideoStateChange?(stateInfo) }