mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
Compare commits
5 Commits
renovate/r
...
fix/remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6494049b66 | ||
|
|
adb20f4b10 | ||
|
|
7dc8132e7a | ||
|
|
9fb04518b0 | ||
|
|
46be3c9465 |
@@ -36,15 +36,6 @@ export default function Layout() {
|
|||||||
animation: "fade",
|
animation: "fade",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
|
||||||
name="transcoding-player"
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
autoHideHomeIndicator: true,
|
|
||||||
title: "",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,17 +36,12 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import { Alert, View, AppState, AppStateStatus, Platform } from "react-native";
|
||||||
Alert,
|
|
||||||
View,
|
|
||||||
AppState,
|
|
||||||
AppStateStatus,
|
|
||||||
Platform,
|
|
||||||
} from "react-native";
|
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
console.log("Direct Player");
|
console.log("Direct Player");
|
||||||
@@ -128,57 +123,80 @@ export default function page() {
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const [stream, setStream] = useState<{
|
||||||
data: stream,
|
mediaSource: MediaSourceInfo;
|
||||||
isLoading: isLoadingStreamUrl,
|
url: string;
|
||||||
isError: isErrorStreamUrl,
|
sessionId: string | undefined;
|
||||||
} = useQuery({
|
} | null>(null);
|
||||||
queryKey: ["stream-url", itemId, mediaSourceId, bitrateValue],
|
const [isLoadingStream, setIsLoadingStream] = useState(true);
|
||||||
queryFn: async () => {
|
const [isErrorStream, setIsErrorStream] = useState(false);
|
||||||
if (offline && !Platform.isTV) {
|
|
||||||
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
|
||||||
if (!data?.mediaSource) return null;
|
|
||||||
|
|
||||||
const url = await getDownloadedFileUrl(data.item.Id!);
|
useEffect(() => {
|
||||||
|
const fetchStream = async () => {
|
||||||
|
setIsLoadingStream(true);
|
||||||
|
setIsErrorStream(false);
|
||||||
|
|
||||||
if (item)
|
try {
|
||||||
return {
|
if (offline && !Platform.isTV) {
|
||||||
mediaSource: data.mediaSource,
|
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
||||||
url,
|
if (!data?.mediaSource) {
|
||||||
sessionId: undefined,
|
setStream(null);
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await getDownloadedFileUrl(data.item.Id!);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
setStream({
|
||||||
|
mediaSource: data.mediaSource as MediaSourceInfo,
|
||||||
|
url,
|
||||||
|
sessionId: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await getStreamUrl({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
||||||
|
userId: user?.Id,
|
||||||
|
audioStreamIndex: audioIndex,
|
||||||
|
maxStreamingBitrate: bitrateValue,
|
||||||
|
mediaSourceId: mediaSourceId,
|
||||||
|
subtitleStreamIndex: subtitleIndex,
|
||||||
|
deviceProfile: native,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
setStream(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mediaSource, sessionId, url } = res;
|
||||||
|
|
||||||
|
if (!sessionId || !mediaSource || !url) {
|
||||||
|
Alert.alert(t("player.error"), t("player.failed_to_get_stream_url"));
|
||||||
|
setStream(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStream({
|
||||||
|
mediaSource,
|
||||||
|
sessionId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching stream:", error);
|
||||||
|
setIsErrorStream(true);
|
||||||
|
setStream(null);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingStream(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const res = await getStreamUrl({
|
fetchStream();
|
||||||
api,
|
}, [itemId, mediaSourceId]);
|
||||||
item,
|
|
||||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
|
||||||
userId: user?.Id,
|
|
||||||
audioStreamIndex: audioIndex,
|
|
||||||
maxStreamingBitrate: bitrateValue,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
subtitleStreamIndex: subtitleIndex,
|
|
||||||
deviceProfile: native,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res) return null;
|
|
||||||
|
|
||||||
const { mediaSource, sessionId, url } = res;
|
|
||||||
|
|
||||||
if (!sessionId || !mediaSource || !url) {
|
|
||||||
Alert.alert(t("player.error"), t("player.failed_to_get_stream_url"));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mediaSource,
|
|
||||||
sessionId,
|
|
||||||
url,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
enabled: !!itemId && !!item,
|
|
||||||
staleTime: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const togglePlay = useCallback(async () => {
|
const togglePlay = useCallback(async () => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
@@ -198,9 +216,7 @@ export default function page() {
|
|||||||
mediaSourceId: mediaSourceId,
|
mediaSourceId: mediaSourceId,
|
||||||
positionTicks: msToTicks(progress.get()),
|
positionTicks: msToTicks(progress.get()),
|
||||||
isPaused: !isPlaying,
|
isPaused: !isPlaying,
|
||||||
playMethod: stream?.url.includes("m3u8")
|
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||||
? "Transcode"
|
|
||||||
: "DirectStream",
|
|
||||||
playSessionId: stream.sessionId,
|
playSessionId: stream.sessionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -238,21 +254,6 @@ export default function page() {
|
|||||||
videoRef.current?.stop();
|
videoRef.current?.stop();
|
||||||
}, [videoRef, reportPlaybackStopped]);
|
}, [videoRef, reportPlaybackStopped]);
|
||||||
|
|
||||||
// TODO: unused should remove.
|
|
||||||
const reportPlaybackStart = useCallback(async () => {
|
|
||||||
if (offline) return;
|
|
||||||
|
|
||||||
if (!stream) return;
|
|
||||||
await getPlaystateApi(api!).onPlaybackStart({
|
|
||||||
itemId: item?.Id!,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
playMethod: stream.url?.includes("m3u8") ? "Transcode" : "DirectStream",
|
|
||||||
playSessionId: stream?.sessionId ? stream?.sessionId : undefined,
|
|
||||||
});
|
|
||||||
}, [api, item, mediaSourceId, stream]);
|
|
||||||
|
|
||||||
const onProgress = useCallback(
|
const onProgress = useCallback(
|
||||||
async (data: ProgressUpdatePayload) => {
|
async (data: ProgressUpdatePayload) => {
|
||||||
if (isSeeking.get() || isPlaybackStopped) return;
|
if (isSeeking.get() || isPlaybackStopped) return;
|
||||||
@@ -294,8 +295,8 @@ export default function page() {
|
|||||||
|
|
||||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
||||||
const { pipStarted } = e.nativeEvent;
|
const { pipStarted } = e.nativeEvent;
|
||||||
setIsPipStarted(pipStarted)
|
setIsPipStarted(pipStarted);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const onPlaybackStateChanged = useCallback((e: PlaybackStatePayload) => {
|
const onPlaybackStateChanged = useCallback((e: PlaybackStatePayload) => {
|
||||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||||
@@ -332,7 +333,7 @@ export default function page() {
|
|||||||
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
||||||
// Handle app going to the background
|
// Handle app going to the background
|
||||||
if (nextAppState.match(/inactive|background/)) {
|
if (nextAppState.match(/inactive|background/)) {
|
||||||
_setShowControls(false)
|
_setShowControls(false);
|
||||||
}
|
}
|
||||||
setAppState(nextAppState);
|
setAppState(nextAppState);
|
||||||
};
|
};
|
||||||
@@ -351,73 +352,67 @@ export default function page() {
|
|||||||
|
|
||||||
// Preselection of audio and subtitle tracks.
|
// Preselection of audio and subtitle tracks.
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|
||||||
let initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
|
let initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
|
||||||
let externalTrack = { name: "", DeliveryUrl: "" };
|
|
||||||
|
|
||||||
const allSubs =
|
|
||||||
stream?.mediaSource.MediaStreams?.filter(
|
|
||||||
(sub: { Type: string }) => sub.Type === "Subtitle"
|
|
||||||
) || [];
|
|
||||||
const chosenSubtitleTrack = allSubs.find(
|
|
||||||
(sub: { Index: number }) => sub.Index === subtitleIndex
|
|
||||||
);
|
|
||||||
const allAudio =
|
const allAudio =
|
||||||
stream?.mediaSource.MediaStreams?.filter(
|
stream?.mediaSource.MediaStreams?.filter(
|
||||||
(audio: { Type: string }) => audio.Type === "Audio"
|
(audio) => audio.Type === "Audio"
|
||||||
) || [];
|
) || [];
|
||||||
const chosenAudioTrack = allAudio.find(
|
const allSubs =
|
||||||
(audio: { Index: number | undefined }) => audio.Index === audioIndex
|
stream?.mediaSource.MediaStreams?.filter(
|
||||||
|
(sub) => sub.Type === "Subtitle"
|
||||||
|
) || [];
|
||||||
|
const textSubs = allSubs.filter((sub) => sub.IsTextSubtitleStream);
|
||||||
|
|
||||||
|
const chosenSubtitleTrack = allSubs.find(
|
||||||
|
(sub) => sub.Index === subtitleIndex
|
||||||
);
|
);
|
||||||
|
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
|
||||||
|
|
||||||
// Direct playback CASE
|
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
|
||||||
if (!bitrateValue) {
|
if (
|
||||||
// If Subtitle is embedded we can use the position to select it straight away.
|
chosenSubtitleTrack &&
|
||||||
if (chosenSubtitleTrack && !chosenSubtitleTrack.DeliveryUrl) {
|
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
|
||||||
initOptions.push(`--sub-track=${allSubs.indexOf(chosenSubtitleTrack)}`);
|
) {
|
||||||
} else if (chosenSubtitleTrack && chosenSubtitleTrack.DeliveryUrl) {
|
const finalIndex = notTranscoding
|
||||||
// If Subtitle is external we need to pass the URL to the player.
|
? allSubs.indexOf(chosenSubtitleTrack)
|
||||||
externalTrack = {
|
: textSubs.indexOf(chosenSubtitleTrack);
|
||||||
name: chosenSubtitleTrack.DisplayTitle || "",
|
initOptions.push(`--sub-track=${finalIndex}`);
|
||||||
DeliveryUrl: `${api?.basePath || ""}${chosenSubtitleTrack.DeliveryUrl}`,
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chosenAudioTrack)
|
if (notTranscoding && chosenAudioTrack) {
|
||||||
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
||||||
} else {
|
|
||||||
// Transcoded playback CASE
|
|
||||||
if (chosenSubtitleTrack?.DeliveryMethod === "Hls") {
|
|
||||||
externalTrack = {
|
|
||||||
name: `subs ${chosenSubtitleTrack.DisplayTitle}`,
|
|
||||||
DeliveryUrl: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const beforeRemoveListener = navigation.addListener('beforeRemove', stop);
|
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
||||||
return () => {
|
return () => {
|
||||||
beforeRemoveListener();
|
beforeRemoveListener();
|
||||||
};
|
};
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
if (!item || isLoadingItem || isLoadingStreamUrl || !stream)
|
if (!item || isLoadingItem || !stream)
|
||||||
return (
|
return (
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||||
<Loader />
|
<Loader />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isErrorItem || isErrorStreamUrl)
|
if (isErrorItem || isErrorStream)
|
||||||
return (
|
return (
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||||
<Text className="text-white">{t("player.error")}</Text>
|
<Text className="text-white">{t("player.error")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const externalSubtitles = allSubs
|
||||||
|
.filter((sub: any) => sub.DeliveryMethod === "External")
|
||||||
|
.map((sub: any) => ({
|
||||||
|
name: sub.DisplayTitle,
|
||||||
|
DeliveryUrl: api?.basePath + sub.DeliveryUrl,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: "black" }}>
|
<View style={{ flex: 1, backgroundColor: "black" }}>
|
||||||
<View
|
<View
|
||||||
@@ -435,11 +430,11 @@ export default function page() {
|
|||||||
<VlcPlayerView
|
<VlcPlayerView
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
source={{
|
source={{
|
||||||
uri: stream.url,
|
uri: stream?.url || "",
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
isNetwork: true,
|
isNetwork: true,
|
||||||
startPosition,
|
startPosition,
|
||||||
externalTrack,
|
externalSubtitles,
|
||||||
initOptions,
|
initOptions,
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%", height: "100%" }}
|
style={{ width: "100%", height: "100%" }}
|
||||||
@@ -494,4 +489,4 @@ export default function page() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,546 +0,0 @@
|
|||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { Loader } from "@/components/Loader";
|
|
||||||
import { Controls } from "@/components/video-player/controls/Controls";
|
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
|
||||||
import { useOrientationSettings } from "@/hooks/useOrientationSettings";
|
|
||||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
|
||||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
|
||||||
import { TrackInfo } from "@/modules/vlc-player";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
|
||||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
|
||||||
import transcoding from "@/utils/profiles/transcoding";
|
|
||||||
import { secondsToTicks } from "@/utils/secondsToTicks";
|
|
||||||
import { Api } from "@jellyfin/sdk";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
|
||||||
import {
|
|
||||||
getPlaystateApi,
|
|
||||||
getUserLibraryApi,
|
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import React, {
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
|
||||||
import Video, {
|
|
||||||
OnProgressData,
|
|
||||||
SelectedTrack,
|
|
||||||
SelectedTrackType,
|
|
||||||
VideoRef,
|
|
||||||
} from "react-native-video";
|
|
||||||
import { SubtitleHelper } from "@/utils/SubtitleHelper";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const Player = () => {
|
|
||||||
console.log("Transcoding Player");
|
|
||||||
|
|
||||||
const api = useAtomValue(apiAtom);
|
|
||||||
const user = useAtomValue(userAtom);
|
|
||||||
const [settings] = useSettings();
|
|
||||||
const videoRef = useRef<VideoRef | null>(null);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const firstTime = useRef(true);
|
|
||||||
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
|
||||||
const [showControls, _setShowControls] = useState(true);
|
|
||||||
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
|
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
|
||||||
const [isBuffering, setIsBuffering] = useState(true);
|
|
||||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
|
||||||
|
|
||||||
const setShowControls = useCallback((show: boolean) => {
|
|
||||||
_setShowControls(show);
|
|
||||||
lightHapticFeedback();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const progress = useSharedValue(0);
|
|
||||||
const isSeeking = useSharedValue(false);
|
|
||||||
const cacheProgress = useSharedValue(0);
|
|
||||||
|
|
||||||
const {
|
|
||||||
itemId,
|
|
||||||
audioIndex: audioIndexStr,
|
|
||||||
subtitleIndex: subtitleIndexStr,
|
|
||||||
mediaSourceId,
|
|
||||||
bitrateValue: bitrateValueStr,
|
|
||||||
} = useLocalSearchParams<{
|
|
||||||
itemId: string;
|
|
||||||
audioIndex: string;
|
|
||||||
subtitleIndex: string;
|
|
||||||
mediaSourceId: string;
|
|
||||||
bitrateValue: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
|
|
||||||
const subtitleIndex = subtitleIndexStr
|
|
||||||
? parseInt(subtitleIndexStr, 10)
|
|
||||||
: undefined;
|
|
||||||
const bitrateValue = bitrateValueStr
|
|
||||||
? parseInt(bitrateValueStr, 10)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: item,
|
|
||||||
isLoading: isLoadingItem,
|
|
||||||
isError: isErrorItem,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["item", itemId],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api) {
|
|
||||||
throw new Error("No api");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!itemId) {
|
|
||||||
console.warn("No itemId");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await getUserLibraryApi(api).getItem({
|
|
||||||
itemId,
|
|
||||||
userId: user?.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
staleTime: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: NEED TO FIND A WAY TO FROM SWITCHING TO IMAGE BASED TO TEXT BASED SUBTITLES, THERE IS A BUG.
|
|
||||||
// MOST LIKELY LIKELY NEED A MASSIVE REFACTOR.
|
|
||||||
const {
|
|
||||||
data: stream,
|
|
||||||
isLoading: isLoadingStreamUrl,
|
|
||||||
isError: isErrorStreamUrl,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["stream-url", itemId, bitrateValue, mediaSourceId, audioIndex],
|
|
||||||
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api) {
|
|
||||||
throw new Error("No api");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
console.warn("No item", itemId, item);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await getStreamUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
|
||||||
userId: user?.Id,
|
|
||||||
audioStreamIndex: audioIndex,
|
|
||||||
maxStreamingBitrate: bitrateValue,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
subtitleStreamIndex: subtitleIndex,
|
|
||||||
deviceProfile: transcoding,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res) return null;
|
|
||||||
|
|
||||||
const { mediaSource, sessionId, url } = res;
|
|
||||||
|
|
||||||
if (!sessionId || !mediaSource || !url) {
|
|
||||||
console.warn("No sessionId or mediaSource or url", url);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mediaSource,
|
|
||||||
sessionId,
|
|
||||||
url,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
enabled: !!item,
|
|
||||||
staleTime: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const poster = usePoster(item, api);
|
|
||||||
const videoSource = useVideoSource(item, api, poster, stream?.url);
|
|
||||||
|
|
||||||
const togglePlay = useCallback(async () => {
|
|
||||||
lightHapticFeedback();
|
|
||||||
if (isPlaying) {
|
|
||||||
videoRef.current?.pause();
|
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
|
||||||
itemId: item?.Id!,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
positionTicks: Math.floor(progress.value),
|
|
||||||
isPaused: true,
|
|
||||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
videoRef.current?.resume();
|
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
|
||||||
itemId: item?.Id!,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
positionTicks: Math.floor(progress.value),
|
|
||||||
isPaused: false,
|
|
||||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
isPlaying,
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
videoRef,
|
|
||||||
settings,
|
|
||||||
stream,
|
|
||||||
audioIndex,
|
|
||||||
subtitleIndex,
|
|
||||||
mediaSourceId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const play = useCallback(() => {
|
|
||||||
videoRef.current?.resume();
|
|
||||||
reportPlaybackStart();
|
|
||||||
}, [videoRef]);
|
|
||||||
|
|
||||||
const pause = useCallback(() => {
|
|
||||||
videoRef.current?.pause();
|
|
||||||
}, [videoRef]);
|
|
||||||
|
|
||||||
const seek = useCallback(
|
|
||||||
(seconds: number) => {
|
|
||||||
videoRef.current?.seek(seconds);
|
|
||||||
},
|
|
||||||
[videoRef]
|
|
||||||
);
|
|
||||||
|
|
||||||
const reportPlaybackStopped = async () => {
|
|
||||||
if (!item?.Id) return;
|
|
||||||
await getPlaystateApi(api!).onPlaybackStopped({
|
|
||||||
itemId: item.Id,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
positionTicks: Math.floor(progress.value),
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
revalidateProgressCache();
|
|
||||||
};
|
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
|
||||||
reportPlaybackStopped();
|
|
||||||
videoRef.current?.pause();
|
|
||||||
setIsPlaybackStopped(true);
|
|
||||||
}, [videoRef, reportPlaybackStopped]);
|
|
||||||
|
|
||||||
const reportPlaybackStart = async () => {
|
|
||||||
if (!item?.Id) return;
|
|
||||||
await getPlaystateApi(api!).onPlaybackStart({
|
|
||||||
itemId: item.Id,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onProgress = useCallback(
|
|
||||||
async (data: OnProgressData) => {
|
|
||||||
if (isSeeking.value === true) return;
|
|
||||||
if (isPlaybackStopped === true) return;
|
|
||||||
|
|
||||||
const ticks = secondsToTicks(data.currentTime);
|
|
||||||
|
|
||||||
progress.value = ticks;
|
|
||||||
cacheProgress.value = secondsToTicks(data.playableDuration);
|
|
||||||
|
|
||||||
// TODO: Use this when streaming with HLS url, but NOT when direct playing
|
|
||||||
// TODO: since playable duration is always 0 then.
|
|
||||||
setIsBuffering(data.playableDuration === 0);
|
|
||||||
|
|
||||||
if (!item?.Id || data.currentTime === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
|
||||||
itemId: item.Id,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
positionTicks: Math.round(ticks),
|
|
||||||
isPaused: !isPlaying,
|
|
||||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[
|
|
||||||
item,
|
|
||||||
isPlaying,
|
|
||||||
api,
|
|
||||||
isPlaybackStopped,
|
|
||||||
isSeeking,
|
|
||||||
stream,
|
|
||||||
mediaSourceId,
|
|
||||||
audioIndex,
|
|
||||||
subtitleIndex,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
useWebSocket({
|
|
||||||
isPlaying: isPlaying,
|
|
||||||
togglePlay: togglePlay,
|
|
||||||
stopPlayback: stop,
|
|
||||||
offline: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [selectedTextTrack, setSelectedTextTrack] = useState<
|
|
||||||
SelectedTrack | undefined
|
|
||||||
>();
|
|
||||||
|
|
||||||
const [embededTextTracks, setEmbededTextTracks] = useState<
|
|
||||||
{
|
|
||||||
index: number;
|
|
||||||
language?: string | undefined;
|
|
||||||
selected?: boolean | undefined;
|
|
||||||
title?: string | undefined;
|
|
||||||
type: any;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const [audioTracks, setAudioTracks] = useState<TrackInfo[]>([]);
|
|
||||||
const [selectedAudioTrack, setSelectedAudioTrack] = useState<
|
|
||||||
SelectedTrack | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedTextTrack === undefined) {
|
|
||||||
const subtitleHelper = new SubtitleHelper(
|
|
||||||
stream?.mediaSource.MediaStreams ?? []
|
|
||||||
);
|
|
||||||
const embeddedTrackIndex = subtitleHelper.getEmbeddedTrackIndex(
|
|
||||||
subtitleIndex!
|
|
||||||
);
|
|
||||||
|
|
||||||
// Most likely the subtitle is burned in.
|
|
||||||
if (embeddedTrackIndex === -1) return;
|
|
||||||
|
|
||||||
setSelectedTextTrack({
|
|
||||||
type: SelectedTrackType.INDEX,
|
|
||||||
value: embeddedTrackIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [embededTextTracks]);
|
|
||||||
|
|
||||||
const getAudioTracks = (): TrackInfo[] => {
|
|
||||||
return audioTracks.map((t) => ({
|
|
||||||
name: t.name,
|
|
||||||
index: t.index,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSubtitleTracks = (): TrackInfo[] => {
|
|
||||||
return embededTextTracks.map((t) => ({
|
|
||||||
name: t.title ?? "",
|
|
||||||
index: t.index,
|
|
||||||
language: t.language,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
React.useCallback(() => {
|
|
||||||
return async () => {
|
|
||||||
stop();
|
|
||||||
};
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoadingItem || isLoadingStreamUrl)
|
|
||||||
return (
|
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
|
||||||
<Loader />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isErrorItem || isErrorStreamUrl)
|
|
||||||
return (
|
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
|
||||||
<Text className="text-white">{t("player.error")}</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{ flex: 1, backgroundColor: "black" }}>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
position: "relative",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{videoSource ? (
|
|
||||||
<>
|
|
||||||
<Video
|
|
||||||
ref={videoRef}
|
|
||||||
source={videoSource}
|
|
||||||
style={{
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
|
|
||||||
onProgress={onProgress}
|
|
||||||
onError={(e) => {
|
|
||||||
console.error("Error playing video", e);
|
|
||||||
}}
|
|
||||||
onLoad={() => {
|
|
||||||
if (firstTime.current === true) {
|
|
||||||
play();
|
|
||||||
firstTime.current = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
progressUpdateInterval={500}
|
|
||||||
playWhenInactive={true}
|
|
||||||
allowsExternalPlayback={true}
|
|
||||||
playInBackground={true}
|
|
||||||
showNotificationControls={true}
|
|
||||||
ignoreSilentSwitch="ignore"
|
|
||||||
fullscreen={false}
|
|
||||||
onPlaybackStateChanged={(state) => {
|
|
||||||
if (isSeeking.value === false) setIsPlaying(state.isPlaying);
|
|
||||||
}}
|
|
||||||
onTextTracks={(data) => {
|
|
||||||
setEmbededTextTracks(data.textTracks as any);
|
|
||||||
}}
|
|
||||||
onBuffer={(e) => {
|
|
||||||
setIsBuffering(e.isBuffering);
|
|
||||||
}}
|
|
||||||
onAudioTracks={(e) => {
|
|
||||||
setAudioTracks(
|
|
||||||
e.audioTracks.map((t) => ({
|
|
||||||
index: t.index,
|
|
||||||
name: t.title ?? "",
|
|
||||||
language: t.language,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
selectedTextTrack={selectedTextTrack}
|
|
||||||
selectedAudioTrack={selectedAudioTrack}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Text>{t("player.no_video_source")}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{item && (
|
|
||||||
<Controls
|
|
||||||
mediaSource={stream?.mediaSource}
|
|
||||||
videoRef={videoRef}
|
|
||||||
enableTrickplay={true}
|
|
||||||
item={item}
|
|
||||||
togglePlay={togglePlay}
|
|
||||||
isPlaying={isPlaying}
|
|
||||||
isSeeking={isSeeking}
|
|
||||||
progress={progress}
|
|
||||||
cacheProgress={cacheProgress}
|
|
||||||
isBuffering={isBuffering}
|
|
||||||
showControls={showControls}
|
|
||||||
setShowControls={setShowControls}
|
|
||||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
|
||||||
ignoreSafeAreas={ignoreSafeAreas}
|
|
||||||
seek={seek}
|
|
||||||
play={play}
|
|
||||||
pause={pause}
|
|
||||||
stop={stop}
|
|
||||||
getSubtitleTracks={getSubtitleTracks}
|
|
||||||
setSubtitleTrack={(i) => {
|
|
||||||
if (i === -1) {
|
|
||||||
setSelectedTextTrack({
|
|
||||||
type: SelectedTrackType.DISABLED,
|
|
||||||
value: undefined,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedTextTrack({
|
|
||||||
type: SelectedTrackType.INDEX,
|
|
||||||
value: i,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
getAudioTracks={getAudioTracks}
|
|
||||||
setAudioTrack={(i) => {
|
|
||||||
setSelectedAudioTrack({
|
|
||||||
type: SelectedTrackType.INDEX,
|
|
||||||
value: i,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function usePoster(
|
|
||||||
item: BaseItemDto | null | undefined,
|
|
||||||
api: Api | null
|
|
||||||
): string | undefined {
|
|
||||||
const poster = useMemo(() => {
|
|
||||||
if (!item || !api) return undefined;
|
|
||||||
return item.Type === "Audio"
|
|
||||||
? `${api.basePath}/Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200`
|
|
||||||
: getBackdropUrl({
|
|
||||||
api,
|
|
||||||
item: item,
|
|
||||||
quality: 70,
|
|
||||||
width: 200,
|
|
||||||
});
|
|
||||||
}, [item, api]);
|
|
||||||
|
|
||||||
return poster ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVideoSource(
|
|
||||||
item: BaseItemDto | null | undefined,
|
|
||||||
api: Api | null,
|
|
||||||
poster: string | undefined,
|
|
||||||
url?: string | null
|
|
||||||
) {
|
|
||||||
const videoSource = useMemo(() => {
|
|
||||||
if (!item || !api || !url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startPosition = item?.UserData?.PlaybackPositionTicks
|
|
||||||
? Math.round(item.UserData.PlaybackPositionTicks / 10000)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
uri: url,
|
|
||||||
isNetwork: true,
|
|
||||||
startPosition,
|
|
||||||
headers: getAuthHeaders(api),
|
|
||||||
metadata: {
|
|
||||||
title: item?.Name || "Unknown",
|
|
||||||
description: item?.Overview ?? undefined,
|
|
||||||
imageUri: poster,
|
|
||||||
subtitle: item?.Album ?? undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [item, api, poster, url]);
|
|
||||||
|
|
||||||
return videoSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Player;
|
|
||||||
@@ -16,7 +16,6 @@ import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
|||||||
import { useImageColors } from "@/hooks/useImageColors";
|
import { useImageColors } from "@/hooks/useImageColors";
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { SubtitleHelper } from "@/utils/SubtitleHelper";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
||||||
import {
|
import {
|
||||||
@@ -118,37 +117,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
const loading = useMemo(() => {
|
const loading = useMemo(() => {
|
||||||
return Boolean(logoUrl && loadingLogo);
|
return Boolean(logoUrl && loadingLogo);
|
||||||
}, [loadingLogo, logoUrl]);
|
}, [loadingLogo, logoUrl]);
|
||||||
|
|
||||||
const [isTranscoding, setIsTranscoding] = useState(false);
|
|
||||||
const [previouslyChosenSubtitleIndex, setPreviouslyChosenSubtitleIndex] =
|
|
||||||
useState<number | undefined>(selectedOptions?.subtitleIndex);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isTranscoding = Boolean(selectedOptions?.bitrate.value);
|
|
||||||
if (isTranscoding) {
|
|
||||||
setPreviouslyChosenSubtitleIndex(selectedOptions?.subtitleIndex);
|
|
||||||
const subHelper = new SubtitleHelper(
|
|
||||||
selectedOptions?.mediaSource?.MediaStreams ?? []
|
|
||||||
);
|
|
||||||
|
|
||||||
const newSubtitleIndex = subHelper.getMostCommonSubtitleByName(
|
|
||||||
selectedOptions?.subtitleIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
setSelectedOptions((prev) => ({
|
|
||||||
...prev!,
|
|
||||||
subtitleIndex: newSubtitleIndex ?? -1,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (!isTranscoding && previouslyChosenSubtitleIndex !== undefined) {
|
|
||||||
setSelectedOptions((prev) => ({
|
|
||||||
...prev!,
|
|
||||||
subtitleIndex: previouslyChosenSubtitleIndex,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
setIsTranscoding(isTranscoding);
|
|
||||||
}, [selectedOptions?.bitrate]);
|
|
||||||
|
|
||||||
if (!selectedOptions) return null;
|
if (!selectedOptions) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -239,7 +207,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
selected={selectedOptions.audioIndex}
|
selected={selectedOptions.audioIndex}
|
||||||
/>
|
/>
|
||||||
<SubtitleTrackSelector
|
<SubtitleTrackSelector
|
||||||
isTranscoding={isTranscoding}
|
|
||||||
source={selectedOptions.mediaSource}
|
source={selectedOptions.mediaSource}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
setSelectedOptions(
|
setSelectedOptions(
|
||||||
|
|||||||
@@ -73,11 +73,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
(q: string, bitrateValue: number | undefined) => {
|
(q: string, bitrateValue: number | undefined) => {
|
||||||
if (!bitrateValue) {
|
router.push(`/player/direct-player?${q}`);
|
||||||
router.push(`/player/direct-player?${q}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
router.push(`/player/transcoding-player?${q}`);
|
|
||||||
},
|
},
|
||||||
[router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -58,11 +58,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
(q: string, bitrateValue: number | undefined) => {
|
(q: string, bitrateValue: number | undefined) => {
|
||||||
if (!bitrateValue) {
|
router.push(`/player/direct-player?${q}`);
|
||||||
router.push(`/player/direct-player?${q}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
router.push(`/player/transcoding-player?${q}`);
|
|
||||||
},
|
},
|
||||||
[router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,40 +4,31 @@ import { useMemo } from "react";
|
|||||||
import { Platform, TouchableOpacity, View } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
import { SubtitleHelper } from "@/utils/SubtitleHelper";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof View> {
|
interface Props extends React.ComponentProps<typeof View> {
|
||||||
source?: MediaSourceInfo;
|
source?: MediaSourceInfo;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
selected?: number | undefined;
|
selected?: number | undefined;
|
||||||
isTranscoding?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubtitleTrackSelector: React.FC<Props> = ({
|
export const SubtitleTrackSelector: React.FC<Props> = ({
|
||||||
source,
|
source,
|
||||||
onChange,
|
onChange,
|
||||||
selected,
|
selected,
|
||||||
isTranscoding,
|
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
if (Platform.isTV) return null;
|
if (Platform.isTV) return null;
|
||||||
const subtitleStreams = useMemo(() => {
|
const subtitleStreams = useMemo(() => {
|
||||||
const subtitleHelper = new SubtitleHelper(source?.MediaStreams ?? []);
|
return source?.MediaStreams?.filter((x) => x.Type === "Subtitle");
|
||||||
|
}, [source]);
|
||||||
if (isTranscoding && Platform.OS === "ios") {
|
|
||||||
return subtitleHelper.getUniqueSubtitles();
|
|
||||||
}
|
|
||||||
|
|
||||||
return subtitleHelper.getSubtitles();
|
|
||||||
}, [source, isTranscoding]);
|
|
||||||
|
|
||||||
const selectedSubtitleSteam = useMemo(
|
const selectedSubtitleSteam = useMemo(
|
||||||
() => subtitleStreams.find((x) => x.Index === selected),
|
() => subtitleStreams?.find((x) => x.Index === selected),
|
||||||
[subtitleStreams, selected]
|
[subtitleStreams, selected]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (subtitleStreams.length === 0) return null;
|
if (subtitleStreams?.length === 0) return null;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
ticksToMs,
|
ticksToMs,
|
||||||
ticksToSeconds,
|
ticksToSeconds,
|
||||||
} from "@/utils/time";
|
} from "@/utils/time";
|
||||||
import {Ionicons, MaterialIcons} from "@expo/vector-icons";
|
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
||||||
import {
|
import {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
@@ -35,7 +35,12 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {Platform, TouchableOpacity, useWindowDimensions, View} from "react-native";
|
import {
|
||||||
|
Platform,
|
||||||
|
TouchableOpacity,
|
||||||
|
useWindowDimensions,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
import { Slider } from "react-native-awesome-slider";
|
import { Slider } from "react-native-awesome-slider";
|
||||||
import {
|
import {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
@@ -49,8 +54,7 @@ import AudioSlider from "./AudioSlider";
|
|||||||
import BrightnessSlider from "./BrightnessSlider";
|
import BrightnessSlider from "./BrightnessSlider";
|
||||||
import { ControlProvider } from "./contexts/ControlContext";
|
import { ControlProvider } from "./contexts/ControlContext";
|
||||||
import { VideoProvider } from "./contexts/VideoContext";
|
import { VideoProvider } from "./contexts/VideoContext";
|
||||||
import DropdownViewDirect from "./dropdown/DropdownViewDirect";
|
import DropdownView from "./dropdown/DropdownView";
|
||||||
import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding";
|
|
||||||
import { EpisodeList } from "./EpisodeList";
|
import { EpisodeList } from "./EpisodeList";
|
||||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||||
import SkipButton from "./SkipButton";
|
import SkipButton from "./SkipButton";
|
||||||
@@ -214,15 +218,10 @@ export const Controls: React.FC<Props> = ({
|
|||||||
bitrateValue: bitrateValue.toString(),
|
bitrateValue: bitrateValue.toString(),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
stop()
|
stop();
|
||||||
|
|
||||||
if (!bitrateValue) {
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/transcoding-player?${queryParams}`);
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
}, [previousItem, settings, subtitleIndex, audioIndex]);
|
}, [previousItem, settings, subtitleIndex, audioIndex]);
|
||||||
|
|
||||||
const goToNextItem = useCallback(() => {
|
const goToNextItem = useCallback(() => {
|
||||||
@@ -254,15 +253,10 @@ export const Controls: React.FC<Props> = ({
|
|||||||
bitrateValue: bitrateValue.toString(),
|
bitrateValue: bitrateValue.toString(),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
stop()
|
stop();
|
||||||
|
|
||||||
if (!bitrateValue) {
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/transcoding-player?${queryParams}`);
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
}, [nextItem, settings, subtitleIndex, audioIndex]);
|
}, [nextItem, settings, subtitleIndex, audioIndex]);
|
||||||
|
|
||||||
const updateTimes = useCallback(
|
const updateTimes = useCallback(
|
||||||
@@ -419,15 +413,10 @@ export const Controls: React.FC<Props> = ({
|
|||||||
bitrateValue: bitrateValue.toString(),
|
bitrateValue: bitrateValue.toString(),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
stop()
|
stop();
|
||||||
|
|
||||||
if (!bitrateValue) {
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/transcoding-player?${queryParams}`);
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in gotoEpisode:", error);
|
console.error("Error in gotoEpisode:", error);
|
||||||
}
|
}
|
||||||
@@ -508,7 +497,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
}, [trickPlayUrl, trickplayInfo, time]);
|
}, [trickPlayUrl, trickplayInfo, time]);
|
||||||
|
|
||||||
const onClose = async () => {
|
const onClose = async () => {
|
||||||
stop()
|
stop();
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
await ScreenOrientation.lockAsync(
|
await ScreenOrientation.lockAsync(
|
||||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||||
@@ -559,19 +548,13 @@ export const Controls: React.FC<Props> = ({
|
|||||||
setSubtitleTrack={setSubtitleTrack}
|
setSubtitleTrack={setSubtitleTrack}
|
||||||
setSubtitleURL={setSubtitleURL}
|
setSubtitleURL={setSubtitleURL}
|
||||||
>
|
>
|
||||||
{!mediaSource?.TranscodingUrl ? (
|
<DropdownView showControls={showControls} />
|
||||||
<DropdownViewDirect showControls={showControls} />
|
|
||||||
) : (
|
|
||||||
<DropdownViewTranscoding showControls={showControls} />
|
|
||||||
)}
|
|
||||||
</VideoProvider>
|
</VideoProvider>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex flex-row items-center space-x-2 ">
|
<View className="flex flex-row items-center space-x-2 ">
|
||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity onPress={startPictureInPicture}>
|
||||||
onPress={startPictureInPicture}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="picture-in-picture"
|
name="picture-in-picture"
|
||||||
size={24}
|
size={24}
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useControlContext } from "./ControlContext";
|
import { useControlContext } from "./ControlContext";
|
||||||
|
import { Track } from "../types";
|
||||||
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
|
||||||
interface VideoContextProps {
|
interface VideoContextProps {
|
||||||
audioTracks: TrackInfo[] | null;
|
audioTracks: Track[] | null;
|
||||||
subtitleTracks: TrackInfo[] | null;
|
subtitleTracks: Track[] | null;
|
||||||
setAudioTrack: ((index: number) => void) | undefined;
|
setAudioTrack: ((index: number) => void) | undefined;
|
||||||
setSubtitleTrack: ((index: number) => void) | undefined;
|
setSubtitleTrack: ((index: number) => void) | undefined;
|
||||||
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
||||||
@@ -45,30 +48,155 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
setSubtitleURL,
|
setSubtitleURL,
|
||||||
setAudioTrack,
|
setAudioTrack,
|
||||||
}) => {
|
}) => {
|
||||||
const [audioTracks, setAudioTracks] = useState<TrackInfo[] | null>(null);
|
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
|
||||||
const [subtitleTracks, setSubtitleTracks] = useState<TrackInfo[] | null>(
|
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const ControlContext = useControlContext();
|
const ControlContext = useControlContext();
|
||||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
||||||
|
const mediaSource = ControlContext?.mediaSource;
|
||||||
|
|
||||||
|
const allSubs =
|
||||||
|
mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || [];
|
||||||
|
|
||||||
|
const { itemId, audioIndex, bitrateValue, subtitleIndex } =
|
||||||
|
useLocalSearchParams<{
|
||||||
|
itemId: string;
|
||||||
|
audioIndex: string;
|
||||||
|
subtitleIndex: string;
|
||||||
|
mediaSourceId: string;
|
||||||
|
bitrateValue: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const onTextBasedSubtitle = useMemo(
|
||||||
|
() =>
|
||||||
|
allSubs.find(
|
||||||
|
(s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream
|
||||||
|
) || subtitleIndex === "-1",
|
||||||
|
[allSubs, subtitleIndex]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setPlayerParams = ({
|
||||||
|
chosenAudioIndex = audioIndex,
|
||||||
|
chosenSubtitleIndex = subtitleIndex,
|
||||||
|
}: {
|
||||||
|
chosenAudioIndex?: string;
|
||||||
|
chosenSubtitleIndex?: string;
|
||||||
|
}) => {
|
||||||
|
console.log("chosenSubtitleIndex", chosenSubtitleIndex);
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
itemId: itemId ?? "",
|
||||||
|
audioIndex: chosenAudioIndex,
|
||||||
|
subtitleIndex: chosenSubtitleIndex,
|
||||||
|
mediaSourceId: mediaSource?.Id ?? "",
|
||||||
|
bitrateValue: bitrateValue,
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTrackParams = (
|
||||||
|
type: "audio" | "subtitle",
|
||||||
|
index: number,
|
||||||
|
serverIndex: number
|
||||||
|
) => {
|
||||||
|
const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack;
|
||||||
|
const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex";
|
||||||
|
|
||||||
|
// If we're transcoding and we're going from a image based subtitle
|
||||||
|
// to a text based subtitle, we need to change the player params.
|
||||||
|
|
||||||
|
const shouldChangePlayerParams =
|
||||||
|
type === "subtitle" &&
|
||||||
|
mediaSource?.TranscodingUrl &&
|
||||||
|
!onTextBasedSubtitle;
|
||||||
|
|
||||||
|
console.log("Set player params", index, serverIndex);
|
||||||
|
if (shouldChangePlayerParams) {
|
||||||
|
setPlayerParams({
|
||||||
|
chosenSubtitleIndex: serverIndex.toString(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTrack && setTrack(index);
|
||||||
|
router.setParams({
|
||||||
|
[paramKey]: serverIndex.toString(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTracks = async () => {
|
const fetchTracks = async () => {
|
||||||
if (
|
if (getSubtitleTracks) {
|
||||||
getSubtitleTracks &&
|
const subtitleData = await getSubtitleTracks();
|
||||||
(subtitleTracks === null || subtitleTracks.length === 0)
|
|
||||||
) {
|
let textSubIndex = 0;
|
||||||
const subtitles = await getSubtitleTracks();
|
const subtitles: Track[] = allSubs?.map((sub) => {
|
||||||
console.log("Getting embeded subtitles...", subtitles);
|
// Always increment for non-transcoding subtitles
|
||||||
|
// Only increment for text-based subtitles when transcoding
|
||||||
|
const shouldIncrement =
|
||||||
|
!mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream;
|
||||||
|
|
||||||
|
const displayTitle = sub.DisplayTitle || "Undefined Subtitle";
|
||||||
|
const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1;
|
||||||
|
|
||||||
|
const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1;
|
||||||
|
|
||||||
|
if (shouldIncrement) textSubIndex++;
|
||||||
|
return {
|
||||||
|
name: displayTitle,
|
||||||
|
index: sub.Index ?? -1,
|
||||||
|
originalIndex: finalIndex,
|
||||||
|
setTrack: () =>
|
||||||
|
shouldIncrement
|
||||||
|
? setTrackParams("subtitle", finalIndex, sub.Index ?? -1)
|
||||||
|
: setPlayerParams({
|
||||||
|
chosenSubtitleIndex: sub.Index?.toString(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a "Disable Subtitles" option
|
||||||
|
subtitles.unshift({
|
||||||
|
name: "Disable",
|
||||||
|
index: -1,
|
||||||
|
setTrack: () =>
|
||||||
|
!mediaSource?.TranscodingUrl || onTextBasedSubtitle
|
||||||
|
? setTrackParams("subtitle", -1, -1)
|
||||||
|
: setPlayerParams({ chosenSubtitleIndex: "-1" }),
|
||||||
|
});
|
||||||
|
|
||||||
setSubtitleTracks(subtitles);
|
setSubtitleTracks(subtitles);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
getAudioTracks &&
|
getAudioTracks &&
|
||||||
(audioTracks === null || audioTracks.length === 0)
|
(audioTracks === null || audioTracks.length === 0)
|
||||||
) {
|
) {
|
||||||
const audio = await getAudioTracks();
|
const audioData = await getAudioTracks();
|
||||||
setAudioTracks(audio);
|
if (!audioData) return;
|
||||||
|
|
||||||
|
console.log("audioData", audioData);
|
||||||
|
|
||||||
|
const allAudio =
|
||||||
|
mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || [];
|
||||||
|
|
||||||
|
const audioTracks: Track[] = allAudio?.map((audio, idx) => {
|
||||||
|
if (!mediaSource?.TranscodingUrl) {
|
||||||
|
const vlcIndex = audioData?.at(idx)?.index ?? -1;
|
||||||
|
return {
|
||||||
|
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||||
|
index: audio.Index ?? -1,
|
||||||
|
setTrack: () =>
|
||||||
|
setTrackParams("audio", vlcIndex, audio.Index ?? -1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||||
|
index: audio.Index ?? -1,
|
||||||
|
setTrack: () =>
|
||||||
|
setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setAudioTracks(audioTracks);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchTracks();
|
fetchTracks();
|
||||||
|
|||||||
@@ -1,67 +1,21 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React from "react";
|
||||||
import { View, TouchableOpacity, Platform } from "react-native";
|
import { TouchableOpacity, Platform } from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||||
import { useControlContext } from "../contexts/ControlContext";
|
|
||||||
import { useVideoContext } from "../contexts/VideoContext";
|
import { useVideoContext } from "../contexts/VideoContext";
|
||||||
import { EmbeddedSubtitle, ExternalSubtitle } from "../types";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
|
||||||
|
|
||||||
interface DropdownViewDirectProps {
|
interface DropdownViewProps {
|
||||||
showControls: boolean;
|
showControls: boolean;
|
||||||
offline?: boolean; // used to disable external subs for downloads
|
offline?: boolean; // used to disable external subs for downloads
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownViewDirect: React.FC<DropdownViewDirectProps> = ({
|
const DropdownView: React.FC<DropdownViewProps> = ({
|
||||||
showControls,
|
showControls,
|
||||||
offline = false,
|
offline = false,
|
||||||
}) => {
|
}) => {
|
||||||
const api = useAtomValue(apiAtom);
|
|
||||||
const ControlContext = useControlContext();
|
|
||||||
const mediaSource = ControlContext?.mediaSource;
|
|
||||||
const item = ControlContext?.item;
|
|
||||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
|
||||||
|
|
||||||
const videoContext = useVideoContext();
|
const videoContext = useVideoContext();
|
||||||
const {
|
const { subtitleTracks, audioTracks } = videoContext;
|
||||||
subtitleTracks,
|
|
||||||
audioTracks,
|
|
||||||
setSubtitleURL,
|
|
||||||
setSubtitleTrack,
|
|
||||||
setAudioTrack,
|
|
||||||
} = videoContext;
|
|
||||||
|
|
||||||
const allSubtitleTracksForDirectPlay = useMemo(() => {
|
|
||||||
if (mediaSource?.TranscodingUrl) return null;
|
|
||||||
const embeddedSubs =
|
|
||||||
subtitleTracks
|
|
||||||
?.map((s) => ({
|
|
||||||
name: s.name,
|
|
||||||
index: s.index,
|
|
||||||
deliveryUrl: undefined,
|
|
||||||
}))
|
|
||||||
.filter((sub) => !sub.name.endsWith("[External]")) || [];
|
|
||||||
|
|
||||||
const externalSubs =
|
|
||||||
mediaSource?.MediaStreams?.filter(
|
|
||||||
(stream) => stream.Type === "Subtitle" && !!stream.DeliveryUrl
|
|
||||||
).map((s) => ({
|
|
||||||
name: s.DisplayTitle! + " [External]",
|
|
||||||
index: s.Index!,
|
|
||||||
deliveryUrl: s.DeliveryUrl,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
// Combine embedded subs with external subs only if not offline
|
|
||||||
if (!offline) {
|
|
||||||
return [...embeddedSubs, ...externalSubs] as (
|
|
||||||
| EmbeddedSubtitle
|
|
||||||
| ExternalSubtitle
|
|
||||||
)[];
|
|
||||||
}
|
|
||||||
return embeddedSubs as EmbeddedSubtitle[];
|
|
||||||
}, [item, isVideoLoaded, subtitleTracks, mediaSource?.MediaStreams, offline]);
|
|
||||||
|
|
||||||
const { subtitleIndex, audioIndex } = useLocalSearchParams<{
|
const { subtitleIndex, audioIndex } = useLocalSearchParams<{
|
||||||
itemId: string;
|
itemId: string;
|
||||||
@@ -98,21 +52,11 @@ const DropdownViewDirect: React.FC<DropdownViewDirectProps> = ({
|
|||||||
loop={true}
|
loop={true}
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
>
|
>
|
||||||
{allSubtitleTracksForDirectPlay?.map((sub, idx: number) => (
|
{subtitleTracks?.map((sub, idx: number) => (
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
key={`subtitle-item-${idx}`}
|
key={`subtitle-item-${idx}`}
|
||||||
value={subtitleIndex === sub.index.toString()}
|
value={subtitleIndex === sub.index.toString()}
|
||||||
onValueChange={() => {
|
onValueChange={() => sub.setTrack()}
|
||||||
if ("deliveryUrl" in sub && sub.deliveryUrl) {
|
|
||||||
setSubtitleURL &&
|
|
||||||
setSubtitleURL(api?.basePath + sub.deliveryUrl, sub.name);
|
|
||||||
} else {
|
|
||||||
setSubtitleTrack && setSubtitleTrack(sub.index);
|
|
||||||
}
|
|
||||||
router.setParams({
|
|
||||||
subtitleIndex: sub.index.toString(),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>
|
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>
|
||||||
{sub.name}
|
{sub.name}
|
||||||
@@ -136,12 +80,7 @@ const DropdownViewDirect: React.FC<DropdownViewDirectProps> = ({
|
|||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
key={`audio-item-${idx}`}
|
key={`audio-item-${idx}`}
|
||||||
value={audioIndex === track.index.toString()}
|
value={audioIndex === track.index.toString()}
|
||||||
onValueChange={() => {
|
onValueChange={() => track.setTrack()}
|
||||||
setAudioTrack && setAudioTrack(track.index);
|
|
||||||
router.setParams({
|
|
||||||
audioIndex: track.index.toString(),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
||||||
{track.name}
|
{track.name}
|
||||||
@@ -155,4 +94,4 @@ const DropdownViewDirect: React.FC<DropdownViewDirectProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DropdownViewDirect;
|
export default DropdownView;
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
import React, { useCallback, useMemo, useState } from "react";
|
|
||||||
import { View, TouchableOpacity, Platform } from "react-native";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
|
||||||
import { useControlContext } from "../contexts/ControlContext";
|
|
||||||
import { useVideoContext } from "../contexts/VideoContext";
|
|
||||||
import { TranscodedSubtitle } from "../types";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
|
||||||
import { SubtitleHelper } from "@/utils/SubtitleHelper";
|
|
||||||
|
|
||||||
interface DropdownViewProps {
|
|
||||||
showControls: boolean;
|
|
||||||
offline?: boolean; // used to disable external subs for downloads
|
|
||||||
}
|
|
||||||
|
|
||||||
const DropdownView: React.FC<DropdownViewProps> = ({ showControls }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const api = useAtomValue(apiAtom);
|
|
||||||
const ControlContext = useControlContext();
|
|
||||||
const mediaSource = ControlContext?.mediaSource;
|
|
||||||
const item = ControlContext?.item;
|
|
||||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
|
||||||
|
|
||||||
const videoContext = useVideoContext();
|
|
||||||
const { subtitleTracks, setSubtitleTrack } = videoContext;
|
|
||||||
|
|
||||||
const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{
|
|
||||||
itemId: string;
|
|
||||||
audioIndex: string;
|
|
||||||
subtitleIndex: string;
|
|
||||||
mediaSourceId: string;
|
|
||||||
bitrateValue: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// Either its on a text subtitle or its on not on any subtitle therefore it should show all the embedded HLS subtitles.
|
|
||||||
|
|
||||||
const isOnTextSubtitle = useMemo(() => {
|
|
||||||
const res = Boolean(
|
|
||||||
mediaSource?.MediaStreams?.find(
|
|
||||||
(x) => x.Index === parseInt(subtitleIndex) && x.IsTextSubtitleStream
|
|
||||||
) || subtitleIndex === "-1"
|
|
||||||
);
|
|
||||||
return res;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const allSubs =
|
|
||||||
mediaSource?.MediaStreams?.filter((x) => x.Type === "Subtitle") ?? [];
|
|
||||||
|
|
||||||
const subtitleHelper = new SubtitleHelper(mediaSource?.MediaStreams ?? []);
|
|
||||||
|
|
||||||
const allSubtitleTracksForTranscodingStream = useMemo(() => {
|
|
||||||
const disableSubtitle = {
|
|
||||||
name: "Disable",
|
|
||||||
index: -1,
|
|
||||||
IsTextSubtitleStream: true,
|
|
||||||
} as TranscodedSubtitle;
|
|
||||||
if (isOnTextSubtitle) {
|
|
||||||
const textSubtitles =
|
|
||||||
subtitleTracks?.map((s) => ({
|
|
||||||
name: s.name,
|
|
||||||
index: s.index,
|
|
||||||
IsTextSubtitleStream: true,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const sortedSubtitles = subtitleHelper.getSortedSubtitles(textSubtitles);
|
|
||||||
|
|
||||||
return [disableSubtitle, ...sortedSubtitles];
|
|
||||||
}
|
|
||||||
|
|
||||||
const transcodedSubtitle: TranscodedSubtitle[] = allSubs.map((x) => ({
|
|
||||||
name: x.DisplayTitle!,
|
|
||||||
index: x.Index!,
|
|
||||||
IsTextSubtitleStream: x.IsTextSubtitleStream!,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [disableSubtitle, ...transcodedSubtitle];
|
|
||||||
}, [item, isVideoLoaded, subtitleTracks, mediaSource?.MediaStreams]);
|
|
||||||
|
|
||||||
const changeToImageBasedSub = useCallback(
|
|
||||||
(subtitleIndex: number) => {
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
itemId: item.Id ?? "", // Ensure itemId is a string
|
|
||||||
audioIndex: audioIndex?.toString() ?? "",
|
|
||||||
subtitleIndex: subtitleIndex?.toString() ?? "",
|
|
||||||
mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
|
||||||
bitrateValue: bitrateValue,
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/transcoding-player?${queryParams}`);
|
|
||||||
},
|
|
||||||
[mediaSource]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Audio tracks for transcoding streams.
|
|
||||||
const allAudio =
|
|
||||||
mediaSource?.MediaStreams?.filter((x) => x.Type === "Audio").map((x) => ({
|
|
||||||
name: x.DisplayTitle!,
|
|
||||||
index: x.Index!,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const ChangeTranscodingAudio = useCallback(
|
|
||||||
(audioIndex: number) => {
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
itemId: item.Id ?? "", // Ensure itemId is a string
|
|
||||||
audioIndex: audioIndex?.toString() ?? "",
|
|
||||||
subtitleIndex: subtitleIndex?.toString() ?? "",
|
|
||||||
mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
|
||||||
bitrateValue: bitrateValue,
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/transcoding-player?${queryParams}`);
|
|
||||||
},
|
|
||||||
[mediaSource, subtitleIndex, audioIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<DropdownMenu.Root>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<TouchableOpacity className="aspect-square flex flex-col rounded-xl items-center justify-center p-2">
|
|
||||||
<Ionicons name="ellipsis-horizontal" size={24} color={"white"} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Content
|
|
||||||
loop={true}
|
|
||||||
side="bottom"
|
|
||||||
align="start"
|
|
||||||
alignOffset={0}
|
|
||||||
avoidCollisions={true}
|
|
||||||
collisionPadding={8}
|
|
||||||
sideOffset={8}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Sub>
|
|
||||||
<DropdownMenu.SubTrigger key="subtitle-trigger">
|
|
||||||
Subtitle
|
|
||||||
</DropdownMenu.SubTrigger>
|
|
||||||
<DropdownMenu.SubContent
|
|
||||||
alignOffset={-10}
|
|
||||||
avoidCollisions={true}
|
|
||||||
collisionPadding={0}
|
|
||||||
loop={true}
|
|
||||||
sideOffset={10}
|
|
||||||
>
|
|
||||||
{allSubtitleTracksForTranscodingStream?.map(
|
|
||||||
(sub, idx: number) => (
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
value={
|
|
||||||
subtitleIndex ===
|
|
||||||
(isOnTextSubtitle && sub.IsTextSubtitleStream
|
|
||||||
? subtitleHelper
|
|
||||||
.getSourceSubtitleIndex(sub.index)
|
|
||||||
.toString()
|
|
||||||
: sub?.index.toString())
|
|
||||||
}
|
|
||||||
key={`subtitle-item-${idx}`}
|
|
||||||
onValueChange={() => {
|
|
||||||
if (
|
|
||||||
subtitleIndex ===
|
|
||||||
(isOnTextSubtitle && sub.IsTextSubtitleStream
|
|
||||||
? subtitleHelper
|
|
||||||
.getSourceSubtitleIndex(sub.index)
|
|
||||||
.toString()
|
|
||||||
: sub?.index.toString())
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
router.setParams({
|
|
||||||
subtitleIndex: subtitleHelper
|
|
||||||
.getSourceSubtitleIndex(sub.index)
|
|
||||||
.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sub.IsTextSubtitleStream && isOnTextSubtitle) {
|
|
||||||
setSubtitleTrack && setSubtitleTrack(sub.index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
changeToImageBasedSub(sub.index);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>
|
|
||||||
{sub.name}
|
|
||||||
</DropdownMenu.ItemTitle>
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</DropdownMenu.SubContent>
|
|
||||||
</DropdownMenu.Sub>
|
|
||||||
<DropdownMenu.Sub>
|
|
||||||
<DropdownMenu.SubTrigger key="audio-trigger">
|
|
||||||
Audio
|
|
||||||
</DropdownMenu.SubTrigger>
|
|
||||||
<DropdownMenu.SubContent
|
|
||||||
alignOffset={-10}
|
|
||||||
avoidCollisions={true}
|
|
||||||
collisionPadding={0}
|
|
||||||
loop={true}
|
|
||||||
sideOffset={10}
|
|
||||||
>
|
|
||||||
{allAudio?.map((track, idx: number) => (
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
key={`audio-item-${idx}`}
|
|
||||||
value={audioIndex === track.index.toString()}
|
|
||||||
onValueChange={() => {
|
|
||||||
if (audioIndex === track.index.toString()) return;
|
|
||||||
router.setParams({
|
|
||||||
audioIndex: track.index.toString(),
|
|
||||||
});
|
|
||||||
ChangeTranscodingAudio(track.index);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
|
||||||
{track.name}
|
|
||||||
</DropdownMenu.ItemTitle>
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenu.SubContent>
|
|
||||||
</DropdownMenu.Sub>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DropdownView;
|
|
||||||
@@ -13,7 +13,14 @@ type ExternalSubtitle = {
|
|||||||
type TranscodedSubtitle = {
|
type TranscodedSubtitle = {
|
||||||
name: string;
|
name: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
deliveryUrl: string;
|
||||||
IsTextSubtitleStream: boolean;
|
IsTextSubtitleStream: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { EmbeddedSubtitle, ExternalSubtitle, TranscodedSubtitle };
|
type Track = {
|
||||||
|
name: string;
|
||||||
|
index: number;
|
||||||
|
setTrack: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { EmbeddedSubtitle, ExternalSubtitle, TranscodedSubtitle, Track };
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
import ExpoModulesCore
|
import ExpoModulesCore
|
||||||
import VLCKit
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import VLCKit
|
||||||
|
|
||||||
public class VLCPlayerView: UIView {
|
public class VLCPlayerView: UIView {
|
||||||
func setupView(parent: UIView) {
|
func setupView(parent: UIView) {
|
||||||
self.backgroundColor = .black
|
self.backgroundColor = .black
|
||||||
self.translatesAutoresizingMaskIntoConstraints = false
|
self.translatesAutoresizingMaskIntoConstraints = false
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
self.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
|
self.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
|
||||||
self.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
|
self.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
|
||||||
self.topAnchor.constraint(equalTo: parent.topAnchor),
|
self.topAnchor.constraint(equalTo: parent.topAnchor),
|
||||||
self.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
|
self.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func layoutSubviews() {
|
public override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
for subview in subviews {
|
for subview in subviews {
|
||||||
subview.frame = bounds
|
subview.frame = bounds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VLCPlayerWrapper: NSObject {
|
class VLCPlayerWrapper: NSObject {
|
||||||
private var lastProgressCall = Date().timeIntervalSince1970
|
private var lastProgressCall = Date().timeIntervalSince1970
|
||||||
public var player: VLCMediaPlayer = VLCMediaPlayer()
|
public var player: VLCMediaPlayer = VLCMediaPlayer()
|
||||||
private var updatePlayerState: (() -> ())?
|
private var updatePlayerState: (() -> Void)?
|
||||||
private var updateVideoProgress: (() -> ())?
|
private var updateVideoProgress: (() -> Void)?
|
||||||
private var playerView: VLCPlayerView = VLCPlayerView()
|
private var playerView: VLCPlayerView = VLCPlayerView()
|
||||||
public weak var pipController: VLCPictureInPictureWindowControlling?
|
public weak var pipController: VLCPictureInPictureWindowControlling?
|
||||||
|
|
||||||
@@ -41,8 +40,8 @@ class VLCPlayerWrapper: NSObject {
|
|||||||
|
|
||||||
public func setup(
|
public func setup(
|
||||||
parent: UIView,
|
parent: UIView,
|
||||||
updatePlayerState: (() -> ())?,
|
updatePlayerState: (() -> Void)?,
|
||||||
updateVideoProgress: (() -> ())?
|
updateVideoProgress: (() -> Void)?
|
||||||
) {
|
) {
|
||||||
self.updatePlayerState = updatePlayerState
|
self.updatePlayerState = updatePlayerState
|
||||||
self.updateVideoProgress = updateVideoProgress
|
self.updateVideoProgress = updateVideoProgress
|
||||||
@@ -52,9 +51,9 @@ class VLCPlayerWrapper: NSObject {
|
|||||||
playerView.setupView(parent: parent)
|
playerView.setupView(parent: parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getPlayerView() -> UIView {
|
public func getPlayerView() -> UIView {
|
||||||
return playerView
|
return playerView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - VLCPictureInPictureDrawable
|
// MARK: - VLCPictureInPictureDrawable
|
||||||
@@ -63,7 +62,8 @@ extension VLCPlayerWrapper: VLCPictureInPictureDrawable {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
public func pictureInPictureReady() -> (((any VLCPictureInPictureWindowControlling)?) -> Void)! {
|
public func pictureInPictureReady() -> (((any VLCPictureInPictureWindowControlling)?) -> Void)!
|
||||||
|
{
|
||||||
return { [weak self] controller in
|
return { [weak self] controller in
|
||||||
self?.pipController = controller
|
self?.pipController = controller
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ extension VLCPlayerWrapper: VLCPictureInPictureMediaControlling {
|
|||||||
player.pause()
|
player.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
func seek(by offset: Int64, completion: @escaping () -> ()) {
|
func seek(by offset: Int64, completion: @escaping () -> Void) {
|
||||||
player.jump(withOffset: Int32(offset), completion: completion)
|
player.jump(withOffset: Int32(offset), completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,20 +115,24 @@ extension VLCPlayerWrapper: VLCDrawable {
|
|||||||
// MARK: - VLCMediaPlayerDelegate
|
// MARK: - VLCMediaPlayerDelegate
|
||||||
extension VLCPlayerWrapper: VLCMediaPlayerDelegate {
|
extension VLCPlayerWrapper: VLCMediaPlayerDelegate {
|
||||||
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||||
let timeNow = Date().timeIntervalSince1970
|
DispatchQueue.main.async { [weak self] in
|
||||||
if timeNow - lastProgressCall >= 1 {
|
guard let self = self else { return }
|
||||||
lastProgressCall = timeNow
|
let timeNow = Date().timeIntervalSince1970
|
||||||
updateVideoProgress?()
|
if timeNow - self.lastProgressCall >= 1 {
|
||||||
|
self.lastProgressCall = timeNow
|
||||||
|
self.updateVideoProgress?()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaPlayerStateChanged(_ state: VLCMediaPlayerState) {
|
func mediaPlayerStateChanged(_ state: VLCMediaPlayerState) {
|
||||||
self.updatePlayerState?()
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.updatePlayerState?()
|
||||||
|
|
||||||
guard let pipController = self.pipController else { return }
|
guard let pipController = self.pipController else { return }
|
||||||
DispatchQueue.main.async(execute: {
|
|
||||||
pipController.invalidatePlaybackState()
|
pipController.invalidatePlaybackState()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,16 +141,15 @@ extension VLCPlayerWrapper: VLCMediaDelegate {
|
|||||||
// Implement VLCMediaDelegate methods if needed
|
// Implement VLCMediaDelegate methods if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class VlcPlayerView: ExpoView {
|
class VlcPlayerView: ExpoView {
|
||||||
private var vlc: VLCPlayerWrapper = VLCPlayerWrapper()
|
private var vlc: VLCPlayerWrapper = VLCPlayerWrapper()
|
||||||
private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second
|
private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second
|
||||||
private var isPaused: Bool = false
|
private var isPaused: Bool = false
|
||||||
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
||||||
private var startPosition: Int32 = 0
|
private var startPosition: Int32 = 0
|
||||||
private var isMediaReady: Bool = false
|
|
||||||
private var externalTrack: [String: String]?
|
private var externalTrack: [String: String]?
|
||||||
private var isStopping: Bool = false // Define isStopping here
|
private var isStopping: Bool = false // Define isStopping here
|
||||||
|
private var externalSubtitles: [[String: String]]?
|
||||||
var hasSource = false
|
var hasSource = false
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
@@ -229,10 +232,12 @@ class VlcPlayerView: ExpoView {
|
|||||||
self.externalTrack = source["externalTrack"] as? [String: String]
|
self.externalTrack = source["externalTrack"] as? [String: String]
|
||||||
let initOptions: [String] = source["initOptions"] as? [String] ?? []
|
let initOptions: [String] = source["initOptions"] as? [String] ?? []
|
||||||
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
||||||
|
self.externalSubtitles = source["externalSubtitles"] as? [[String: String]]
|
||||||
|
|
||||||
for item in initOptions {
|
for item in initOptions {
|
||||||
let option = item.components(separatedBy: "=")
|
let option = item.components(separatedBy: "=")
|
||||||
mediaOptions.updateValue(option[1], forKey: option[0].replacingOccurrences(of: "--", with: ""))
|
mediaOptions.updateValue(
|
||||||
|
option[1], forKey: option[0].replacingOccurrences(of: "--", with: ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
||||||
@@ -263,8 +268,8 @@ class VlcPlayerView: ExpoView {
|
|||||||
media.addOptions(mediaOptions)
|
media.addOptions(mediaOptions)
|
||||||
|
|
||||||
self.vlc.player.media = media
|
self.vlc.player.media = media
|
||||||
|
self.setInitialExternalSubtitles()
|
||||||
self.hasSource = true
|
self.hasSource = true
|
||||||
|
|
||||||
if autoplay {
|
if autoplay {
|
||||||
print("Playing...")
|
print("Playing...")
|
||||||
self.play()
|
self.play()
|
||||||
@@ -274,20 +279,28 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func setAudioTrack(_ trackIndex: Int) {
|
@objc func setAudioTrack(_ trackIndex: Int) {
|
||||||
|
print("Setting audio track: \(trackIndex)")
|
||||||
let track = self.vlc.player.audioTracks[trackIndex]
|
let track = self.vlc.player.audioTracks[trackIndex]
|
||||||
track.isSelectedExclusively = true;
|
track.isSelectedExclusively = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getAudioTracks() -> [[String: Any]]? {
|
@objc func getAudioTracks() -> [[String: Any]]? {
|
||||||
return vlc.player.audioTracks.enumerated().map {
|
return vlc.player.audioTracks.enumerated().map {
|
||||||
return ["name": $1.trackName, "index": $0 ]
|
return ["name": $1.trackName, "index": $0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
||||||
print("Debug: Attempting to set subtitle track to index: \(trackIndex)")
|
print("Debug: Attempting to set subtitle track to index: \(trackIndex)")
|
||||||
|
if trackIndex == -1 {
|
||||||
|
print("Debug: Disabling all subtitles")
|
||||||
|
for track in self.vlc.player.textTracks {
|
||||||
|
track.isSelected = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
let track = self.vlc.player.textTracks[trackIndex]
|
let track = self.vlc.player.textTracks[trackIndex]
|
||||||
track.isSelectedExclusively = true;
|
track.isSelectedExclusively = true
|
||||||
print("Debug: Current subtitle track index after setting: \(track.trackName)")
|
print("Debug: Current subtitle track index after setting: \(track.trackName)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,12 +309,11 @@ class VlcPlayerView: ExpoView {
|
|||||||
print("Error: Invalid subtitle URL")
|
print("Error: Invalid subtitle URL")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let result = self.vlc.player.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
let result = self.vlc.player.addPlaybackSlave(url, type: .subtitle, enforce: false)
|
||||||
|
if result == 0 {
|
||||||
if result > 0 {
|
let internalName = "Track \(self.customSubtitles.count)"
|
||||||
let internalName = "Track \(self.customSubtitles.count + 1)"
|
|
||||||
print("Subtitle added with result: \(result) \(internalName)")
|
|
||||||
self.customSubtitles.append((internalName: internalName, originalName: name))
|
self.customSubtitles.append((internalName: internalName, originalName: name))
|
||||||
|
print("Subtitle added with result: \(result) \(internalName)")
|
||||||
} else {
|
} else {
|
||||||
print("Failed to add subtitle")
|
print("Failed to add subtitle")
|
||||||
}
|
}
|
||||||
@@ -313,30 +325,17 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print("Debug: Number of subtitle tracks: \(self.vlc.player.textTracks.count)")
|
print("Debug: Number of subtitle tracks: \(self.vlc.player.textTracks.count)")
|
||||||
|
|
||||||
let tracks = self.vlc.player.textTracks.enumerated().map { (index, track) in
|
let tracks = self.vlc.player.textTracks.enumerated().map { (index, track) in
|
||||||
if let customSubtitle = customSubtitles.first(where: { $0.internalName == track.trackName }) {
|
if let customSubtitle = customSubtitles.first(where: {
|
||||||
return ["name": customSubtitle.originalName, "index": index ]
|
$0.internalName == track.trackName
|
||||||
}
|
}) {
|
||||||
else {
|
return ["name": customSubtitle.originalName, "index": index]
|
||||||
return ["name": track.trackName, "index": index ]
|
} else {
|
||||||
|
return ["name": track.trackName, "index": index]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
print("Debug: Subtitle tracks: \(tracks)")
|
||||||
print("Debug: Subtitle tracks: \(tracks)")
|
return tracks
|
||||||
return tracks
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setSubtitleTrackByName(_ trackName: String) {
|
|
||||||
for track in self.vlc.player.textTracks {
|
|
||||||
if (track.trackName.starts(with: trackName)) {
|
|
||||||
print("Track Index setting to: \(track.trackName)")
|
|
||||||
track.isSelectedExclusively = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Track not found for name: \(trackName)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func stop(completion: (() -> Void)? = nil) {
|
@objc func stop(completion: (() -> Void)? = nil) {
|
||||||
@@ -366,6 +365,19 @@ class VlcPlayerView: ExpoView {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setInitialExternalSubtitles() {
|
||||||
|
if let externalSubtitles = self.externalSubtitles {
|
||||||
|
for subtitle in externalSubtitles {
|
||||||
|
if let subtitleName = subtitle["name"],
|
||||||
|
let subtitleURL = subtitle["DeliveryUrl"]
|
||||||
|
{
|
||||||
|
print("Setting external subtitle: \(subtitleName) \(subtitleURL)")
|
||||||
|
self.setSubtitleURL(subtitleURL, name: subtitleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func performStop(completion: (() -> Void)? = nil) {
|
private func performStop(completion: (() -> Void)? = nil) {
|
||||||
// Stop the media player
|
// Stop the media player
|
||||||
vlc.player.stop()
|
vlc.player.stop()
|
||||||
@@ -387,18 +399,6 @@ class VlcPlayerView: ExpoView {
|
|||||||
let durationMs = self.vlc.player.media?.length.intValue ?? 0
|
let durationMs = self.vlc.player.media?.length.intValue ?? 0
|
||||||
|
|
||||||
print("Debug: Current time: \(currentTimeMs)")
|
print("Debug: Current time: \(currentTimeMs)")
|
||||||
if currentTimeMs >= 0 && currentTimeMs < durationMs {
|
|
||||||
if !self.isMediaReady {
|
|
||||||
self.isMediaReady = true
|
|
||||||
// Set external track subtitle when starting.
|
|
||||||
if let externalTrack = self.externalTrack {
|
|
||||||
if let name = externalTrack["name"], !name.isEmpty {
|
|
||||||
let deliveryUrl = externalTrack["DeliveryUrl"] ?? ""
|
|
||||||
self.setSubtitleURL(deliveryUrl, name: name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.onVideoProgress?([
|
self.onVideoProgress?([
|
||||||
"currentTime": currentTimeMs,
|
"currentTime": currentTimeMs,
|
||||||
"duration": durationMs,
|
"duration": durationMs,
|
||||||
@@ -414,7 +414,7 @@ class VlcPlayerView: ExpoView {
|
|||||||
"error": false,
|
"error": false,
|
||||||
"isPlaying": player.isPlaying,
|
"isPlaying": player.isPlaying,
|
||||||
"isBuffering": !player.isPlaying && player.state == VLCMediaPlayerState.buffering,
|
"isBuffering": !player.isPlaying && player.state == VLCMediaPlayerState.buffering,
|
||||||
"state": player.state.description
|
"state": player.state.description,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export type VlcPlayerSource = {
|
|||||||
type?: string;
|
type?: string;
|
||||||
isNetwork?: boolean;
|
isNetwork?: boolean;
|
||||||
autoplay?: boolean;
|
autoplay?: boolean;
|
||||||
externalTrack?: { name: string, DeliveryUrl: string };
|
externalSubtitles: { name: string; DeliveryUrl: string }[];
|
||||||
initOptions?: any[];
|
initOptions?: any[];
|
||||||
mediaOptions?: { [key: string]: any };
|
mediaOptions?: { [key: string]: any };
|
||||||
startPosition?: number;
|
startPosition?: number;
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { TranscodedSubtitle } from "@/components/video-player/controls/types";
|
|
||||||
import { TrackInfo } from "@/modules/vlc-player";
|
|
||||||
import { MediaStream } from "@jellyfin/sdk/lib/generated-client";
|
|
||||||
import { Platform } from "react-native";
|
|
||||||
|
|
||||||
const disableSubtitle = {
|
|
||||||
name: "Disable",
|
|
||||||
index: -1,
|
|
||||||
IsTextSubtitleStream: true,
|
|
||||||
} as TranscodedSubtitle;
|
|
||||||
|
|
||||||
export class SubtitleHelper {
|
|
||||||
private mediaStreams: MediaStream[];
|
|
||||||
|
|
||||||
constructor(mediaStreams: MediaStream[]) {
|
|
||||||
this.mediaStreams = mediaStreams.filter((x) => x.Type === "Subtitle");
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubtitles(): MediaStream[] {
|
|
||||||
return this.mediaStreams;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUniqueSubtitles(): MediaStream[] {
|
|
||||||
const uniqueSubs: MediaStream[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
|
|
||||||
this.mediaStreams.forEach((x) => {
|
|
||||||
if (!seen.has(x.DisplayTitle!)) {
|
|
||||||
seen.add(x.DisplayTitle!);
|
|
||||||
uniqueSubs.push(x);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return uniqueSubs;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentSubtitle(subtitleIndex?: number): MediaStream | undefined {
|
|
||||||
return this.mediaStreams.find((x) => x.Index === subtitleIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMostCommonSubtitleByName(
|
|
||||||
subtitleIndex: number | undefined
|
|
||||||
): number | undefined {
|
|
||||||
if (subtitleIndex === undefined) -1;
|
|
||||||
const uniqueSubs = this.getUniqueSubtitles();
|
|
||||||
const currentSub = this.getCurrentSubtitle(subtitleIndex);
|
|
||||||
|
|
||||||
return uniqueSubs.find((x) => x.DisplayTitle === currentSub?.DisplayTitle)
|
|
||||||
?.Index;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTextSubtitles(): MediaStream[] {
|
|
||||||
return this.mediaStreams.filter((x) => x.IsTextSubtitleStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
getImageSubtitles(): MediaStream[] {
|
|
||||||
return this.mediaStreams.filter((x) => !x.IsTextSubtitleStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmbeddedTrackIndex(sourceSubtitleIndex: number): number {
|
|
||||||
if (Platform.OS === "android") {
|
|
||||||
const textSubs = this.getTextSubtitles();
|
|
||||||
const matchingSubtitle = textSubs.find(
|
|
||||||
(sub) => sub.Index === sourceSubtitleIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!matchingSubtitle) return -1;
|
|
||||||
return textSubs.indexOf(matchingSubtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get unique text-based subtitles because react-native-video removes hls text tracks duplicates. (iOS)
|
|
||||||
const uniqueTextSubs = this.getUniqueTextBasedSubtitles();
|
|
||||||
const matchingSubtitle = uniqueTextSubs.find(
|
|
||||||
(sub) => sub.Index === sourceSubtitleIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!matchingSubtitle) return -1;
|
|
||||||
return uniqueTextSubs.indexOf(matchingSubtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
sortSubtitles(
|
|
||||||
textSubs: TranscodedSubtitle[],
|
|
||||||
allSubs: MediaStream[]
|
|
||||||
): TranscodedSubtitle[] {
|
|
||||||
let textIndex = 0; // To track position in textSubtitles
|
|
||||||
// Merge text and image subtitles in the order of allSubs
|
|
||||||
const sortedSubtitles = allSubs.map((sub) => {
|
|
||||||
if (sub.IsTextSubtitleStream) {
|
|
||||||
if (textSubs.length === 0) return disableSubtitle;
|
|
||||||
const textSubtitle = textSubs[textIndex];
|
|
||||||
if (!textSubtitle) return disableSubtitle;
|
|
||||||
textIndex++;
|
|
||||||
return textSubtitle;
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
name: sub.DisplayTitle!,
|
|
||||||
index: sub.Index!,
|
|
||||||
IsTextSubtitleStream: sub.IsTextSubtitleStream,
|
|
||||||
} as TranscodedSubtitle;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortedSubtitles;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSortedSubtitles(subtitleTracks: TrackInfo[]): TranscodedSubtitle[] {
|
|
||||||
const textSubtitles =
|
|
||||||
subtitleTracks.map((s) => ({
|
|
||||||
name: s.name,
|
|
||||||
index: s.index,
|
|
||||||
IsTextSubtitleStream: true,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const sortedSubs =
|
|
||||||
Platform.OS === "android"
|
|
||||||
? this.sortSubtitles(textSubtitles, this.mediaStreams)
|
|
||||||
: this.sortSubtitles(textSubtitles, this.getUniqueSubtitles());
|
|
||||||
|
|
||||||
return sortedSubs;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUniqueTextBasedSubtitles(): MediaStream[] {
|
|
||||||
return this.getUniqueSubtitles().filter((x) => x.IsTextSubtitleStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HLS stream indexes are not the same as the actual source indexes.
|
|
||||||
// This function aims to get the source subtitle index from the embedded track index.
|
|
||||||
getSourceSubtitleIndex = (embeddedTrackIndex: number): number => {
|
|
||||||
if (Platform.OS === "android") {
|
|
||||||
return this.getTextSubtitles()[embeddedTrackIndex]?.Index ?? -1;
|
|
||||||
}
|
|
||||||
return this.getUniqueTextBasedSubtitles()[embeddedTrackIndex]?.Index ?? -1;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -111,15 +111,6 @@ export const getStreamUrl = async ({
|
|||||||
if (mediaSource?.TranscodingUrl) {
|
if (mediaSource?.TranscodingUrl) {
|
||||||
const urlObj = new URL(api.basePath + mediaSource?.TranscodingUrl); // Create a URL object
|
const urlObj = new URL(api.basePath + mediaSource?.TranscodingUrl); // Create a URL object
|
||||||
|
|
||||||
// If there is no subtitle stream index, add it to the URL.
|
|
||||||
if (subtitleStreamIndex == -1) {
|
|
||||||
urlObj.searchParams.set("SubtitleMethod", "Hls");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add 'SubtitleMethod=Hls' if it doesn't exist
|
|
||||||
if (!urlObj.searchParams.has("SubtitleMethod")) {
|
|
||||||
urlObj.searchParams.append("SubtitleMethod", "Hls");
|
|
||||||
}
|
|
||||||
// Get the updated URL
|
// Get the updated URL
|
||||||
const transcodeUrl = urlObj.toString();
|
const transcodeUrl = urlObj.toString();
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,9 @@ export default {
|
|||||||
Type: MediaTypes.Video,
|
Type: MediaTypes.Video,
|
||||||
Context: "Streaming",
|
Context: "Streaming",
|
||||||
Protocol: "hls",
|
Protocol: "hls",
|
||||||
Container: "ts",
|
Container: "fmp4",
|
||||||
VideoCodec: "h264, hevc",
|
VideoCodec: "h264, hevc",
|
||||||
AudioCodec: "aac,mp3,ac3",
|
AudioCodec: "aac,mp3,ac3,dts",
|
||||||
CopyTimestamps: false,
|
|
||||||
EnableSubtitlesInManifest: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Type: MediaTypes.Audio,
|
Type: MediaTypes.Audio,
|
||||||
@@ -58,131 +56,81 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
SubtitleProfiles: [
|
SubtitleProfiles: [
|
||||||
// Official foramts
|
// Official formats
|
||||||
{ Format: "vtt", Method: "Embed" },
|
{ Format: "vtt", Method: "Embed" },
|
||||||
{ Format: "vtt", Method: "Hls" },
|
|
||||||
{ Format: "vtt", Method: "External" },
|
{ Format: "vtt", Method: "External" },
|
||||||
{ Format: "vtt", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "webvtt", Method: "Embed" },
|
{ Format: "webvtt", Method: "Embed" },
|
||||||
{ Format: "webvtt", Method: "Hls" },
|
|
||||||
{ Format: "webvtt", Method: "External" },
|
{ Format: "webvtt", Method: "External" },
|
||||||
{ Format: "webvtt", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "srt", Method: "Embed" },
|
{ Format: "srt", Method: "Embed" },
|
||||||
{ Format: "srt", Method: "Hls" },
|
|
||||||
{ Format: "srt", Method: "External" },
|
{ Format: "srt", Method: "External" },
|
||||||
{ Format: "srt", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "subrip", Method: "Embed" },
|
{ Format: "subrip", Method: "Embed" },
|
||||||
{ Format: "subrip", Method: "Hls" },
|
|
||||||
{ Format: "subrip", Method: "External" },
|
{ Format: "subrip", Method: "External" },
|
||||||
{ Format: "subrip", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "ttml", Method: "Embed" },
|
{ Format: "ttml", Method: "Embed" },
|
||||||
{ Format: "ttml", Method: "Hls" },
|
|
||||||
{ Format: "ttml", Method: "External" },
|
{ Format: "ttml", Method: "External" },
|
||||||
{ Format: "ttml", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "dvbsub", Method: "Embed" },
|
{ Format: "dvbsub", Method: "Embed" },
|
||||||
{ Format: "dvbsub", Method: "Hls" },
|
|
||||||
{ Format: "dvbsub", Method: "External" },
|
|
||||||
{ Format: "dvdsub", Method: "Encode" },
|
{ Format: "dvdsub", Method: "Encode" },
|
||||||
|
|
||||||
{ Format: "ass", Method: "Embed" },
|
{ Format: "ass", Method: "Embed" },
|
||||||
{ Format: "ass", Method: "Hls" },
|
|
||||||
{ Format: "ass", Method: "External" },
|
{ Format: "ass", Method: "External" },
|
||||||
{ Format: "ass", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "idx", Method: "Embed" },
|
{ Format: "idx", Method: "Embed" },
|
||||||
{ Format: "idx", Method: "Hls" },
|
|
||||||
{ Format: "idx", Method: "External" },
|
|
||||||
{ Format: "idx", Method: "Encode" },
|
{ Format: "idx", Method: "Encode" },
|
||||||
|
|
||||||
{ Format: "pgs", Method: "Embed" },
|
{ Format: "pgs", Method: "Embed" },
|
||||||
{ Format: "pgs", Method: "Hls" },
|
|
||||||
{ Format: "pgs", Method: "External" },
|
|
||||||
{ Format: "pgs", Method: "Encode" },
|
{ Format: "pgs", Method: "Encode" },
|
||||||
|
|
||||||
{ Format: "pgssub", Method: "Embed" },
|
{ Format: "pgssub", Method: "Embed" },
|
||||||
{ Format: "pgssub", Method: "Hls" },
|
|
||||||
{ Format: "pgssub", Method: "External" },
|
|
||||||
{ Format: "pgssub", Method: "Encode" },
|
{ Format: "pgssub", Method: "Encode" },
|
||||||
|
|
||||||
{ Format: "ssa", Method: "Embed" },
|
{ Format: "ssa", Method: "Embed" },
|
||||||
{ Format: "ssa", Method: "Hls" },
|
|
||||||
{ Format: "ssa", Method: "External" },
|
{ Format: "ssa", Method: "External" },
|
||||||
{ Format: "ssa", Method: "Encode" },
|
|
||||||
|
|
||||||
// Other formats
|
// Other formats
|
||||||
{ Format: "microdvd", Method: "Embed" },
|
{ Format: "microdvd", Method: "Embed" },
|
||||||
{ Format: "microdvd", Method: "Hls" },
|
|
||||||
{ Format: "microdvd", Method: "External" },
|
{ Format: "microdvd", Method: "External" },
|
||||||
{ Format: "microdvd", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "mov_text", Method: "Embed" },
|
{ Format: "mov_text", Method: "Embed" },
|
||||||
{ Format: "mov_text", Method: "Hls" },
|
|
||||||
{ Format: "mov_text", Method: "External" },
|
{ Format: "mov_text", Method: "External" },
|
||||||
{ Format: "mov_text", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "mpl2", Method: "Embed" },
|
{ Format: "mpl2", Method: "Embed" },
|
||||||
{ Format: "mpl2", Method: "Hls" },
|
|
||||||
{ Format: "mpl2", Method: "External" },
|
{ Format: "mpl2", Method: "External" },
|
||||||
{ Format: "mpl2", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "pjs", Method: "Embed" },
|
{ Format: "pjs", Method: "Embed" },
|
||||||
{ Format: "pjs", Method: "Hls" },
|
|
||||||
{ Format: "pjs", Method: "External" },
|
{ Format: "pjs", Method: "External" },
|
||||||
{ Format: "pjs", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "realtext", Method: "Embed" },
|
{ Format: "realtext", Method: "Embed" },
|
||||||
{ Format: "realtext", Method: "Hls" },
|
|
||||||
{ Format: "realtext", Method: "External" },
|
{ Format: "realtext", Method: "External" },
|
||||||
{ Format: "realtext", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "scc", Method: "Embed" },
|
{ Format: "scc", Method: "Embed" },
|
||||||
{ Format: "scc", Method: "Hls" },
|
|
||||||
{ Format: "scc", Method: "External" },
|
{ Format: "scc", Method: "External" },
|
||||||
{ Format: "scc", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "smi", Method: "Embed" },
|
{ Format: "smi", Method: "Embed" },
|
||||||
{ Format: "smi", Method: "Hls" },
|
|
||||||
{ Format: "smi", Method: "External" },
|
{ Format: "smi", Method: "External" },
|
||||||
{ Format: "smi", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "stl", Method: "Embed" },
|
{ Format: "stl", Method: "Embed" },
|
||||||
{ Format: "stl", Method: "Hls" },
|
|
||||||
{ Format: "stl", Method: "External" },
|
{ Format: "stl", Method: "External" },
|
||||||
{ Format: "stl", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "sub", Method: "Embed" },
|
{ Format: "sub", Method: "Embed" },
|
||||||
{ Format: "sub", Method: "Hls" },
|
|
||||||
{ Format: "sub", Method: "External" },
|
{ Format: "sub", Method: "External" },
|
||||||
{ Format: "sub", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "subviewer", Method: "Embed" },
|
{ Format: "subviewer", Method: "Embed" },
|
||||||
{ Format: "subviewer", Method: "Hls" },
|
|
||||||
{ Format: "subviewer", Method: "External" },
|
{ Format: "subviewer", Method: "External" },
|
||||||
{ Format: "subviewer", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "teletext", Method: "Embed" },
|
{ Format: "teletext", Method: "Embed" },
|
||||||
{ Format: "teletext", Method: "Hls" },
|
|
||||||
{ Format: "teletext", Method: "External" },
|
|
||||||
{ Format: "teletext", Method: "Encode" },
|
{ Format: "teletext", Method: "Encode" },
|
||||||
|
|
||||||
{ Format: "text", Method: "Embed" },
|
{ Format: "text", Method: "Embed" },
|
||||||
{ Format: "text", Method: "Hls" },
|
|
||||||
{ Format: "text", Method: "External" },
|
{ Format: "text", Method: "External" },
|
||||||
{ Format: "text", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "vplayer", Method: "Embed" },
|
{ Format: "vplayer", Method: "Embed" },
|
||||||
{ Format: "vplayer", Method: "Hls" },
|
|
||||||
{ Format: "vplayer", Method: "External" },
|
{ Format: "vplayer", Method: "External" },
|
||||||
{ Format: "vplayer", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "xsub", Method: "Embed" },
|
{ Format: "xsub", Method: "Embed" },
|
||||||
{ Format: "xsub", Method: "Hls" },
|
|
||||||
{ Format: "xsub", Method: "External" },
|
{ Format: "xsub", Method: "External" },
|
||||||
{ Format: "xsub", Method: "Encode" },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
/**
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
import MediaTypes from "../../constants/MediaTypes";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
Name: "Vlc Player for HLS streams.",
|
|
||||||
MaxStaticBitrate: 20_000_000,
|
|
||||||
MaxStreamingBitrate: 12_000_000,
|
|
||||||
CodecProfiles: [
|
|
||||||
{
|
|
||||||
Type: MediaTypes.Video,
|
|
||||||
Codec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: MediaTypes.Audio,
|
|
||||||
Codec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,pcm,wma",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
DirectPlayProfiles: [
|
|
||||||
{
|
|
||||||
Type: MediaTypes.Video,
|
|
||||||
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
|
|
||||||
VideoCodec:
|
|
||||||
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
|
|
||||||
AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: MediaTypes.Audio,
|
|
||||||
Container: "mp3,aac,flac,alac,wav,ogg,wma",
|
|
||||||
AudioCodec:
|
|
||||||
"mp3,aac,flac,alac,opus,vorbis,wma,pcm,mpa,wav,ogg,oga,webma,ape",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
TranscodingProfiles: [
|
|
||||||
{
|
|
||||||
Type: MediaTypes.Video,
|
|
||||||
Context: "Streaming",
|
|
||||||
Protocol: "hls",
|
|
||||||
Container: "fmp4",
|
|
||||||
VideoCodec: "h264, hevc",
|
|
||||||
AudioCodec: "aac,mp3,ac3",
|
|
||||||
CopyTimestamps: false,
|
|
||||||
EnableSubtitlesInManifest: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: MediaTypes.Audio,
|
|
||||||
Context: "Streaming",
|
|
||||||
Protocol: "http",
|
|
||||||
Container: "mp3",
|
|
||||||
AudioCodec: "mp3",
|
|
||||||
MaxAudioChannels: "2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
SubtitleProfiles: [
|
|
||||||
// Text based subtitles must use HLS.
|
|
||||||
{ Format: "ass", Method: "Hls" },
|
|
||||||
{ Format: "microdvd", Method: "Hls" },
|
|
||||||
{ Format: "mov_text", Method: "Hls" },
|
|
||||||
{ Format: "mpl2", Method: "Hls" },
|
|
||||||
{ Format: "pjs", Method: "Hls" },
|
|
||||||
{ Format: "realtext", Method: "Hls" },
|
|
||||||
{ Format: "scc", Method: "Hls" },
|
|
||||||
{ Format: "smi", Method: "Hls" },
|
|
||||||
{ Format: "srt", Method: "Hls" },
|
|
||||||
{ Format: "ssa", Method: "Hls" },
|
|
||||||
{ Format: "stl", Method: "Hls" },
|
|
||||||
{ Format: "sub", Method: "Hls" },
|
|
||||||
{ Format: "subrip", Method: "Hls" },
|
|
||||||
{ Format: "subviewer", Method: "Hls" },
|
|
||||||
{ Format: "teletext", Method: "Hls" },
|
|
||||||
{ Format: "text", Method: "Hls" },
|
|
||||||
{ Format: "ttml", Method: "Hls" },
|
|
||||||
{ Format: "vplayer", Method: "Hls" },
|
|
||||||
{ Format: "vtt", Method: "Hls" },
|
|
||||||
{ Format: "webvtt", Method: "Hls" },
|
|
||||||
|
|
||||||
// Image based subs use encode.
|
|
||||||
{ Format: "dvdsub", Method: "Encode" },
|
|
||||||
{ Format: "pgs", Method: "Encode" },
|
|
||||||
{ Format: "pgssub", Method: "Encode" },
|
|
||||||
{ Format: "xsub", Method: "Encode" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user