fix(subtitles): re-negotiate the stream for unresolvable and burned tracks

TV subtitle changes now honor the same transcode guard as the mobile menu
(switching to/from a burned-in image sub re-negotiates the stream instead of a
no-op track change). Menu-listed tracks the player can't select (server-burned
Encode, sidecars never sub-added) fall back to replacePlayer so the server
delivers them. Freshly downloaded local subs move the live index to the local
sentinel before the sub-add so onTracksReady doesn't clobber them. Shares the
external-URL builder and the LOCAL_SUBTITLE_INDEX_START constant.
This commit is contained in:
Gauvain
2026-07-06 23:48:35 +02:00
parent 80602ecd23
commit e5d56d6ad1
4 changed files with 108 additions and 47 deletions

View File

@@ -40,7 +40,10 @@ import {
TVSeriesNavigation,
TVTechnicalDetails,
} from "@/components/tv";
import type { Track } from "@/components/video-player/controls/types";
import {
LOCAL_SUBTITLE_INDEX_START,
type Track,
} from "@/components/video-player/controls/types";
import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
@@ -250,9 +253,6 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
// State to trigger refresh of local subtitles list
const [localSubtitlesRefreshKey, setLocalSubtitlesRefreshKey] = useState(0);
// Starting index for local (client-downloaded) subtitles
const LOCAL_SUBTITLE_INDEX_START = -100;
// Convert MediaStream[] to Track[] for the modal (with setTrack callbacks)
// Also includes locally downloaded subtitles from OpenSubtitles
const subtitleTracksForModal = useMemo((): Track[] => {

View File

@@ -64,15 +64,12 @@ import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
import {
applyMpvSubtitleSelection,
compareTracksForMenu,
getExternalSubtitleUrl,
isImageBasedSubtitle,
} from "@/utils/jellyfin/subtitleUtils";
import type { Track } from "../types";
import { LOCAL_SUBTITLE_INDEX_START, type Track } from "../types";
import { usePlayerContext, usePlayerControls } from "./PlayerContext";
// Starting index for local (client-downloaded) subtitles
// Uses negative indices to avoid collision with Jellyfin indices
const LOCAL_SUBTITLE_INDEX_START = -100;
interface VideoContextProps {
subtitleTracks: Track[] | null;
audioTracks: Track[] | null;
@@ -292,13 +289,15 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
// Mirror how external subs are loaded into MPV (online: basePath +
// DeliveryUrl, offline: local DeliveryUrl) so identity matching by
// external-filename lines up.
getExpectedExternalUrl: (s) => {
if (!s.DeliveryUrl) return undefined;
if (offline) return s.DeliveryUrl;
return api?.basePath
? `${api.basePath}${s.DeliveryUrl}`
: undefined;
},
getExpectedExternalUrl: (s) =>
getExternalSubtitleUrl(s, { offline, basePath: api?.basePath }),
}).then((result) => {
// Safety net: a menu-listed sub the player can't select (server-
// burned Encode, sidecar never sub-added) only shows up after the
// server re-processes the stream with it.
if (result.kind === "notFound" || result.kind === "burnedIn") {
replacePlayer({ subtitleIndex: String(sub.Index) });
}
});
},
});

View File

@@ -28,4 +28,12 @@ type Track = {
localPath?: string;
};
/**
* Synthetic `Track.index` base for client-side downloaded subtitles (loaded via
* `addSubtitleFile`, no matching Jellyfin MediaStream). Local sub k gets
* `LOCAL_SUBTITLE_INDEX_START - k`, so `index <= LOCAL_SUBTITLE_INDEX_START`
* identifies a local track.
*/
export const LOCAL_SUBTITLE_INDEX_START = -100;
export type { EmbeddedSubtitle, ExternalSubtitle, Track, TranscodedSubtitle };