mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
wip
This commit is contained in:
@@ -4,6 +4,7 @@ import { useNavigationBarVisibility } from "@/hooks/useNavigationBarVisibility";
|
|||||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { usePlayback } from "@/providers/PlaybackProvider";
|
import { usePlayback } from "@/providers/PlaybackProvider";
|
||||||
|
import { parseM3U8ForSubtitles } from "@/utils/hls/parseM3U8ForSubtitles";
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
@@ -34,6 +35,16 @@ import Video from "react-native-video";
|
|||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
import { itemRouter } from "./common/TouchableItemRouter";
|
import { itemRouter } from "./common/TouchableItemRouter";
|
||||||
import { Loader } from "./Loader";
|
import { Loader } from "./Loader";
|
||||||
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
|
async function setOrientation(orientation: ScreenOrientation.OrientationLock) {
|
||||||
|
await ScreenOrientation.lockAsync(orientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetOrientation() {
|
||||||
|
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
export const CurrentlyPlayingBar: React.FC = () => {
|
export const CurrentlyPlayingBar: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -45,7 +56,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
setIsPlaying,
|
setIsPlaying,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
videoRef,
|
videoRef,
|
||||||
progressTicks,
|
|
||||||
onProgress,
|
onProgress,
|
||||||
isBuffering: _isBuffering,
|
isBuffering: _isBuffering,
|
||||||
setIsBuffering,
|
setIsBuffering,
|
||||||
@@ -53,6 +63,8 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
|
|
||||||
useNavigationBarVisibility(isPlaying);
|
useNavigationBarVisibility(isPlaying);
|
||||||
|
|
||||||
|
const [settings] = useSettings();
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -69,7 +81,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
const screenHeight = Dimensions.get("window").height;
|
const screenHeight = Dimensions.get("window").height;
|
||||||
const screenWidth = Dimensions.get("window").width;
|
const screenWidth = Dimensions.get("window").width;
|
||||||
|
|
||||||
const progress = useSharedValue(progressTicks || 0);
|
const progress = useSharedValue(0);
|
||||||
const min = useSharedValue(0);
|
const min = useSharedValue(0);
|
||||||
const max = useSharedValue(currentlyPlaying?.item.RunTimeTicks || 0);
|
const max = useSharedValue(currentlyPlaying?.item.RunTimeTicks || 0);
|
||||||
const sliding = useRef(false);
|
const sliding = useRef(false);
|
||||||
@@ -222,6 +234,56 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
showControls();
|
showControls();
|
||||||
}, [currentlyPlaying]);
|
}, [currentlyPlaying]);
|
||||||
|
|
||||||
|
const { data: subtitleTracks } = useQuery({
|
||||||
|
queryKey: ["subtitleTracks", currentlyPlaying?.url],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!currentlyPlaying?.url) {
|
||||||
|
console.log("No item url");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracks = await parseM3U8ForSubtitles(currentlyPlaying.url);
|
||||||
|
|
||||||
|
console.log("Subtitle tracks", tracks);
|
||||||
|
return tracks;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This should clean up all values if curentlyPlaying sets to null or changes
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentlyPlaying) {
|
||||||
|
// Reset all local state and shared values
|
||||||
|
progress.value = 0;
|
||||||
|
min.value = 0;
|
||||||
|
max.value = 0;
|
||||||
|
cacheProgress.value = 0;
|
||||||
|
localIsBuffering.value = false;
|
||||||
|
sliding.current = false;
|
||||||
|
hideControls();
|
||||||
|
|
||||||
|
resetOrientation();
|
||||||
|
} else {
|
||||||
|
// Initialize or update values based on the new currentlyPlaying item
|
||||||
|
progress.value =
|
||||||
|
currentlyPlaying.item?.UserData?.PlaybackPositionTicks || 0;
|
||||||
|
max.value = currentlyPlaying.item.RunTimeTicks || 0;
|
||||||
|
showControls();
|
||||||
|
setOrientation(
|
||||||
|
settings?.defaultVideoOrientation ||
|
||||||
|
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
// Cancel any subscriptions or timers if you have any
|
||||||
|
// clearTimeout(timerId);
|
||||||
|
// unsubscribe();
|
||||||
|
};
|
||||||
|
}, [currentlyPlaying, settings]);
|
||||||
|
|
||||||
if (!api || !currentlyPlaying) return null;
|
if (!api || !currentlyPlaying) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -232,68 +294,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
position: "absolute",
|
|
||||||
top: insets.top,
|
|
||||||
right: insets.right + 20,
|
|
||||||
height: 70,
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
animatedControlsStyle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View className="flex flex-row items-center h-full">
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
if (!isVisible) return;
|
|
||||||
toggleIgnoreSafeArea();
|
|
||||||
}}
|
|
||||||
className="aspect-square rounded flex flex-col items-center justify-center p-2"
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={ignoreSafeArea ? "contract-outline" : "expand"}
|
|
||||||
size={24}
|
|
||||||
color="white"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
if (!isVisible) return;
|
|
||||||
stopPlayback();
|
|
||||||
}}
|
|
||||||
className="aspect-square rounded flex flex-col items-center justify-center p-2"
|
|
||||||
>
|
|
||||||
<Ionicons name="close" size={24} color="white" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
position: "absolute",
|
|
||||||
bottom: insets.bottom + 8 * 7,
|
|
||||||
right: insets.right + 32,
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
animatedIntroSkipperStyle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View className="flex flex-row items-center h-full">
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
if (!isVisible) return;
|
|
||||||
skipIntro();
|
|
||||||
}}
|
|
||||||
className="flex flex-col items-center justify-center px-2 py-1.5 bg-purple-600 rounded-full"
|
|
||||||
>
|
|
||||||
<Text>Skip intro</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
<Animated.View style={[videoContainerStyle, animatedVideoContainerStyle]}>
|
<Animated.View style={[videoContainerStyle, animatedVideoContainerStyle]}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -340,6 +340,15 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
subtitleStyle={{
|
subtitleStyle={{
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
|
onTextTracks={(e) => {
|
||||||
|
console.log("onTextTracks ~", e.textTracks);
|
||||||
|
}}
|
||||||
|
onTextTrackDataChanged={(e) => {
|
||||||
|
console.log("onTextTrackDataChanged ~", e);
|
||||||
|
}}
|
||||||
|
onVideoTracks={(e) => {
|
||||||
|
console.log("onVideoTracks ~", e.videoTracks);
|
||||||
|
}}
|
||||||
source={videoSource}
|
source={videoSource}
|
||||||
onPlaybackStateChanged={(e) => {
|
onPlaybackStateChanged={(e) => {
|
||||||
if (e.isPlaying === true) {
|
if (e.isPlaying === true) {
|
||||||
@@ -362,7 +371,10 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
}}
|
}}
|
||||||
renderLoader={
|
renderLoader={
|
||||||
<View className="absolute w-screen h-screen flex flex-col items-center justify-center">
|
<View
|
||||||
|
pointerEvents="none"
|
||||||
|
className="absolute w-screen h-screen top-0 left-0 items-center justify-center"
|
||||||
|
>
|
||||||
<Loader />
|
<Loader />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
@@ -371,6 +383,87 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
<Animated.View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
position: "absolute" as const,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: ignoreSafeArea ? 0 : insets.left,
|
||||||
|
right: ignoreSafeArea ? 0 : insets.right,
|
||||||
|
width: ignoreSafeArea
|
||||||
|
? screenWidth
|
||||||
|
: screenWidth - (insets.left + insets.right),
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
animatedLoaderStyle,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Loader />
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: insets.bottom + 8 * 7,
|
||||||
|
right: insets.right + 32,
|
||||||
|
},
|
||||||
|
animatedIntroSkipperStyle,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View className="flex flex-row items-center h-full">
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
if (!isVisible) return;
|
||||||
|
skipIntro();
|
||||||
|
}}
|
||||||
|
className="flex flex-col items-center justify-center px-2 py-1.5 bg-purple-600 rounded-full"
|
||||||
|
>
|
||||||
|
<Text>Skip intro</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
position: "absolute",
|
||||||
|
top: insets.top,
|
||||||
|
right: insets.right + 20,
|
||||||
|
height: 70,
|
||||||
|
},
|
||||||
|
animatedControlsStyle,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View className="flex flex-row items-center h-full">
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
if (!isVisible) return;
|
||||||
|
toggleIgnoreSafeArea();
|
||||||
|
}}
|
||||||
|
className="aspect-square rounded flex flex-col items-center justify-center p-2"
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={ignoreSafeArea ? "contract-outline" : "expand"}
|
||||||
|
size={24}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
if (!isVisible) return;
|
||||||
|
stopPlayback();
|
||||||
|
}}
|
||||||
|
className="aspect-square rounded flex flex-col items-center justify-center p-2"
|
||||||
|
>
|
||||||
|
<Ionicons name="close" size={24} color="white" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
@@ -401,7 +494,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View className="flex flex-row items-center space-x-6 rounded-full py-1.5 pl-4 pr-4 z-10 bg-neutral-800">
|
<View className="flex flex-row items-center space-x-6 rounded-full py-1.5 pl-4 pr-4 bg-neutral-800">
|
||||||
<View className="flex flex-row items-center space-x-2">
|
<View className="flex flex-row items-center space-x-2">
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
disabled={!previousItem}
|
disabled={!previousItem}
|
||||||
@@ -562,28 +655,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
<Animated.View
|
|
||||||
pointerEvents="none"
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
position: "absolute" as const,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
left: ignoreSafeArea ? 0 : insets.left,
|
|
||||||
right: ignoreSafeArea ? 0 : insets.right,
|
|
||||||
width: ignoreSafeArea
|
|
||||||
? screenWidth
|
|
||||||
: screenWidth - (insets.left + insets.right),
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
animatedLoaderStyle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Loader />
|
|
||||||
</Animated.View>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
|||||||
useState<MediaSourceInfo | null>(null);
|
useState<MediaSourceInfo | null>(null);
|
||||||
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
|
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
|
||||||
const [selectedSubtitleStream, setSelectedSubtitleStream] =
|
const [selectedSubtitleStream, setSelectedSubtitleStream] =
|
||||||
useState<number>(0);
|
useState<number>(-1);
|
||||||
const [maxBitrate, setMaxBitrate] = useState<Bitrate>({
|
const [maxBitrate, setMaxBitrate] = useState<Bitrate>({
|
||||||
key: "Max",
|
key: "Max",
|
||||||
value: undefined,
|
value: undefined,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|||||||
import {
|
import {
|
||||||
DefaultLanguageOption,
|
DefaultLanguageOption,
|
||||||
DownloadOptions,
|
DownloadOptions,
|
||||||
|
ScreenOrientationEnum,
|
||||||
useSettings,
|
useSettings,
|
||||||
} from "@/utils/atoms/settings";
|
} from "@/utils/atoms/settings";
|
||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
@@ -21,6 +22,7 @@ import { Input } from "../common/Input";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import { MediaToggles } from "./MediaToggles";
|
import { MediaToggles } from "./MediaToggles";
|
||||||
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -56,6 +58,8 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!settings) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
{/* <View>
|
{/* <View>
|
||||||
@@ -88,7 +92,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings?.autoRotate}
|
value={settings.autoRotate}
|
||||||
onValueChange={(value) => updateSettings({ autoRotate: value })}
|
onValueChange={(value) => updateSettings({ autoRotate: value })}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -102,7 +106,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings?.openFullScreenVideoPlayerByDefault}
|
value={settings.openFullScreenVideoPlayerByDefault}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateSettings({ openFullScreenVideoPlayerByDefault: value })
|
updateSettings({ openFullScreenVideoPlayerByDefault: value })
|
||||||
}
|
}
|
||||||
@@ -118,7 +122,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings?.openInVLC}
|
value={settings.openInVLC}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateSettings({ openInVLC: value, forceDirectPlay: value });
|
updateSettings({ openInVLC: value, forceDirectPlay: value });
|
||||||
}}
|
}}
|
||||||
@@ -141,13 +145,13 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings?.usePopularPlugin}
|
value={settings.usePopularPlugin}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateSettings({ usePopularPlugin: value })
|
updateSettings({ usePopularPlugin: value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{settings?.usePopularPlugin && (
|
{settings.usePopularPlugin && (
|
||||||
<View className="flex flex-col py-2 bg-neutral-900">
|
<View className="flex flex-col py-2 bg-neutral-900">
|
||||||
{mediaListCollections?.map((mlc) => (
|
{mediaListCollections?.map((mlc) => (
|
||||||
<View
|
<View
|
||||||
@@ -158,9 +162,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<Text className="font-semibold">{mlc.Name}</Text>
|
<Text className="font-semibold">{mlc.Name}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings?.mediaListCollectionIds?.includes(
|
value={settings.mediaListCollectionIds?.includes(mlc.Id!)}
|
||||||
mlc.Id!
|
|
||||||
)}
|
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (!settings.mediaListCollectionIds) {
|
if (!settings.mediaListCollectionIds) {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
@@ -171,11 +173,11 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
|
|
||||||
updateSettings({
|
updateSettings({
|
||||||
mediaListCollectionIds:
|
mediaListCollectionIds:
|
||||||
settings?.mediaListCollectionIds.includes(mlc.Id!)
|
settings.mediaListCollectionIds.includes(mlc.Id!)
|
||||||
? settings?.mediaListCollectionIds.filter(
|
? settings.mediaListCollectionIds.filter(
|
||||||
(id) => id !== mlc.Id
|
(id) => id !== mlc.Id
|
||||||
)
|
)
|
||||||
: [...settings?.mediaListCollectionIds, mlc.Id!],
|
: [...settings.mediaListCollectionIds, mlc.Id!],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -206,7 +208,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings?.forceDirectPlay}
|
value={settings.forceDirectPlay}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateSettings({ forceDirectPlay: value })
|
updateSettings({ forceDirectPlay: value })
|
||||||
}
|
}
|
||||||
@@ -216,7 +218,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<View
|
<View
|
||||||
className={`
|
className={`
|
||||||
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
|
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
|
||||||
${settings?.forceDirectPlay ? "opacity-50 select-none" : ""}
|
${settings.forceDirectPlay ? "opacity-50 select-none" : ""}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col shrink">
|
<View className="flex flex-col shrink">
|
||||||
@@ -229,7 +231,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
<Text>{settings?.deviceProfile}</Text>
|
<Text>{settings.deviceProfile}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
@@ -272,8 +274,108 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<View className="flex flex-col">
|
<View className="flex flex-col">
|
||||||
<View
|
<View
|
||||||
className={`
|
className={`
|
||||||
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
|
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
|
||||||
`}
|
`}
|
||||||
|
>
|
||||||
|
<View className="flex flex-col shrink">
|
||||||
|
<Text className="font-semibold">Video orientation</Text>
|
||||||
|
<Text className="text-xs opacity-50">
|
||||||
|
Set the full screen video player orientation.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
|
<Text>
|
||||||
|
{ScreenOrientationEnum[settings.defaultVideoOrientation]}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
loop={true}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
alignOffset={0}
|
||||||
|
avoidCollisions={true}
|
||||||
|
collisionPadding={8}
|
||||||
|
sideOffset={8}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Label>Orientation</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key="1"
|
||||||
|
onSelect={() => {
|
||||||
|
updateSettings({
|
||||||
|
defaultVideoOrientation:
|
||||||
|
ScreenOrientation.OrientationLock.DEFAULT,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>
|
||||||
|
{
|
||||||
|
ScreenOrientationEnum[
|
||||||
|
ScreenOrientation.OrientationLock.DEFAULT
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key="2"
|
||||||
|
onSelect={() => {
|
||||||
|
updateSettings({
|
||||||
|
defaultVideoOrientation:
|
||||||
|
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>
|
||||||
|
{
|
||||||
|
ScreenOrientationEnum[
|
||||||
|
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key="3"
|
||||||
|
onSelect={() => {
|
||||||
|
updateSettings({
|
||||||
|
defaultVideoOrientation:
|
||||||
|
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>
|
||||||
|
{
|
||||||
|
ScreenOrientationEnum[
|
||||||
|
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key="4"
|
||||||
|
onSelect={() => {
|
||||||
|
updateSettings({
|
||||||
|
defaultVideoOrientation:
|
||||||
|
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>
|
||||||
|
{
|
||||||
|
ScreenOrientationEnum[
|
||||||
|
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className={`
|
||||||
|
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col shrink">
|
<View className="flex flex-col shrink">
|
||||||
<Text className="font-semibold">Search engine</Text>
|
<Text className="font-semibold">Search engine</Text>
|
||||||
@@ -284,7 +386,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
<Text>{settings?.searchEngine}</Text>
|
<Text>{settings.searchEngine}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
@@ -318,7 +420,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</View>
|
</View>
|
||||||
{settings?.searchEngine === "Marlin" && (
|
{settings.searchEngine === "Marlin" && (
|
||||||
<View className="flex flex-col bg-neutral-900 px-4 pb-4">
|
<View className="flex flex-col bg-neutral-900 px-4 pb-4">
|
||||||
<>
|
<>
|
||||||
<View className="flex flex-row items-center space-x-2">
|
<View className="flex flex-row items-center space-x-2">
|
||||||
@@ -346,7 +448,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text className="text-neutral-500 mt-2">
|
<Text className="text-neutral-500 mt-2">
|
||||||
{settings?.marlinServerUrl}
|
{settings.marlinServerUrl}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ export const useAdjacentEpisodes = ({
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Prev: ",
|
|
||||||
res.data.Items?.map((i) => i.Name)
|
|
||||||
);
|
|
||||||
return res.data.Items?.[0] || null;
|
return res.data.Items?.[0] || null;
|
||||||
},
|
},
|
||||||
enabled: currentlyPlaying?.item.Type === "Episode",
|
enabled: currentlyPlaying?.item.Type === "Episode",
|
||||||
@@ -71,10 +67,6 @@ export const useAdjacentEpisodes = ({
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Next: ",
|
|
||||||
res.data.Items?.map((i) => i.Name)
|
|
||||||
);
|
|
||||||
return res.data.Items?.[0] || null;
|
return res.data.Items?.[0] || null;
|
||||||
},
|
},
|
||||||
enabled: currentlyPlaying?.item.Type === "Episode",
|
enabled: currentlyPlaying?.item.Type === "Episode",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
"@gorhom/bottom-sheet": "^4",
|
"@gorhom/bottom-sheet": "^4",
|
||||||
"@jellyfin/sdk": "^0.10.0",
|
"@jellyfin/sdk": "^0.10.0",
|
||||||
"@kesha-antonov/react-native-background-downloader": "^3.2.0",
|
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/netinfo": "11.3.1",
|
"@react-native-community/netinfo": "11.3.1",
|
||||||
"@react-native-menu/menu": "^1.1.2",
|
"@react-native-menu/menu": "^1.1.2",
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import { debounce } from "lodash";
|
|||||||
import { Alert } from "react-native";
|
import { Alert } from "react-native";
|
||||||
import { OnProgressData, type VideoRef } from "react-native-video";
|
import { OnProgressData, type VideoRef } from "react-native-video";
|
||||||
import { apiAtom, userAtom } from "./JellyfinProvider";
|
import { apiAtom, userAtom } from "./JellyfinProvider";
|
||||||
|
import {
|
||||||
|
parseM3U8ForSubtitles,
|
||||||
|
SubtitleTrack,
|
||||||
|
} from "@/utils/hls/parseM3U8ForSubtitles";
|
||||||
|
|
||||||
export type CurrentlyPlayingState = {
|
export type CurrentlyPlayingState = {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -55,6 +59,7 @@ interface PlaybackContextType {
|
|||||||
startDownloadedFilePlayback: (
|
startDownloadedFilePlayback: (
|
||||||
currentlyPlaying: CurrentlyPlayingState | null
|
currentlyPlaying: CurrentlyPlayingState | null
|
||||||
) => void;
|
) => void;
|
||||||
|
subtitles: SubtitleTrack[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlaybackContext = createContext<PlaybackContextType | null>(null);
|
const PlaybackContext = createContext<PlaybackContextType | null>(null);
|
||||||
@@ -77,6 +82,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
const [progressTicks, setProgressTicks] = useState<number | null>(0);
|
const [progressTicks, setProgressTicks] = useState<number | null>(0);
|
||||||
const [volume, _setVolume] = useState<number | null>(null);
|
const [volume, _setVolume] = useState<number | null>(null);
|
||||||
const [session, setSession] = useState<PlaybackInfoResponse | null>(null);
|
const [session, setSession] = useState<PlaybackInfoResponse | null>(null);
|
||||||
|
const [subtitles, setSubtitles] = useState<SubtitleTrack[]>([]);
|
||||||
const [currentlyPlaying, setCurrentlyPlaying] =
|
const [currentlyPlaying, setCurrentlyPlaying] =
|
||||||
useState<CurrentlyPlayingState | null>(null);
|
useState<CurrentlyPlayingState | null>(null);
|
||||||
|
|
||||||
@@ -141,12 +147,6 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
setSession(res.data);
|
setSession(res.data);
|
||||||
setCurrentlyPlaying(state);
|
setCurrentlyPlaying(state);
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
|
|
||||||
if (settings?.openFullScreenVideoPlayerByDefault) {
|
|
||||||
setTimeout(() => {
|
|
||||||
presentFullscreenPlayer();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setCurrentlyPlaying(null);
|
setCurrentlyPlaying(null);
|
||||||
setIsFullscreen(false);
|
setIsFullscreen(false);
|
||||||
@@ -164,11 +164,6 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
onPress: () => {
|
onPress: () => {
|
||||||
setCurrentlyPlaying(state);
|
setCurrentlyPlaying(state);
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
if (settings?.openFullScreenVideoPlayerByDefault) {
|
|
||||||
setTimeout(() => {
|
|
||||||
presentFullscreenPlayer();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -375,6 +370,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
presentFullscreenPlayer,
|
presentFullscreenPlayer,
|
||||||
dismissFullscreenPlayer,
|
dismissFullscreenPlayer,
|
||||||
startDownloadedFilePlayback,
|
startDownloadedFilePlayback,
|
||||||
|
subtitles,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
|
|
||||||
export type DownloadQuality = "original" | "high" | "low";
|
export type DownloadQuality = "original" | "high" | "low";
|
||||||
|
|
||||||
@@ -9,6 +10,22 @@ export type DownloadOption = {
|
|||||||
value: DownloadQuality;
|
value: DownloadQuality;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ScreenOrientationEnum: Record<
|
||||||
|
ScreenOrientation.OrientationLock,
|
||||||
|
string
|
||||||
|
> = {
|
||||||
|
[ScreenOrientation.OrientationLock.DEFAULT]: "Default",
|
||||||
|
[ScreenOrientation.OrientationLock.ALL]: "All",
|
||||||
|
[ScreenOrientation.OrientationLock.PORTRAIT]: "Portrait",
|
||||||
|
[ScreenOrientation.OrientationLock.PORTRAIT_UP]: "Portrait Up",
|
||||||
|
[ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: "Portrait Down",
|
||||||
|
[ScreenOrientation.OrientationLock.LANDSCAPE]: "Landscape",
|
||||||
|
[ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: "Landscape Left",
|
||||||
|
[ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: "Landscape Right",
|
||||||
|
[ScreenOrientation.OrientationLock.OTHER]: "Other",
|
||||||
|
[ScreenOrientation.OrientationLock.UNKNOWN]: "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
export const DownloadOptions: DownloadOption[] = [
|
export const DownloadOptions: DownloadOption[] = [
|
||||||
{
|
{
|
||||||
label: "Original quality",
|
label: "Original quality",
|
||||||
@@ -53,6 +70,7 @@ type Settings = {
|
|||||||
defaultSubtitleLanguage: DefaultLanguageOption | null;
|
defaultSubtitleLanguage: DefaultLanguageOption | null;
|
||||||
defaultAudioLanguage: DefaultLanguageOption | null;
|
defaultAudioLanguage: DefaultLanguageOption | null;
|
||||||
showHomeTitles: boolean;
|
showHomeTitles: boolean;
|
||||||
|
defaultVideoOrientation: ScreenOrientation.OrientationLock;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,6 +104,7 @@ const loadSettings = async (): Promise<Settings> => {
|
|||||||
defaultAudioLanguage: null,
|
defaultAudioLanguage: null,
|
||||||
defaultSubtitleLanguage: null,
|
defaultSubtitleLanguage: null,
|
||||||
showHomeTitles: true,
|
showHomeTitles: true,
|
||||||
|
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
55
utils/hls/parseM3U8ForSubtitles.ts
Normal file
55
utils/hls/parseM3U8ForSubtitles.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export interface SubtitleTrack {
|
||||||
|
index: number;
|
||||||
|
name: string;
|
||||||
|
uri: string;
|
||||||
|
language: string;
|
||||||
|
default: boolean;
|
||||||
|
forced: boolean;
|
||||||
|
autoSelect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseM3U8ForSubtitles(
|
||||||
|
url: string
|
||||||
|
): Promise<SubtitleTrack[]> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, { responseType: "text" });
|
||||||
|
const lines = response.data.split(/\r?\n/);
|
||||||
|
const subtitleTracks: SubtitleTrack[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
lines.forEach((line: string) => {
|
||||||
|
if (line.startsWith("#EXT-X-MEDIA:TYPE=SUBTITLES")) {
|
||||||
|
const attributes = parseAttributes(line);
|
||||||
|
const track: SubtitleTrack = {
|
||||||
|
index: index++,
|
||||||
|
name: attributes["NAME"] || "",
|
||||||
|
uri: attributes["URI"] || "",
|
||||||
|
language: attributes["LANGUAGE"] || "",
|
||||||
|
default: attributes["DEFAULT"] === "YES",
|
||||||
|
forced: attributes["FORCED"] === "YES",
|
||||||
|
autoSelect: attributes["AUTOSELECT"] === "YES",
|
||||||
|
};
|
||||||
|
subtitleTracks.push(track);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return subtitleTracks;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch or parse the M3U8 file:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAttributes(line: string): { [key: string]: string } {
|
||||||
|
const attributes: { [key: string]: string } = {};
|
||||||
|
const parts = line.split(",");
|
||||||
|
parts.forEach((part) => {
|
||||||
|
const [key, value] = part.split("=");
|
||||||
|
if (key && value) {
|
||||||
|
attributes[key.trim()] = value.replace(/"/g, "").trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
PlaybackInfoResponse,
|
PlaybackInfoResponse,
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import { getAuthHeaders } from "../jellyfin";
|
||||||
|
|
||||||
export const getStreamUrl = async ({
|
export const getStreamUrl = async ({
|
||||||
api,
|
api,
|
||||||
@@ -15,7 +16,7 @@ export const getStreamUrl = async ({
|
|||||||
sessionData,
|
sessionData,
|
||||||
deviceProfile = ios,
|
deviceProfile = ios,
|
||||||
audioStreamIndex = 0,
|
audioStreamIndex = 0,
|
||||||
subtitleStreamIndex = 0,
|
subtitleStreamIndex = undefined,
|
||||||
forceDirectPlay = false,
|
forceDirectPlay = false,
|
||||||
height,
|
height,
|
||||||
mediaSourceId,
|
mediaSourceId,
|
||||||
@@ -39,6 +40,9 @@ export const getStreamUrl = async ({
|
|||||||
|
|
||||||
const itemId = item.Id;
|
const itemId = item.Id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the stream URL for videos
|
||||||
|
*/
|
||||||
const response = await api.axiosInstance.post(
|
const response = await api.axiosInstance.post(
|
||||||
`${api.basePath}/Items/${itemId}/PlaybackInfo`,
|
`${api.basePath}/Items/${itemId}/PlaybackInfo`,
|
||||||
{
|
{
|
||||||
@@ -58,9 +62,7 @@ export const getStreamUrl = async ({
|
|||||||
EnableMpegtsM2TsMode: false,
|
EnableMpegtsM2TsMode: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: getAuthHeaders(api),
|
||||||
Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -80,10 +82,8 @@ export const getStreamUrl = async ({
|
|||||||
|
|
||||||
if (mediaSource.SupportsDirectPlay || forceDirectPlay === true) {
|
if (mediaSource.SupportsDirectPlay || forceDirectPlay === true) {
|
||||||
if (item.MediaType === "Video") {
|
if (item.MediaType === "Video") {
|
||||||
console.log("Using direct stream for video!");
|
|
||||||
url = `${api.basePath}/Videos/${itemId}/stream.mp4?playSessionId=${sessionData.PlaySessionId}&mediaSourceId=${mediaSource.Id}&static=true&subtitleStreamIndex=${subtitleStreamIndex}&audioStreamIndex=${audioStreamIndex}&deviceId=${api.deviceInfo.id}&api_key=${api.accessToken}`;
|
url = `${api.basePath}/Videos/${itemId}/stream.mp4?playSessionId=${sessionData.PlaySessionId}&mediaSourceId=${mediaSource.Id}&static=true&subtitleStreamIndex=${subtitleStreamIndex}&audioStreamIndex=${audioStreamIndex}&deviceId=${api.deviceInfo.id}&api_key=${api.accessToken}`;
|
||||||
} else if (item.MediaType === "Audio") {
|
} else if (item.MediaType === "Audio") {
|
||||||
console.log("Using direct stream for audio!");
|
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
DeviceId: api.deviceInfo.id,
|
DeviceId: api.deviceInfo.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user