diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 27d7ddd9..8f722302 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -385,14 +385,12 @@ export default function page() { } const insets = useSafeAreaInsets(); - - if (!item || isLoadingItem || isLoadingStream) - useEffect(() => { - const beforeRemoveListener = navigation.addListener("beforeRemove", stop); - return () => { - beforeRemoveListener(); - }; - }, [navigation]); + useEffect(() => { + const beforeRemoveListener = navigation.addListener("beforeRemove", stop); + return () => { + beforeRemoveListener(); + }; + }, [navigation]); if (!item || isLoadingItem || !stream) return ( 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/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/contexts/VideoContext.tsx b/components/video-player/controls/contexts/VideoContext.tsx index 1fef149e..7a4e5161 100644 --- a/components/video-player/controls/contexts/VideoContext.tsx +++ b/components/video-player/controls/contexts/VideoContext.tsx @@ -9,6 +9,7 @@ import React, { useState, ReactNode, useEffect, + useMemo, } from "react"; import { useControlContext } from "./ControlContext"; import { Track } from "../types"; @@ -57,10 +58,6 @@ export const VideoProvider: React.FC = ({ const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; - const onTextBasedSubtitle = allSubs.find( - (s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream - ); - const { itemId, audioIndex, bitrateValue, subtitleIndex } = useLocalSearchParams<{ itemId: string; @@ -70,6 +67,14 @@ export const VideoProvider: React.FC = ({ bitrateValue: string; }>(); + const onTextBasedSubtitle = useMemo( + () => + allSubs.find( + (s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream + ) || subtitleIndex === "-1", + [allSubs, subtitleIndex] + ); + const setPlayerParams = ({ chosenAudioIndex = audioIndex, chosenSubtitleIndex = subtitleIndex, @@ -98,10 +103,13 @@ export const VideoProvider: React.FC = ({ const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack; const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex"; - // If we're transcoding and we're going from a text based subtitle - // to an image based subtitle, we need to change the player params. + // 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; + type === "subtitle" && + mediaSource?.TranscodingUrl && + !onTextBasedSubtitle; console.log("Set player params", index, serverIndex); if (shouldChangePlayerParams) { @@ -118,13 +126,8 @@ export const VideoProvider: React.FC = ({ useEffect(() => { const fetchTracks = async () => { - if ( - getSubtitleTracks && - (subtitleTracks === null || subtitleTracks.length === 0) - ) { + if (getSubtitleTracks) { const subtitleData = await getSubtitleTracks(); - // Means subtitles are still loading. - if (!subtitleData) return; let textSubIndex = 0; const subtitles: Track[] = allSubs?.map((sub) => { @@ -135,12 +138,14 @@ export const VideoProvider: React.FC = ({ 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) @@ -160,7 +165,6 @@ export const VideoProvider: React.FC = ({ : setPlayerParams({ chosenSubtitleIndex: "-1" }), }); - console.log("subtitles", subtitles); setSubtitleTracks(subtitles); } if ( diff --git a/utils/SubtitleHelper.ts b/utils/SubtitleHelper.ts deleted file mode 100644 index e060f271..00000000 --- a/utils/SubtitleHelper.ts +++ /dev/null @@ -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(); - - 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; - }; -}