mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-09 22:12:54 +01:00
Revamped transcoding subtitles
This commit is contained in:
@@ -125,14 +125,7 @@ export default function page() {
|
|||||||
isLoading: isLoadingStreamUrl,
|
isLoading: isLoadingStreamUrl,
|
||||||
isError: isErrorStreamUrl,
|
isError: isErrorStreamUrl,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: [
|
queryKey: ["stream-url", itemId, mediaSourceId, bitrateValue],
|
||||||
"stream-url",
|
|
||||||
itemId,
|
|
||||||
audioIndex,
|
|
||||||
subtitleIndex,
|
|
||||||
mediaSourceId,
|
|
||||||
bitrateValue,
|
|
||||||
],
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
console.log("Offline:", offline);
|
console.log("Offline:", offline);
|
||||||
if (offline) {
|
if (offline) {
|
||||||
@@ -254,6 +247,7 @@ export default function page() {
|
|||||||
videoRef.current?.stop();
|
videoRef.current?.stop();
|
||||||
}, [videoRef, reportPlaybackStopped]);
|
}, [videoRef, reportPlaybackStopped]);
|
||||||
|
|
||||||
|
// TODO: unused should remove.
|
||||||
const reportPlaybackStart = useCallback(async () => {
|
const reportPlaybackStart = useCallback(async () => {
|
||||||
if (offline) return;
|
if (offline) return;
|
||||||
|
|
||||||
@@ -287,7 +281,12 @@ export default function page() {
|
|||||||
|
|
||||||
if (!item?.Id || !stream) return;
|
if (!item?.Id || !stream) return;
|
||||||
|
|
||||||
console.log("onProgress ~", currentTimeInTicks, isPlaying);
|
console.log(
|
||||||
|
"onProgress ~",
|
||||||
|
currentTimeInTicks,
|
||||||
|
isPlaying,
|
||||||
|
`AUDIO index: ${audioIndex} SUB index" ${subtitleIndex}`
|
||||||
|
);
|
||||||
|
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
await getPlaystateApi(api!).onPlaybackProgress({
|
||||||
itemId: item.Id,
|
itemId: item.Id,
|
||||||
@@ -300,7 +299,7 @@ export default function page() {
|
|||||||
playSessionId: stream.sessionId,
|
playSessionId: stream.sessionId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[item?.Id, isPlaying, api, isPlaybackStopped]
|
[item?.Id, isPlaying, api, isPlaybackStopped, audioIndex, subtitleIndex]
|
||||||
);
|
);
|
||||||
|
|
||||||
useOrientation();
|
useOrientation();
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import Video, {
|
|||||||
SelectedTrackType,
|
SelectedTrackType,
|
||||||
VideoRef,
|
VideoRef,
|
||||||
} from "react-native-video";
|
} from "react-native-video";
|
||||||
|
import index from "../(tabs)/(home)";
|
||||||
|
|
||||||
const Player = () => {
|
const Player = () => {
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
@@ -116,14 +117,7 @@ const Player = () => {
|
|||||||
isLoading: isLoadingStreamUrl,
|
isLoading: isLoadingStreamUrl,
|
||||||
isError: isErrorStreamUrl,
|
isError: isErrorStreamUrl,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: [
|
queryKey: ["stream-url", itemId, bitrateValue, mediaSourceId],
|
||||||
"stream-url",
|
|
||||||
itemId,
|
|
||||||
audioIndex,
|
|
||||||
subtitleIndex,
|
|
||||||
bitrateValue,
|
|
||||||
mediaSourceId,
|
|
||||||
],
|
|
||||||
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!api) {
|
if (!api) {
|
||||||
@@ -263,6 +257,13 @@ const Player = () => {
|
|||||||
progress.value = ticks;
|
progress.value = ticks;
|
||||||
cacheProgress.value = secondsToTicks(data.playableDuration);
|
cacheProgress.value = secondsToTicks(data.playableDuration);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"onProgress ~",
|
||||||
|
ticks,
|
||||||
|
isPlaying,
|
||||||
|
`AUDIO index: ${audioIndex} SUB index" ${subtitleIndex}`
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: Use this when streaming with HLS url, but NOT when direct playing
|
// TODO: Use this when streaming with HLS url, but NOT when direct playing
|
||||||
// TODO: since playable duration is always 0 then.
|
// TODO: since playable duration is always 0 then.
|
||||||
setIsBuffering(data.playableDuration === 0);
|
setIsBuffering(data.playableDuration === 0);
|
||||||
@@ -326,23 +327,39 @@ const Player = () => {
|
|||||||
|
|
||||||
// Set intial Subtitle Track.
|
// Set intial Subtitle Track.
|
||||||
// We will only select external tracks if they are are text based. Else it should be burned in already.
|
// We will only select external tracks if they are are text based. Else it should be burned in already.
|
||||||
const textSubs =
|
// This function aims to get the embedded track index from the source subtitle index.
|
||||||
stream?.mediaSource.MediaStreams?.filter(
|
const getEmbeddedTrackIndex = (sourceSubtitleIndex: number) => {
|
||||||
(sub) => sub.Type === "Subtitle" && sub.IsTextSubtitleStream
|
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 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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const uniqueTextSubs = Array.from(
|
|
||||||
new Set(textSubs.map((sub) => sub.DisplayTitle))
|
|
||||||
).map((title) => textSubs.find((sub) => sub.DisplayTitle === title));
|
|
||||||
const chosenSubtitleTrack = textSubs.find(
|
|
||||||
(sub) => sub.Index === subtitleIndex
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chosenSubtitleTrack && selectedTextTrack === undefined) {
|
if (selectedTextTrack === undefined) {
|
||||||
console.log("Setting selected text track", chosenSubtitleTrack);
|
const embeddedTrackIndex = getEmbeddedTrackIndex(subtitleIndex!);
|
||||||
|
console.log(
|
||||||
|
"Setting selected text track",
|
||||||
|
subtitleIndex,
|
||||||
|
embeddedTrackIndex
|
||||||
|
);
|
||||||
setSelectedTextTrack({
|
setSelectedTextTrack({
|
||||||
type: SelectedTrackType.INDEX,
|
type: SelectedTrackType.INDEX,
|
||||||
value: uniqueTextSubs.indexOf(chosenSubtitleTrack),
|
value: embeddedTrackIndex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [embededTextTracks]);
|
}, [embededTextTracks]);
|
||||||
|
|||||||
@@ -128,8 +128,9 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const wasPlayingRef = useRef(false);
|
const wasPlayingRef = useRef(false);
|
||||||
const lastProgressRef = useRef<number>(0);
|
const lastProgressRef = useRef<number>(0);
|
||||||
|
|
||||||
const { bitrateValue } = useLocalSearchParams<{
|
const { bitrateValue, usedSubtitleIndex } = useLocalSearchParams<{
|
||||||
bitrateValue: string;
|
bitrateValue: string;
|
||||||
|
subtitleIndex: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { showSkipButton, skipIntro } = useIntroSkipper(
|
const { showSkipButton, skipIntro } = useIntroSkipper(
|
||||||
@@ -153,16 +154,22 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
|
||||||
const { mediaSource, audioIndex, subtitleIndex } = getDefaultPlaySettings(
|
const {
|
||||||
|
mediaSource: newMediaSource,
|
||||||
|
audioIndex,
|
||||||
|
subtitleIndex,
|
||||||
|
} = getDefaultPlaySettings(
|
||||||
previousItem,
|
previousItem,
|
||||||
settings
|
settings,
|
||||||
|
item,
|
||||||
|
mediaSource ?? undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
const queryParams = new URLSearchParams({
|
||||||
itemId: previousItem.Id ?? "", // Ensure itemId is a string
|
itemId: previousItem.Id ?? "", // Ensure itemId is a string
|
||||||
audioIndex: audioIndex?.toString() ?? "",
|
audioIndex: audioIndex?.toString() ?? "",
|
||||||
subtitleIndex: subtitleIndex?.toString() ?? "",
|
subtitleIndex: subtitleIndex?.toString() ?? "",
|
||||||
mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
||||||
bitrateValue: bitrateValue.toString(),
|
bitrateValue: bitrateValue.toString(),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useVideoContext } from "../contexts/VideoContext";
|
|||||||
import { EmbeddedSubtitle, ExternalSubtitle } from "../types";
|
import { EmbeddedSubtitle, ExternalSubtitle } from "../types";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
|
||||||
interface DropdownViewDirectProps {
|
interface DropdownViewDirectProps {
|
||||||
showControls: boolean;
|
showControls: boolean;
|
||||||
@@ -71,13 +71,6 @@ const DropdownViewDirect: React.FC<DropdownViewDirectProps> = ({
|
|||||||
bitrateValue: string;
|
bitrateValue: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const [selectedSubtitleIndex, setSelectedSubtitleIndex] = useState<Number>(
|
|
||||||
parseInt(subtitleIndex)
|
|
||||||
);
|
|
||||||
const [selectedAudioIndex, setSelectedAudioIndex] = useState<Number>(
|
|
||||||
parseInt(audioIndex)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -116,7 +109,7 @@ const DropdownViewDirect: React.FC<DropdownViewDirectProps> = ({
|
|||||||
{allSubtitleTracksForDirectPlay?.map((sub, idx: number) => (
|
{allSubtitleTracksForDirectPlay?.map((sub, idx: number) => (
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
key={`subtitle-item-${idx}`}
|
key={`subtitle-item-${idx}`}
|
||||||
value={selectedSubtitleIndex === sub.index}
|
value={subtitleIndex === sub.index.toString()}
|
||||||
onValueChange={() => {
|
onValueChange={() => {
|
||||||
if ("deliveryUrl" in sub && sub.deliveryUrl) {
|
if ("deliveryUrl" in sub && sub.deliveryUrl) {
|
||||||
setSubtitleURL &&
|
setSubtitleURL &&
|
||||||
@@ -133,8 +126,9 @@ const DropdownViewDirect: React.FC<DropdownViewDirectProps> = ({
|
|||||||
console.log("Set sub index: ", sub.index);
|
console.log("Set sub index: ", sub.index);
|
||||||
setSubtitleTrack && setSubtitleTrack(sub.index);
|
setSubtitleTrack && setSubtitleTrack(sub.index);
|
||||||
}
|
}
|
||||||
|
router.setParams({
|
||||||
setSelectedSubtitleIndex(sub.index);
|
subtitleIndex: sub.index.toString(),
|
||||||
|
});
|
||||||
console.log("Subtitle: ", sub);
|
console.log("Subtitle: ", sub);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -159,10 +153,12 @@ const DropdownViewDirect: React.FC<DropdownViewDirectProps> = ({
|
|||||||
{audioTracks?.map((track, idx: number) => (
|
{audioTracks?.map((track, idx: number) => (
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
key={`audio-item-${idx}`}
|
key={`audio-item-${idx}`}
|
||||||
value={selectedAudioIndex === track.index}
|
value={audioIndex === track.index.toString()}
|
||||||
onValueChange={() => {
|
onValueChange={() => {
|
||||||
setSelectedAudioIndex(track.index);
|
|
||||||
setAudioTrack && setAudioTrack(track.index);
|
setAudioTrack && setAudioTrack(track.index);
|
||||||
|
router.setParams({
|
||||||
|
audioIndex: track.index.toString(),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ interface DropdownViewProps {
|
|||||||
offline?: boolean; // used to disable external subs for downloads
|
offline?: boolean; // used to disable external subs for downloads
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownView: React.FC<DropdownViewProps> = ({
|
const DropdownView: React.FC<DropdownViewProps> = ({ showControls }) => {
|
||||||
showControls,
|
|
||||||
offline = false,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const ControlContext = useControlContext();
|
const ControlContext = useControlContext();
|
||||||
@@ -46,24 +43,6 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
mediaSource?.MediaStreams?.filter((x) => x.Type === "Subtitle") ?? [];
|
mediaSource?.MediaStreams?.filter((x) => x.Type === "Subtitle") ?? [];
|
||||||
const textBasedSubs = allSubs.filter((x) => x.IsTextSubtitleStream);
|
const textBasedSubs = allSubs.filter((x) => x.IsTextSubtitleStream);
|
||||||
|
|
||||||
// This is used in the case where it is transcoding stream.
|
|
||||||
const chosenSubtitle = textBasedSubs.find(
|
|
||||||
(x) => x.Index === parseInt(subtitleIndex)
|
|
||||||
);
|
|
||||||
|
|
||||||
let initialSubtitleIndex = -1;
|
|
||||||
if (!isOnTextSubtitle) {
|
|
||||||
initialSubtitleIndex = parseInt(subtitleIndex);
|
|
||||||
} else if (chosenSubtitle) {
|
|
||||||
initialSubtitleIndex = textBasedSubs.indexOf(chosenSubtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [selectedSubtitleIndex, setSelectedSubtitleIndex] =
|
|
||||||
useState<number>(initialSubtitleIndex);
|
|
||||||
const [selectedAudioIndex, setSelectedAudioIndex] = useState<number>(
|
|
||||||
parseInt(audioIndex)
|
|
||||||
);
|
|
||||||
|
|
||||||
const allSubtitleTracksForTranscodingStream = useMemo(() => {
|
const allSubtitleTracksForTranscodingStream = useMemo(() => {
|
||||||
const disableSubtitle = {
|
const disableSubtitle = {
|
||||||
name: "Disable",
|
name: "Disable",
|
||||||
@@ -78,38 +57,26 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
IsTextSubtitleStream: true,
|
IsTextSubtitleStream: true,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const imageSubtitles = allSubs
|
console.log("textSubtitles", textSubtitles);
|
||||||
.filter((x) => !x.IsTextSubtitleStream)
|
|
||||||
.map(
|
|
||||||
(x) =>
|
|
||||||
({
|
|
||||||
name: x.DisplayTitle!,
|
|
||||||
index: x.Index!,
|
|
||||||
IsTextSubtitleStream: x.IsTextSubtitleStream,
|
|
||||||
} as TranscodedSubtitle)
|
|
||||||
);
|
|
||||||
|
|
||||||
const textSubtitlesMap = new Map(textSubtitles.map((s) => [s.name, s]));
|
let textIndex = 0; // To track position in textSubtitles
|
||||||
const imageSubtitlesMap = new Map(imageSubtitles.map((s) => [s.name, s]));
|
// 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];
|
||||||
|
textIndex++;
|
||||||
|
return textSubtitle;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
name: sub.DisplayTitle!,
|
||||||
|
index: sub.Index!,
|
||||||
|
IsTextSubtitleStream: sub.IsTextSubtitleStream,
|
||||||
|
} as TranscodedSubtitle;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const sortedSubtitles = Array.from(
|
console.log("sortedSubtitles", sortedSubtitles);
|
||||||
new Set(
|
|
||||||
allSubs
|
|
||||||
.map((sub) => {
|
|
||||||
const displayTitle = sub.DisplayTitle ?? "";
|
|
||||||
if (textSubtitlesMap.has(displayTitle)) {
|
|
||||||
return textSubtitlesMap.get(displayTitle);
|
|
||||||
}
|
|
||||||
if (imageSubtitlesMap.has(displayTitle)) {
|
|
||||||
return imageSubtitlesMap.get(displayTitle);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter(
|
|
||||||
(subtitle): subtitle is TranscodedSubtitle => subtitle !== null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return [disableSubtitle, ...sortedSubtitles];
|
return [disableSubtitle, ...sortedSubtitles];
|
||||||
}
|
}
|
||||||
@@ -145,26 +112,24 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
name: x.DisplayTitle!,
|
name: x.DisplayTitle!,
|
||||||
index: x.Index!,
|
index: x.Index!,
|
||||||
})) || [];
|
})) || [];
|
||||||
const ChangeTranscodingAudio = useCallback(
|
|
||||||
(audioIndex: number, currentSelectedSubtitleIndex: number) => {
|
|
||||||
let newSubtitleIndex: number;
|
|
||||||
|
|
||||||
if (!isOnTextSubtitle) {
|
// HLS stream indexes are not the same as the actual source indexes.
|
||||||
newSubtitleIndex = parseInt(subtitleIndex);
|
// This function aims to get the source subtitle index from the embedded track index.
|
||||||
} else if (
|
const getSourceSubtitleIndex = (embeddedTrackIndex: number): number => {
|
||||||
currentSelectedSubtitleIndex >= 0 &&
|
// If we're not on text-based subtitles, return the embedded track index
|
||||||
currentSelectedSubtitleIndex < textBasedSubs.length
|
if (!isOnTextSubtitle) {
|
||||||
) {
|
return parseInt(subtitleIndex);
|
||||||
console.log("setHere SubtitleIndex", currentSelectedSubtitleIndex);
|
}
|
||||||
newSubtitleIndex = textBasedSubs[currentSelectedSubtitleIndex].Index!;
|
return textBasedSubs[embeddedTrackIndex]?.Index ?? -1;
|
||||||
console.log("newSubtitleIndex", newSubtitleIndex);
|
};
|
||||||
} else {
|
|
||||||
newSubtitleIndex = -1;
|
const ChangeTranscodingAudio = useCallback(
|
||||||
}
|
(audioIndex: number) => {
|
||||||
|
console.log("ChangeTranscodingAudio", subtitleIndex, audioIndex);
|
||||||
const queryParams = new URLSearchParams({
|
const queryParams = new URLSearchParams({
|
||||||
itemId: item.Id ?? "", // Ensure itemId is a string
|
itemId: item.Id ?? "", // Ensure itemId is a string
|
||||||
audioIndex: audioIndex?.toString() ?? "",
|
audioIndex: audioIndex?.toString() ?? "",
|
||||||
subtitleIndex: newSubtitleIndex?.toString() ?? "",
|
subtitleIndex: subtitleIndex?.toString() ?? "",
|
||||||
mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
||||||
bitrateValue: bitrateValue,
|
bitrateValue: bitrateValue,
|
||||||
}).toString();
|
}).toString();
|
||||||
@@ -172,7 +137,7 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/transcoding-player?${queryParams}`);
|
router.replace(`player/transcoding-player?${queryParams}`);
|
||||||
},
|
},
|
||||||
[mediaSource]
|
[mediaSource, subtitleIndex, audioIndex]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -213,17 +178,27 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
{allSubtitleTracksForTranscodingStream?.map(
|
{allSubtitleTracksForTranscodingStream?.map(
|
||||||
(sub, idx: number) => (
|
(sub, idx: number) => (
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
value={selectedSubtitleIndex === sub.index}
|
value={
|
||||||
|
subtitleIndex ===
|
||||||
|
(sub.IsTextSubtitleStream && isOnTextSubtitle
|
||||||
|
? getSourceSubtitleIndex(sub.index).toString()
|
||||||
|
: sub?.index.toString())
|
||||||
|
}
|
||||||
key={`subtitle-item-${idx}`}
|
key={`subtitle-item-${idx}`}
|
||||||
onValueChange={() => {
|
onValueChange={() => {
|
||||||
console.log("sub", sub);
|
console.log("sub", sub);
|
||||||
if (selectedSubtitleIndex === sub?.index) return;
|
if (subtitleIndex === sub?.index.toString()) return;
|
||||||
setSelectedSubtitleIndex(sub.index);
|
router.setParams({
|
||||||
|
subtitleIndex: getSourceSubtitleIndex(
|
||||||
|
sub.index
|
||||||
|
).toString(),
|
||||||
|
});
|
||||||
|
|
||||||
if (sub.IsTextSubtitleStream && isOnTextSubtitle) {
|
if (sub.IsTextSubtitleStream && isOnTextSubtitle) {
|
||||||
setSubtitleTrack && setSubtitleTrack(sub.index);
|
setSubtitleTrack && setSubtitleTrack(sub.index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log("ChangeTranscodingSubtitle", subtitleIndex);
|
||||||
ChangeTranscodingSubtitle(sub.index);
|
ChangeTranscodingSubtitle(sub.index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -249,11 +224,14 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
{allAudio?.map((track, idx: number) => (
|
{allAudio?.map((track, idx: number) => (
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
key={`audio-item-${idx}`}
|
key={`audio-item-${idx}`}
|
||||||
value={selectedAudioIndex === track.index}
|
value={audioIndex === track.index.toString()}
|
||||||
onValueChange={() => {
|
onValueChange={() => {
|
||||||
if (selectedAudioIndex === track.index) return;
|
if (audioIndex === track.index.toString()) return;
|
||||||
setSelectedAudioIndex(track.index);
|
console.log("Setting audio track to: ", track.index);
|
||||||
ChangeTranscodingAudio(track.index, selectedSubtitleIndex);
|
router.setParams({
|
||||||
|
audioIndex: track.index.toString(),
|
||||||
|
});
|
||||||
|
ChangeTranscodingAudio(track.index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
// Used only for intial play settings.
|
||||||
const useDefaultPlaySettings = (
|
const useDefaultPlaySettings = (
|
||||||
item: BaseItemDto,
|
item: BaseItemDto,
|
||||||
settings: Settings | null
|
settings: Settings | null
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { Settings } from "../atoms/settings";
|
import { Settings, useSettings } from "../atoms/settings";
|
||||||
|
import { StreamRanker, SubtitleStreamRanker } from "../streamRanker";
|
||||||
|
|
||||||
interface PlaySettings {
|
interface PlaySettings {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -14,9 +15,13 @@ interface PlaySettings {
|
|||||||
subtitleIndex?: number | undefined;
|
subtitleIndex?: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used getting default values for the next player.
|
||||||
export function getDefaultPlaySettings(
|
export function getDefaultPlaySettings(
|
||||||
item: BaseItemDto,
|
item: BaseItemDto,
|
||||||
settings: Settings
|
settings: Settings,
|
||||||
|
previousIndex?: number,
|
||||||
|
previousItem?: BaseItemDto,
|
||||||
|
previousSource?: MediaSourceInfo
|
||||||
): PlaySettings {
|
): PlaySettings {
|
||||||
if (item.Type === "Program") {
|
if (item.Type === "Program") {
|
||||||
return {
|
return {
|
||||||
@@ -41,7 +46,22 @@ export function getDefaultPlaySettings(
|
|||||||
(x) => x.Type === "Audio"
|
(x) => x.Type === "Audio"
|
||||||
)?.Index;
|
)?.Index;
|
||||||
|
|
||||||
// TODO: Need to most common next subtitle index as an option.
|
// We prefer the previous track over the default track.
|
||||||
|
let trackOptions = {};
|
||||||
|
const mediaStreams = mediaSource?.MediaStreams ?? [];
|
||||||
|
if (settings?.rememberSubtitleSelections) {
|
||||||
|
if (previousIndex !== undefined && previousSource) {
|
||||||
|
const subtitleRanker = new SubtitleStreamRanker();
|
||||||
|
const ranker = new StreamRanker(subtitleRanker);
|
||||||
|
ranker.rankStream(
|
||||||
|
previousIndex,
|
||||||
|
previousSource,
|
||||||
|
mediaStreams,
|
||||||
|
trackOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const finalSubtitleIndex = mediaSource?.DefaultAudioStreamIndex;
|
const finalSubtitleIndex = mediaSource?.DefaultAudioStreamIndex;
|
||||||
|
|
||||||
// 4. Get default bitrate
|
// 4. Get default bitrate
|
||||||
|
|||||||
@@ -1,248 +1,35 @@
|
|||||||
interface StreamRankerStrategy {
|
import {
|
||||||
rankStream(
|
MediaSourceInfo,
|
||||||
prevIndex: number,
|
MediaStream,
|
||||||
prevSource: any,
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
mediaStreams: any[],
|
|
||||||
trackOptions: any,
|
|
||||||
isSecondarySubtitle: boolean
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SubtitleStreamRanker implements StreamRankerStrategy {
|
abstract class StreamRankerStrategy {
|
||||||
rankStream(
|
abstract streamType: string;
|
||||||
|
|
||||||
|
abstract rankStream(
|
||||||
prevIndex: number,
|
prevIndex: number,
|
||||||
prevSource: any,
|
prevSource: MediaSourceInfo,
|
||||||
mediaStreams: any[],
|
mediaStreams: MediaStream[],
|
||||||
trackOptions: any,
|
trackOptions: any
|
||||||
isSecondarySubtitle: boolean
|
): void;
|
||||||
|
|
||||||
|
protected rank(
|
||||||
|
prevIndex: number,
|
||||||
|
prevSource: MediaSourceInfo,
|
||||||
|
mediaStreams: MediaStream[],
|
||||||
|
trackOptions: any
|
||||||
): void {
|
): void {
|
||||||
if (prevIndex == -1) {
|
if (prevIndex == -1) {
|
||||||
console.debug(`AutoSet Subtitle - No Stream Set`);
|
console.debug(`AutoSet Subtitle - No Stream Set`);
|
||||||
if (isSecondarySubtitle) {
|
trackOptions[`Default${this.streamType}StreamIndex`] = -1;
|
||||||
trackOptions.DefaultSecondarySubtitleStreamIndex = -1;
|
|
||||||
} else {
|
|
||||||
trackOptions.DefaultSubtitleStreamIndex = -1;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prevSource.MediaStreams || !mediaStreams) {
|
if (!prevSource.MediaStreams || !mediaStreams) {
|
||||||
console.debug(`AutoSet Subtitle - No MediaStreams`);
|
console.debug(`AutoSet ${this.streamType} - No MediaStreams`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rank(
|
|
||||||
prevIndex,
|
|
||||||
prevSource,
|
|
||||||
mediaStreams,
|
|
||||||
trackOptions,
|
|
||||||
isSecondarySubtitle,
|
|
||||||
"Subtitle"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private rank(
|
|
||||||
prevIndex: number,
|
|
||||||
prevSource: any,
|
|
||||||
mediaStreams: any[],
|
|
||||||
trackOptions: any,
|
|
||||||
isSecondarySubtitle: boolean,
|
|
||||||
streamType: string
|
|
||||||
): void {
|
|
||||||
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}.`
|
|
||||||
);
|
|
||||||
trackOptions.DefaultSubtitleStreamIndex = bestStreamIndex;
|
|
||||||
} else {
|
|
||||||
console.debug(
|
|
||||||
`AutoSet ${streamType} - Threshold not met. Using default.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AudioStreamRanker implements StreamRankerStrategy {
|
|
||||||
rankStream(
|
|
||||||
prevIndex: number,
|
|
||||||
prevSource: any,
|
|
||||||
mediaStreams: any[],
|
|
||||||
trackOptions: any
|
|
||||||
): void {
|
|
||||||
if (prevIndex == -1) {
|
|
||||||
console.debug(`AutoSet Audio - No Stream Set`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prevSource.MediaStreams || !mediaStreams) {
|
|
||||||
console.debug(`AutoSet Audio - No MediaStreams`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rank(prevIndex, prevSource, mediaStreams, trackOptions, "Audio");
|
|
||||||
}
|
|
||||||
|
|
||||||
private rank(
|
|
||||||
prevIndex: number,
|
|
||||||
prevSource: any,
|
|
||||||
mediaStreams: any[],
|
|
||||||
trackOptions: any,
|
|
||||||
streamType: string
|
|
||||||
): void {
|
|
||||||
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}.`
|
|
||||||
);
|
|
||||||
trackOptions.DefaultAudioStreamIndex = bestStreamIndex;
|
|
||||||
} else {
|
|
||||||
console.debug(
|
|
||||||
`AutoSet ${streamType} - Threshold not met. Using default.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class StreamRanker {
|
|
||||||
private strategy: StreamRankerStrategy;
|
|
||||||
abstract streamType: string;
|
|
||||||
|
|
||||||
constructor(strategy: StreamRankerStrategy) {
|
|
||||||
this.strategy = strategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStrategy(strategy: StreamRankerStrategy) {
|
|
||||||
this.strategy = strategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
rankStream(
|
|
||||||
prevIndex: number,
|
|
||||||
prevSource: any,
|
|
||||||
mediaStreams: any[],
|
|
||||||
trackOptions: any,
|
|
||||||
streamType: string,
|
|
||||||
isSecondarySubtitle: boolean
|
|
||||||
) {
|
|
||||||
this.strategy.rankStream(
|
|
||||||
prevIndex,
|
|
||||||
prevSource,
|
|
||||||
mediaStreams,
|
|
||||||
trackOptions,
|
|
||||||
isSecondarySubtitle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private rank(
|
|
||||||
prevIndex: number,
|
|
||||||
prevSource: any,
|
|
||||||
mediaStreams: any[],
|
|
||||||
trackOptions: any
|
|
||||||
): void {
|
|
||||||
let bestStreamIndex = null;
|
let bestStreamIndex = null;
|
||||||
let bestStreamScore = 0;
|
let bestStreamScore = 0;
|
||||||
const prevStream = prevSource.MediaStreams[prevIndex];
|
const prevStream = prevSource.MediaStreams[prevIndex];
|
||||||
@@ -308,3 +95,52 @@ abstract class StreamRanker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SubtitleStreamRanker extends StreamRankerStrategy {
|
||||||
|
streamType = "Subtitle";
|
||||||
|
|
||||||
|
rankStream(
|
||||||
|
prevIndex: number,
|
||||||
|
prevSource: MediaSourceInfo,
|
||||||
|
mediaStreams: MediaStream[],
|
||||||
|
trackOptions: any
|
||||||
|
): void {
|
||||||
|
super.rank(prevIndex, prevSource, mediaStreams, trackOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioStreamRanker extends StreamRankerStrategy {
|
||||||
|
streamType = "Audio";
|
||||||
|
|
||||||
|
rankStream(
|
||||||
|
prevIndex: number,
|
||||||
|
prevSource: MediaSourceInfo,
|
||||||
|
mediaStreams: MediaStream[],
|
||||||
|
trackOptions: any
|
||||||
|
): void {
|
||||||
|
super.rank(prevIndex, prevSource, mediaStreams, trackOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StreamRanker {
|
||||||
|
private strategy: StreamRankerStrategy;
|
||||||
|
|
||||||
|
constructor(strategy: StreamRankerStrategy) {
|
||||||
|
this.strategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStrategy(strategy: StreamRankerStrategy) {
|
||||||
|
this.strategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
rankStream(
|
||||||
|
prevIndex: number,
|
||||||
|
prevSource: MediaSourceInfo,
|
||||||
|
mediaStreams: MediaStream[],
|
||||||
|
trackOptions: any
|
||||||
|
) {
|
||||||
|
this.strategy.rankStream(prevIndex, prevSource, mediaStreams, trackOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { StreamRanker, SubtitleStreamRanker, AudioStreamRanker };
|
||||||
|
|||||||
Reference in New Issue
Block a user