mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
Completed subtitle feature
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<Props> = ({
|
||||
const wasPlayingRef = useRef(false);
|
||||
const lastProgressRef = useRef<number>(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<Props> = ({
|
||||
|
||||
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<Props> = ({
|
||||
}
|
||||
// @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<Props> = ({
|
||||
}
|
||||
// @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<Props> = ({
|
||||
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<Props> = ({
|
||||
isVideoLoaded={isVideoLoaded}
|
||||
>
|
||||
{EpisodeView ? (
|
||||
<EpisodeList item={item} close={() => setEpisodeView(false)} />
|
||||
<EpisodeList
|
||||
item={item}
|
||||
close={() => setEpisodeView(false)}
|
||||
goToItem={goToItem}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<VideoProvider
|
||||
|
||||
@@ -17,10 +17,6 @@ import {
|
||||
HorizontalScroll,
|
||||
HorizontalScrollRef,
|
||||
} from "@/components/common/HorrizontalScroll";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||
import { getItemById } from "@/utils/jellyfin/user-library/getItemById";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import {
|
||||
SeasonDropdown,
|
||||
SeasonIndexState,
|
||||
@@ -29,15 +25,15 @@ import {
|
||||
type Props = {
|
||||
item: BaseItemDto;
|
||||
close: () => void;
|
||||
goToItem: (itemId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const seasonIndexAtom = atom<SeasonIndexState>({});
|
||||
|
||||
export const EpisodeList: React.FC<Props> = ({ item, close }) => {
|
||||
export const EpisodeList: React.FC<Props> = ({ 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<HorizontalScrollRef>(null); // Reference to the HorizontalScroll
|
||||
const scrollToIndex = (index: number) => {
|
||||
@@ -154,36 +150,6 @@ export const EpisodeList: React.FC<Props> = ({ 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 <Loader />;
|
||||
}
|
||||
@@ -241,7 +207,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close }) => {
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
gotoEpisode(_item.Id);
|
||||
goToItem(_item.Id);
|
||||
}}
|
||||
>
|
||||
<ContinueWatchingPoster
|
||||
|
||||
@@ -187,12 +187,20 @@ const DropdownView: React.FC<DropdownViewProps> = ({ 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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ abstract class StreamRankerStrategy {
|
||||
|
||||
let bestStreamIndex = null;
|
||||
let bestStreamScore = 0;
|
||||
|
||||
const prevStream = prevSource.MediaStreams[prevIndex];
|
||||
|
||||
if (!prevStream) {
|
||||
|
||||
Reference in New Issue
Block a user