/** * Subtitle utility functions for mapping between Jellyfin and MPV track indices. * * Jellyfin uses server-side indices (e.g., 3, 4, 5 for subtitles in MediaStreams). * MPV uses its own track IDs starting from 1, only counting tracks loaded into MPV. * * Image-based subtitles (PGS, VOBSUB) during transcoding are burned into the video * and NOT available in MPV's track list. */ import { type MediaSourceInfo, type MediaStream, SubtitleDeliveryMethod, } from "@jellyfin/sdk/lib/generated-client"; /** Check if subtitle is image-based (PGS, VOBSUB, etc.) */ export const isImageBasedSubtitle = (sub: MediaStream): boolean => sub.IsTextSubtitleStream === false; /** * Determine if a subtitle will be available in MPV's track list. * * A subtitle is in MPV if: * - Delivery is Embed/Hls/External AND not an image-based sub during transcode */ export const isSubtitleInMpv = ( sub: MediaStream, isTranscoding: boolean, ): boolean => { // During transcoding, image-based subs are burned in, not in MPV if (isTranscoding && isImageBasedSubtitle(sub)) { return false; } // Embed/Hls/External methods mean the sub is loaded into MPV return ( sub.DeliveryMethod === SubtitleDeliveryMethod.Embed || sub.DeliveryMethod === SubtitleDeliveryMethod.Hls || sub.DeliveryMethod === SubtitleDeliveryMethod.External ); }; /** * Calculate the MPV track ID for a given Jellyfin subtitle index. * * MPV track IDs are 1-based and only count subtitles that are actually in MPV. * We iterate through all subtitles, counting only those in MPV, until we find * the one matching the Jellyfin index. * * @param mediaSource - The media source containing subtitle streams * @param jellyfinSubtitleIndex - The Jellyfin server-side subtitle index (-1 = disabled) * @param isTranscoding - Whether the stream is being transcoded * @returns MPV track ID (1-based), or -1 if disabled, or undefined if not in MPV */ export const getMpvSubtitleId = ( mediaSource: MediaSourceInfo | null | undefined, jellyfinSubtitleIndex: number | undefined, isTranscoding: boolean, ): number | undefined => { // -1 or undefined means disabled if (jellyfinSubtitleIndex === undefined || jellyfinSubtitleIndex === -1) { return -1; } const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; // Find the subtitle with the matching Jellyfin index const targetSub = allSubs.find((s) => s.Index === jellyfinSubtitleIndex); // If the target subtitle isn't in MPV (e.g., image-based during transcode), return undefined if (!targetSub || !isSubtitleInMpv(targetSub, isTranscoding)) { return undefined; } // Count MPV track position (1-based) let mpvIndex = 0; for (const sub of allSubs) { if (isSubtitleInMpv(sub, isTranscoding)) { mpvIndex++; if (sub.Index === jellyfinSubtitleIndex) { return mpvIndex; } } } return undefined; }; /** * Calculate the MPV track ID for a given Jellyfin audio index. * * For direct play: Audio tracks map to their position in the file (1-based). * For transcoding: Only ONE audio track exists in the HLS stream (the selected one), * so we should return 1 or undefined to use the default track. * * MPV track IDs are 1-based. * * @param mediaSource - The media source containing audio streams * @param jellyfinAudioIndex - The Jellyfin server-side audio index * @param isTranscoding - Whether the stream is being transcoded * @returns MPV track ID (1-based), or undefined if not found */ export const getMpvAudioId = ( mediaSource: MediaSourceInfo | null | undefined, jellyfinAudioIndex: number | undefined, isTranscoding: boolean, ): number | undefined => { if (jellyfinAudioIndex === undefined) { return undefined; } // When transcoding, Jellyfin only includes the selected audio track in the HLS stream. // So there's only 1 audio track - no need to specify an ID. if (isTranscoding) { return undefined; } const allAudio = mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || []; // Find position in audio list (1-based for MPV) const position = allAudio.findIndex((a) => a.Index === jellyfinAudioIndex); return position >= 0 ? position + 1 : undefined; };