From a9b1d9fb0a0d6e99434d6c27b3bb4247308dffe9 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Fri, 13 Dec 2024 05:03:16 +1100 Subject: [PATCH] Added bandaid fix --- app/(auth)/player/transcoding-player.tsx | 30 ++-- components/ItemContent.tsx | 33 +++++ components/SubtitleTrackSelector.tsx | 18 ++- .../dropdown/DropdownViewTranscoding.tsx | 49 ++----- utils/SubtitleHelper.ts | 134 ++++++++++++++++++ utils/profiles/transcoding.js | 6 +- 6 files changed, 205 insertions(+), 65 deletions(-) create mode 100644 utils/SubtitleHelper.ts diff --git a/app/(auth)/player/transcoding-player.tsx b/app/(auth)/player/transcoding-player.tsx index b72a9185..9be90a51 100644 --- a/app/(auth)/player/transcoding-player.tsx +++ b/app/(auth)/player/transcoding-player.tsx @@ -39,6 +39,7 @@ import Video, { VideoRef, } from "react-native-video"; import index from "../(tabs)/(home)"; +import { SubtitleHelper } from "@/utils/SubtitleHelper"; const Player = () => { const api = useAtomValue(apiAtom); @@ -112,12 +113,14 @@ const Player = () => { 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], + queryKey: ["stream-url", itemId, bitrateValue, mediaSourceId, audioIndex], queryFn: async () => { if (!api) { @@ -325,27 +328,14 @@ const Player = () => { SelectedTrack | undefined >(undefined); - // Set intial Subtitle Track. - // We will only select external tracks if they are are text based. Else it should be burned in already. - // This function aims to get the embedded track index from the source subtitle index. - const getEmbeddedTrackIndex = (sourceSubtitleIndex: number) => { - const textSubs = - stream?.mediaSource.MediaStreams?.filter( - (sub) => sub.Type === "Subtitle" && sub.IsTextSubtitleStream - ) || []; - - // Get unique text-based subtitles because react-native-video removes hls text tracks duplicates. - const matchingSubtitle = textSubs.find( - (sub) => sub?.Index === sourceSubtitleIndex - ); - - if (!matchingSubtitle) return -1; - return textSubs.indexOf(matchingSubtitle); - }; - useEffect(() => { if (selectedTextTrack === undefined) { - const embeddedTrackIndex = getEmbeddedTrackIndex(subtitleIndex!); + const subtitleHelper = new SubtitleHelper( + stream?.mediaSource.MediaStreams ?? [] + ); + const embeddedTrackIndex = subtitleHelper.getEmbeddedTrackIndex( + subtitleIndex! + ); // Most likely the subtitle is burned in. if (embeddedTrackIndex === -1) return; diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index b3eb242e..3044b5d3 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -20,6 +20,7 @@ import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById" import { BaseItemDto, MediaSourceInfo, + MediaStream, } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useNavigation } from "expo-router"; @@ -32,6 +33,7 @@ import { Chromecast } from "./Chromecast"; import { ItemHeader } from "./ItemHeader"; import { MediaSourceSelector } from "./MediaSourceSelector"; import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; +import { SubtitleHelper } from "@/utils/SubtitleHelper"; export type SelectedOptions = { bitrate: Bitrate; @@ -109,6 +111,36 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( 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 ( @@ -199,6 +231,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( selected={selectedOptions.audioIndex} /> setSelectedOptions( diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx index 144d20d6..087363a3 100644 --- a/components/SubtitleTrackSelector.tsx +++ b/components/SubtitleTrackSelector.tsx @@ -1,26 +1,34 @@ import { tc } from "@/utils/textTools"; import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"; import { useMemo } from "react"; -import { TouchableOpacity, View } from "react-native"; +import { Platform, TouchableOpacity, View } from "react-native"; import * as DropdownMenu from "zeego/dropdown-menu"; import { Text } from "./common/Text"; +import { SubtitleHelper } from "@/utils/SubtitleHelper"; 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 }) => { - const subtitleStreams = useMemo( - () => source?.MediaStreams?.filter((x) => x.Type === "Subtitle") ?? [], - [source] - ); + const subtitleStreams = useMemo(() => { + const subtitleHelper = new SubtitleHelper(source?.MediaStreams ?? []); + + if (isTranscoding && Platform.OS === "ios") { + return subtitleHelper.getUniqueSubtitles(); + } + + return subtitleHelper.getSubtitles(); + }, [source, isTranscoding]); const selectedSubtitleSteam = useMemo( () => subtitleStreams.find((x) => x.Index === selected), diff --git a/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx b/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx index f87bf84e..4ce58084 100644 --- a/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx +++ b/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx @@ -8,6 +8,7 @@ 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; @@ -43,6 +44,8 @@ const DropdownView: React.FC = ({ showControls }) => { mediaSource?.MediaStreams?.filter((x) => x.Type === "Subtitle") ?? []; const textBasedSubs = allSubs.filter((x) => x.IsTextSubtitleStream); + const subtitleHelper = new SubtitleHelper(mediaSource?.MediaStreams ?? []); + const allSubtitleTracksForTranscodingStream = useMemo(() => { const disableSubtitle = { name: "Disable", @@ -57,28 +60,9 @@ const DropdownView: React.FC = ({ showControls }) => { IsTextSubtitleStream: true, })) || []; - console.log("textSubtitles", textSubtitles); - - 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 (textSubtitles.length === 0) return disableSubtitle; - const textSubtitle = textSubtitles[textIndex]; - if (!textSubtitle) return disableSubtitle; - textIndex++; - return textSubtitle; - } else { - return { - name: sub.DisplayTitle!, - index: sub.Index!, - IsTextSubtitleStream: sub.IsTextSubtitleStream, - } as TranscodedSubtitle; - } - }); + const sortedSubtitles = subtitleHelper.getSortedSubtitles(textSubtitles); console.log("sortedSubtitles", sortedSubtitles); - return [disableSubtitle, ...sortedSubtitles]; } @@ -114,16 +98,6 @@ const DropdownView: React.FC = ({ showControls }) => { index: x.Index!, })) || []; - // 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. - const getSourceSubtitleIndex = (embeddedTrackIndex: number): number => { - // If we're not on text-based subtitles, return the embedded track index - if (!isOnTextSubtitle) { - return parseInt(subtitleIndex); - } - return textBasedSubs[embeddedTrackIndex]?.Index ?? -1; - }; - const ChangeTranscodingAudio = useCallback( (audioIndex: number) => { console.log("ChangeTranscodingAudio", subtitleIndex, audioIndex); @@ -182,7 +156,9 @@ const DropdownView: React.FC = ({ showControls }) => { value={ subtitleIndex === (isOnTextSubtitle && sub.IsTextSubtitleStream - ? getSourceSubtitleIndex(sub.index).toString() + ? subtitleHelper + .getSourceSubtitleIndex(sub.index) + .toString() : sub?.index.toString()) } key={`subtitle-item-${idx}`} @@ -191,17 +167,18 @@ const DropdownView: React.FC = ({ showControls }) => { if ( subtitleIndex === (isOnTextSubtitle && sub.IsTextSubtitleStream - ? getSourceSubtitleIndex(sub.index).toString() + ? subtitleHelper + .getSourceSubtitleIndex(sub.index) + .toString() : sub?.index.toString()) ) return; router.setParams({ - subtitleIndex: getSourceSubtitleIndex( - sub.index - ).toString(), + subtitleIndex: subtitleHelper + .getSourceSubtitleIndex(sub.index) + .toString(), }); - console.log("Got here"); if (sub.IsTextSubtitleStream && isOnTextSubtitle) { setSubtitleTrack && setSubtitleTrack(sub.index); diff --git a/utils/SubtitleHelper.ts b/utils/SubtitleHelper.ts new file mode 100644 index 00000000..e957fa0d --- /dev/null +++ b/utils/SubtitleHelper.ts @@ -0,0 +1,134 @@ +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.getSubtitles()[embeddedTrackIndex]?.Index ?? -1; + } + return this.getUniqueTextBasedSubtitles()[embeddedTrackIndex]?.Index ?? -1; + }; +} diff --git a/utils/profiles/transcoding.js b/utils/profiles/transcoding.js index cad16a63..f45c498a 100644 --- a/utils/profiles/transcoding.js +++ b/utils/profiles/transcoding.js @@ -5,7 +5,6 @@ */ import MediaTypes from "../../constants/MediaTypes"; - export default { Name: "Vlc Player for HLS streams.", MaxStaticBitrate: 20_000_000, @@ -40,7 +39,7 @@ export default { Type: MediaTypes.Video, Context: "Streaming", Protocol: "hls", - Container: "ts", + Container: "fmp4", VideoCodec: "h264, hevc", AudioCodec: "aac,mp3,ac3", CopyTimestamps: false, @@ -78,11 +77,10 @@ export default { { 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" }, ], -}; \ No newline at end of file +};