From d54cf8857f2e128e60d175c369091f999f961ca1 Mon Sep 17 00:00:00 2001 From: altqx Date: Fri, 3 Apr 2026 00:50:29 +0700 Subject: [PATCH 1/7] Add support for VobSub subtitle streams --- .../Library/MediaSourceManager.cs | 5 ++ .../Subtitles/SubtitleEncoder.cs | 86 +++++++++++++++++-- MediaBrowser.Model/Dlna/StreamBuilder.cs | 13 ++- MediaBrowser.Model/Entities/MediaStream.cs | 30 ++++++- 4 files changed, 121 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c667fb0600..7719dfb3a5 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -126,6 +126,11 @@ namespace Emby.Server.Implementations.Library return true; } + if (stream.IsVobSubSubtitleStream) + { + return true; + } + return false; } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index aeaf7f4423..e4dd52f64a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -203,12 +203,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles MediaStream subtitleStream, CancellationToken cancellationToken) { - if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) + var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path) + .TrimStart('.'); + + var isVobSub = MediaStream.IsVobSubFormat(currentFormat); + + if (isVobSub || !subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); - var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream); - var outputFormat = GetExtractableSubtitleFormat(subtitleStream); + var outputFileExtension = isVobSub ? "mks" : GetExtractableSubtitleFileExtension(subtitleStream); + var outputFormat = isVobSub ? "mks" : GetExtractableSubtitleFormat(subtitleStream); var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension); return new SubtitleInfo() @@ -216,13 +221,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles Path = outputPath, Protocol = MediaProtocol.File, Format = outputFormat, - IsExternal = false + IsExternal = isVobSub }; } - var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path) - .TrimStart('.'); - // Handle PGS subtitles as raw streams for the client to render if (MediaStream.IsPgsFormat(currentFormat)) { @@ -470,6 +472,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles { return subtitleStream.Codec; } + else if (MediaStream.IsVobSubFormat(subtitleStream.Codec)) + { + return "mks"; + } else { return "srt"; @@ -483,6 +489,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles { return "sup"; } + else if (MediaStream.IsVobSubFormat(subtitleStream.Codec)) + { + return "mks"; + } else { return GetExtractableSubtitleFormat(subtitleStream); @@ -495,7 +505,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles || string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase); + || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase) + || MediaStream.IsVobSubFormat(codec); } /// @@ -511,7 +522,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles foreach (var subtitleStream in subtitleStreams) { - if (subtitleStream.IsExternal && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) + if (subtitleStream.IsExternal + && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase) + && !subtitleStream.IsVobSubSubtitleStream) { continue; } @@ -534,6 +547,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); await ExtractAllExtractableSubtitlesMKS(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); + await ExtractAllExtractableExternalVobSubSubtitles(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); } } catch (Exception ex) @@ -632,6 +646,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles continue; } + if (subtitleStream.IsExternal && subtitleStream.IsVobSubSubtitleStream) + { + _logger.LogDebug("Subtitle {Index} is an external VobSub track. Handling it with a dedicated remux path", subtitleStream.Index); + continue; + } + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); @@ -661,6 +681,54 @@ namespace MediaBrowser.MediaEncoding.Subtitles await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false); } + private async Task ExtractAllExtractableExternalVobSubSubtitles( + MediaSourceInfo mediaSource, + List subtitleStreams, + CancellationToken cancellationToken) + { + foreach (var subtitleStream in subtitleStreams) + { + if (!subtitleStream.IsExternal || !subtitleStream.IsVobSubSubtitleStream || string.IsNullOrEmpty(subtitleStream.Path)) + { + continue; + } + + if (subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".mks"); + var inputSubtitlePath = subtitleStream.Path; + var inputExtension = Path.GetExtension(inputSubtitlePath.AsSpan()); + + if (inputExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)) + { + var idxPath = Path.ChangeExtension(inputSubtitlePath, ".idx"); + if (File.Exists(idxPath)) + { + inputSubtitlePath = idxPath; + } + } + + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid.")); + + var inputPath = _mediaEncoder.GetInputArgument(inputSubtitlePath, new MediaSourceInfo + { + Path = inputSubtitlePath, + Protocol = _mediaSourceManager.GetPathProtocol(inputSubtitlePath) + }); + + var args = string.Format( + CultureInfo.InvariantCulture, + "-i {0} -copyts -map 0:0 -an -vn -c:s copy \"{1}\"", + inputPath, + outputPath); + + await ExtractSubtitlesForFile(inputPath, args, [outputPath], cancellationToken).ConfigureAwait(false); + } + } + private async Task ExtractSubtitlesForFile( string inputPath, string args, diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 75b8c137f7..da01540eb6 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -572,7 +572,9 @@ namespace MediaBrowser.Model.Dlna { foreach (var profile in subtitleProfiles) { - if (profile.Method == SubtitleDeliveryMethod.External && string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase)) + if (profile.Method == SubtitleDeliveryMethod.External + && (string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase) + || (string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase) && stream.IsVobSubSubtitleStream))) { return stream.Index; } @@ -1560,10 +1562,15 @@ namespace MediaBrowser.Model.Dlna continue; } - if ((profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format)) || + bool isVobSubMksProfile = string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase) + && subtitleStream.IsVobSubSubtitleStream; + + if ((profile.Method == SubtitleDeliveryMethod.External + && (isVobSubMksProfile || subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))) || (profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream)) { - bool requiresConversion = !string.Equals(subtitleStream.Codec, profile.Format, StringComparison.OrdinalIgnoreCase); + bool requiresConversion = !isVobSubMksProfile + && !string.Equals(subtitleStream.Codec, profile.Format, StringComparison.OrdinalIgnoreCase); if (!requiresConversion) { diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index c443af32cf..395957bf08 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -633,13 +633,32 @@ namespace MediaBrowser.Model.Entities } } + [JsonIgnore] + public bool IsVobSubSubtitleStream + { + get + { + if (Type != MediaStreamType.Subtitle) + { + return false; + } + + if (string.IsNullOrEmpty(Codec) && !IsExternal) + { + return false; + } + + return IsVobSubFormat(Codec); + } + } + /// /// Gets a value indicating whether this is a subtitle steam that is extractable by ffmpeg. /// All text-based and pgs subtitles can be extracted. /// /// true if this is a extractable subtitle steam otherwise, false. [JsonIgnore] - public bool IsExtractableSubtitleStream => IsTextSubtitleStream || IsPgsSubtitleStream; + public bool IsExtractableSubtitleStream => IsTextSubtitleStream || IsPgsSubtitleStream || IsVobSubSubtitleStream; /// /// Gets or sets a value indicating whether [supports external stream]. @@ -717,6 +736,7 @@ namespace MediaBrowser.Model.Entities return codec.Contains("microdvd", StringComparison.OrdinalIgnoreCase) || (!codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) && !codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase) + && !codec.Contains("vobsub", StringComparison.OrdinalIgnoreCase) && !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) && !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase) && !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase)); @@ -730,6 +750,14 @@ namespace MediaBrowser.Model.Entities || string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase); } + public static bool IsVobSubFormat(string format) + { + string codec = format ?? string.Empty; + + return codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase) + || codec.Contains("vobsub", StringComparison.OrdinalIgnoreCase); + } + public bool SupportsSubtitleConversionTo(string toCodec) { if (!IsTextSubtitleStream) From 3e9a1ca387944649e46ee3e59be09f49f82d4901 Mon Sep 17 00:00:00 2001 From: altqx Date: Fri, 10 Apr 2026 20:15:28 +0700 Subject: [PATCH 2/7] update logic to determine separate extraction for VobSub subtitles --- .../Subtitles/SubtitleEncoder.cs | 79 +++++++++++++------ 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index e4dd52f64a..8ad2280a69 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -509,6 +509,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles || MediaStream.IsVobSubFormat(codec); } + internal static bool ShouldExtractVobSubSeparately(MediaStream subtitleStream) + { + if (!subtitleStream.IsVobSubSubtitleStream) + { + return false; + } + + return !subtitleStream.IsExternal + || string.IsNullOrEmpty(subtitleStream.Path) + || !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase); + } + /// public async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) { @@ -547,7 +559,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles { await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); await ExtractAllExtractableSubtitlesMKS(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); - await ExtractAllExtractableExternalVobSubSubtitles(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); } } catch (Exception ex) @@ -633,6 +644,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource); var outputPaths = new List(); + var dedicatedVobSubStreams = new List(); var args = string.Format( CultureInfo.InvariantCulture, "-i {0} -copyts", @@ -646,9 +658,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles continue; } - if (subtitleStream.IsExternal && subtitleStream.IsVobSubSubtitleStream) + if (ShouldExtractVobSubSeparately(subtitleStream)) { - _logger.LogDebug("Subtitle {Index} is an external VobSub track. Handling it with a dedicated remux path", subtitleStream.Index); + _logger.LogDebug("Subtitle {Index} is a VobSub track. Handling it in the internal extraction path", subtitleStream.Index); + dedicatedVobSubStreams.Add(subtitleStream); continue; } @@ -673,33 +686,30 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputPath); } - if (outputPaths.Count == 0) + if (outputPaths.Count > 0) { - return; + await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false); } - await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false); + foreach (var subtitleStream in dedicatedVobSubStreams) + { + await ExtractSeparateVobSubSubtitle(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false); + } } - private async Task ExtractAllExtractableExternalVobSubSubtitles( + private async Task ExtractSeparateVobSubSubtitle( MediaSourceInfo mediaSource, - List subtitleStreams, + MediaStream subtitleStream, CancellationToken cancellationToken) { - foreach (var subtitleStream in subtitleStreams) + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".mks"); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid.")); + + if (subtitleStream.IsExternal) { - if (!subtitleStream.IsExternal || !subtitleStream.IsVobSubSubtitleStream || string.IsNullOrEmpty(subtitleStream.Path)) - { - continue; - } - - if (subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".mks"); var inputSubtitlePath = subtitleStream.Path; + ArgumentException.ThrowIfNullOrEmpty(inputSubtitlePath); + var inputExtension = Path.GetExtension(inputSubtitlePath.AsSpan()); if (inputExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)) @@ -711,22 +721,39 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid.")); - - var inputPath = _mediaEncoder.GetInputArgument(inputSubtitlePath, new MediaSourceInfo + var externalInputPath = _mediaEncoder.GetInputArgument(inputSubtitlePath, new MediaSourceInfo { Path = inputSubtitlePath, Protocol = _mediaSourceManager.GetPathProtocol(inputSubtitlePath) }); - var args = string.Format( + var externalArgs = string.Format( CultureInfo.InvariantCulture, "-i {0} -copyts -map 0:0 -an -vn -c:s copy \"{1}\"", - inputPath, + externalInputPath, outputPath); - await ExtractSubtitlesForFile(inputPath, args, [outputPath], cancellationToken).ConfigureAwait(false); + await ExtractSubtitlesForFile(externalInputPath, externalArgs, [outputPath], cancellationToken).ConfigureAwait(false); + return; } + + var internalInputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource); + var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); + + if (streamIndex == -1) + { + _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", internalInputPath, subtitleStream.Index); + return; + } + + var internalArgs = string.Format( + CultureInfo.InvariantCulture, + "-i {0} -copyts -map 0:{1} -an -vn -c:s copy \"{2}\"", + internalInputPath, + streamIndex, + outputPath); + + await ExtractSubtitlesForFile(internalInputPath, internalArgs, [outputPath], cancellationToken).ConfigureAwait(false); } private async Task ExtractSubtitlesForFile( From 6153fab62080f984226953da9d977b297838b35e Mon Sep 17 00:00:00 2001 From: altqx Date: Fri, 10 Apr 2026 23:19:10 +0700 Subject: [PATCH 3/7] simplify VobSub extraction logic and fix ffmpeg command --- .../Subtitles/SubtitleEncoder.cs | 93 +------------------ MediaBrowser.Model/Dlna/StreamBuilder.cs | 7 +- 2 files changed, 10 insertions(+), 90 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 8ad2280a69..113c32049a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -208,7 +208,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var isVobSub = MediaStream.IsVobSubFormat(currentFormat); - if (isVobSub || !subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) + if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); @@ -509,18 +509,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles || MediaStream.IsVobSubFormat(codec); } - internal static bool ShouldExtractVobSubSeparately(MediaStream subtitleStream) - { - if (!subtitleStream.IsVobSubSubtitleStream) - { - return false; - } - - return !subtitleStream.IsExternal - || string.IsNullOrEmpty(subtitleStream.Path) - || !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase); - } - /// public async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) { @@ -535,8 +523,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles foreach (var subtitleStream in subtitleStreams) { if (subtitleStream.IsExternal - && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase) - && !subtitleStream.IsVobSubSubtitleStream) + && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -644,7 +631,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles { var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource); var outputPaths = new List(); - var dedicatedVobSubStreams = new List(); var args = string.Format( CultureInfo.InvariantCulture, "-i {0} -copyts", @@ -658,15 +644,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles continue; } - if (ShouldExtractVobSubSeparately(subtitleStream)) - { - _logger.LogDebug("Subtitle {Index} is a VobSub track. Handling it in the internal extraction path", subtitleStream.Index); - dedicatedVobSubStreams.Add(subtitleStream); - continue; - } - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; + var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.Empty; var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); if (streamIndex == -1) @@ -680,9 +660,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputPaths.Add(outputPath); args += string.Format( CultureInfo.InvariantCulture, - " -map 0:{0} -an -vn -c:s {1} \"{2}\"", + " -map 0:{0} -an -vn -c:s {1}{2} \"{3}\"", streamIndex, outputCodec, + outputFormatOption, outputPath); } @@ -690,70 +671,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles { await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false); } - - foreach (var subtitleStream in dedicatedVobSubStreams) - { - await ExtractSeparateVobSubSubtitle(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false); - } - } - - private async Task ExtractSeparateVobSubSubtitle( - MediaSourceInfo mediaSource, - MediaStream subtitleStream, - CancellationToken cancellationToken) - { - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".mks"); - Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid.")); - - if (subtitleStream.IsExternal) - { - var inputSubtitlePath = subtitleStream.Path; - ArgumentException.ThrowIfNullOrEmpty(inputSubtitlePath); - - var inputExtension = Path.GetExtension(inputSubtitlePath.AsSpan()); - - if (inputExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)) - { - var idxPath = Path.ChangeExtension(inputSubtitlePath, ".idx"); - if (File.Exists(idxPath)) - { - inputSubtitlePath = idxPath; - } - } - - var externalInputPath = _mediaEncoder.GetInputArgument(inputSubtitlePath, new MediaSourceInfo - { - Path = inputSubtitlePath, - Protocol = _mediaSourceManager.GetPathProtocol(inputSubtitlePath) - }); - - var externalArgs = string.Format( - CultureInfo.InvariantCulture, - "-i {0} -copyts -map 0:0 -an -vn -c:s copy \"{1}\"", - externalInputPath, - outputPath); - - await ExtractSubtitlesForFile(externalInputPath, externalArgs, [outputPath], cancellationToken).ConfigureAwait(false); - return; - } - - var internalInputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource); - var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); - - if (streamIndex == -1) - { - _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", internalInputPath, subtitleStream.Index); - return; - } - - var internalArgs = string.Format( - CultureInfo.InvariantCulture, - "-i {0} -copyts -map 0:{1} -an -vn -c:s copy \"{2}\"", - internalInputPath, - streamIndex, - outputPath); - - await ExtractSubtitlesForFile(internalInputPath, internalArgs, [outputPath], cancellationToken).ConfigureAwait(false); } private async Task ExtractSubtitlesForFile( diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index da01540eb6..df756271bf 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -574,7 +574,9 @@ namespace MediaBrowser.Model.Dlna { if (profile.Method == SubtitleDeliveryMethod.External && (string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase) - || (string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase) && stream.IsVobSubSubtitleStream))) + || (string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase) + && stream.IsVobSubSubtitleStream + && (!stream.IsExternal || stream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))))) { return stream.Index; } @@ -1563,7 +1565,8 @@ namespace MediaBrowser.Model.Dlna } bool isVobSubMksProfile = string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase) - && subtitleStream.IsVobSubSubtitleStream; + && subtitleStream.IsVobSubSubtitleStream + && (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)); if ((profile.Method == SubtitleDeliveryMethod.External && (isVobSubMksProfile || subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))) || From 71bdc1d409834e3fc4160902d4ccc9e5aa86261c Mon Sep 17 00:00:00 2001 From: altqx Date: Sat, 11 Apr 2026 11:59:01 +0700 Subject: [PATCH 4/7] Match `ExtractAllExtractableSubtitlesMKS` with `ExtractAllExtractableSubtitlesInternal` Matroska's VobSub option --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index dfeac5c6e7..a96ce391af 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -601,6 +601,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; + var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.Empty; var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); if (streamIndex == -1) @@ -614,9 +615,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputPaths.Add(outputPath); args += string.Format( CultureInfo.InvariantCulture, - " -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"", + " -map 0:{0} -an -vn -c:s {1}{2} -flush_packets 1 \"{3}\"", streamIndex, outputCodec, + outputFormatOption, outputPath); } From 33c0450d1f4851f6f90a221fdbfafd67ccdadfff Mon Sep 17 00:00:00 2001 From: altqx Date: Sat, 11 Apr 2026 14:08:12 +0700 Subject: [PATCH 5/7] Add a comments clarify why MKS was used, and remove the redundant VobSub extension branch --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 5 ++++- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index a96ce391af..20ea691ce4 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -212,7 +212,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); - var outputFileExtension = isVobSub ? "mks" : GetExtractableSubtitleFileExtension(subtitleStream); + var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream); var outputFormat = isVobSub ? "mks" : GetExtractableSubtitleFormat(subtitleStream); var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension); @@ -491,6 +491,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } else if (MediaStream.IsVobSubFormat(subtitleStream.Codec)) { + // FFmpeg cannot mux VobSub subtitle streams back into the .idx/.sub pair, so we use .mks container instead. return "mks"; } else @@ -601,6 +602,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; + // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files. var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.Empty; var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); @@ -648,6 +650,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; + // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files. var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.Empty; var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index df756271bf..52a3ba44ee 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -574,6 +574,7 @@ namespace MediaBrowser.Model.Dlna { if (profile.Method == SubtitleDeliveryMethod.External && (string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase) + // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are exposed as .mks. || (string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase) && stream.IsVobSubSubtitleStream && (!stream.IsExternal || stream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))))) @@ -1564,6 +1565,7 @@ namespace MediaBrowser.Model.Dlna continue; } + // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are matched against external .mks delivery profiles. bool isVobSubMksProfile = string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase) && subtitleStream.IsVobSubSubtitleStream && (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)); From ae4cead16da4dd52c6a9ccef6cc29479a8ee3d41 Mon Sep 17 00:00:00 2001 From: altqx Date: Sat, 11 Apr 2026 14:16:31 +0700 Subject: [PATCH 6/7] remove redundant VobSub format check --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 20ea691ce4..8f62c5d335 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -206,14 +206,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path) .TrimStart('.'); - var isVobSub = MediaStream.IsVobSubFormat(currentFormat); - if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream); - var outputFormat = isVobSub ? "mks" : GetExtractableSubtitleFormat(subtitleStream); + var outputFormat = GetExtractableSubtitleFormat(subtitleStream); var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension); return new SubtitleInfo() @@ -221,7 +219,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles Path = outputPath, Protocol = MediaProtocol.File, Format = outputFormat, - IsExternal = isVobSub + IsExternal = MediaStream.IsVobSubFormat(outputFormat) }; } From 8dd989b5a8a5f8c592da8774e1d34407c175f799 Mon Sep 17 00:00:00 2001 From: altqx Date: Sat, 11 Apr 2026 14:42:32 +0700 Subject: [PATCH 7/7] fix type errors --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 8f62c5d335..b9c9a0b69a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -203,9 +203,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles MediaStream subtitleStream, CancellationToken cancellationToken) { - var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path) - .TrimStart('.'); - if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); @@ -219,10 +216,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles Path = outputPath, Protocol = MediaProtocol.File, Format = outputFormat, - IsExternal = MediaStream.IsVobSubFormat(outputFormat) + IsExternal = MediaStream.IsVobSubFormat(outputFormat) }; } + var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path).TrimStart('.'); + // Handle PGS subtitles as raw streams for the client to render if (MediaStream.IsPgsFormat(currentFormat)) {