mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-31 19:18:26 +01:00
Compare commits
1 Commits
sync-subti
...
renovate/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8557d38834 |
@@ -147,9 +147,9 @@ export default function DirectPlayerPage() {
|
||||
const audioIndexFromUrl = audioIndexStr
|
||||
? Number.parseInt(audioIndexStr, 10)
|
||||
: undefined;
|
||||
const subtitleIndexFromUrl = subtitleIndexStr
|
||||
const subtitleIndex = subtitleIndexStr
|
||||
? Number.parseInt(subtitleIndexStr, 10)
|
||||
: undefined;
|
||||
: -1;
|
||||
const bitrateValue = bitrateValueStr
|
||||
? Number.parseInt(bitrateValueStr, 10)
|
||||
: BITRATES[0].value;
|
||||
@@ -185,23 +185,6 @@ export default function DirectPlayerPage() {
|
||||
return undefined;
|
||||
}, [audioIndexFromUrl, offline, downloadedItem?.userData?.audioStreamIndex]);
|
||||
|
||||
// Resolve subtitle index: use URL param if provided, otherwise use stored index for offline playback
|
||||
const subtitleIndex = useMemo(() => {
|
||||
if (subtitleIndexFromUrl !== undefined) {
|
||||
return subtitleIndexFromUrl;
|
||||
}
|
||||
if (
|
||||
offline &&
|
||||
downloadedItem?.userData?.subtitleStreamIndex !== undefined
|
||||
) {
|
||||
return downloadedItem.userData.subtitleStreamIndex;
|
||||
}
|
||||
return -1;
|
||||
}, [
|
||||
subtitleIndexFromUrl,
|
||||
offline,
|
||||
downloadedItem?.userData?.subtitleStreamIndex,
|
||||
]);
|
||||
// Initialize TV audio/subtitle indices from URL params.
|
||||
// No undefined guard: when a new episode's URL omits audioIndex, reset to
|
||||
// undefined (media default) rather than leaking the previous episode's track.
|
||||
|
||||
10
bun.lock
10
bun.lock
@@ -68,7 +68,7 @@
|
||||
"react-native-device-info": "^15.0.0",
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-edge-to-edge": "^1.7.0",
|
||||
"react-native-gesture-handler": "~2.31.1",
|
||||
"react-native-gesture-handler": "~3.0.0",
|
||||
"react-native-glass-effect-view": "^1.0.0",
|
||||
"react-native-google-cast": "^4.9.1",
|
||||
"react-native-image-colors": "^2.4.0",
|
||||
@@ -292,8 +292,6 @@
|
||||
|
||||
"@douglowder/expo-av-route-picker-view": ["@douglowder/expo-av-route-picker-view@0.0.5", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-oT4wf8aYYNfLEuZEkwZIH7CtEHKnEHWnjs6/hNwbFGEC0FnfjjWBNrQEt4fo5/gkafqa2G5ILkxndMyBZvk5dg=="],
|
||||
|
||||
"@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="],
|
||||
|
||||
"@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
|
||||
|
||||
"@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.38", "", {}, "sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A=="],
|
||||
@@ -588,8 +586,6 @@
|
||||
|
||||
"@types/emscripten": ["@types/emscripten@1.41.5", "", {}, "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q=="],
|
||||
|
||||
"@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="],
|
||||
|
||||
"@types/hoist-non-react-statics": ["@types/hoist-non-react-statics@3.3.7", "", { "dependencies": { "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "@types/react": "*" } }, "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g=="],
|
||||
|
||||
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
||||
@@ -1562,7 +1558,7 @@
|
||||
|
||||
"react-native-edge-to-edge": ["react-native-edge-to-edge@1.8.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-bhvsKqeX9PGkY9wBUk9vni/tJNJdKtLPbs/j3e/3CdV4JmUWfTXYYoL+4Hc8Wmej+5eJxkc8KOFa454ruFWBCA=="],
|
||||
|
||||
"react-native-gesture-handler": ["react-native-gesture-handler@2.31.2", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "@types/react-test-renderer": "^19.1.0", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-rw5q74i2AfS7YGYdbxQDhOU7xqgY6WRM1132/CCm3erqjblhECZDZFHIm0tteHoC9ih24wogVBVVzcTBQtZ+5A=="],
|
||||
"react-native-gesture-handler": ["react-native-gesture-handler@3.0.0", "", { "dependencies": { "@types/react-test-renderer": "^19.1.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-6E8o9D2sHwhFGiU0c4aCweMdJwIbQeBV+dq3IQ3HcqKhVGzg7ccEycap6i0zGCtIYfs3V29Xd4OycwcRj5qxBQ=="],
|
||||
|
||||
"react-native-glass-effect-view": ["react-native-glass-effect-view@1.0.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ABYG0oIiqbXsxe2R/cMhNgDn3YgwDLz/2TIN2XOxQopXC+MiGsG9C32VYQvO2sYehcu5JmI3h3EzwLwl6lJhhA=="],
|
||||
|
||||
@@ -1596,7 +1592,7 @@
|
||||
|
||||
"react-native-text-ticker": ["react-native-text-ticker@1.15.0", "", {}, "sha512-d/uK+PIOhsYMy1r8h825iq/nADiHsabz3WMbRJSnkpQYn+K9aykUAXRRhu8ZbTAzk4CgnUWajJEFxS5ZDygsdg=="],
|
||||
|
||||
"react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd"],
|
||||
"react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd", "sha512-vfkld2jUj7EPkAjIc/Vbx4Q4MtOOLmYtCYCE2dWJsyLnPqgj1f0xVzBxbeVP7dfT+eSh4KIXfdxESXaHgrXIlw=="],
|
||||
|
||||
"react-native-udp": ["react-native-udp@4.1.7", "", { "dependencies": { "buffer": "^5.6.0", "events": "^3.1.0" } }, "sha512-NUE3zewu61NCdSsLlj+l0ad6qojcVEZPT4hVG/x6DU9U4iCzwtfZSASh9vm7teAcVzLkdD+cO3411LHshAi/wA=="],
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
||||
import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
|
||||
import { useOrientation } from "@/hooks/useOrientation";
|
||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
@@ -60,9 +59,6 @@ const ItemContentMobile: React.FC<ItemContentProps> = ({
|
||||
}) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const isOffline = useOfflineMode();
|
||||
const { getDownloadedItemById } = useDownload();
|
||||
const downloadedItem =
|
||||
isOffline && item?.Id ? getDownloadedItemById(item.Id) : null;
|
||||
const { settings } = useSettings();
|
||||
const { orientation } = useOrientation();
|
||||
const navigation = useNavigation();
|
||||
@@ -109,30 +105,17 @@ const ItemContentMobile: React.FC<ItemContentProps> = ({
|
||||
|
||||
// Needs to automatically change the selected to the default values for default indexes.
|
||||
useEffect(() => {
|
||||
// When offline, use the indices stored in userData (the last-used tracks for this file)
|
||||
// rather than the server's defaults, so MediaSourceButton reflects what will actually play.
|
||||
const offlineUserData = downloadedItem?.userData;
|
||||
|
||||
setSelectedOptions(() => ({
|
||||
bitrate: defaultBitrate,
|
||||
mediaSource: defaultMediaSource ?? undefined,
|
||||
subtitleIndex:
|
||||
offlineUserData && !offlineUserData.isTranscoded
|
||||
? offlineUserData.subtitleStreamIndex
|
||||
: (defaultSubtitleIndex ?? -1),
|
||||
audioIndex:
|
||||
offlineUserData && !offlineUserData.isTranscoded
|
||||
? offlineUserData.audioStreamIndex
|
||||
: defaultAudioIndex,
|
||||
subtitleIndex: defaultSubtitleIndex ?? -1,
|
||||
audioIndex: defaultAudioIndex,
|
||||
}));
|
||||
}, [
|
||||
defaultAudioIndex,
|
||||
defaultBitrate,
|
||||
defaultSubtitleIndex,
|
||||
defaultMediaSource,
|
||||
downloadedItem?.userData?.audioStreamIndex,
|
||||
downloadedItem?.userData?.subtitleStreamIndex,
|
||||
downloadedItem?.userData?.isTranscoded,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,8 +7,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||
import { BITRATES } from "./BitRateSheet";
|
||||
import type { SelectedOptions } from "./ItemContent";
|
||||
import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
|
||||
@@ -30,14 +28,6 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const isOffline = useOfflineMode();
|
||||
const { getDownloadedItemById } = useDownload();
|
||||
|
||||
// For transcoded downloads there's only one burned-in track — nothing to pick
|
||||
const isTranscodedDownload = useMemo(() => {
|
||||
if (!isOffline || !item?.Id) return false;
|
||||
return getDownloadedItemById(item.Id)?.userData?.isTranscoded === true;
|
||||
}, [isOffline, item?.Id, getDownloadedItemById]);
|
||||
|
||||
const effectiveColors = colors || {
|
||||
primary: "#7c3aed",
|
||||
@@ -82,36 +72,34 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
const optionGroups: OptionGroup[] = useMemo(() => {
|
||||
const groups: OptionGroup[] = [];
|
||||
|
||||
if (!isOffline) {
|
||||
// Bitrate group
|
||||
// Bitrate group
|
||||
groups.push({
|
||||
title: t("item_card.quality"),
|
||||
options: BITRATES.map((bitrate) => ({
|
||||
type: "radio" as const,
|
||||
label: bitrate.key,
|
||||
value: bitrate,
|
||||
selected: bitrate.value === selectedOptions.bitrate?.value,
|
||||
onPress: () =>
|
||||
setSelectedOptions((prev) => prev && { ...prev, bitrate }),
|
||||
})),
|
||||
});
|
||||
|
||||
// Media Source group (only if multiple sources)
|
||||
if (item?.MediaSources && item.MediaSources.length > 1) {
|
||||
groups.push({
|
||||
title: t("item_card.quality"),
|
||||
options: BITRATES.map((bitrate) => ({
|
||||
title: t("item_card.video"),
|
||||
options: item.MediaSources.map((source) => ({
|
||||
type: "radio" as const,
|
||||
label: bitrate.key,
|
||||
value: bitrate,
|
||||
selected: bitrate.value === selectedOptions.bitrate?.value,
|
||||
label: getMediaSourceDisplayName(source),
|
||||
value: source,
|
||||
selected: source.Id === selectedOptions.mediaSource?.Id,
|
||||
onPress: () =>
|
||||
setSelectedOptions((prev) => prev && { ...prev, bitrate }),
|
||||
setSelectedOptions(
|
||||
(prev) => prev && { ...prev, mediaSource: source },
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
||||
// Media Source group (only if multiple sources)
|
||||
if (item?.MediaSources && item.MediaSources.length > 1) {
|
||||
groups.push({
|
||||
title: t("item_card.video"),
|
||||
options: item.MediaSources.map((source) => ({
|
||||
type: "radio" as const,
|
||||
label: getMediaSourceDisplayName(source),
|
||||
value: source,
|
||||
selected: source.Id === selectedOptions.mediaSource?.Id,
|
||||
onPress: () =>
|
||||
setSelectedOptions(
|
||||
(prev) => prev && { ...prev, mediaSource: source },
|
||||
),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Audio track group
|
||||
@@ -162,7 +150,6 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
return groups;
|
||||
}, [
|
||||
item,
|
||||
isOffline,
|
||||
selectedOptions,
|
||||
audioStreams,
|
||||
subtitleStreams,
|
||||
@@ -191,8 +178,6 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
if (isTranscodedDownload) return null;
|
||||
|
||||
return (
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
|
||||
@@ -96,23 +96,14 @@ export const PlayButton: React.FC<Props> = ({
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
audioIndex: selectedOptions.audioIndex?.toString() ?? "",
|
||||
subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "",
|
||||
mediaSourceId: selectedOptions.mediaSource?.Id ?? "",
|
||||
bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "",
|
||||
playbackPosition: item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
offline: isOffline ? "true" : "false",
|
||||
});
|
||||
|
||||
if (selectedOptions.audioIndex !== undefined) {
|
||||
queryParams.set("audioIndex", selectedOptions.audioIndex.toString());
|
||||
}
|
||||
|
||||
if (selectedOptions.subtitleIndex !== undefined) {
|
||||
queryParams.set(
|
||||
"subtitleIndex",
|
||||
selectedOptions.subtitleIndex.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
|
||||
if (!client) {
|
||||
@@ -301,29 +292,6 @@ export const PlayButton: React.FC<Props> = ({
|
||||
t,
|
||||
]);
|
||||
|
||||
const buildOfflineQueryParams = useCallback(
|
||||
(downloadedItem: NonNullable<ReturnType<typeof getDownloadedItemById>>) => {
|
||||
const isTranscoded = downloadedItem.userData?.isTranscoded === true;
|
||||
const audioIdx = isTranscoded
|
||||
? downloadedItem.userData?.audioStreamIndex
|
||||
: selectedOptions.audioIndex;
|
||||
const subtitleIdx = isTranscoded
|
||||
? downloadedItem.userData?.subtitleStreamIndex
|
||||
: selectedOptions.subtitleIndex;
|
||||
const params = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
if (audioIdx !== undefined) params.set("audioIndex", audioIdx.toString());
|
||||
if (subtitleIdx !== undefined)
|
||||
params.set("subtitleIndex", subtitleIdx.toString());
|
||||
return params;
|
||||
},
|
||||
[item, selectedOptions],
|
||||
);
|
||||
|
||||
const onPress = useCallback(async () => {
|
||||
if (!item) return;
|
||||
|
||||
@@ -334,7 +302,13 @@ export const PlayButton: React.FC<Props> = ({
|
||||
|
||||
// If already in offline mode, play downloaded file directly
|
||||
if (isOffline && downloadedItem) {
|
||||
goToPlayer(buildOfflineQueryParams(downloadedItem).toString());
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
goToPlayer(queryParams.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -357,9 +331,13 @@ export const PlayButton: React.FC<Props> = ({
|
||||
<Button
|
||||
onPress={() => {
|
||||
hideModal();
|
||||
goToPlayer(
|
||||
buildOfflineQueryParams(downloadedItem).toString(),
|
||||
);
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
goToPlayer(queryParams.toString());
|
||||
}}
|
||||
color='purple'
|
||||
>
|
||||
@@ -396,7 +374,13 @@ export const PlayButton: React.FC<Props> = ({
|
||||
{
|
||||
text: t("player.downloaded_file_yes"),
|
||||
onPress: () => {
|
||||
goToPlayer(buildOfflineQueryParams(downloadedItem).toString());
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
goToPlayer(queryParams.toString());
|
||||
},
|
||||
isPreferred: true,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useCallback } from "react";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { getDownloadedItemById } from "@/providers/Downloads/database";
|
||||
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
||||
import { writeToLog } from "@/utils/log";
|
||||
|
||||
@@ -16,27 +15,12 @@ export const useDownloadedFileOpener = () => {
|
||||
console.error("Attempted to open a file without an ID.");
|
||||
return;
|
||||
}
|
||||
const downloadedItem = getDownloadedItemById(item.Id);
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id,
|
||||
offline: "true",
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
|
||||
if (downloadedItem?.userData?.audioStreamIndex !== undefined) {
|
||||
queryParams.set(
|
||||
"audioIndex",
|
||||
downloadedItem.userData.audioStreamIndex.toString(),
|
||||
);
|
||||
}
|
||||
if (downloadedItem?.userData?.subtitleStreamIndex !== undefined) {
|
||||
queryParams.set(
|
||||
"subtitleIndex",
|
||||
downloadedItem.userData.subtitleStreamIndex.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
router.push(`/player/direct-player?${queryParams.toString()}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -186,20 +186,6 @@ export const usePlaybackManager = ({
|
||||
: playedPercentage,
|
||||
},
|
||||
},
|
||||
// Sync selected audio/subtitle tracks so next playback resumes with
|
||||
// the same tracks the user had active — but only for non-transcoded
|
||||
// downloads where the user can freely switch tracks.
|
||||
userData: localItem.userData.isTranscoded
|
||||
? localItem.userData
|
||||
: {
|
||||
...localItem.userData,
|
||||
audioStreamIndex:
|
||||
playbackProgressInfo.AudioStreamIndex ??
|
||||
localItem.userData.audioStreamIndex,
|
||||
subtitleStreamIndex:
|
||||
playbackProgressInfo.SubtitleStreamIndex ??
|
||||
localItem.userData.subtitleStreamIndex,
|
||||
},
|
||||
});
|
||||
// Force invalidate queries so they refetch from updated local database
|
||||
queryClient.invalidateQueries({ queryKey: ["item", itemId] });
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"react-native-device-info": "^15.0.0",
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-edge-to-edge": "^1.7.0",
|
||||
"react-native-gesture-handler": "~2.31.1",
|
||||
"react-native-gesture-handler": "~3.0.0",
|
||||
"react-native-glass-effect-view": "^1.0.0",
|
||||
"react-native-google-cast": "^4.9.1",
|
||||
"react-native-image-colors": "^2.4.0",
|
||||
|
||||
Reference in New Issue
Block a user