From 58f3646477a65f7bf8a4c3d3ec960605b88bd3ca Mon Sep 17 00:00:00 2001 From: Gauvain Date: Tue, 7 Jul 2026 00:23:38 +0200 Subject: [PATCH] fix(subtitles): teardown mpv before re-navigation and record the real local-sub index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/(auth)/player/direct-player.tsx | 34 ++++++++++++++----- .../video-player/controls/TVSubtitleSheet.tsx | 15 +++----- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 2c640535..75934e70 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -931,6 +931,10 @@ export default function DirectPlayerPage() { bitrateValue: bitrateValue?.toString() ?? "", playbackPosition: msToTicks(progress.get()).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); }, [ @@ -1105,6 +1109,10 @@ export default function DirectPlayerPage() { previousItem.UserData?.PlaybackPositionTicks?.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); }, [ previousItem, @@ -1117,14 +1125,24 @@ export default function DirectPlayerPage() { ]); // TV: Add subtitle file to player (for client-side downloaded subtitles) - const addSubtitleFile = useCallback(async (path: string) => { - // Move the live index off the previous server sub BEFORE the add: the - // sub-add fires onTracksReady, whose re-apply would otherwise resolve the - // stale index and clobber the freshly selected local sub (disable it, or - // re-select the old track). The sentinel resolves to notFound → no-op. - setCurrentSubtitleIndex(LOCAL_SUBTITLE_INDEX_START); - await videoRef.current?.addSubtitleFile?.(path, true); - }, []); + const addSubtitleFile = useCallback( + async (path: string) => { + // Set the live index to the new local sub's REAL index BEFORE the add. + // Local subs are keyed LOCAL_SUBTITLE_INDEX_START - position, so use the + // downloaded path's position (not a blanket sentinel, which would collide + // with the first local sub at -100 and mis-record the selection). Any + // 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 // Re-fetches the media source to pick up newly downloaded subtitles diff --git a/components/video-player/controls/TVSubtitleSheet.tsx b/components/video-player/controls/TVSubtitleSheet.tsx index c2fa2575..6284b566 100644 --- a/components/video-player/controls/TVSubtitleSheet.tsx +++ b/components/video-player/controls/TVSubtitleSheet.tsx @@ -33,7 +33,6 @@ import { type SubtitleSearchResult, useRemoteSubtitles, } from "@/hooks/useRemoteSubtitles"; -import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils"; import { COMMON_SUBTITLE_LANGUAGES } from "@/utils/opensubtitles/api"; interface TVSubtitleSheetProps { @@ -97,19 +96,13 @@ export const TVSubtitleSheet: React.FC = ({ const overlayOpacity = useRef(new Animated.Value(0)).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(() => { if (currentSubtitleIndex === -1) return 0; - const trackIdx = sortedTracks.findIndex( + const trackIdx = subtitleTracks.findIndex( (t) => t.Index === currentSubtitleIndex, ); return trackIdx >= 0 ? trackIdx + 1 : 0; - }, [sortedTracks, currentSubtitleIndex]); + }, [subtitleTracks, currentSubtitleIndex]); useEffect(() => { if (visible) { @@ -222,7 +215,7 @@ export const TVSubtitleSheet: React.FC = ({ value: -1, selected: currentSubtitleIndex === -1, }; - const options = sortedTracks.map((track) => ({ + const options = subtitleTracks.map((track) => ({ label: track.DisplayTitle || `${track.Language || "Unknown"} (${track.Codec})`, sublabel: track.Codec?.toUpperCase(), @@ -230,7 +223,7 @@ export const TVSubtitleSheet: React.FC = ({ selected: track.Index === currentSubtitleIndex, })); return [noneOption, ...options]; - }, [sortedTracks, currentSubtitleIndex, t]); + }, [subtitleTracks, currentSubtitleIndex, t]); if (!visible) return null;