mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-07 13:02:52 +01:00
fix(subtitles): teardown mpv before re-navigation and record the real local-sub index
Destroy the mpv instance before the track-change router.replace in replaceWithTrackSelection and goToPreviousItem, matching goToNextItem — two overlapping decoders/surfaces during the transition OOM low-RAM devices (CodeRabbit). addSubtitleFile now sets the live index to the downloaded local sub's real index (LOCAL_SUBTITLE_INDEX_START - position) instead of a blanket -100, which collided with the first local sub and mis-recorded the selection (Copilot); it still resolves to notFound on the onTracksReady re-apply so it doesn't clobber the freshly selected track.
This commit is contained in:
@@ -931,6 +931,10 @@ export default function DirectPlayerPage() {
|
|||||||
bitrateValue: bitrateValue?.toString() ?? "",
|
bitrateValue: bitrateValue?.toString() ?? "",
|
||||||
playbackPosition: msToTicks(progress.get()).toString(),
|
playbackPosition: msToTicks(progress.get()).toString(),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
// Destroy the current mpv instance before re-navigating, same rationale as
|
||||||
|
// goToNextItem: Expo Router briefly holds two players during the
|
||||||
|
// transition and two decoders/surfaces OOM-kill low-RAM devices.
|
||||||
|
videoRef.current?.destroy().catch(() => {});
|
||||||
router.replace(`player/direct-player?${queryParams}` as any);
|
router.replace(`player/direct-player?${queryParams}` as any);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -1105,6 +1109,10 @@ export default function DirectPlayerPage() {
|
|||||||
previousItem.UserData?.PlaybackPositionTicks?.toString() ?? "",
|
previousItem.UserData?.PlaybackPositionTicks?.toString() ?? "",
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
|
// Free the current mpv instance before navigating, matching goToNextItem —
|
||||||
|
// otherwise two decoders/surfaces overlap during the transition and can
|
||||||
|
// OOM-kill low-RAM devices.
|
||||||
|
videoRef.current?.destroy().catch(() => {});
|
||||||
router.replace(`player/direct-player?${queryParams}` as any);
|
router.replace(`player/direct-player?${queryParams}` as any);
|
||||||
}, [
|
}, [
|
||||||
previousItem,
|
previousItem,
|
||||||
@@ -1117,14 +1125,24 @@ export default function DirectPlayerPage() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// TV: Add subtitle file to player (for client-side downloaded subtitles)
|
// TV: Add subtitle file to player (for client-side downloaded subtitles)
|
||||||
const addSubtitleFile = useCallback(async (path: string) => {
|
const addSubtitleFile = useCallback(
|
||||||
// Move the live index off the previous server sub BEFORE the add: the
|
async (path: string) => {
|
||||||
// sub-add fires onTracksReady, whose re-apply would otherwise resolve the
|
// Set the live index to the new local sub's REAL index BEFORE the add.
|
||||||
// stale index and clobber the freshly selected local sub (disable it, or
|
// Local subs are keyed LOCAL_SUBTITLE_INDEX_START - position, so use the
|
||||||
// re-select the old track). The sentinel resolves to notFound → no-op.
|
// downloaded path's position (not a blanket sentinel, which would collide
|
||||||
setCurrentSubtitleIndex(LOCAL_SUBTITLE_INDEX_START);
|
// with the first local sub at -100 and mis-record the selection). Any
|
||||||
await videoRef.current?.addSubtitleFile?.(path, true);
|
// local index resolves to notFound on the onTracksReady re-apply, so it
|
||||||
}, []);
|
// still doesn't clobber the freshly selected track; carry-over now keeps
|
||||||
|
// the correct local sub.
|
||||||
|
const locals = itemId ? getSubtitlesForItem(itemId) : [];
|
||||||
|
const pos = locals.findIndex((s) => s.filePath === path);
|
||||||
|
setCurrentSubtitleIndex(
|
||||||
|
LOCAL_SUBTITLE_INDEX_START - (pos >= 0 ? pos : 0),
|
||||||
|
);
|
||||||
|
await videoRef.current?.addSubtitleFile?.(path, true);
|
||||||
|
},
|
||||||
|
[itemId],
|
||||||
|
);
|
||||||
|
|
||||||
// TV: Refresh subtitle tracks after server-side subtitle download
|
// TV: Refresh subtitle tracks after server-side subtitle download
|
||||||
// Re-fetches the media source to pick up newly downloaded subtitles
|
// Re-fetches the media source to pick up newly downloaded subtitles
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
type SubtitleSearchResult,
|
type SubtitleSearchResult,
|
||||||
useRemoteSubtitles,
|
useRemoteSubtitles,
|
||||||
} from "@/hooks/useRemoteSubtitles";
|
} from "@/hooks/useRemoteSubtitles";
|
||||||
import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils";
|
|
||||||
import { COMMON_SUBTITLE_LANGUAGES } from "@/utils/opensubtitles/api";
|
import { COMMON_SUBTITLE_LANGUAGES } from "@/utils/opensubtitles/api";
|
||||||
|
|
||||||
interface TVSubtitleSheetProps {
|
interface TVSubtitleSheetProps {
|
||||||
@@ -97,19 +96,13 @@ export const TVSubtitleSheet: React.FC<TVSubtitleSheetProps> = ({
|
|||||||
const overlayOpacity = useRef(new Animated.Value(0)).current;
|
const overlayOpacity = useRef(new Animated.Value(0)).current;
|
||||||
const sheetTranslateY = useRef(new Animated.Value(300)).current;
|
const sheetTranslateY = useRef(new Animated.Value(300)).current;
|
||||||
|
|
||||||
// Order like jellyfin-web (embedded first, externals last, forced/default up).
|
|
||||||
const sortedTracks = useMemo(
|
|
||||||
() => [...subtitleTracks].sort(compareTracksForMenu),
|
|
||||||
[subtitleTracks],
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialSelectedTrackIndex = useMemo(() => {
|
const initialSelectedTrackIndex = useMemo(() => {
|
||||||
if (currentSubtitleIndex === -1) return 0;
|
if (currentSubtitleIndex === -1) return 0;
|
||||||
const trackIdx = sortedTracks.findIndex(
|
const trackIdx = subtitleTracks.findIndex(
|
||||||
(t) => t.Index === currentSubtitleIndex,
|
(t) => t.Index === currentSubtitleIndex,
|
||||||
);
|
);
|
||||||
return trackIdx >= 0 ? trackIdx + 1 : 0;
|
return trackIdx >= 0 ? trackIdx + 1 : 0;
|
||||||
}, [sortedTracks, currentSubtitleIndex]);
|
}, [subtitleTracks, currentSubtitleIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
@@ -222,7 +215,7 @@ export const TVSubtitleSheet: React.FC<TVSubtitleSheetProps> = ({
|
|||||||
value: -1,
|
value: -1,
|
||||||
selected: currentSubtitleIndex === -1,
|
selected: currentSubtitleIndex === -1,
|
||||||
};
|
};
|
||||||
const options = sortedTracks.map((track) => ({
|
const options = subtitleTracks.map((track) => ({
|
||||||
label:
|
label:
|
||||||
track.DisplayTitle || `${track.Language || "Unknown"} (${track.Codec})`,
|
track.DisplayTitle || `${track.Language || "Unknown"} (${track.Codec})`,
|
||||||
sublabel: track.Codec?.toUpperCase(),
|
sublabel: track.Codec?.toUpperCase(),
|
||||||
@@ -230,7 +223,7 @@ export const TVSubtitleSheet: React.FC<TVSubtitleSheetProps> = ({
|
|||||||
selected: track.Index === currentSubtitleIndex,
|
selected: track.Index === currentSubtitleIndex,
|
||||||
}));
|
}));
|
||||||
return [noneOption, ...options];
|
return [noneOption, ...options];
|
||||||
}, [sortedTracks, currentSubtitleIndex, t]);
|
}, [subtitleTracks, currentSubtitleIndex, t]);
|
||||||
|
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user