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; }; }