mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
Added bandaid fix
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<number | undefined>(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}
|
||||
/>
|
||||
<SubtitleTrackSelector
|
||||
isTranscoding={isTranscoding}
|
||||
source={selectedOptions.mediaSource}
|
||||
onChange={(val) =>
|
||||
setSelectedOptions(
|
||||
|
||||
@@ -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<typeof View> {
|
||||
source?: MediaSourceInfo;
|
||||
onChange: (value: number) => void;
|
||||
selected?: number | undefined;
|
||||
isTranscoding?: boolean;
|
||||
}
|
||||
|
||||
export const SubtitleTrackSelector: React.FC<Props> = ({
|
||||
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),
|
||||
|
||||
@@ -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<DropdownViewProps> = ({ 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<DropdownViewProps> = ({ 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<DropdownViewProps> = ({ 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<DropdownViewProps> = ({ 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<DropdownViewProps> = ({ 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);
|
||||
|
||||
134
utils/SubtitleHelper.ts
Normal file
134
utils/SubtitleHelper.ts
Normal file
@@ -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<string>();
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -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" },
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user