aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs58
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs15
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs54
3 files changed, 104 insertions, 23 deletions
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 65f6b79656..1fdb5fd4bd 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1267,22 +1267,23 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append(_mediaEncoder.GetInputPathArgument(state));
}
- // sub2video for external graphical subtitles
- if (state.SubtitleStream is not null
- && ShouldEncodeSubtitle(state)
- && !state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleStream.IsExternal)
+ if (NeedsExternalSubtitleMuxing(state))
{
var subtitlePath = state.SubtitleStream.Path;
- var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
+ var isGraphicalBurnIn = ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream;
- // dvdsub/vobsub graphical subtitles use .sub+.idx pairs
- if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
+ if (isGraphicalBurnIn)
{
- var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
- if (File.Exists(idxFile))
+ var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
+
+ // dvdsub/vobsub graphical subtitles use .sub+.idx pairs
+ if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
{
- subtitlePath = idxFile;
+ var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
+ if (File.Exists(idxFile))
+ {
+ subtitlePath = idxFile;
+ }
}
}
@@ -1307,7 +1308,7 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(' ').Append(seekSubParam);
}
- if (!string.IsNullOrEmpty(canvasArgs))
+ if (isGraphicalBurnIn && !string.IsNullOrEmpty(canvasArgs))
{
arg.Append(canvasArgs);
}
@@ -3072,11 +3073,8 @@ namespace MediaBrowser.Controller.MediaEncoding
int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
if (state.AudioStream.IsExternal)
{
- bool hasExternalGraphicsSubs = state.SubtitleStream is not null
- && ShouldEncodeSubtitle(state)
- && state.SubtitleStream.IsExternal
- && !state.SubtitleStream.IsTextSubtitleStream;
- int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
+ bool hasExternalSubAsInput = NeedsExternalSubtitleMuxing(state);
+ int externalAudioMapIndex = hasExternalSubAsInput ? 2 : 1;
args += string.Format(
CultureInfo.InvariantCulture,
@@ -3104,12 +3102,20 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (subtitleMethod == SubtitleDeliveryMethod.Embed)
{
- int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+ if (state.SubtitleStream.IsExternal)
+ {
+ // External subtitle file is added as second FFmpeg input
+ args += " -map 1:0";
+ }
+ else
+ {
+ int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
- args += string.Format(
- CultureInfo.InvariantCulture,
- " -map 0:{0}",
- subtitleStreamIndex);
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 0:{0}",
+ subtitleStreamIndex);
+ }
}
else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
{
@@ -7886,6 +7892,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|| (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec));
}
+ private static bool NeedsExternalSubtitleMuxing(EncodingJobInfo state)
+ {
+ return state.SubtitleStream is not null
+ && state.SubtitleStream.IsExternal
+ && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed
+ || (ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream));
+ }
+
public static string GetVideoSyncOption(string videoSync, Version encoderVersion)
{
if (string.IsNullOrEmpty(videoSync))
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 44697837ca..2ccd2a6c28 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -1451,7 +1451,7 @@ namespace MediaBrowser.Model.Dlna
string? outputContainer,
MediaStreamProtocol? transcodingSubProtocol)
{
- if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.hls))
+ if (CanConsiderEmbedSubtitle(subtitleStream, playMethod, transcodingSubProtocol, outputContainer))
{
// Look for supported embedded subs of the same format
foreach (var profile in subtitleProfiles)
@@ -1540,6 +1540,19 @@ namespace MediaBrowser.Model.Dlna
return false;
}
+ private static bool CanConsiderEmbedSubtitle(MediaStream subtitleStream, PlayMethod playMethod, MediaStreamProtocol? transcodingSubProtocol, string? outputContainer)
+ {
+ if (subtitleStream.IsExternal)
+ {
+ return playMethod == PlayMethod.Transcode
+ && transcodingSubProtocol != MediaStreamProtocol.hls
+ && IsSubtitleEmbedSupported(outputContainer);
+ }
+
+ return playMethod != PlayMethod.Transcode
+ || transcodingSubProtocol != MediaStreamProtocol.hls;
+ }
+
private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
{
foreach (var profile in subtitleProfiles)
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 0b103debad..16c586bcda 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -675,5 +675,59 @@ namespace Jellyfin.Model.Tests
Assert.Equal(expectedMethod, result.Method);
}
+
+ [Theory]
+ // External text subs embedded into MKV when transcoding (#16403)
+ [InlineData("srt", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ [InlineData("ass", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ // External graphical subs embedded into MKV when transcoding
+ [InlineData("pgssub", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ [InlineData("dvdsub", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ // External subs remain external when transcoding to non-MKV containers
+ [InlineData("srt", true, PlayMethod.Transcode, "mp4", MediaStreamProtocol.hls, SubtitleDeliveryMethod.External)]
+ [InlineData("srt", true, PlayMethod.Transcode, "ts", MediaStreamProtocol.hls, SubtitleDeliveryMethod.External)]
+ // External subs remain external during DirectPlay even with MKV
+ [InlineData("srt", true, PlayMethod.DirectPlay, "mkv", null, SubtitleDeliveryMethod.External)]
+ // Internal subs still embedded into MKV when transcoding (existing behavior)
+ [InlineData("srt", false, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ [InlineData("pgssub", false, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ public void GetSubtitleProfile_ReturnsExpectedDeliveryMethod(
+ string codec,
+ bool isExternal,
+ PlayMethod playMethod,
+ string outputContainer,
+ MediaStreamProtocol? transcodingSubProtocol,
+ SubtitleDeliveryMethod expectedMethod)
+ {
+ var mediaSource = new MediaSourceInfo();
+ var subtitleStream = new MediaStream
+ {
+ Codec = codec,
+ Language = "eng",
+ IsExternal = isExternal,
+ Type = MediaStreamType.Subtitle,
+ SupportsExternalStream = true
+ };
+
+ var subtitleProfiles = new[]
+ {
+ new SubtitleProfile { Format = codec, Method = SubtitleDeliveryMethod.Embed },
+ new SubtitleProfile { Format = codec, Method = SubtitleDeliveryMethod.External }
+ };
+
+ var transcoderSupport = new Mock<ITranscoderSupport>();
+ transcoderSupport.Setup(x => x.CanExtractSubtitles(It.IsAny<string>())).Returns(true);
+
+ var result = StreamBuilder.GetSubtitleProfile(
+ mediaSource,
+ subtitleStream,
+ subtitleProfiles,
+ playMethod,
+ transcoderSupport.Object,
+ outputContainer,
+ transcodingSubProtocol);
+
+ Assert.Equal(expectedMethod, result.Method);
+ }
}
}