diff --git a/app/(auth)/player/_layout.tsx b/app/(auth)/player/_layout.tsx
index 1d6c7188..bee527e5 100644
--- a/app/(auth)/player/_layout.tsx
+++ b/app/(auth)/player/_layout.tsx
@@ -36,15 +36,6 @@ export default function Layout() {
animation: "fade",
}}
/>
-
>
);
diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx
index 51a5f54d..8f722302 100644
--- a/app/(auth)/player/direct-player.tsx
+++ b/app/(auth)/player/direct-player.tsx
@@ -36,17 +36,12 @@ import React, {
useState,
useEffect,
} from "react";
-import {
- Alert,
- View,
- AppState,
- AppStateStatus,
- Platform,
-} from "react-native";
+import { Alert, View, AppState, AppStateStatus, Platform } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import { useSettings } from "@/utils/atoms/settings";
import { useTranslation } from "react-i18next";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client";
export default function page() {
console.log("Direct Player");
@@ -128,57 +123,80 @@ export default function page() {
staleTime: 0,
});
- const {
- data: stream,
- isLoading: isLoadingStreamUrl,
- isError: isErrorStreamUrl,
- } = useQuery({
- queryKey: ["stream-url", itemId, mediaSourceId, bitrateValue],
- queryFn: async () => {
- if (offline && !Platform.isTV) {
- const data = await getDownloadedItem.getDownloadedItem(itemId);
- if (!data?.mediaSource) return null;
+ const [stream, setStream] = useState<{
+ mediaSource: MediaSourceInfo;
+ url: string;
+ sessionId: string | undefined;
+ } | null>(null);
+ const [isLoadingStream, setIsLoadingStream] = useState(true);
+ const [isErrorStream, setIsErrorStream] = useState(false);
- const url = await getDownloadedFileUrl(data.item.Id!);
+ useEffect(() => {
+ const fetchStream = async () => {
+ setIsLoadingStream(true);
+ setIsErrorStream(false);
- if (item)
- return {
- mediaSource: data.mediaSource,
- url,
- sessionId: undefined,
- };
+ try {
+ if (offline && !Platform.isTV) {
+ const data = await getDownloadedItem.getDownloadedItem(itemId);
+ if (!data?.mediaSource) {
+ 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({
- api,
- 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,
- });
+ fetchStream();
+ }, [itemId, mediaSourceId]);
const togglePlay = useCallback(async () => {
if (!api) return;
@@ -198,9 +216,7 @@ export default function page() {
mediaSourceId: mediaSourceId,
positionTicks: msToTicks(progress.get()),
isPaused: !isPlaying,
- playMethod: stream?.url.includes("m3u8")
- ? "Transcode"
- : "DirectStream",
+ playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
playSessionId: stream.sessionId,
});
}
@@ -238,21 +254,6 @@ export default function page() {
videoRef.current?.stop();
}, [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(
async (data: ProgressUpdatePayload) => {
if (isSeeking.get() || isPlaybackStopped) return;
@@ -294,8 +295,8 @@ export default function page() {
const onPipStarted = useCallback((e: PipStartedPayload) => {
const { pipStarted } = e.nativeEvent;
- setIsPipStarted(pipStarted)
- }, [])
+ setIsPipStarted(pipStarted);
+ }, []);
const onPlaybackStateChanged = useCallback((e: PlaybackStatePayload) => {
const { state, isBuffering, isPlaying } = e.nativeEvent;
@@ -332,7 +333,7 @@ export default function page() {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
// Handle app going to the background
if (nextAppState.match(/inactive|background/)) {
- _setShowControls(false)
+ _setShowControls(false);
}
setAppState(nextAppState);
};
@@ -351,73 +352,67 @@ export default function page() {
// Preselection of audio and subtitle tracks.
if (!settings) return null;
-
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 =
stream?.mediaSource.MediaStreams?.filter(
- (audio: { Type: string }) => audio.Type === "Audio"
+ (audio) => audio.Type === "Audio"
) || [];
- const chosenAudioTrack = allAudio.find(
- (audio: { Index: number | undefined }) => audio.Index === audioIndex
+ const allSubs =
+ 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
- if (!bitrateValue) {
- // If Subtitle is embedded we can use the position to select it straight away.
- if (chosenSubtitleTrack && !chosenSubtitleTrack.DeliveryUrl) {
- initOptions.push(`--sub-track=${allSubs.indexOf(chosenSubtitleTrack)}`);
- } else if (chosenSubtitleTrack && chosenSubtitleTrack.DeliveryUrl) {
- // If Subtitle is external we need to pass the URL to the player.
- externalTrack = {
- name: chosenSubtitleTrack.DisplayTitle || "",
- DeliveryUrl: `${api?.basePath || ""}${chosenSubtitleTrack.DeliveryUrl}`,
- };
- }
+ const notTranscoding = !stream?.mediaSource.TranscodingUrl;
+ if (
+ chosenSubtitleTrack &&
+ (notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
+ ) {
+ const finalIndex = notTranscoding
+ ? allSubs.indexOf(chosenSubtitleTrack)
+ : textSubs.indexOf(chosenSubtitleTrack);
+ initOptions.push(`--sub-track=${finalIndex}`);
+ }
- if (chosenAudioTrack)
- initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
- } else {
- // Transcoded playback CASE
- if (chosenSubtitleTrack?.DeliveryMethod === "Hls") {
- externalTrack = {
- name: `subs ${chosenSubtitleTrack.DisplayTitle}`,
- DeliveryUrl: "",
- };
- }
+ if (notTranscoding && chosenAudioTrack) {
+ initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
}
const insets = useSafeAreaInsets();
-
useEffect(() => {
- const beforeRemoveListener = navigation.addListener('beforeRemove', stop);
+ const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
return () => {
beforeRemoveListener();
};
}, [navigation]);
- if (!item || isLoadingItem || isLoadingStreamUrl || !stream)
+ if (!item || isLoadingItem || !stream)
return (
);
- if (isErrorItem || isErrorStreamUrl)
+ if (isErrorItem || isErrorStream)
return (
{t("player.error")}
);
+ const externalSubtitles = allSubs
+ .filter((sub: any) => sub.DeliveryMethod === "External")
+ .map((sub: any) => ({
+ name: sub.DisplayTitle,
+ DeliveryUrl: api?.basePath + sub.DeliveryUrl,
+ }));
+
return (
);
-}
\ No newline at end of file
+}
diff --git a/app/(auth)/player/transcoding-player.tsx b/app/(auth)/player/transcoding-player.tsx
deleted file mode 100644
index b1768b36..00000000
--- a/app/(auth)/player/transcoding-player.tsx
+++ /dev/null
@@ -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(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([]);
- 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 (
-
-
-
- );
-
- if (isErrorItem || isErrorStreamUrl)
- return (
-
- {t("player.error")}
-
- );
-
- return (
-
-
- {videoSource ? (
- <>
-
-
- {item && (
- {
- 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,
- });
- }}
- />
- )}
-
- );
-};
-
-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;
diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx
index f39db05f..39aa1660 100644
--- a/components/ItemContent.tsx
+++ b/components/ItemContent.tsx
@@ -16,7 +16,6 @@ import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColors } from "@/hooks/useImageColors";
import { useOrientation } from "@/hooks/useOrientation";
import { apiAtom } from "@/providers/JellyfinProvider";
-import { SubtitleHelper } from "@/utils/SubtitleHelper";
import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import {
@@ -118,37 +117,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
const loading = useMemo(() => {
return Boolean(logoUrl && loadingLogo);
}, [loadingLogo, logoUrl]);
-
- const [isTranscoding, setIsTranscoding] = useState(false);
- const [previouslyChosenSubtitleIndex, setPreviouslyChosenSubtitleIndex] =
- useState(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;
return (
@@ -239,7 +207,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
selected={selectedOptions.audioIndex}
/>
setSelectedOptions(
diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx
index a9195aa6..4a6a2e30 100644
--- a/components/PlayButton.tsx
+++ b/components/PlayButton.tsx
@@ -73,11 +73,7 @@ export const PlayButton: React.FC = ({
const goToPlayer = useCallback(
(q: string, bitrateValue: number | undefined) => {
- if (!bitrateValue) {
- router.push(`/player/direct-player?${q}`);
- return;
- }
- router.push(`/player/transcoding-player?${q}`);
+ router.push(`/player/direct-player?${q}`);
},
[router]
);
diff --git a/components/PlayButton.tv.tsx b/components/PlayButton.tv.tsx
index 65f70ad3..128c2184 100644
--- a/components/PlayButton.tv.tsx
+++ b/components/PlayButton.tv.tsx
@@ -58,11 +58,7 @@ export const PlayButton: React.FC = ({
const goToPlayer = useCallback(
(q: string, bitrateValue: number | undefined) => {
- if (!bitrateValue) {
- router.push(`/player/direct-player?${q}`);
- return;
- }
- router.push(`/player/transcoding-player?${q}`);
+ router.push(`/player/direct-player?${q}`);
},
[router]
);
diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx
index 9b864f29..77e26c1b 100644
--- a/components/SubtitleTrackSelector.tsx
+++ b/components/SubtitleTrackSelector.tsx
@@ -4,40 +4,31 @@ import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { Text } from "./common/Text";
-import { SubtitleHelper } from "@/utils/SubtitleHelper";
import { useTranslation } from "react-i18next";
interface Props extends React.ComponentProps {
source?: MediaSourceInfo;
onChange: (value: number) => void;
selected?: number | undefined;
- isTranscoding?: boolean;
}
export const SubtitleTrackSelector: React.FC = ({
source,
onChange,
selected,
- isTranscoding,
...props
}) => {
if (Platform.isTV) return null;
const subtitleStreams = useMemo(() => {
- const subtitleHelper = new SubtitleHelper(source?.MediaStreams ?? []);
-
- if (isTranscoding && Platform.OS === "ios") {
- return subtitleHelper.getUniqueSubtitles();
- }
-
- return subtitleHelper.getSubtitles();
- }, [source, isTranscoding]);
+ return source?.MediaStreams?.filter((x) => x.Type === "Subtitle");
+ }, [source]);
const selectedSubtitleSteam = useMemo(
- () => subtitleStreams.find((x) => x.Index === selected),
+ () => subtitleStreams?.find((x) => x.Index === selected),
[subtitleStreams, selected]
);
- if (subtitleStreams.length === 0) return null;
+ if (subtitleStreams?.length === 0) return null;
const { t } = useTranslation();
diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx
index b52e98c7..b8453ea5 100644
--- a/components/video-player/controls/Controls.tsx
+++ b/components/video-player/controls/Controls.tsx
@@ -24,7 +24,7 @@ import {
ticksToMs,
ticksToSeconds,
} from "@/utils/time";
-import {Ionicons, MaterialIcons} from "@expo/vector-icons";
+import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import {
BaseItemDto,
MediaSourceInfo,
@@ -35,7 +35,12 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useAtom } from "jotai";
import { debounce } from "lodash";
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 {
runOnJS,
@@ -49,8 +54,7 @@ import AudioSlider from "./AudioSlider";
import BrightnessSlider from "./BrightnessSlider";
import { ControlProvider } from "./contexts/ControlContext";
import { VideoProvider } from "./contexts/VideoContext";
-import DropdownViewDirect from "./dropdown/DropdownViewDirect";
-import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding";
+import DropdownView from "./dropdown/DropdownView";
import { EpisodeList } from "./EpisodeList";
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
import SkipButton from "./SkipButton";
@@ -214,15 +218,10 @@ export const Controls: React.FC = ({
bitrateValue: bitrateValue.toString(),
}).toString();
- stop()
+ stop();
- if (!bitrateValue) {
- // @ts-expect-error
- router.replace(`player/direct-player?${queryParams}`);
- return;
- }
// @ts-expect-error
- router.replace(`player/transcoding-player?${queryParams}`);
+ router.replace(`player/direct-player?${queryParams}`);
}, [previousItem, settings, subtitleIndex, audioIndex]);
const goToNextItem = useCallback(() => {
@@ -254,15 +253,10 @@ export const Controls: React.FC = ({
bitrateValue: bitrateValue.toString(),
}).toString();
- stop()
+ stop();
- if (!bitrateValue) {
- // @ts-expect-error
- router.replace(`player/direct-player?${queryParams}`);
- return;
- }
// @ts-expect-error
- router.replace(`player/transcoding-player?${queryParams}`);
+ router.replace(`player/direct-player?${queryParams}`);
}, [nextItem, settings, subtitleIndex, audioIndex]);
const updateTimes = useCallback(
@@ -419,15 +413,10 @@ export const Controls: React.FC = ({
bitrateValue: bitrateValue.toString(),
}).toString();
- stop()
+ stop();
- if (!bitrateValue) {
- // @ts-expect-error
- router.replace(`player/direct-player?${queryParams}`);
- return;
- }
// @ts-expect-error
- router.replace(`player/transcoding-player?${queryParams}`);
+ router.replace(`player/direct-player?${queryParams}`);
} catch (error) {
console.error("Error in gotoEpisode:", error);
}
@@ -508,7 +497,7 @@ export const Controls: React.FC = ({
}, [trickPlayUrl, trickplayInfo, time]);
const onClose = async () => {
- stop()
+ stop();
lightHapticFeedback();
await ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP
@@ -559,19 +548,13 @@ export const Controls: React.FC = ({
setSubtitleTrack={setSubtitleTrack}
setSubtitleURL={setSubtitleURL}
>
- {!mediaSource?.TranscodingUrl ? (
-
- ) : (
-
- )}
+
{!Platform.isTV && (
-
+
void) | undefined;
setSubtitleTrack: ((index: number) => void) | undefined;
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
@@ -45,30 +48,155 @@ export const VideoProvider: React.FC = ({
setSubtitleURL,
setAudioTrack,
}) => {
- const [audioTracks, setAudioTracks] = useState(null);
- const [subtitleTracks, setSubtitleTracks] = useState(
- null
- );
+ const [audioTracks, setAudioTracks] = useState