From 35fcb5ca0c8736ce75a8718ba8f8796a8da4f927 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Thu, 12 Dec 2024 04:23:09 +1100 Subject: [PATCH] Completed subtitle feature --- app/(auth)/player/transcoding-player.tsx | 15 +-- components/video-player/controls/Controls.tsx | 117 ++++++++++++------ .../video-player/controls/EpisodeList.tsx | 40 +----- .../dropdown/DropdownViewTranscoding.tsx | 10 +- utils/jellyfin/getDefaultPlaySettings.ts | 48 +++++-- utils/rankStreamType.ts | 96 -------------- utils/streamRanker.ts | 1 + 7 files changed, 137 insertions(+), 190 deletions(-) delete mode 100644 utils/rankStreamType.ts diff --git a/app/(auth)/player/transcoding-player.tsx b/app/(auth)/player/transcoding-player.tsx index 9e16261c..b72a9185 100644 --- a/app/(auth)/player/transcoding-player.tsx +++ b/app/(auth)/player/transcoding-player.tsx @@ -335,23 +335,20 @@ const Player = () => { ) || []; // 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 - ); + + if (!matchingSubtitle) return -1; + return textSubs.indexOf(matchingSubtitle); }; useEffect(() => { if (selectedTextTrack === undefined) { const embeddedTrackIndex = getEmbeddedTrackIndex(subtitleIndex!); + + // Most likely the subtitle is burned in. + if (embeddedTrackIndex === -1) return; console.log( "Setting selected text track", subtitleIndex, diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 6e3f2cd4..5c5d67a0 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -9,7 +9,10 @@ import { VlcPlayerViewRef, } from "@/modules/vlc-player/src/VlcPlayer.types"; import { useSettings } from "@/utils/atoms/settings"; -import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; +import { + getDefaultPlaySettings, + previousIndexes, +} from "@/utils/jellyfin/getDefaultPlaySettings"; import { writeToLog } from "@/utils/log"; import { formatTimeString, @@ -128,8 +131,9 @@ export const Controls: React.FC = ({ const wasPlayingRef = useRef(false); const lastProgressRef = useRef(0); - const { bitrateValue, usedSubtitleIndex } = useLocalSearchParams<{ + const { bitrateValue, subtitleIndex, audioIndex } = useLocalSearchParams<{ bitrateValue: string; + audioIndex: string; subtitleIndex: string; }>(); @@ -154,21 +158,26 @@ export const Controls: React.FC = ({ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + const previousIndexes: previousIndexes = { + subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, + audioIndex: audioIndex ? parseInt(audioIndex) : undefined, + }; + const { mediaSource: newMediaSource, - audioIndex, - subtitleIndex, + audioIndex: defaultAudioIndex, + subtitleIndex: defaultSubtitleIndex, } = getDefaultPlaySettings( previousItem, settings, - item, + previousIndexes, mediaSource ?? undefined ); const queryParams = new URLSearchParams({ itemId: previousItem.Id ?? "", // Ensure itemId is a string - audioIndex: audioIndex?.toString() ?? "", - subtitleIndex: subtitleIndex?.toString() ?? "", + audioIndex: defaultAudioIndex?.toString() ?? "", + subtitleIndex: defaultSubtitleIndex?.toString() ?? "", mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string bitrateValue: bitrateValue.toString(), }).toString(); @@ -180,23 +189,34 @@ export const Controls: React.FC = ({ } // @ts-expect-error router.replace(`player/transcoding-player?${queryParams}`); - }, [previousItem, settings]); + }, [previousItem, settings, subtitleIndex, audioIndex]); const goToNextItem = useCallback(() => { if (!nextItem || !settings) return; Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - const { mediaSource, audioIndex, subtitleIndex } = getDefaultPlaySettings( + const previousIndexes: previousIndexes = { + subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, + audioIndex: audioIndex ? parseInt(audioIndex) : undefined, + }; + + const { + mediaSource: newMediaSource, + audioIndex: defaultAudioIndex, + subtitleIndex: defaultSubtitleIndex, + } = getDefaultPlaySettings( nextItem, - settings + settings, + previousIndexes, + mediaSource ?? undefined ); const queryParams = new URLSearchParams({ itemId: nextItem.Id ?? "", // Ensure itemId is a string - audioIndex: audioIndex?.toString() ?? "", - subtitleIndex: subtitleIndex?.toString() ?? "", - mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string + audioIndex: defaultAudioIndex?.toString() ?? "", + subtitleIndex: defaultSubtitleIndex?.toString() ?? "", + mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string bitrateValue: bitrateValue.toString(), }).toString(); @@ -207,7 +227,7 @@ export const Controls: React.FC = ({ } // @ts-expect-error router.replace(`player/transcoding-player?${queryParams}`); - }, [nextItem, settings]); + }, [nextItem, settings, subtitleIndex, audioIndex]); const updateTimes = useCallback( (currentProgress: number, maxValue: number) => { @@ -422,32 +442,51 @@ export const Controls: React.FC = ({ if (isPlaying) togglePlay(); }; - const gotoEpisode = async (itemId: string) => { - const item = await getItemById(api, itemId); - console.log("Item", item); - if (!settings || !item) return; + const goToItem = useCallback( + async (itemId: string) => { + try { + const gotoItem = await getItemById(api, itemId); + if (!settings || !gotoItem) return; - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - const { bitrate, mediaSource, audioIndex, subtitleIndex } = - getDefaultPlaySettings(item, settings); + const previousIndexes: previousIndexes = { + subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, + audioIndex: audioIndex ? parseInt(audioIndex) : undefined, + }; - 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: bitrate.toString(), - }).toString(); + const { + mediaSource: newMediaSource, + audioIndex: defaultAudioIndex, + subtitleIndex: defaultSubtitleIndex, + } = getDefaultPlaySettings( + gotoItem, + settings, + previousIndexes, + mediaSource ?? undefined + ); - if (!bitrate.value) { - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - return; - } - // @ts-expect-error - router.replace(`player/transcoding-player?${queryParams}`); - }; + const queryParams = new URLSearchParams({ + itemId: gotoItem.Id ?? "", // Ensure itemId is a string + audioIndex: defaultAudioIndex?.toString() ?? "", + subtitleIndex: defaultSubtitleIndex?.toString() ?? "", + mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string + bitrateValue: bitrateValue.toString(), + }).toString(); + + if (!bitrateValue) { + // @ts-expect-error + router.replace(`player/direct-player?${queryParams}`); + return; + } + // @ts-expect-error + router.replace(`player/transcoding-player?${queryParams}`); + } catch (error) { + console.error("Error in gotoEpisode:", error); + } + }, + [settings, subtitleIndex, audioIndex] + ); // Used when user changes audio through audio button on device. const [showAudioSlider, setShowAudioSlider] = useState(false); @@ -459,7 +498,11 @@ export const Controls: React.FC = ({ isVideoLoaded={isVideoLoaded} > {EpisodeView ? ( - setEpisodeView(false)} /> + setEpisodeView(false)} + goToItem={goToItem} + /> ) : ( <> void; + goToItem: (itemId: string) => Promise; }; export const seasonIndexAtom = atom({}); -export const EpisodeList: React.FC = ({ item, close }) => { +export const EpisodeList: React.FC = ({ item, close, goToItem }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const insets = useSafeAreaInsets(); // Get safe area insets - const [settings] = useSettings(); const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom); const scrollViewRef = useRef(null); // Reference to the HorizontalScroll const scrollToIndex = (index: number) => { @@ -154,36 +150,6 @@ export const EpisodeList: React.FC = ({ item, close }) => { } }, [episodes, item.Id]); - const { bitrateValue } = useLocalSearchParams<{ - bitrateValue: string; - }>(); - - const gotoEpisode = async (itemId: string) => { - const item = await getItemById(api, itemId); - if (!settings || !item) return; - - const { mediaSource, audioIndex, subtitleIndex } = getDefaultPlaySettings( - item, - settings - ); - - 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(); - - if (!bitrateValue) { - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - return; - } - // @ts-expect-error - router.replace(`player/transcoding-player?${queryParams}`); - }; - if (!episodes) { return ; } @@ -241,7 +207,7 @@ export const EpisodeList: React.FC = ({ item, close }) => { > { - gotoEpisode(_item.Id); + goToItem(_item.Id); }} > = ({ showControls }) => { key={`subtitle-item-${idx}`} onValueChange={() => { console.log("sub", sub); - if (subtitleIndex === sub?.index.toString()) return; + if ( + subtitleIndex === + (sub.IsTextSubtitleStream && isOnTextSubtitle + ? getSourceSubtitleIndex(sub.index).toString() + : sub?.index.toString()) + ) + return; + router.setParams({ subtitleIndex: getSourceSubtitleIndex( sub.index ).toString(), }); + console.log("Got here"); if (sub.IsTextSubtitleStream && isOnTextSubtitle) { setSubtitleTrack && setSubtitleTrack(sub.index); diff --git a/utils/jellyfin/getDefaultPlaySettings.ts b/utils/jellyfin/getDefaultPlaySettings.ts index c62c55f5..2ef7cd68 100644 --- a/utils/jellyfin/getDefaultPlaySettings.ts +++ b/utils/jellyfin/getDefaultPlaySettings.ts @@ -5,7 +5,11 @@ import { MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; import { Settings, useSettings } from "../atoms/settings"; -import { StreamRanker, SubtitleStreamRanker } from "../streamRanker"; +import { + AudioStreamRanker, + StreamRanker, + SubtitleStreamRanker, +} from "../streamRanker"; interface PlaySettings { item: BaseItemDto; @@ -15,12 +19,21 @@ interface PlaySettings { subtitleIndex?: number | undefined; } +export interface previousIndexes { + audioIndex?: number; + subtitleIndex?: number; +} + +interface TrackOptions { + DefaultAudioStreamIndex: number | undefined; + DefaultSubtitleStreamIndex: number | undefined; +} + // Used getting default values for the next player. export function getDefaultPlaySettings( item: BaseItemDto, settings: Settings, - previousIndex?: number, - previousItem?: BaseItemDto, + previousIndexes?: previousIndexes, previousSource?: MediaSourceInfo ): PlaySettings { if (item.Type === "Program") { @@ -47,14 +60,18 @@ export function getDefaultPlaySettings( )?.Index; // We prefer the previous track over the default track. - let trackOptions = {}; + let trackOptions: TrackOptions = { + DefaultAudioStreamIndex: defaultAudioIndex ?? -1, + DefaultSubtitleStreamIndex: mediaSource?.DefaultSubtitleStreamIndex ?? -1, + }; + const mediaStreams = mediaSource?.MediaStreams ?? []; - if (settings?.rememberSubtitleSelections) { - if (previousIndex !== undefined && previousSource) { + if (settings?.rememberSubtitleSelections && previousIndexes) { + if (previousIndexes.subtitleIndex !== undefined && previousSource) { const subtitleRanker = new SubtitleStreamRanker(); const ranker = new StreamRanker(subtitleRanker); ranker.rankStream( - previousIndex, + previousIndexes.subtitleIndex, previousSource, mediaStreams, trackOptions @@ -62,7 +79,18 @@ export function getDefaultPlaySettings( } } - const finalSubtitleIndex = mediaSource?.DefaultAudioStreamIndex; + if (settings?.rememberAudioSelections && previousIndexes) { + if (previousIndexes.audioIndex !== undefined && previousSource) { + const audioRanker = new AudioStreamRanker(); + const ranker = new StreamRanker(audioRanker); + ranker.rankStream( + previousIndexes.audioIndex, + previousSource, + mediaStreams, + trackOptions + ); + } + } // 4. Get default bitrate const bitrate = BITRATES.sort( @@ -73,7 +101,7 @@ export function getDefaultPlaySettings( item, bitrate, mediaSource, - audioIndex: preferedAudioIndex ?? defaultAudioIndex ?? firstAudioIndex, - subtitleIndex: finalSubtitleIndex || -1, + audioIndex: trackOptions.DefaultAudioStreamIndex, + subtitleIndex: trackOptions.DefaultSubtitleStreamIndex, }; } diff --git a/utils/rankStreamType.ts b/utils/rankStreamType.ts deleted file mode 100644 index 49469acb..00000000 --- a/utils/rankStreamType.ts +++ /dev/null @@ -1,96 +0,0 @@ -export function rankStreamType( - prevIndex, - prevSource, - mediaStreams, - trackOptions, - streamType, - isSecondarySubtitle -) { - if (prevIndex == -1) { - console.debug(`AutoSet ${streamType} - No Stream Set`); - if (streamType == "Subtitle") { - if (isSecondarySubtitle) { - trackOptions.DefaultSecondarySubtitleStreamIndex = -1; - } else { - trackOptions.DefaultSubtitleStreamIndex = -1; - } - } - return; - } - - if (!prevSource.MediaStreams || !mediaStreams) { - console.debug(`AutoSet ${streamType} - No MediaStreams`); - return; - } - - 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}.` - ); - if (streamType == "Subtitle") { - if (isSecondarySubtitle) { - trackOptions.DefaultSecondarySubtitleStreamIndex = bestStreamIndex; - } else { - trackOptions.DefaultSubtitleStreamIndex = bestStreamIndex; - } - } - if (streamType == "Audio") { - trackOptions.DefaultAudioStreamIndex = bestStreamIndex; - } - } else { - console.debug(`AutoSet ${streamType} - Threshold not met. Using default.`); - } -} diff --git a/utils/streamRanker.ts b/utils/streamRanker.ts index f3a9f1af..665e57be 100644 --- a/utils/streamRanker.ts +++ b/utils/streamRanker.ts @@ -32,6 +32,7 @@ abstract class StreamRankerStrategy { let bestStreamIndex = null; let bestStreamScore = 0; + const prevStream = prevSource.MediaStreams[prevIndex]; if (!prevStream) {