Merge pull request #14809 from lostb1t/fix/subtitleencoder

fix: prevent premature disposal of HTTP subtitle streams
This commit is contained in:
Niels van Velzen
2026-01-26 12:00:14 +01:00
committed by GitHub
2 changed files with 44 additions and 40 deletions

View File

@@ -172,23 +172,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken) private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken)
{ {
if (fileInfo.IsExternal) if (fileInfo.Protocol == MediaProtocol.Http)
{ {
var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false); var result = await DetectCharset(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false)) var detected = result.Detected;
if (detected is not null)
{ {
var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); _logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path);
var detected = result.Detected;
stream.Position = 0;
if (detected is not null) using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync(new Uri(fileInfo.Path), cancellationToken)
.ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{ {
_logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path); using var reader = new StreamReader(stream, detected.Encoding);
var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, detected.Encoding); return new MemoryStream(Encoding.UTF8.GetBytes(text));
var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
return new MemoryStream(Encoding.UTF8.GetBytes(text));
} }
} }
} }
@@ -218,7 +220,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}; };
} }
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path)
.TrimStart('.'); .TrimStart('.');
// Handle PGS subtitles as raw streams for the client to render // Handle PGS subtitles as raw streams for the client to render
@@ -941,42 +943,44 @@ namespace MediaBrowser.MediaEncoding.Subtitles
.ConfigureAwait(false); .ConfigureAwait(false);
} }
var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); var result = await DetectCharset(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false)) var charset = result.Detected?.EncodingName ?? string.Empty;
// UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal))
&& (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase)
|| string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase)))
{ {
var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); charset = string.Empty;
var charset = result.Detected?.EncodingName ?? string.Empty;
// UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal))
&& (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase)
|| string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase)))
{
charset = string.Empty;
}
_logger.LogDebug("charset {0} detected for {Path}", charset, path);
return charset;
} }
_logger.LogDebug("charset {0} detected for {Path}", charset, path);
return charset;
} }
private async Task<Stream> GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) private async Task<DetectionResult> DetectCharset(string path, MediaProtocol protocol, CancellationToken cancellationToken)
{ {
switch (protocol) switch (protocol)
{ {
case MediaProtocol.Http: case MediaProtocol.Http:
{ {
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) using var stream = await _httpClientFactory
.GetAsync(new Uri(path), cancellationToken) .CreateClient(NamedClient.Default)
.ConfigureAwait(false); .GetStreamAsync(new Uri(path), cancellationToken)
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); .ConfigureAwait(false);
}
return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
}
case MediaProtocol.File: case MediaProtocol.File:
return AsyncFile.OpenRead(path); {
return await CharsetDetector.DetectFromFileAsync(path, cancellationToken)
.ConfigureAwait(false);
}
default: default:
throw new ArgumentOutOfRangeException(nameof(protocol)); throw new ArgumentOutOfRangeException(nameof(protocol), protocol, "Unsupported protocol");
} }
} }

View File

@@ -1252,11 +1252,11 @@ public class StreamInfo
stream.Index.ToString(CultureInfo.InvariantCulture), stream.Index.ToString(CultureInfo.InvariantCulture),
startPositionTicks.ToString(CultureInfo.InvariantCulture), startPositionTicks.ToString(CultureInfo.InvariantCulture),
subtitleProfile.Format); subtitleProfile.Format);
info.IsExternalUrl = false; // Default to API URL info.IsExternalUrl = false;
// Check conditions for potentially using the direct path // Check conditions for potentially using the direct path
if (stream.IsExternal // Must be external if (stream.IsExternal // Must be external
&& MediaSource?.Protocol != MediaProtocol.File // Main media must not be a local file && stream.SupportsExternalStream
&& string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) // Format must match (no conversion needed) && string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) // Format must match (no conversion needed)
&& !string.IsNullOrEmpty(stream.Path) // Path must exist && !string.IsNullOrEmpty(stream.Path) // Path must exist
&& Uri.TryCreate(stream.Path, UriKind.Absolute, out Uri? uriResult) // Path must be an absolute URI && Uri.TryCreate(stream.Path, UriKind.Absolute, out Uri? uriResult) // Path must be an absolute URI