From 3fb20a8ca2a298e5ddbf549f8845e43df13673a0 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Thu, 12 Dec 2024 02:41:30 +1100 Subject: [PATCH] Revamped transcoding subtitles --- app/(auth)/player/direct-player.tsx | 19 +- app/(auth)/player/transcoding-player.tsx | 59 ++-- components/video-player/controls/Controls.tsx | 15 +- .../controls/dropdown/DropdownViewDirect.tsx | 22 +- .../dropdown/DropdownViewTranscoding.tsx | 132 ++++---- hooks/useDefaultPlaySettings.ts | 1 + utils/jellyfin/getDefaultPlaySettings.ts | 26 +- utils/streamRanker.ts | 302 ++++-------------- 8 files changed, 215 insertions(+), 361 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index b0997e88..138a6c4a 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -125,14 +125,7 @@ export default function page() { isLoading: isLoadingStreamUrl, isError: isErrorStreamUrl, } = useQuery({ - queryKey: [ - "stream-url", - itemId, - audioIndex, - subtitleIndex, - mediaSourceId, - bitrateValue, - ], + queryKey: ["stream-url", itemId, mediaSourceId, bitrateValue], queryFn: async () => { console.log("Offline:", offline); if (offline) { @@ -254,6 +247,7 @@ export default function page() { videoRef.current?.stop(); }, [videoRef, reportPlaybackStopped]); + // TODO: unused should remove. const reportPlaybackStart = useCallback(async () => { if (offline) return; @@ -287,7 +281,12 @@ export default function page() { if (!item?.Id || !stream) return; - console.log("onProgress ~", currentTimeInTicks, isPlaying); + console.log( + "onProgress ~", + currentTimeInTicks, + isPlaying, + `AUDIO index: ${audioIndex} SUB index" ${subtitleIndex}` + ); await getPlaystateApi(api!).onPlaybackProgress({ itemId: item.Id, @@ -300,7 +299,7 @@ export default function page() { playSessionId: stream.sessionId, }); }, - [item?.Id, isPlaying, api, isPlaybackStopped] + [item?.Id, isPlaying, api, isPlaybackStopped, audioIndex, subtitleIndex] ); useOrientation(); diff --git a/app/(auth)/player/transcoding-player.tsx b/app/(auth)/player/transcoding-player.tsx index e2d23431..9e16261c 100644 --- a/app/(auth)/player/transcoding-player.tsx +++ b/app/(auth)/player/transcoding-player.tsx @@ -38,6 +38,7 @@ import Video, { SelectedTrackType, VideoRef, } from "react-native-video"; +import index from "../(tabs)/(home)"; const Player = () => { const api = useAtomValue(apiAtom); @@ -116,14 +117,7 @@ const Player = () => { isLoading: isLoadingStreamUrl, isError: isErrorStreamUrl, } = useQuery({ - queryKey: [ - "stream-url", - itemId, - audioIndex, - subtitleIndex, - bitrateValue, - mediaSourceId, - ], + queryKey: ["stream-url", itemId, bitrateValue, mediaSourceId], queryFn: async () => { if (!api) { @@ -263,6 +257,13 @@ const Player = () => { progress.value = ticks; cacheProgress.value = secondsToTicks(data.playableDuration); + console.log( + "onProgress ~", + ticks, + isPlaying, + `AUDIO index: ${audioIndex} SUB index" ${subtitleIndex}` + ); + // 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); @@ -326,23 +327,39 @@ const Player = () => { // Set intial Subtitle Track. // We will only select external tracks if they are are text based. Else it should be burned in already. - const textSubs = - stream?.mediaSource.MediaStreams?.filter( - (sub) => sub.Type === "Subtitle" && sub.IsTextSubtitleStream - ) || []; + // 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 uniqueTextSubs = Array.from( + new Set(textSubs.map((sub) => sub.DisplayTitle)) + ).map((title) => textSubs.find((sub) => sub.DisplayTitle === title)); + + const matchingSubtitle = textSubs.find( + (sub) => sub?.Index === sourceSubtitleIndex + ); + return ( + uniqueTextSubs.findIndex( + (sub) => sub?.DisplayTitle === matchingSubtitle?.DisplayTitle + ) ?? -1 + ); + }; - const uniqueTextSubs = Array.from( - new Set(textSubs.map((sub) => sub.DisplayTitle)) - ).map((title) => textSubs.find((sub) => sub.DisplayTitle === title)); - const chosenSubtitleTrack = textSubs.find( - (sub) => sub.Index === subtitleIndex - ); useEffect(() => { - if (chosenSubtitleTrack && selectedTextTrack === undefined) { - console.log("Setting selected text track", chosenSubtitleTrack); + if (selectedTextTrack === undefined) { + const embeddedTrackIndex = getEmbeddedTrackIndex(subtitleIndex!); + console.log( + "Setting selected text track", + subtitleIndex, + embeddedTrackIndex + ); setSelectedTextTrack({ type: SelectedTrackType.INDEX, - value: uniqueTextSubs.indexOf(chosenSubtitleTrack), + value: embeddedTrackIndex, }); } }, [embededTextTracks]); diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 4bad27f0..6e3f2cd4 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -128,8 +128,9 @@ export const Controls: React.FC = ({ const wasPlayingRef = useRef(false); const lastProgressRef = useRef(0); - const { bitrateValue } = useLocalSearchParams<{ + const { bitrateValue, usedSubtitleIndex } = useLocalSearchParams<{ bitrateValue: string; + subtitleIndex: string; }>(); const { showSkipButton, skipIntro } = useIntroSkipper( @@ -153,16 +154,22 @@ export const Controls: React.FC = ({ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - const { mediaSource, audioIndex, subtitleIndex } = getDefaultPlaySettings( + const { + mediaSource: newMediaSource, + audioIndex, + subtitleIndex, + } = getDefaultPlaySettings( previousItem, - settings + settings, + item, + mediaSource ?? undefined ); const queryParams = new URLSearchParams({ itemId: previousItem.Id ?? "", // Ensure itemId is a string audioIndex: audioIndex?.toString() ?? "", subtitleIndex: subtitleIndex?.toString() ?? "", - mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string + mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string bitrateValue: bitrateValue.toString(), }).toString(); diff --git a/components/video-player/controls/dropdown/DropdownViewDirect.tsx b/components/video-player/controls/dropdown/DropdownViewDirect.tsx index b58cf907..67212a27 100644 --- a/components/video-player/controls/dropdown/DropdownViewDirect.tsx +++ b/components/video-player/controls/dropdown/DropdownViewDirect.tsx @@ -7,7 +7,7 @@ import { useVideoContext } from "../contexts/VideoContext"; import { EmbeddedSubtitle, ExternalSubtitle } from "../types"; import { useAtomValue } from "jotai"; import { apiAtom } from "@/providers/JellyfinProvider"; -import { useLocalSearchParams } from "expo-router"; +import { router, useLocalSearchParams } from "expo-router"; interface DropdownViewDirectProps { showControls: boolean; @@ -71,13 +71,6 @@ const DropdownViewDirect: React.FC = ({ bitrateValue: string; }>(); - const [selectedSubtitleIndex, setSelectedSubtitleIndex] = useState( - parseInt(subtitleIndex) - ); - const [selectedAudioIndex, setSelectedAudioIndex] = useState( - parseInt(audioIndex) - ); - return ( = ({ {allSubtitleTracksForDirectPlay?.map((sub, idx: number) => ( { if ("deliveryUrl" in sub && sub.deliveryUrl) { setSubtitleURL && @@ -133,8 +126,9 @@ const DropdownViewDirect: React.FC = ({ console.log("Set sub index: ", sub.index); setSubtitleTrack && setSubtitleTrack(sub.index); } - - setSelectedSubtitleIndex(sub.index); + router.setParams({ + subtitleIndex: sub.index.toString(), + }); console.log("Subtitle: ", sub); }} > @@ -159,10 +153,12 @@ const DropdownViewDirect: React.FC = ({ {audioTracks?.map((track, idx: number) => ( { - setSelectedAudioIndex(track.index); setAudioTrack && setAudioTrack(track.index); + router.setParams({ + audioIndex: track.index.toString(), + }); }} > diff --git a/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx b/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx index 66e68450..1a019d02 100644 --- a/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx +++ b/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx @@ -14,10 +14,7 @@ interface DropdownViewProps { offline?: boolean; // used to disable external subs for downloads } -const DropdownView: React.FC = ({ - showControls, - offline = false, -}) => { +const DropdownView: React.FC = ({ showControls }) => { const router = useRouter(); const api = useAtomValue(apiAtom); const ControlContext = useControlContext(); @@ -46,24 +43,6 @@ const DropdownView: React.FC = ({ mediaSource?.MediaStreams?.filter((x) => x.Type === "Subtitle") ?? []; const textBasedSubs = allSubs.filter((x) => x.IsTextSubtitleStream); - // This is used in the case where it is transcoding stream. - const chosenSubtitle = textBasedSubs.find( - (x) => x.Index === parseInt(subtitleIndex) - ); - - let initialSubtitleIndex = -1; - if (!isOnTextSubtitle) { - initialSubtitleIndex = parseInt(subtitleIndex); - } else if (chosenSubtitle) { - initialSubtitleIndex = textBasedSubs.indexOf(chosenSubtitle); - } - - const [selectedSubtitleIndex, setSelectedSubtitleIndex] = - useState(initialSubtitleIndex); - const [selectedAudioIndex, setSelectedAudioIndex] = useState( - parseInt(audioIndex) - ); - const allSubtitleTracksForTranscodingStream = useMemo(() => { const disableSubtitle = { name: "Disable", @@ -78,38 +57,26 @@ const DropdownView: React.FC = ({ IsTextSubtitleStream: true, })) || []; - const imageSubtitles = allSubs - .filter((x) => !x.IsTextSubtitleStream) - .map( - (x) => - ({ - name: x.DisplayTitle!, - index: x.Index!, - IsTextSubtitleStream: x.IsTextSubtitleStream, - } as TranscodedSubtitle) - ); + console.log("textSubtitles", textSubtitles); - const textSubtitlesMap = new Map(textSubtitles.map((s) => [s.name, s])); - const imageSubtitlesMap = new Map(imageSubtitles.map((s) => [s.name, s])); + 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]; + textIndex++; + return textSubtitle; + } else { + return { + name: sub.DisplayTitle!, + index: sub.Index!, + IsTextSubtitleStream: sub.IsTextSubtitleStream, + } as TranscodedSubtitle; + } + }); - const sortedSubtitles = Array.from( - new Set( - allSubs - .map((sub) => { - const displayTitle = sub.DisplayTitle ?? ""; - if (textSubtitlesMap.has(displayTitle)) { - return textSubtitlesMap.get(displayTitle); - } - if (imageSubtitlesMap.has(displayTitle)) { - return imageSubtitlesMap.get(displayTitle); - } - return null; - }) - .filter( - (subtitle): subtitle is TranscodedSubtitle => subtitle !== null - ) - ) - ); + console.log("sortedSubtitles", sortedSubtitles); return [disableSubtitle, ...sortedSubtitles]; } @@ -145,26 +112,24 @@ const DropdownView: React.FC = ({ name: x.DisplayTitle!, index: x.Index!, })) || []; - const ChangeTranscodingAudio = useCallback( - (audioIndex: number, currentSelectedSubtitleIndex: number) => { - let newSubtitleIndex: number; - if (!isOnTextSubtitle) { - newSubtitleIndex = parseInt(subtitleIndex); - } else if ( - currentSelectedSubtitleIndex >= 0 && - currentSelectedSubtitleIndex < textBasedSubs.length - ) { - console.log("setHere SubtitleIndex", currentSelectedSubtitleIndex); - newSubtitleIndex = textBasedSubs[currentSelectedSubtitleIndex].Index!; - console.log("newSubtitleIndex", newSubtitleIndex); - } else { - newSubtitleIndex = -1; - } + // 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); const queryParams = new URLSearchParams({ itemId: item.Id ?? "", // Ensure itemId is a string audioIndex: audioIndex?.toString() ?? "", - subtitleIndex: newSubtitleIndex?.toString() ?? "", + subtitleIndex: subtitleIndex?.toString() ?? "", mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string bitrateValue: bitrateValue, }).toString(); @@ -172,7 +137,7 @@ const DropdownView: React.FC = ({ // @ts-expect-error router.replace(`player/transcoding-player?${queryParams}`); }, - [mediaSource] + [mediaSource, subtitleIndex, audioIndex] ); return ( @@ -213,17 +178,27 @@ const DropdownView: React.FC = ({ {allSubtitleTracksForTranscodingStream?.map( (sub, idx: number) => ( { console.log("sub", sub); - if (selectedSubtitleIndex === sub?.index) return; - setSelectedSubtitleIndex(sub.index); + if (subtitleIndex === sub?.index.toString()) return; + router.setParams({ + subtitleIndex: getSourceSubtitleIndex( + sub.index + ).toString(), + }); + if (sub.IsTextSubtitleStream && isOnTextSubtitle) { setSubtitleTrack && setSubtitleTrack(sub.index); return; } - + console.log("ChangeTranscodingSubtitle", subtitleIndex); ChangeTranscodingSubtitle(sub.index); }} > @@ -249,11 +224,14 @@ const DropdownView: React.FC = ({ {allAudio?.map((track, idx: number) => ( { - if (selectedAudioIndex === track.index) return; - setSelectedAudioIndex(track.index); - ChangeTranscodingAudio(track.index, selectedSubtitleIndex); + if (audioIndex === track.index.toString()) return; + console.log("Setting audio track to: ", track.index); + router.setParams({ + audioIndex: track.index.toString(), + }); + ChangeTranscodingAudio(track.index); }} > diff --git a/hooks/useDefaultPlaySettings.ts b/hooks/useDefaultPlaySettings.ts index 45062e59..5c2d9cc6 100644 --- a/hooks/useDefaultPlaySettings.ts +++ b/hooks/useDefaultPlaySettings.ts @@ -6,6 +6,7 @@ import { } from "@jellyfin/sdk/lib/generated-client"; import { useMemo } from "react"; +// Used only for intial play settings. const useDefaultPlaySettings = ( item: BaseItemDto, settings: Settings | null diff --git a/utils/jellyfin/getDefaultPlaySettings.ts b/utils/jellyfin/getDefaultPlaySettings.ts index 97c8e1e8..c62c55f5 100644 --- a/utils/jellyfin/getDefaultPlaySettings.ts +++ b/utils/jellyfin/getDefaultPlaySettings.ts @@ -4,7 +4,8 @@ import { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; -import { Settings } from "../atoms/settings"; +import { Settings, useSettings } from "../atoms/settings"; +import { StreamRanker, SubtitleStreamRanker } from "../streamRanker"; interface PlaySettings { item: BaseItemDto; @@ -14,9 +15,13 @@ interface PlaySettings { subtitleIndex?: number | undefined; } +// Used getting default values for the next player. export function getDefaultPlaySettings( item: BaseItemDto, - settings: Settings + settings: Settings, + previousIndex?: number, + previousItem?: BaseItemDto, + previousSource?: MediaSourceInfo ): PlaySettings { if (item.Type === "Program") { return { @@ -41,7 +46,22 @@ export function getDefaultPlaySettings( (x) => x.Type === "Audio" )?.Index; - // TODO: Need to most common next subtitle index as an option. + // We prefer the previous track over the default track. + let trackOptions = {}; + const mediaStreams = mediaSource?.MediaStreams ?? []; + if (settings?.rememberSubtitleSelections) { + if (previousIndex !== undefined && previousSource) { + const subtitleRanker = new SubtitleStreamRanker(); + const ranker = new StreamRanker(subtitleRanker); + ranker.rankStream( + previousIndex, + previousSource, + mediaStreams, + trackOptions + ); + } + } + const finalSubtitleIndex = mediaSource?.DefaultAudioStreamIndex; // 4. Get default bitrate diff --git a/utils/streamRanker.ts b/utils/streamRanker.ts index e3a3746f..f3a9f1af 100644 --- a/utils/streamRanker.ts +++ b/utils/streamRanker.ts @@ -1,248 +1,35 @@ -interface StreamRankerStrategy { - rankStream( - prevIndex: number, - prevSource: any, - mediaStreams: any[], - trackOptions: any, - isSecondarySubtitle: boolean - ): void; -} +import { + MediaSourceInfo, + MediaStream, +} from "@jellyfin/sdk/lib/generated-client"; -class SubtitleStreamRanker implements StreamRankerStrategy { - rankStream( +abstract class StreamRankerStrategy { + abstract streamType: string; + + abstract rankStream( prevIndex: number, - prevSource: any, - mediaStreams: any[], - trackOptions: any, - isSecondarySubtitle: boolean + prevSource: MediaSourceInfo, + mediaStreams: MediaStream[], + trackOptions: any + ): void; + + protected rank( + prevIndex: number, + prevSource: MediaSourceInfo, + mediaStreams: MediaStream[], + trackOptions: any ): void { if (prevIndex == -1) { console.debug(`AutoSet Subtitle - No Stream Set`); - if (isSecondarySubtitle) { - trackOptions.DefaultSecondarySubtitleStreamIndex = -1; - } else { - trackOptions.DefaultSubtitleStreamIndex = -1; - } + trackOptions[`Default${this.streamType}StreamIndex`] = -1; return; } if (!prevSource.MediaStreams || !mediaStreams) { - console.debug(`AutoSet Subtitle - No MediaStreams`); + console.debug(`AutoSet ${this.streamType} - No MediaStreams`); return; } - this.rank( - prevIndex, - prevSource, - mediaStreams, - trackOptions, - isSecondarySubtitle, - "Subtitle" - ); - } - - private rank( - prevIndex: number, - prevSource: any, - mediaStreams: any[], - trackOptions: any, - isSecondarySubtitle: boolean, - streamType: string - ): void { - let bestStreamIndex = null; - let bestStreamScore = 0; - const prevStream = prevSource.MediaStreams[prevIndex]; - - if (!prevStream) { - console.debug(`AutoSet ${streamType} - No prevStream`); - return; - } - - console.debug( - `AutoSet ${streamType} - Previous was ${prevStream.Index} - ${prevStream.DisplayTitle}` - ); - - let prevRelIndex = 0; - for (const stream of prevSource.MediaStreams) { - if (stream.Type != streamType) continue; - - if (stream.Index == prevIndex) break; - - prevRelIndex += 1; - } - - let newRelIndex = 0; - for (const stream of mediaStreams) { - if (stream.Type != streamType) continue; - - let score = 0; - - if (prevStream.Codec == stream.Codec) score += 1; - if (prevRelIndex == newRelIndex) score += 1; - if ( - prevStream.DisplayTitle && - prevStream.DisplayTitle == stream.DisplayTitle - ) - score += 2; - if ( - prevStream.Language && - prevStream.Language != "und" && - prevStream.Language == stream.Language - ) - score += 2; - - console.debug( - `AutoSet ${streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}` - ); - if (score > bestStreamScore && score >= 3) { - bestStreamScore = score; - bestStreamIndex = stream.Index; - } - - newRelIndex += 1; - } - - if (bestStreamIndex != null) { - console.debug( - `AutoSet ${streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.` - ); - trackOptions.DefaultSubtitleStreamIndex = bestStreamIndex; - } else { - console.debug( - `AutoSet ${streamType} - Threshold not met. Using default.` - ); - } - } -} - -class AudioStreamRanker implements StreamRankerStrategy { - rankStream( - prevIndex: number, - prevSource: any, - mediaStreams: any[], - trackOptions: any - ): void { - if (prevIndex == -1) { - console.debug(`AutoSet Audio - No Stream Set`); - return; - } - - if (!prevSource.MediaStreams || !mediaStreams) { - console.debug(`AutoSet Audio - No MediaStreams`); - return; - } - - this.rank(prevIndex, prevSource, mediaStreams, trackOptions, "Audio"); - } - - private rank( - prevIndex: number, - prevSource: any, - mediaStreams: any[], - trackOptions: any, - streamType: string - ): void { - let bestStreamIndex = null; - let bestStreamScore = 0; - const prevStream = prevSource.MediaStreams[prevIndex]; - - if (!prevStream) { - console.debug(`AutoSet ${streamType} - No prevStream`); - return; - } - - console.debug( - `AutoSet ${streamType} - Previous was ${prevStream.Index} - ${prevStream.DisplayTitle}` - ); - - let prevRelIndex = 0; - for (const stream of prevSource.MediaStreams) { - if (stream.Type != streamType) continue; - - if (stream.Index == prevIndex) break; - - prevRelIndex += 1; - } - - let newRelIndex = 0; - for (const stream of mediaStreams) { - if (stream.Type != streamType) continue; - - let score = 0; - - if (prevStream.Codec == stream.Codec) score += 1; - if (prevRelIndex == newRelIndex) score += 1; - if ( - prevStream.DisplayTitle && - prevStream.DisplayTitle == stream.DisplayTitle - ) - score += 2; - if ( - prevStream.Language && - prevStream.Language != "und" && - prevStream.Language == stream.Language - ) - score += 2; - - console.debug( - `AutoSet ${streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}` - ); - if (score > bestStreamScore && score >= 3) { - bestStreamScore = score; - bestStreamIndex = stream.Index; - } - - newRelIndex += 1; - } - - if (bestStreamIndex != null) { - console.debug( - `AutoSet ${streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.` - ); - trackOptions.DefaultAudioStreamIndex = bestStreamIndex; - } else { - console.debug( - `AutoSet ${streamType} - Threshold not met. Using default.` - ); - } - } -} - -abstract class StreamRanker { - private strategy: StreamRankerStrategy; - abstract streamType: string; - - constructor(strategy: StreamRankerStrategy) { - this.strategy = strategy; - } - - setStrategy(strategy: StreamRankerStrategy) { - this.strategy = strategy; - } - - rankStream( - prevIndex: number, - prevSource: any, - mediaStreams: any[], - trackOptions: any, - streamType: string, - isSecondarySubtitle: boolean - ) { - this.strategy.rankStream( - prevIndex, - prevSource, - mediaStreams, - trackOptions, - isSecondarySubtitle - ); - } - - private rank( - prevIndex: number, - prevSource: any, - mediaStreams: any[], - trackOptions: any - ): void { let bestStreamIndex = null; let bestStreamScore = 0; const prevStream = prevSource.MediaStreams[prevIndex]; @@ -308,3 +95,52 @@ abstract class StreamRanker { } } } + +class SubtitleStreamRanker extends StreamRankerStrategy { + streamType = "Subtitle"; + + rankStream( + prevIndex: number, + prevSource: MediaSourceInfo, + mediaStreams: MediaStream[], + trackOptions: any + ): void { + super.rank(prevIndex, prevSource, mediaStreams, trackOptions); + } +} + +class AudioStreamRanker extends StreamRankerStrategy { + streamType = "Audio"; + + rankStream( + prevIndex: number, + prevSource: MediaSourceInfo, + mediaStreams: MediaStream[], + trackOptions: any + ): void { + super.rank(prevIndex, prevSource, mediaStreams, trackOptions); + } +} + +class StreamRanker { + private strategy: StreamRankerStrategy; + + constructor(strategy: StreamRankerStrategy) { + this.strategy = strategy; + } + + setStrategy(strategy: StreamRankerStrategy) { + this.strategy = strategy; + } + + rankStream( + prevIndex: number, + prevSource: MediaSourceInfo, + mediaStreams: MediaStream[], + trackOptions: any + ) { + this.strategy.rankStream(prevIndex, prevSource, mediaStreams, trackOptions); + } +} + +export { StreamRanker, SubtitleStreamRanker, AudioStreamRanker };