This commit is contained in:
Fredrik Burmester
2024-09-16 18:18:08 +02:00
parent 595120229f
commit 402bdec5ab
10 changed files with 368 additions and 134 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -4,6 +4,7 @@ import { useNavigationBarVisibility } from "@/hooks/useNavigationBarVisibility";
import { useTrickplay } from "@/hooks/useTrickplay";
import { apiAtom } from "@/providers/JellyfinProvider";
import { usePlayback } from "@/providers/PlaybackProvider";
import { parseM3U8ForSubtitles } from "@/utils/hls/parseM3U8ForSubtitles";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log";
@@ -34,6 +35,16 @@ import Video from "react-native-video";
import { Text } from "./common/Text";
import { itemRouter } from "./common/TouchableItemRouter";
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 = () => {
const {
@@ -45,7 +56,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
setIsPlaying,
isPlaying,
videoRef,
progressTicks,
onProgress,
isBuffering: _isBuffering,
setIsBuffering,
@@ -53,6 +63,8 @@ export const CurrentlyPlayingBar: React.FC = () => {
useNavigationBarVisibility(isPlaying);
const [settings] = useSettings();
const insets = useSafeAreaInsets();
const segments = useSegments();
const router = useRouter();
@@ -69,7 +81,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
const screenHeight = Dimensions.get("window").height;
const screenWidth = Dimensions.get("window").width;
const progress = useSharedValue(progressTicks || 0);
const progress = useSharedValue(0);
const min = useSharedValue(0);
const max = useSharedValue(currentlyPlaying?.item.RunTimeTicks || 0);
const sliding = useRef(false);
@@ -222,6 +234,56 @@ export const CurrentlyPlayingBar: React.FC = () => {
showControls();
}, [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;
return (
@@ -232,68 +294,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
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]}>
<Pressable
onPress={() => {
@@ -340,6 +340,15 @@ export const CurrentlyPlayingBar: React.FC = () => {
subtitleStyle={{
fontSize: 16,
}}
onTextTracks={(e) => {
console.log("onTextTracks ~", e.textTracks);
}}
onTextTrackDataChanged={(e) => {
console.log("onTextTrackDataChanged ~", e);
}}
onVideoTracks={(e) => {
console.log("onVideoTracks ~", e.videoTracks);
}}
source={videoSource}
onPlaybackStateChanged={(e) => {
if (e.isPlaying === true) {
@@ -362,7 +371,10 @@ export const CurrentlyPlayingBar: React.FC = () => {
setIsPlaying(false);
}}
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 />
</View>
}
@@ -371,6 +383,87 @@ export const CurrentlyPlayingBar: React.FC = () => {
</Pressable>
</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
style={[
{
@@ -401,7 +494,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
</Text>
)}
</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">
<TouchableOpacity
disabled={!previousItem}
@@ -562,28 +655,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
</View>
</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 File

@@ -56,7 +56,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
useState<MediaSourceInfo | null>(null);
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
const [selectedSubtitleStream, setSelectedSubtitleStream] =
useState<number>(0);
useState<number>(-1);
const [maxBitrate, setMaxBitrate] = useState<Bitrate>({
key: "Max",
value: undefined,

View File

@@ -2,6 +2,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
DefaultLanguageOption,
DownloadOptions,
ScreenOrientationEnum,
useSettings,
} from "@/utils/atoms/settings";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
@@ -21,6 +22,7 @@ import { Input } from "../common/Input";
import { useState } from "react";
import { Button } from "../Button";
import { MediaToggles } from "./MediaToggles";
import * as ScreenOrientation from "expo-screen-orientation";
interface Props extends ViewProps {}
@@ -56,6 +58,8 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
staleTime: 0,
});
if (!settings) return null;
return (
<View {...props}>
{/* <View>
@@ -88,7 +92,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</Text>
</View>
<Switch
value={settings?.autoRotate}
value={settings.autoRotate}
onValueChange={(value) => updateSettings({ autoRotate: value })}
/>
</View>
@@ -102,7 +106,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</Text>
</View>
<Switch
value={settings?.openFullScreenVideoPlayerByDefault}
value={settings.openFullScreenVideoPlayerByDefault}
onValueChange={(value) =>
updateSettings({ openFullScreenVideoPlayerByDefault: value })
}
@@ -118,7 +122,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</Text>
</View>
<Switch
value={settings?.openInVLC}
value={settings.openInVLC}
onValueChange={(value) => {
updateSettings({ openInVLC: value, forceDirectPlay: value });
}}
@@ -141,13 +145,13 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</TouchableOpacity>
</View>
<Switch
value={settings?.usePopularPlugin}
value={settings.usePopularPlugin}
onValueChange={(value) =>
updateSettings({ usePopularPlugin: value })
}
/>
</View>
{settings?.usePopularPlugin && (
{settings.usePopularPlugin && (
<View className="flex flex-col py-2 bg-neutral-900">
{mediaListCollections?.map((mlc) => (
<View
@@ -158,9 +162,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<Text className="font-semibold">{mlc.Name}</Text>
</View>
<Switch
value={settings?.mediaListCollectionIds?.includes(
mlc.Id!
)}
value={settings.mediaListCollectionIds?.includes(mlc.Id!)}
onValueChange={(value) => {
if (!settings.mediaListCollectionIds) {
updateSettings({
@@ -171,11 +173,11 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
updateSettings({
mediaListCollectionIds:
settings?.mediaListCollectionIds.includes(mlc.Id!)
? settings?.mediaListCollectionIds.filter(
settings.mediaListCollectionIds.includes(mlc.Id!)
? settings.mediaListCollectionIds.filter(
(id) => id !== mlc.Id
)
: [...settings?.mediaListCollectionIds, mlc.Id!],
: [...settings.mediaListCollectionIds, mlc.Id!],
});
}}
/>
@@ -206,7 +208,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</Text>
</View>
<Switch
value={settings?.forceDirectPlay}
value={settings.forceDirectPlay}
onValueChange={(value) =>
updateSettings({ forceDirectPlay: value })
}
@@ -216,7 +218,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<View
className={`
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">
@@ -229,7 +231,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<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>{settings?.deviceProfile}</Text>
<Text>{settings.deviceProfile}</Text>
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content
@@ -272,8 +274,108 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<View className="flex flex-col">
<View
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">
<Text className="font-semibold">Search engine</Text>
@@ -284,7 +386,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<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>{settings?.searchEngine}</Text>
<Text>{settings.searchEngine}</Text>
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content
@@ -318,7 +420,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</DropdownMenu.Content>
</DropdownMenu.Root>
</View>
{settings?.searchEngine === "Marlin" && (
{settings.searchEngine === "Marlin" && (
<View className="flex flex-col bg-neutral-900 px-4 pb-4">
<>
<View className="flex flex-row items-center space-x-2">
@@ -346,7 +448,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</View>
<Text className="text-neutral-500 mt-2">
{settings?.marlinServerUrl}
{settings.marlinServerUrl}
</Text>
</>
</View>

View File

@@ -39,10 +39,6 @@ export const useAdjacentEpisodes = ({
limit: 1,
});
console.log(
"Prev: ",
res.data.Items?.map((i) => i.Name)
);
return res.data.Items?.[0] || null;
},
enabled: currentlyPlaying?.item.Type === "Episode",
@@ -71,10 +67,6 @@ export const useAdjacentEpisodes = ({
limit: 1,
});
console.log(
"Next: ",
res.data.Items?.map((i) => i.Name)
);
return res.data.Items?.[0] || null;
},
enabled: currentlyPlaying?.item.Type === "Episode",

View File

@@ -20,7 +20,6 @@
"@expo/vector-icons": "^14.0.2",
"@gorhom/bottom-sheet": "^4",
"@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-community/netinfo": "11.3.1",
"@react-native-menu/menu": "^1.1.2",

View File

@@ -25,6 +25,10 @@ import { debounce } from "lodash";
import { Alert } from "react-native";
import { OnProgressData, type VideoRef } from "react-native-video";
import { apiAtom, userAtom } from "./JellyfinProvider";
import {
parseM3U8ForSubtitles,
SubtitleTrack,
} from "@/utils/hls/parseM3U8ForSubtitles";
export type CurrentlyPlayingState = {
url: string;
@@ -55,6 +59,7 @@ interface PlaybackContextType {
startDownloadedFilePlayback: (
currentlyPlaying: CurrentlyPlayingState | null
) => void;
subtitles: SubtitleTrack[];
}
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 [volume, _setVolume] = useState<number | null>(null);
const [session, setSession] = useState<PlaybackInfoResponse | null>(null);
const [subtitles, setSubtitles] = useState<SubtitleTrack[]>([]);
const [currentlyPlaying, setCurrentlyPlaying] =
useState<CurrentlyPlayingState | null>(null);
@@ -141,12 +147,6 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
setSession(res.data);
setCurrentlyPlaying(state);
setIsPlaying(true);
if (settings?.openFullScreenVideoPlayerByDefault) {
setTimeout(() => {
presentFullscreenPlayer();
}, 300);
}
} else {
setCurrentlyPlaying(null);
setIsFullscreen(false);
@@ -164,11 +164,6 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
onPress: () => {
setCurrentlyPlaying(state);
setIsPlaying(true);
if (settings?.openFullScreenVideoPlayerByDefault) {
setTimeout(() => {
presentFullscreenPlayer();
}, 300);
}
},
},
{
@@ -375,6 +370,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
presentFullscreenPlayer,
dismissFullscreenPlayer,
startDownloadedFilePlayback,
subtitles,
}}
>
{children}

View File

@@ -1,6 +1,7 @@
import { atom, useAtom } from "jotai";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useEffect } from "react";
import * as ScreenOrientation from "expo-screen-orientation";
export type DownloadQuality = "original" | "high" | "low";
@@ -9,6 +10,22 @@ export type DownloadOption = {
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[] = [
{
label: "Original quality",
@@ -53,6 +70,7 @@ type Settings = {
defaultSubtitleLanguage: DefaultLanguageOption | null;
defaultAudioLanguage: DefaultLanguageOption | null;
showHomeTitles: boolean;
defaultVideoOrientation: ScreenOrientation.OrientationLock;
};
/**
@@ -86,6 +104,7 @@ const loadSettings = async (): Promise<Settings> => {
defaultAudioLanguage: null,
defaultSubtitleLanguage: null,
showHomeTitles: true,
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
};
try {

View 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;
}

View File

@@ -5,6 +5,7 @@ import {
MediaSourceInfo,
PlaybackInfoResponse,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getAuthHeaders } from "../jellyfin";
export const getStreamUrl = async ({
api,
@@ -15,7 +16,7 @@ export const getStreamUrl = async ({
sessionData,
deviceProfile = ios,
audioStreamIndex = 0,
subtitleStreamIndex = 0,
subtitleStreamIndex = undefined,
forceDirectPlay = false,
height,
mediaSourceId,
@@ -39,6 +40,9 @@ export const getStreamUrl = async ({
const itemId = item.Id;
/**
* Build the stream URL for videos
*/
const response = await api.axiosInstance.post(
`${api.basePath}/Items/${itemId}/PlaybackInfo`,
{
@@ -58,9 +62,7 @@ export const getStreamUrl = async ({
EnableMpegtsM2TsMode: false,
},
{
headers: {
Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
},
headers: getAuthHeaders(api),
}
);
@@ -80,10 +82,8 @@ export const getStreamUrl = async ({
if (mediaSource.SupportsDirectPlay || forceDirectPlay === true) {
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}`;
} else if (item.MediaType === "Audio") {
console.log("Using direct stream for audio!");
const searchParams = new URLSearchParams({
UserId: userId,
DeviceId: api.deviceInfo.id,