Completed subtitle feature

This commit is contained in:
Alex Kim
2024-12-12 04:23:09 +11:00
parent 3fb20a8ca2
commit 35fcb5ca0c
7 changed files with 137 additions and 190 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

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

View File

@@ -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.`);
}
}

View File

@@ -32,6 +32,7 @@ abstract class StreamRankerStrategy {
let bestStreamIndex = null;
let bestStreamScore = 0;
const prevStream = prevSource.MediaStreams[prevIndex];
if (!prevStream) {