fix: local playback

This commit is contained in:
Fredrik Burmester
2024-10-13 16:07:03 +02:00
parent 43d64bc3d0
commit eefd1d9d13
4 changed files with 149 additions and 97 deletions

View File

@@ -2,43 +2,44 @@ import { Controls } from "@/components/video-player/Controls";
import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar"; import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar";
import { useOrientation } from "@/hooks/useOrientation"; import { useOrientation } from "@/hooks/useOrientation";
import { useOrientationSettings } from "@/hooks/useOrientationSettings"; 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 { apiAtom } from "@/providers/JellyfinProvider";
import { import {
PlaybackType, PlaybackType,
usePlaySettings, usePlaySettings,
} from "@/providers/PlaySettingsProvider"; } from "@/providers/PlaySettingsProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import orientationToOrientationLock from "@/utils/OrientationLockConverter"; import { ticksToSeconds } from "@/utils/time";
import { secondsToTicks } from "@/utils/secondsToTicks";
import { Api } from "@jellyfin/sdk"; import { Api } from "@jellyfin/sdk";
import * as Haptics from "expo-haptics"; import * as Haptics from "expo-haptics";
import * as NavigationBar from "expo-navigation-bar";
import { useFocusEffect } from "expo-router"; import { useFocusEffect } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } 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 { useSharedValue } from "react-native-reanimated";
import Video, { OnProgressData, VideoRef } from "react-native-video"; import { SelectedTrackType } from "react-native-video";
export default function page() { export default function page() {
const { playSettings, playUrl } = usePlaySettings(); const { playSettings, playUrl } = usePlaySettings();
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const videoRef = useRef<VideoRef | null>(null); const [settings] = useSettings();
const videoSource = useVideoSource(playSettings, api, playUrl); const videoRef = useRef<VlcPlayerViewRef>(null);
const firstTime = useRef(true);
const screenDimensions = Dimensions.get("screen"); const screenDimensions = Dimensions.get("screen");
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
const [showControls, setShowControls] = useState(true); const [showControls, setShowControls] = useState(true);
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false); const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
@@ -48,25 +49,53 @@ export default function page() {
const isSeeking = useSharedValue(false); const isSeeking = useSharedValue(false);
const cacheProgress = useSharedValue(0); const cacheProgress = useSharedValue(0);
const togglePlay = useCallback(async () => { const [playbackState, setPlaybackState] = useState<
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); PlaybackStatePayload["nativeEvent"] | null
if (isPlaying) { >(null);
videoRef.current?.pause();
} else { if (!playSettings || !playUrl || !api || !playSettings.item) return null;
videoRef.current?.resume();
} const togglePlay = useCallback(
}, [isPlaying]); 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(() => { const play = useCallback(() => {
setIsPlaying(true); videoRef.current?.play();
videoRef.current?.resume(); }, [videoRef]);
const pause = useCallback(() => {
videoRef.current?.pause();
}, [videoRef]); }, [videoRef]);
const stop = useCallback(() => { const stop = useCallback(() => {
setIsPlaying(false); setIsPlaybackStopped(true);
videoRef.current?.pause(); videoRef.current?.stop();
}, [videoRef]); }, [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( useFocusEffect(
useCallback(() => { useCallback(() => {
play(); play();
@@ -77,19 +106,72 @@ export default function page() {
}, [play, stop]) }, [play, stop])
); );
const { orientation } = useOrientation(); useOrientation();
useOrientationSettings(); useOrientationSettings();
useAndroidNavigationBar(); useAndroidNavigationBar();
const onProgress = useCallback(async (data: OnProgressData) => { const selectedSubtitleTrack = useMemo(() => {
if (isSeeking.value === true) return; const a = playSettings?.mediaSource?.MediaStreams?.find(
progress.value = secondsToTicks(data.currentTime); (s) => s.Index === playSettings.subtitleIndex
cacheProgress.value = secondsToTicks(data.playableDuration); );
setIsBuffering(data.playableDuration === 0); 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) useEffect(() => {
return null; console.log(playUrl);
}, [playUrl]);
return ( return (
<View <View
@@ -107,29 +189,17 @@ export default function page() {
}} }}
className="absolute z-0 h-full w-full" className="absolute z-0 h-full w-full"
> >
<Video <VlcPlayerView
ref={videoRef} ref={videoRef}
source={videoSource} source={{
uri: playUrl,
autoplay: true,
isNetwork: false,
}}
style={{ width: "100%", height: "100%" }} style={{ width: "100%", height: "100%" }}
resizeMode={ignoreSafeAreas ? "cover" : "contain"} onVideoProgress={onProgress}
onProgress={onProgress} progressUpdateInterval={1000}
onError={() => {}} onVideoStateChange={onPlaybackStateChanged}
onLoad={() => {
if (firstTime.current === true) {
play();
firstTime.current = false;
}
}}
playWhenInactive={true}
allowsExternalPlayback={true}
playInBackground={true}
pictureInPicture={true}
showNotificationControls={true}
ignoreSilentSwitch="ignore"
fullscreen={false}
onPlaybackStateChanged={(state) => {
if (isSeeking.value === false) setIsPlaying(state.isPlaying);
}}
/> />
</Pressable> </Pressable>
@@ -146,35 +216,9 @@ export default function page() {
setShowControls={setShowControls} setShowControls={setShowControls}
setIgnoreSafeAreas={setIgnoreSafeAreas} setIgnoreSafeAreas={setIgnoreSafeAreas}
ignoreSafeAreas={ignoreSafeAreas} ignoreSafeAreas={ignoreSafeAreas}
enableTrickplay={false}
offline={true}
/> />
</View> </View>
); );
} }
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;
}

View File

@@ -49,6 +49,7 @@ interface Props {
enableTrickplay?: boolean; enableTrickplay?: boolean;
togglePlay: (ticks: number) => void; togglePlay: (ticks: number) => void;
setShowControls: (shown: boolean) => void; setShowControls: (shown: boolean) => void;
offline?: boolean;
} }
export const Controls: React.FC<Props> = ({ export const Controls: React.FC<Props> = ({
@@ -64,7 +65,7 @@ export const Controls: React.FC<Props> = ({
setShowControls, setShowControls,
ignoreSafeAreas, ignoreSafeAreas,
setIgnoreSafeAreas, setIgnoreSafeAreas,
enableTrickplay = true, offline = false,
}) => { }) => {
const [settings] = useSettings(); const [settings] = useSettings();
const router = useRouter(); const router = useRouter();
@@ -111,10 +112,12 @@ export const Controls: React.FC<Props> = ({
} }
}, [showControls, isBuffering]); }, [showControls, isBuffering]);
const { previousItem, nextItem } = useAdjacentItems({ item }); const { previousItem, nextItem } = useAdjacentItems({
item: offline ? undefined : item,
});
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay( const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
item, item,
enableTrickplay !offline
); );
const [currentTime, setCurrentTime] = useState(0); // Seconds const [currentTime, setCurrentTime] = useState(0); // Seconds
@@ -127,13 +130,13 @@ export const Controls: React.FC<Props> = ({
const lastProgressRef = useRef<number>(0); const lastProgressRef = useRef<number>(0);
const { showSkipButton, skipIntro } = useIntroSkipper( const { showSkipButton, skipIntro } = useIntroSkipper(
item.Id, offline ? undefined : item.Id,
currentTime, currentTime,
videoRef videoRef
); );
const { showSkipCreditButton, skipCredit } = useCreditSkipper( const { showSkipCreditButton, skipCredit } = useCreditSkipper(
item.Id, offline ? undefined : item.Id,
currentTime, currentTime,
videoRef videoRef
); );

View File

@@ -18,7 +18,6 @@ export const useAdjacentItems = ({ item }: AdjacentEpisodesProps) => {
const parentId = item?.AlbumId || item?.ParentId; const parentId = item?.AlbumId || item?.ParentId;
const indexNumber = item?.IndexNumber; const indexNumber = item?.IndexNumber;
console.log("Getting previous item for " + indexNumber);
if ( if (
!api || !api ||
!parentId || !parentId ||
@@ -26,15 +25,11 @@ export const useAdjacentItems = ({ item }: AdjacentEpisodesProps) => {
indexNumber === null || indexNumber === null ||
indexNumber - 1 < 1 indexNumber - 1 < 1
) { ) {
console.log("No previous item", {
itemIndex: indexNumber,
itemId: item?.Id,
parentId: parentId,
indexNumber: indexNumber,
});
return null; return null;
} }
console.log("Getting previous item for " + indexNumber);
const newIndexNumber = indexNumber - 2; const newIndexNumber = indexNumber - 2;
const res = await getItemsApi(api).getItems({ const res = await getItemsApi(api).getItems({

View File

@@ -110,9 +110,20 @@ class VlcPlayerView: ExpoView {
let media: VLCMedia let media: VLCMedia
if isNetwork { if isNetwork {
print("Loading network file: \(uri)")
media = VLCMedia(url: URL(string: uri)!) media = VLCMedia(url: URL(string: uri)!)
} else { } 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 media.delegate = self
@@ -454,9 +465,6 @@ extension VlcPlayerView: VLCMediaPlayerDelegate {
stateInfo["state"] = "Buffering" stateInfo["state"] = "Buffering"
} }
print("VLC Player State Changed: \(currentState.description)")
print("VLC Player State Changed: \(player.isPlaying)")
// switch currentState { // switch currentState {
// case .opening: // case .opening:
// stateInfo["state"] = "Opening" // stateInfo["state"] = "Opening"
@@ -477,6 +485,8 @@ extension VlcPlayerView: VLCMediaPlayerDelegate {
// stateInfo["state"] = "Unknown" // stateInfo["state"] = "Unknown"
// } // }
print("State changed: \(stateInfo)")
self.lastReportedState = currentState self.lastReportedState = currentState
self.onVideoStateChange?(stateInfo) self.onVideoStateChange?(stateInfo)
} }