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 { 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<VideoRef | null>(null);
const videoSource = useVideoSource(playSettings, api, playUrl);
const firstTime = useRef(true);
const [settings] = useSettings();
const videoRef = useRef<VlcPlayerViewRef>(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 (
<View
@@ -107,29 +189,17 @@ export default function page() {
}}
className="absolute z-0 h-full w-full"
>
<Video
<VlcPlayerView
ref={videoRef}
source={videoSource}
source={{
uri: playUrl,
autoplay: true,
isNetwork: false,
}}
style={{ width: "100%", height: "100%" }}
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
onProgress={onProgress}
onError={() => {}}
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);
}}
onVideoProgress={onProgress}
progressUpdateInterval={1000}
onVideoStateChange={onPlaybackStateChanged}
/>
</Pressable>
@@ -146,35 +216,9 @@ export default function page() {
setShowControls={setShowControls}
setIgnoreSafeAreas={setIgnoreSafeAreas}
ignoreSafeAreas={ignoreSafeAreas}
enableTrickplay={false}
offline={true}
/>
</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;
togglePlay: (ticks: number) => void;
setShowControls: (shown: boolean) => void;
offline?: boolean;
}
export const Controls: React.FC<Props> = ({
@@ -64,7 +65,7 @@ export const Controls: React.FC<Props> = ({
setShowControls,
ignoreSafeAreas,
setIgnoreSafeAreas,
enableTrickplay = true,
offline = false,
}) => {
const [settings] = useSettings();
const router = useRouter();
@@ -111,10 +112,12 @@ export const Controls: React.FC<Props> = ({
}
}, [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<Props> = ({
const lastProgressRef = useRef<number>(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
);

View File

@@ -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({

View File

@@ -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)
}