Compare commits

..

1 Commits

Author SHA1 Message Date
Lance Chant
d6980cfc8e fix: subtitle ordering
Fixed an issue where external and subrip subtitles were not ordered
correctly

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-06-23 12:00:11 +02:00
3 changed files with 41 additions and 12 deletions

View File

@@ -92,7 +92,7 @@
"react-native-uuid": "^2.0.3", "react-native-uuid": "^2.0.3",
"react-native-volume-manager": "^2.0.8", "react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-worklets": "0.10.0", "react-native-worklets": "0.8.3",
"sonner-native": "0.21.2", "sonner-native": "0.21.2",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
@@ -1598,7 +1598,7 @@
"react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="], "react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="],
"react-native-worklets": ["react-native-worklets@0.10.0", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.28.5", "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.4" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.83 - 0.86" } }, "sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw=="], "react-native-worklets": ["react-native-worklets@0.8.3", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.3" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.81 - 0.85" } }, "sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg=="],
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],

View File

@@ -115,7 +115,7 @@
"react-native-uuid": "^2.0.3", "react-native-uuid": "^2.0.3",
"react-native-volume-manager": "^2.0.8", "react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-worklets": "0.10.0", "react-native-worklets": "0.8.3",
"sonner-native": "0.21.2", "sonner-native": "0.21.2",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",

View File

@@ -44,9 +44,22 @@ export const isSubtitleInMpv = (
/** /**
* Calculate the MPV track ID for a given Jellyfin subtitle index. * 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. * MPV track IDs are 1-based, but MPV's track list is NOT in MediaStreams order:
* We iterate through all subtitles, counting only those in MPV, until we find * 1. Embedded/HLS subs are enumerated from the container (or HLS playlist)
* the one matching the Jellyfin index. * first, in MediaStreams order.
* 2. External subs are appended via `sub-add` AFTER the file loads, in the
* order they are passed to MPV (here, also MediaStreams order — see
* direct-player.tsx where the externalSubtitles array is built by
* filtering MediaStreams).
*
* Iterating in pure MediaStreams order produces the wrong MPV ID whenever an
* External sub is listed before an Embed sub in MediaStreams (common when
* Jellyfin prepends a converted SRT/VTT ahead of an original PGS/ASS track),
* causing e.g. English to select Spanish. We therefore count in two phases
* that mirror MPV's actual ordering.
*
* Image-based subs (PGS/VOBSUB) during transcoding are burned into the video
* and absent from MPV's track list; they are skipped in both phases.
* *
* @param mediaSource - The media source containing subtitle streams * @param mediaSource - The media source containing subtitle streams
* @param jellyfinSubtitleIndex - The Jellyfin server-side subtitle index (-1 = disabled) * @param jellyfinSubtitleIndex - The Jellyfin server-side subtitle index (-1 = disabled)
@@ -74,15 +87,31 @@ export const getMpvSubtitleId = (
return undefined; return undefined;
} }
// Count MPV track position (1-based) const isExternal = (sub: MediaStream) =>
sub.DeliveryMethod === SubtitleDeliveryMethod.External;
let mpvIndex = 0; let mpvIndex = 0;
// Phase 1: embedded / HLS subs — these occupy MPV track IDs first because
// they come from the container or HLS playlist.
for (const sub of allSubs) { for (const sub of allSubs) {
if (isSubtitleInMpv(sub, isTranscoding)) { if (isExternal(sub)) continue;
if (!isSubtitleInMpv(sub, isTranscoding)) continue;
mpvIndex++; mpvIndex++;
if (sub.Index === jellyfinSubtitleIndex) { if (sub.Index === jellyfinSubtitleIndex) {
return mpvIndex; return mpvIndex;
} }
} }
// Phase 2: external subs — appended via `sub-add` after the file loads,
// so they come last in MPV's track list.
for (const sub of allSubs) {
if (!isExternal(sub)) continue;
if (!isSubtitleInMpv(sub, isTranscoding)) continue;
mpvIndex++;
if (sub.Index === jellyfinSubtitleIndex) {
return mpvIndex;
}
} }
return undefined; return undefined;