mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
fix: local playback
This commit is contained in:
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user