wip: external subs and cleanup

This commit is contained in:
Fredrik Burmester
2024-10-15 07:32:25 +02:00
parent 3807f847fd
commit 13d4117cc1
21 changed files with 75 additions and 152 deletions

View File

@@ -2,6 +2,10 @@ import { Text } from "@/components/common/Text";
import { ItemContent } from "@/components/ItemContent";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import {
getMediaInfoApi,
getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
@@ -22,16 +26,16 @@ const Page: React.FC = () => {
const { data: item, isError } = useQuery({
queryKey: ["item", id],
queryFn: async () => {
const res = await getUserItemData({
api,
userId: user?.Id,
if (!api) return;
const res = await getUserLibraryApi(api).getItem({
itemId: id,
userId: user?.Id,
});
return res;
return res.data;
},
enabled: !!id && !!api,
staleTime: 60 * 1000 * 5, // 5 minutes
staleTime: 0,
});
const opacity = useSharedValue(1);

View File

@@ -52,7 +52,6 @@ export default function page() {
const togglePlay = useCallback(
async (ticks: number) => {
console.log("togglePlay");
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
if (isPlaying) {
videoRef.current?.pause();

View File

@@ -9,7 +9,7 @@ import {
ProgressUpdatePayload,
VlcPlayerViewRef,
} from "@/modules/vlc-player/src/VlcPlayer.types";
import { apiAtom } from "@/providers/JellyfinProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
PlaybackType,
usePlaySettings,
@@ -17,9 +17,11 @@ import {
import { useSettings } from "@/utils/atoms/settings";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import native from "@/utils/profiles/native";
import { ticksToSeconds } from "@/utils/time";
import { Api } from "@jellyfin/sdk";
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
import { getMediaInfoApi, getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import * as Haptics from "expo-haptics";
import { useFocusEffect } from "expo-router";
import { useAtomValue } from "jotai";
@@ -35,13 +37,13 @@ import { useSharedValue } from "react-native-reanimated";
import { SelectedTrackType } from "react-native-video";
export default function page() {
const { playSettings, playUrl, playSessionId } = usePlaySettings();
const { playSettings, playUrl, playSessionId, mediaSource } =
usePlaySettings();
const api = useAtomValue(apiAtom);
const [settings] = useSettings();
const videoRef = useRef<VlcPlayerViewRef>(null);
const poster = usePoster(playSettings, api);
const videoSource = useVideoSource(playSettings, api, poster, playUrl);
const firstTime = useRef(true);
const screenDimensions = Dimensions.get("screen");
@@ -54,12 +56,20 @@ export default function page() {
const progress = useSharedValue(0);
const isSeeking = useSharedValue(false);
const cacheProgress = useSharedValue(0);
const user = useAtomValue(userAtom);
const [playbackState, setPlaybackState] = useState<
PlaybackStatePayload["nativeEvent"] | null
>(null);
if (!playSettings || !playUrl || !api || !videoSource || !playSettings.item)
if (
!playSettings ||
!playUrl ||
!api ||
!videoSource ||
!playSettings.item ||
!mediaSource
)
return null;
const togglePlay = useCallback(
@@ -99,7 +109,7 @@ export default function page() {
});
}
},
[isPlaying, api, playSettings?.item?.Id, videoRef, settings]
[isPlaying, api, playSettings?.item?.Id, videoRef]
);
const play = useCallback(() => {
@@ -151,13 +161,6 @@ export default function page() {
setIsBuffering(isBuffering);
// console.log("onProgress ~", {
// currentTime,
// duration,
// isBuffering,
// isPlaying,
// });
progress.value = currentTime;
// cacheProgress.value = secondsToTicks(data.playableDuration);
@@ -204,46 +207,9 @@ export default function page() {
stopPlayback: stop,
});
const selectedSubtitleTrack = useMemo(() => {
const a = playSettings?.mediaSource?.MediaStreams?.find(
(s) => s.Index === playSettings.subtitleIndex
);
console.log(a);
return a;
}, [playSettings]);
const [hlsSubTracks, setHlsSubTracks] = useState<
{
index: number;
language?: string | undefined;
selected?: boolean | undefined;
title?: string | undefined;
type: any;
}[]
>([]);
const selectedTextTrack = useMemo(() => {
for (let st of hlsSubTracks) {
if (st.title === selectedSubtitleTrack?.DisplayTitle) {
return {
type: SelectedTrackType.TITLE,
value: selectedSubtitleTrack?.DisplayTitle ?? "",
};
}
}
return undefined;
}, [hlsSubTracks]);
const onPlaybackStateChanged = (e: PlaybackStatePayload) => {
const { target, state, isBuffering, isPlaying } = e.nativeEvent;
console.log("onPlaybackStateChanged", {
target,
state,
isBuffering,
isPlaying,
});
if (state === "Playing") {
setIsPlaying(true);
return;
@@ -299,12 +265,9 @@ export default function page() {
onVideoProgress={onProgress}
progressUpdateInterval={1000}
onVideoStateChange={onPlaybackStateChanged}
onVideoLoadStart={() => {
console.log("onVideoLoadStart");
}}
onVideoLoadStart={() => {}}
onVideoLoadEnd={() => {
setIsVideoLoaded(true);
console.log("onVideoLoadEnd");
}}
/>
</Pressable>
@@ -325,6 +288,7 @@ export default function page() {
/> */}
<Controls
mediaSource={mediaSource}
item={playSettings.item}
videoRef={videoRef}
togglePlay={togglePlay}

View File

@@ -103,12 +103,6 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
if (!token || !deviceId || !baseDirectory)
return BackgroundFetch.BackgroundFetchResult.NoData;
console.log({
token,
url,
deviceId,
});
const jobs = await getAllJobsByDeviceId({
deviceId,
authHeader: token,
@@ -120,14 +114,6 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
for (let job of jobs) {
if (job.status === "completed") {
const downloadUrl = url + "download/" + job.id;
console.log({
token,
deviceId,
baseDirectory,
url,
downloadUrl,
});
const tasks = await checkForExistingDownloads();
if (tasks.find((task) => task.id === job.id)) {
@@ -137,7 +123,7 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
download({
id: job.id,
url: url + "download/" + job.id,
url: downloadUrl,
destination: `${baseDirectory}${job.item.Id}.mp4`,
headers: {
Authorization: token,

View File

@@ -128,9 +128,9 @@ const Login: React.FC = () => {
} catch (e) {
const error = e as Error;
if (error.name === "AbortError") {
console.log(`Request to ${protocol}${url} timed out`);
console.error(`Request to ${protocol}${url} timed out`);
} else {
console.log(`Error checking ${protocol}${url}:`, error);
console.error(`Error checking ${protocol}${url}:`, error);
}
}
}

View File

@@ -3,7 +3,8 @@ import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueActions, queueAtom } from "@/utils/atoms/queue";
import { useSettings } from "@/utils/atoms/settings";
import ios from "@/utils/profiles/ios";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import iosFmp4 from "@/utils/profiles/iosFmp4";
import native from "@/utils/profiles/native";
import old from "@/utils/profiles/old";
import Ionicons from "@expo/vector-icons/Ionicons";
@@ -20,18 +21,16 @@ import {
import { router, useFocusEffect } from "expo-router";
import { useAtom } from "jotai";
import { useCallback, useMemo, useRef, useState } from "react";
import { Alert, TouchableOpacity, View, ViewProps } from "react-native";
import { TouchableOpacity, View, ViewProps } from "react-native";
import { toast } from "sonner-native";
import { AudioTrackSelector } from "./AudioTrackSelector";
import { Bitrate, BITRATES, BitrateSelector } from "./BitrateSelector";
import { Bitrate, BitrateSelector } from "./BitrateSelector";
import { Button } from "./Button";
import { Text } from "./common/Text";
import { Loader } from "./Loader";
import { MediaSourceSelector } from "./MediaSourceSelector";
import ProgressCircle from "./ProgressCircle";
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
import { toast } from "sonner-native";
import iosFmp4 from "@/utils/profiles/iosFmp4";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
interface DownloadProps extends ViewProps {
item: BaseItemDto;
@@ -46,7 +45,7 @@ export const DownloadItem: React.FC<DownloadProps> = ({ item, ...props }) => {
const { startRemuxing } = useRemuxHlsToMp4(item);
const [selectedMediaSource, setSelectedMediaSource] = useState<
MediaSourceInfo | undefined
MediaSourceInfo | undefined | null
>(undefined);
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
const [selectedSubtitleStream, setSelectedSubtitleStream] =

View File

@@ -105,7 +105,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
}, [playSettings?.bitrate]);
const setMaxBitrate = (bitrate: Bitrate | undefined) => {
console.log("setMaxBitrate", bitrate);
setPlaySettings((prev) => ({
...prev,
bitrate,

View File

@@ -205,7 +205,7 @@ export const PlayButton: React.FC<Props> = ({ ...props }) => {
});
break;
case 1:
router.push("/play-video");
router.push("/vlc-player");
break;
case cancelButtonIndex:
break;

View File

@@ -85,7 +85,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
toast.success("Download canceled");
},
onError: (e) => {
console.log(e);
console.error(e);
toast.error("Could not cancel download");
},
});

View File

@@ -14,6 +14,7 @@ import { formatTimeString, secondsToMs, ticksToMs } from "@/utils/time";
import { Ionicons } from "@expo/vector-icons";
import {
BaseItemDto,
MediaSourceInfo,
type MediaStream,
} from "@jellyfin/sdk/lib/generated-client";
import { Image } from "expo-image";
@@ -26,6 +27,7 @@ import React, {
useState,
} from "react";
import {
Alert,
Dimensions,
Platform,
Pressable,
@@ -43,6 +45,8 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "../common/Text";
import { Loader } from "../Loader";
import { useAtomValue } from "jotai";
import { apiAtom } from "@/providers/JellyfinProvider";
interface Props {
item: BaseItemDto;
@@ -60,6 +64,7 @@ interface Props {
setShowControls: (shown: boolean) => void;
offline?: boolean;
isVideoLoaded?: boolean;
mediaSource: MediaSourceInfo;
}
export const Controls: React.FC<Props> = ({
@@ -75,14 +80,15 @@ export const Controls: React.FC<Props> = ({
setShowControls,
ignoreSafeAreas,
setIgnoreSafeAreas,
mediaSource,
isVideoLoaded,
offline = false,
}) => {
const [settings] = useSettings();
const router = useRouter();
const insets = useSafeAreaInsets();
const { setPlaySettings } = usePlaySettings();
const { setPlaySettings, playSettings } = usePlaySettings();
const api = useAtomValue(apiAtom);
const windowDimensions = Dimensions.get("window");
const { previousItem, nextItem } = useAdjacentItems({ item });
@@ -209,7 +215,6 @@ export const Controls: React.FC<Props> = ({
}, [showControls, isPlaying]);
const handleSkipBackward = useCallback(async () => {
console.log("handleSkipBackward");
if (!settings?.rewindSkipTime) return;
wasPlayingRef.current = isPlaying;
try {
@@ -232,7 +237,6 @@ export const Controls: React.FC<Props> = ({
const curr = progress.value;
if (curr !== undefined) {
const newTime = curr + secondsToMs(settings.forwardSkipTime);
console.log("handleSkipForward", newTime);
await videoRef.current?.seekTo(Math.max(0, newTime));
if (wasPlayingRef.current === true) videoRef.current?.play();
}
@@ -261,8 +265,6 @@ export const Controls: React.FC<Props> = ({
const subtitles = await videoRef.current.getSubtitleTracks();
setAudioTracks(audio);
setSubtitleTracks(subtitles);
console.log("embedded audio", audio);
console.log("embedded sutitles", subtitles);
}
};
@@ -292,12 +294,12 @@ export const Controls: React.FC<Props> = ({
})) || [];
const externalSubs =
item.MediaStreams?.filter(
mediaSource?.MediaStreams?.filter(
(stream) => stream.Type === "Subtitle" && stream.IsExternal
).map((s) => ({
name: s.DisplayTitle!,
index: s.Index!,
isExternal: s.DeliveryMethod === "External",
isExternal: true,
deliveryUrl: s.DeliveryUrl,
})) || [];
@@ -309,13 +311,12 @@ export const Controls: React.FC<Props> = ({
(sub) => !embeddedSubNames.has(sub.name)
);
console.log([...embeddedSubs, ...uniqueExternalSubs]);
// Combine embedded and unique external subs
return [...embeddedSubs, ...uniqueExternalSubs] as (
| EmbeddedSubtitle
| ExternalSubtitle
)[];
}, [item, isVideoLoaded, subtitleTracks]);
}, [item, isVideoLoaded, subtitleTracks, mediaSource]);
return (
<View
@@ -386,17 +387,13 @@ export const Controls: React.FC<Props> = ({
value="off"
onValueChange={() => {
if (sub.isExternal) {
videoRef.current?.setSubtitleURL(sub.deliveryUrl);
console.log(
"Setting external subtitle:",
sub.deliveryUrl
videoRef.current?.setSubtitleURL(
api?.basePath + sub.deliveryUrl
);
return;
}
console.log("Settings embedded subtitle", sub.name);
videoRef.current?.setSubtitleTrack(sub.index);
console.log(sub);
}}
>
<DropdownMenu.ItemIndicator />

View File

@@ -28,8 +28,6 @@ export const useAdjacentItems = ({ item }: AdjacentEpisodesProps) => {
return null;
}
console.log("Getting previous item for " + indexNumber);
const newIndexNumber = indexNumber - 2;
const res = await getItemsApi(api).getItems({

View File

@@ -30,7 +30,6 @@ export const useCreditSkipper = (
queryKey: ["creditTimestamps", itemId],
queryFn: async () => {
if (!itemId) {
console.log("No item id");
return null;
}
@@ -61,7 +60,6 @@ export const useCreditSkipper = (
}, [creditTimestamps, currentTime]);
const skipCredit = useCallback(() => {
console.log("skipCredits");
if (!creditTimestamps || !videoRef.current) return;
try {
videoRef.current.seek(creditTimestamps.Credits.End);

View File

@@ -24,9 +24,6 @@ export const useFileOpener = () => {
try {
const files = await FileSystem.readDirectoryAsync(directory);
for (let f of files) {
console.log(f);
}
const path = item.Id!;
const matchingFile = files.find((file) => file.startsWith(path));

View File

@@ -8,7 +8,6 @@ const useImageStorage = () => {
try {
// Save the base64 string to AsyncStorage
storage.set(key, base64);
console.log("Image saved successfully");
} catch (error) {
console.error("Error saving image:", error);
throw error;

View File

@@ -26,7 +26,6 @@ export const useIntroSkipper = (
queryKey: ["introTimestamps", itemId],
queryFn: async () => {
if (!itemId) {
console.log("No item id");
return null;
}

View File

@@ -26,8 +26,7 @@ export const useRemuxHlsToMp4 = (item: BaseItemDto) => {
const queryClient = useQueryClient();
const { saveDownloadedItemInfo, setProcesses } = useDownload();
const router = useRouter();
const { loadImage, saveImage, image2Base64, saveBase64Image } =
useImageStorage();
const { saveImage } = useImageStorage();
if (!item.Id || !item.Name) {
writeToLog("ERROR", "useRemuxHlsToMp4 ~ missing arguments");

View File

@@ -174,7 +174,7 @@ function useDownloadProvider() {
url: settings?.optimizedVersionsServerUrl,
});
} catch (error) {
console.log(error);
console.error(error);
}
},
[settings?.optimizedVersionsServerUrl, authHeader]
@@ -184,7 +184,6 @@ function useDownloadProvider() {
async (process: JobStatus) => {
if (!process?.item.Id || !authHeader) throw new Error("No item id");
console.log("[0] Setting process to downloading");
setProcesses((prev) =>
prev.map((p) =>
p.id === process.id
@@ -239,7 +238,6 @@ function useDownloadProvider() {
})
.progress((data) => {
const percent = (data.bytesDownloaded / data.bytesTotal) * 100;
console.log("Download progress:", percent);
setProcesses((prev) =>
prev.map((p) =>
p.id === process.id
@@ -467,7 +465,6 @@ function useDownloadProvider() {
if (itemNameWithoutExtension === id) {
const filePath = `${directory}${item}`;
await FileSystem.deleteAsync(filePath, { idempotent: true });
console.log(`Successfully deleted file: ${item}`);
break;
}
}
@@ -480,10 +477,6 @@ function useDownloadProvider() {
}
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
console.log(
`Successfully deleted file and AsyncStorage entry for ID ${id}`
);
} catch (error) {
console.error(
`Failed to delete file and AsyncStorage entry for ID ${id}:`,

View File

@@ -7,6 +7,7 @@ import old from "@/utils/profiles/old";
import {
BaseItemDto,
MediaSourceInfo,
PlaybackInfoResponse,
} from "@jellyfin/sdk/lib/generated-client";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api";
import { useAtomValue } from "jotai";
@@ -30,6 +31,7 @@ export type PlaybackType = {
type PlaySettingsContextType = {
playSettings: PlaybackType | null;
mediaSource: MediaSourceInfo | null;
setPlaySettings: (
dataOrUpdater:
| PlaybackType
@@ -51,6 +53,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [playSettings, _setPlaySettings] = useState<PlaybackType | null>(null);
const [mediaSource, setMediaSource] = useState<MediaSourceInfo | null>(null);
const [playUrl, setPlayUrl] = useState<string | null>(null);
const [playSessionId, setPlaySessionId] = useState<string | null>(null);
@@ -109,11 +112,10 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
forceDirectPlay: settings.forceDirectPlay,
});
console.log("getStreamUrl ~ ", data?.url);
_setPlaySettings(newSettings);
setPlayUrl(data?.url!);
setPlaySessionId(data?.sessionId!);
setMediaSource(data?.mediaSource!);
return data;
} catch (error) {
@@ -158,6 +160,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
setMusicPlaySettings,
setOfflineSettings,
playSessionId,
mediaSource,
}}
>
{children}

View File

@@ -1,19 +0,0 @@
import { Api } from "@jellyfin/sdk";
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
export const getPlaybackInfo = async (
api?: Api | null | undefined,
itemId?: string | null | undefined,
userId?: string | null | undefined,
) => {
if (!api || !itemId || !userId) {
return null;
}
const a = await getMediaInfoApi(api).getPlaybackInfo({
itemId,
userId,
});
return a.data;
};

View File

@@ -37,6 +37,7 @@ export const getStreamUrl = async ({
}): Promise<{
url: string | null;
sessionId: string | null;
mediaSource: MediaSourceInfo | undefined;
} | null> => {
if (!api || !userId || !item?.Id) {
return null;
@@ -70,7 +71,11 @@ export const getStreamUrl = async ({
sessionId = res0.data.PlaySessionId || null;
if (transcodeUrl) {
return { url: `${api.basePath}${transcodeUrl}`, sessionId };
return {
url: `${api.basePath}${transcodeUrl}`,
sessionId,
mediaSource: res0.data.MediaSources?.[0],
};
}
}
@@ -108,13 +113,12 @@ export const getStreamUrl = async ({
(source: MediaSourceInfo) => source.Id === mediaSourceId
);
console.log("getStreamUrl ~ ", item.MediaType);
if (item.MediaType === "Video") {
if (mediaSource?.SupportsDirectPlay || forceDirectPlay === true) {
return {
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}`,
sessionId: sessionId,
mediaSource,
};
}
@@ -122,15 +126,18 @@ export const getStreamUrl = async ({
return {
url: `${api.basePath}${mediaSource.TranscodingUrl}`,
sessionId: sessionId,
mediaSource,
};
}
}
if (item.MediaType === "Audio") {
console.log("getStreamUrl ~ Audio");
if (mediaSource?.TranscodingUrl) {
return { url: `${api.basePath}${mediaSource.TranscodingUrl}`, sessionId };
return {
url: `${api.basePath}${mediaSource.TranscodingUrl}`,
sessionId,
mediaSource,
};
}
const searchParams = new URLSearchParams({
@@ -153,6 +160,7 @@ export const getStreamUrl = async ({
api.basePath
}/Audio/${itemId}/universal?${searchParams.toString()}`,
sessionId,
mediaSource,
};
}

View File

@@ -259,11 +259,11 @@ export default {
],
SubtitleProfiles: [
{
Format: "pgssub",
Method: "embed",
Format: "srt",
Method: "external",
},
{
Format: "subrip",
Format: "pgssub",
Method: "embed",
},
{