forked from Ninjalama/streamyfin_mirror
WIP
This commit is contained in:
@@ -21,8 +21,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { queueAtom } from "@/utils/atoms/queue";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import download from "@/utils/profiles/download";
|
||||
import { getDownloadUrl } from "@/utils/jellyfin/media/getDownloadUrl";
|
||||
import { AudioTrackSelector } from "./AudioTrackSelector";
|
||||
import { type Bitrate, BitrateSelector } from "./BitrateSelector";
|
||||
import { Button } from "./Button";
|
||||
@@ -167,31 +166,29 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
audioIndex: selectedAudioStream,
|
||||
subtitleIndex: selectedSubtitleStream,
|
||||
};
|
||||
const res = await getStreamUrl({
|
||||
|
||||
const url = await getDownloadUrl({
|
||||
api,
|
||||
item,
|
||||
startTimeTicks: 0,
|
||||
userId: user?.Id,
|
||||
audioStreamIndex: audioIndex,
|
||||
maxStreamingBitrate: maxBitrate.value,
|
||||
mediaSourceId: mediaSource?.Id,
|
||||
subtitleStreamIndex: subtitleIndex,
|
||||
deviceProfile: download,
|
||||
download: true,
|
||||
userId: user.Id!,
|
||||
mediaSource: mediaSource!,
|
||||
audioStreamIndex: audioIndex ?? -1,
|
||||
subtitleStreamIndex: subtitleIndex ?? -1,
|
||||
maxBitrate,
|
||||
deviceId: api.deviceInfo.id,
|
||||
});
|
||||
return { res, item };
|
||||
return { url, item, mediaSource };
|
||||
});
|
||||
const downloadDetails = await Promise.all(downloadDetailsPromises);
|
||||
for (const { res, item } of downloadDetails) {
|
||||
if (!res) {
|
||||
for (const { url, item, mediaSource } of downloadDetails) {
|
||||
if (!url) {
|
||||
Alert.alert(
|
||||
t("home.downloads.something_went_wrong"),
|
||||
t("home.downloads.could_not_get_stream_url_from_jellyfin"),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const { mediaSource: source, url } = res;
|
||||
if (!url || !source) {
|
||||
if (!mediaSource) {
|
||||
console.error(`Could not get download URL for ${item.Name}`);
|
||||
toast.error(
|
||||
t("Could not get download URL for {{itemName}}", {
|
||||
@@ -200,7 +197,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
);
|
||||
continue;
|
||||
}
|
||||
await startBackgroundDownload(url, item, source, maxBitrate);
|
||||
await startBackgroundDownload(url, item, mediaSource, maxBitrate);
|
||||
}
|
||||
},
|
||||
[
|
||||
|
||||
@@ -70,13 +70,6 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||
const cancelJobMutation = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
if (!process) throw new Error("No active download");
|
||||
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.id === id) {
|
||||
await task.stop();
|
||||
}
|
||||
}
|
||||
removeProcess(id);
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -575,7 +575,7 @@ export const Controls: FC<Props> = ({
|
||||
className={"flex flex-row w-full pt-2"}
|
||||
>
|
||||
<View className='mr-auto'>
|
||||
{!Platform.isTV && !offline && (
|
||||
{!Platform.isTV && (!offline || !mediaSource?.TranscodingUrl) && (
|
||||
<VideoProvider
|
||||
getAudioTracks={getAudioTracks}
|
||||
getSubtitleTracks={getSubtitleTracks}
|
||||
|
||||
@@ -19,7 +19,7 @@ const DropdownView = () => {
|
||||
];
|
||||
const router = useRouter();
|
||||
|
||||
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition } =
|
||||
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition, offline } =
|
||||
useLocalSearchParams<{
|
||||
itemId: string;
|
||||
audioIndex: string;
|
||||
@@ -27,8 +27,11 @@ const DropdownView = () => {
|
||||
mediaSourceId: string;
|
||||
bitrateValue: string;
|
||||
playbackPosition: string;
|
||||
offline: string;
|
||||
}>();
|
||||
|
||||
const isOffline = offline === "true";
|
||||
|
||||
const changeBitrate = useCallback(
|
||||
(bitrate: string) => {
|
||||
const queryParams = new URLSearchParams({
|
||||
@@ -61,32 +64,34 @@ const DropdownView = () => {
|
||||
collisionPadding={8}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger key='qualitytrigger'>
|
||||
Quality
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
alignOffset={-10}
|
||||
avoidCollisions={true}
|
||||
collisionPadding={0}
|
||||
loop={true}
|
||||
sideOffset={10}
|
||||
>
|
||||
{BITRATES?.map((bitrate, idx: number) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={`quality-item-${idx}`}
|
||||
value={bitrateValue === (bitrate.value?.toString() ?? "")}
|
||||
onValueChange={() =>
|
||||
changeBitrate(bitrate.value?.toString() ?? "")
|
||||
}
|
||||
>
|
||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
||||
{bitrate.key}
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
{!isOffline && (
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger key='qualitytrigger'>
|
||||
Quality
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
alignOffset={-10}
|
||||
avoidCollisions={true}
|
||||
collisionPadding={0}
|
||||
loop={true}
|
||||
sideOffset={10}
|
||||
>
|
||||
{BITRATES?.map((bitrate, idx: number) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={`quality-item-${idx}`}
|
||||
value={bitrateValue === (bitrate.value?.toString() ?? "")}
|
||||
onValueChange={() =>
|
||||
changeBitrate(bitrate.value?.toString() ?? "")
|
||||
}
|
||||
>
|
||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
||||
{bitrate.key}
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
)}
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger key='subtitle-trigger'>
|
||||
Subtitle
|
||||
|
||||
@@ -9,16 +9,15 @@ import * as FileSystem from "expo-file-system";
|
||||
import * as Notifications from "expo-notifications";
|
||||
import { router } from "expo-router";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { throttle } from "lodash";
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AppState, type AppStateStatus } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import useImageStorage from "@/hooks/useImageStorage";
|
||||
@@ -40,7 +39,6 @@ import {
|
||||
} from "./Downloads/types";
|
||||
import { apiAtom } from "./JellyfinProvider";
|
||||
|
||||
// Helper to calculate estimated download size based on bitrate
|
||||
const calculateEstimatedSize = (p: JobStatus): number => {
|
||||
let size = p.mediaSource.Size;
|
||||
const maxBitrate = p.maxBitrate.value;
|
||||
@@ -91,6 +89,14 @@ function useDownloadProvider() {
|
||||
const [settings] = useSettings();
|
||||
const successHapticFeedback = useHaptic("success");
|
||||
|
||||
const removeProcess = useCallback(async (id: string) => {
|
||||
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
||||
const task = tasks?.find((t) => t.id === id);
|
||||
task?.stop();
|
||||
BackGroundDownloader.completeHandler(id);
|
||||
setProcesses((prev) => prev.filter((process) => process.id !== id));
|
||||
}, [setProcesses]);
|
||||
|
||||
/// Cant use the background downloader callback. As its not triggered if size is unknown.
|
||||
const updateProgress = async () => {
|
||||
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
||||
@@ -98,40 +104,42 @@ function useDownloadProvider() {
|
||||
return;
|
||||
}
|
||||
// check if processes are missing
|
||||
const missingProcesses = tasks
|
||||
.filter((t) => t.metadata && !processes.some((p) => p.id === t.id))
|
||||
.map((t) => {
|
||||
return t.metadata as JobStatus;
|
||||
setProcesses((processes) => {
|
||||
const missingProcesses = tasks
|
||||
.filter((t) => t.metadata && !processes.some((p) => p.id === t.id))
|
||||
.map((t) => {
|
||||
return t.metadata as JobStatus;
|
||||
});
|
||||
|
||||
const currentProcesses = [...processes, ...missingProcesses];
|
||||
const updatedProcesses = currentProcesses.map((p) => {
|
||||
// fallback. Doesn't really work for transcodes as they may be a lot smaller.
|
||||
// We make an wild guess by comparing bitrates
|
||||
const task = tasks.find((s) => s.id === p.id);
|
||||
if (task && p.status === "downloading") {
|
||||
const estimatedSize = calculateEstimatedSize(p);
|
||||
let progress = p.progress;
|
||||
if (estimatedSize > 0) {
|
||||
progress = (100 / estimatedSize) * task.bytesDownloaded;
|
||||
}
|
||||
if (progress >= 100) {
|
||||
progress = 99;
|
||||
}
|
||||
const speed = calculateSpeed(p, task.bytesDownloaded);
|
||||
return {
|
||||
...p,
|
||||
progress,
|
||||
speed,
|
||||
bytesDownloaded: task.bytesDownloaded,
|
||||
lastProgressUpdateTime: new Date(),
|
||||
estimatedTotalSizeBytes: estimatedSize,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
const currentProcesses = [...processes, ...missingProcesses];
|
||||
const updatedProcesses = currentProcesses.map((p) => {
|
||||
// fallback. Doesn't really work for transcodes as they may be a lot smaller.
|
||||
// We make an wild guess by comparing bitrates
|
||||
const task = tasks.find((s) => s.id === p.id);
|
||||
if (task && p.status === "downloading") {
|
||||
const estimatedSize = calculateEstimatedSize(p);
|
||||
let progress = p.progress;
|
||||
if (estimatedSize > 0) {
|
||||
progress = (100 / estimatedSize) * task.bytesDownloaded;
|
||||
}
|
||||
if (progress >= 100) {
|
||||
progress = 99;
|
||||
}
|
||||
const speed = calculateSpeed(p, task.bytesDownloaded);
|
||||
return {
|
||||
...p,
|
||||
progress,
|
||||
speed,
|
||||
bytesDownloaded: task.bytesDownloaded,
|
||||
lastProgressUpdateTime: new Date(),
|
||||
estimatedTotalSizeBytes: estimatedSize,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
return updatedProcesses;
|
||||
});
|
||||
|
||||
setProcesses(updatedProcesses);
|
||||
};
|
||||
|
||||
useInterval(updateProgress, 2000);
|
||||
@@ -159,16 +167,21 @@ function useDownloadProvider() {
|
||||
};
|
||||
|
||||
const updateProcess = useCallback(
|
||||
(processId: string, newStatus: Partial<JobStatus>) => {
|
||||
(
|
||||
processId: string,
|
||||
updater:
|
||||
| Partial<JobStatus>
|
||||
| ((current: JobStatus) => Partial<JobStatus>),
|
||||
) => {
|
||||
setProcesses((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === processId
|
||||
? {
|
||||
...p,
|
||||
...newStatus,
|
||||
}
|
||||
: p,
|
||||
),
|
||||
prev.map((p) => {
|
||||
if (p.id !== processId) return p;
|
||||
const newStatus = typeof updater === "function" ? updater(p) : updater;
|
||||
return {
|
||||
...p,
|
||||
...newStatus,
|
||||
};
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setProcesses],
|
||||
@@ -201,10 +214,6 @@ function useDownloadProvider() {
|
||||
networkMode: "always",
|
||||
});
|
||||
|
||||
const removeProcess = useCallback((id: string) => {
|
||||
setProcesses((prev) => prev.filter((process) => process.id !== id));
|
||||
}, [setProcesses]);
|
||||
|
||||
const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`;
|
||||
|
||||
const getDownloadsDatabase = (): DownloadsDatabase => {
|
||||
@@ -314,16 +323,24 @@ function useDownloadProvider() {
|
||||
updateProcess(process.id, {
|
||||
status: "downloading",
|
||||
progress: 0,
|
||||
bytesDownloaded: 0,
|
||||
lastProgressUpdateTime: new Date(),
|
||||
});
|
||||
})
|
||||
.progress((data) => {
|
||||
const percent = (data.bytesDownloaded / data.bytesTotal) * 100;
|
||||
updateProcess(process.id, {
|
||||
speed: undefined,
|
||||
status: "downloading",
|
||||
progress: percent,
|
||||
});
|
||||
})
|
||||
.progress(
|
||||
throttle((data) => {
|
||||
updateProcess(process.id, (currentProcess) => {
|
||||
const percent = (data.bytesDownloaded / data.bytesTotal) * 100;
|
||||
return {
|
||||
speed: calculateSpeed(currentProcess, data.bytesDownloaded),
|
||||
status: "downloading",
|
||||
progress: percent,
|
||||
bytesDownloaded: data.bytesDownloaded,
|
||||
lastProgressUpdateTime: new Date(),
|
||||
};
|
||||
});
|
||||
}, 500),
|
||||
)
|
||||
.done(async () => {
|
||||
const trickPlayData = await downloadTrickplayImages(process.item);
|
||||
const videoFileInfo = await FileSystem.getInfoAsync(videoFilePath);
|
||||
@@ -393,7 +410,6 @@ function useDownloadProvider() {
|
||||
item: process.item.Name,
|
||||
}),
|
||||
);
|
||||
BackGroundDownloader.completeHandler(process.id);
|
||||
removeProcess(process.id);
|
||||
const itemName =
|
||||
process.item.Type === "Episode" &&
|
||||
@@ -410,7 +426,7 @@ function useDownloadProvider() {
|
||||
item: itemName,
|
||||
}),
|
||||
data: {
|
||||
url: `/items/${process.item.Id}`,
|
||||
url: `/(auth)/(tabs)/home/items/page?id=${process.item.Id}?offline=true`,
|
||||
},
|
||||
},
|
||||
trigger: null,
|
||||
|
||||
@@ -91,9 +91,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
const headers = useMemo(() => {
|
||||
if (!deviceId) return {};
|
||||
return {
|
||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||
Platform.OS === "android" ? "Android" : "iOS"
|
||||
}, DeviceId="${deviceId}", Version="0.28.1"`,
|
||||
authorization: `MediaBrowser Client="Streamyfin", Device=${Platform.OS === "android" ? "Android" : "iOS"
|
||||
}, DeviceId="${deviceId}", Version="0.28.1"`,
|
||||
};
|
||||
}, [deviceId]);
|
||||
|
||||
|
||||
61
utils/jellyfin/media/getDownloadUrl.ts
Normal file
61
utils/jellyfin/media/getDownloadUrl.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Api } from "@jellyfin/sdk";
|
||||
import type {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getDownloadStreamUrl, getStreamUrl } from "./getStreamUrl";
|
||||
import { Bitrate } from "@/components/BitrateSelector";
|
||||
import native from "@/utils/profiles/native";
|
||||
|
||||
export const getDownloadUrl = async ({
|
||||
api,
|
||||
item,
|
||||
userId,
|
||||
mediaSource,
|
||||
maxBitrate,
|
||||
audioStreamIndex,
|
||||
subtitleStreamIndex,
|
||||
deviceId,
|
||||
}: {
|
||||
api: Api;
|
||||
item: BaseItemDto;
|
||||
userId: string;
|
||||
mediaSource: MediaSourceInfo;
|
||||
maxBitrate: Bitrate;
|
||||
audioStreamIndex: number;
|
||||
subtitleStreamIndex: number;
|
||||
deviceId: string;
|
||||
}): Promise<string | null> => {
|
||||
|
||||
// Try check if we can play the item directly
|
||||
const directPlayUrl = await getStreamUrl({
|
||||
api,
|
||||
item,
|
||||
userId,
|
||||
startTimeTicks: 0,
|
||||
mediaSourceId: mediaSource.Id,
|
||||
maxStreamingBitrate: maxBitrate.value,
|
||||
audioStreamIndex,
|
||||
subtitleStreamIndex,
|
||||
deviceId,
|
||||
deviceProfile: native,
|
||||
});
|
||||
|
||||
if (maxBitrate.key === "Max" && !directPlayUrl?.mediaSource?.TranscodingUrl) {
|
||||
console.log("Downloading item directly");
|
||||
return `${api.basePath}/Items/${item.Id}/Download?api_key=${api.accessToken}`;
|
||||
}
|
||||
|
||||
const streamUrl = await getDownloadStreamUrl({
|
||||
api,
|
||||
item,
|
||||
userId,
|
||||
mediaSourceId: mediaSource.Id,
|
||||
maxStreamingBitrate: maxBitrate.value,
|
||||
audioStreamIndex,
|
||||
subtitleStreamIndex,
|
||||
deviceId,
|
||||
});
|
||||
|
||||
return streamUrl?.url ?? null;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import generateDeviceProfile from "@/utils/profiles/native";
|
||||
import download from "@/utils/profiles/download";
|
||||
|
||||
export const getStreamUrl = async ({
|
||||
api,
|
||||
@@ -17,7 +18,6 @@ export const getStreamUrl = async ({
|
||||
audioStreamIndex = 0,
|
||||
subtitleStreamIndex = undefined,
|
||||
mediaSourceId,
|
||||
download = false,
|
||||
deviceId,
|
||||
}: {
|
||||
api: Api | null | undefined;
|
||||
@@ -31,7 +31,6 @@ export const getStreamUrl = async ({
|
||||
subtitleStreamIndex?: number;
|
||||
height?: number;
|
||||
mediaSourceId?: string | null;
|
||||
download?: boolean;
|
||||
deviceId?: string | null;
|
||||
}): Promise<{
|
||||
url: string | null;
|
||||
@@ -75,9 +74,6 @@ export const getStreamUrl = async ({
|
||||
let transcodeUrl = mediaSource?.TranscodingUrl;
|
||||
|
||||
if (transcodeUrl) {
|
||||
if (download) {
|
||||
transcodeUrl = transcodeUrl.replace("master.m3u8", "stream");
|
||||
}
|
||||
console.log("Video is being transcoded:", transcodeUrl);
|
||||
return {
|
||||
url: `${api.basePath}${transcodeUrl}`,
|
||||
@@ -86,21 +82,6 @@ export const getStreamUrl = async ({
|
||||
};
|
||||
}
|
||||
|
||||
let downloadParams = {};
|
||||
|
||||
if (download) {
|
||||
// We need to disable static so we can have a remux with subtitle.
|
||||
downloadParams = {
|
||||
subtitleMethod: "Embed",
|
||||
enableSubtitlesInManifest: true,
|
||||
static: "false",
|
||||
allowVideoStreamCopy: true,
|
||||
allowAudioStreamCopy: true,
|
||||
playSessionId: sessionId || "",
|
||||
container: "ts",
|
||||
};
|
||||
}
|
||||
|
||||
const streamParams = new URLSearchParams({
|
||||
static: "true",
|
||||
container: "mp4",
|
||||
@@ -112,7 +93,6 @@ export const getStreamUrl = async ({
|
||||
startTimeTicks: startTimeTicks.toString(),
|
||||
maxStreamingBitrate: maxStreamingBitrate?.toString() || "",
|
||||
userId: userId || "",
|
||||
...downloadParams,
|
||||
});
|
||||
|
||||
const directPlayUrl = `${
|
||||
@@ -127,3 +107,111 @@ export const getStreamUrl = async ({
|
||||
mediaSource,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDownloadStreamUrl = async ({
|
||||
api,
|
||||
item,
|
||||
userId,
|
||||
maxStreamingBitrate,
|
||||
audioStreamIndex = 0,
|
||||
subtitleStreamIndex = undefined,
|
||||
mediaSourceId,
|
||||
deviceId,
|
||||
}: {
|
||||
api: Api | null | undefined;
|
||||
item: BaseItemDto | null | undefined;
|
||||
userId: string | null | undefined;
|
||||
maxStreamingBitrate?: number;
|
||||
audioStreamIndex?: number;
|
||||
subtitleStreamIndex?: number;
|
||||
mediaSourceId?: string | null;
|
||||
deviceId?: string | null;
|
||||
}): Promise<{
|
||||
url: string | null;
|
||||
sessionId: string | null;
|
||||
mediaSource: MediaSourceInfo | undefined;
|
||||
} | null> => {
|
||||
if (!api || !userId || !item?.Id) {
|
||||
console.warn("Missing required parameters for getStreamUrl");
|
||||
return null;
|
||||
}
|
||||
|
||||
let mediaSource: MediaSourceInfo | undefined;
|
||||
let sessionId: string | null | undefined;
|
||||
|
||||
const res = await getMediaInfoApi(api).getPlaybackInfo(
|
||||
{
|
||||
itemId: item.Id!,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
data: {
|
||||
userId,
|
||||
deviceProfile: download,
|
||||
subtitleStreamIndex,
|
||||
startTimeTicks: 0,
|
||||
isPlayback: true,
|
||||
autoOpenLiveStream: true,
|
||||
maxStreamingBitrate,
|
||||
audioStreamIndex,
|
||||
mediaSourceId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error("Error getting playback info:", res.status, res.statusText);
|
||||
}
|
||||
|
||||
sessionId = res.data.PlaySessionId || null;
|
||||
mediaSource = res.data.MediaSources?.[0];
|
||||
let transcodeUrl = mediaSource?.TranscodingUrl;
|
||||
|
||||
if (transcodeUrl) {
|
||||
transcodeUrl = transcodeUrl.replace("master.m3u8", "stream");
|
||||
console.log("Video is being transcoded:", transcodeUrl);
|
||||
return {
|
||||
url: `${api.basePath}${transcodeUrl}`,
|
||||
sessionId,
|
||||
mediaSource,
|
||||
};
|
||||
}
|
||||
|
||||
const downloadParams = {
|
||||
// We need to disable static so we can have a remux with subtitle.
|
||||
subtitleMethod: "Embed",
|
||||
enableSubtitlesInManifest: true,
|
||||
allowVideoStreamCopy: true,
|
||||
allowAudioStreamCopy: true,
|
||||
playSessionId: sessionId || "",
|
||||
};
|
||||
|
||||
const streamParams = new URLSearchParams({
|
||||
static: "false",
|
||||
container: "ts",
|
||||
mediaSourceId: mediaSource?.Id || "",
|
||||
subtitleStreamIndex: subtitleStreamIndex?.toString() || "",
|
||||
audioStreamIndex: audioStreamIndex?.toString() || "",
|
||||
deviceId: deviceId || api.deviceInfo.id,
|
||||
api_key: api.accessToken,
|
||||
startTimeTicks: "0",
|
||||
maxStreamingBitrate: maxStreamingBitrate?.toString() || "",
|
||||
userId: userId || "",
|
||||
});
|
||||
|
||||
Object.entries(downloadParams).forEach(([key, value]) => {
|
||||
streamParams.append(key, value.toString());
|
||||
});
|
||||
|
||||
const directPlayUrl = `${
|
||||
api.basePath
|
||||
}/Videos/${item.Id}/stream?${streamParams.toString()}`;
|
||||
|
||||
console.log("Video is being direct played:", directPlayUrl);
|
||||
|
||||
return {
|
||||
url: directPlayUrl,
|
||||
sessionId: sessionId || null,
|
||||
mediaSource,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user