diff --git a/components/ItemContent.tv.tsx b/components/ItemContent.tv.tsx index 05d0d1ce..7a667e18 100644 --- a/components/ItemContent.tv.tsx +++ b/components/ItemContent.tv.tsx @@ -56,6 +56,7 @@ import { useSettings } from "@/utils/atoms/settings"; import type { TVOptionItem } from "@/utils/atoms/tvOptionModal"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; +import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils"; import { formatDuration, runtimeTicksToMinutes } from "@/utils/time"; const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window"); @@ -232,12 +233,13 @@ export const ItemContentTV: React.FC = React.memo( return streams ?? []; }, [selectedOptions?.mediaSource]); - // Get available subtitle tracks (raw MediaStream[] for label lookup) + // Get available subtitle tracks (raw MediaStream[] for label lookup), + // ordered like jellyfin-web (embedded first, externals last, forced/default up). const subtitleStreams = useMemo(() => { const streams = selectedOptions?.mediaSource?.MediaStreams?.filter( (s) => s.Type === "Subtitle", ); - return streams ?? []; + return streams ? [...streams].sort(compareTracksForMenu) : []; }, [selectedOptions?.mediaSource]); // Store handleSubtitleChange in a ref for stable callback reference diff --git a/components/MediaSourceButton.tsx b/components/MediaSourceButton.tsx index 6413b462..c2af02b9 100644 --- a/components/MediaSourceButton.tsx +++ b/components/MediaSourceButton.tsx @@ -7,6 +7,7 @@ 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 { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils"; import { BITRATES } from "./BitRateSheet"; import type { SelectedOptions } from "./ItemContent"; import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown"; @@ -63,9 +64,12 @@ export const MediaSourceButton: React.FC = ({ const subtitleStreams = useMemo( () => - selectedOptions.mediaSource?.MediaStreams?.filter( - (x) => x.Type === "Subtitle", - ) || [], + // Order like jellyfin-web (embedded first, externals last, forced/default up). + [ + ...(selectedOptions.mediaSource?.MediaStreams?.filter( + (x) => x.Type === "Subtitle", + ) || []), + ].sort(compareTracksForMenu), [selectedOptions.mediaSource], ); diff --git a/components/video-player/controls/Controls.tv.tsx b/components/video-player/controls/Controls.tv.tsx index a85f6215..e0175104 100644 --- a/components/video-player/controls/Controls.tv.tsx +++ b/components/video-player/controls/Controls.tv.tsx @@ -51,6 +51,7 @@ import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { useSettings } from "@/utils/atoms/settings"; import type { TVOptionItem } from "@/utils/atoms/tvOptionModal"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; +import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils"; import { formatTimeString, msToTicks, ticksToMs } from "@/utils/time"; import { CONTROLS_CONSTANTS } from "./constants"; import { useVideoContext } from "./contexts/VideoContext"; @@ -317,8 +318,10 @@ export const Controls: FC = ({ try { const streams = (await onRefreshSubtitleTracks?.()) ?? []; // Skip streams without a real index: `?? -1` would alias them to the - // "disable subtitles" sentinel and mis-route selection. - return streams + // "disable subtitles" sentinel and mis-route selection. Order like + // jellyfin-web (embedded first, externals last, forced/default up). + return [...streams] + .sort(compareTracksForMenu) .filter((stream) => typeof stream.Index === "number") .map((stream) => { const index = stream.Index as number;