diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs')
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 176 |
1 files changed, 101 insertions, 75 deletions
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 894d0a3574..77aadee704 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -26,7 +26,10 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Core.SubtitleFormats; using UtfUnknown; +using SubtitleFormat = MediaBrowser.Model.MediaInfo.SubtitleFormat; namespace MediaBrowser.MediaEncoding.Subtitles { @@ -72,55 +75,42 @@ namespace MediaBrowser.MediaEncoding.Subtitles private MemoryStream ConvertSubtitles( Stream stream, - string inputFormat, + SubtitleInfo inputInfo, string outputFormat, long startTimeTicks, long endTimeTicks, - bool preserveOriginalTimestamps, - CancellationToken cancellationToken) + bool preserveOriginalTimestamps) { - var ms = new MemoryStream(); + var subtitle = Subtitle.Parse(stream, Path.GetExtension(inputInfo.Path)); - try - { - var trackInfo = _subtitleParser.Parse(stream, inputFormat); + FilterEvents(subtitle, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); - FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); + var formatter = GetWriter(outputFormat); - var writer = GetWriter(outputFormat); + var text = formatter.ToText(subtitle, "untitled"); + var bytes = Encoding.UTF8.GetBytes(text); - writer.Write(trackInfo, ms, cancellationToken); - ms.Position = 0; - } - catch - { - ms.Dispose(); - throw; - } - - return ms; + return new MemoryStream(bytes, 0, bytes.Length, false, true); } - internal void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps) + internal void FilterEvents(Subtitle track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps) { // Drop subs that have fully elapsed before the requested start position - track.TrackEvents = track.TrackEvents - .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 && (i.EndPositionTicks - startPositionTicks) < 0) - .ToArray(); + track.Paragraphs + .RemoveAll(i => (i.StartTime.TimeSpan.Ticks - startPositionTicks) < 0 && (i.EndTime.TimeSpan.Ticks - startPositionTicks) < 0); if (endTimeTicks > 0) { - track.TrackEvents = track.TrackEvents - .TakeWhile(i => i.StartPositionTicks <= endTimeTicks) - .ToArray(); + track.Paragraphs + .RemoveAll(i => i.StartTime.TimeSpan.Ticks > endTimeTicks); } if (!preserveTimestamps) { - foreach (var trackEvent in track.TrackEvents) + foreach (var trackEvent in track.Paragraphs) { - trackEvent.EndPositionTicks = Math.Max(0, trackEvent.EndPositionTicks - startPositionTicks); - trackEvent.StartPositionTicks = Math.Max(0, trackEvent.StartPositionTicks - startPositionTicks); + trackEvent.StartTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.StartTime.TimeSpan.Ticks - startPositionTicks))); + trackEvent.EndTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.EndTime.TimeSpan.Ticks - startPositionTicks))); } } } @@ -142,14 +132,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles var subtitleStream = mediaSource.MediaStreams .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex); - var (stream, inputFormat) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken) + var (stream, info) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken) .ConfigureAwait(false); // Return the original if the same format is being requested // Character encoding was already handled in GetSubtitleStream // ASS is a superset of SSA, skipping the conversion and preserving the styles - if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase) - || (string.Equals(inputFormat, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase) + if (string.Equals(info.Format, outputFormat, StringComparison.OrdinalIgnoreCase) + || (string.Equals(info.Format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase) && string.Equals(outputFormat, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))) { return stream; @@ -157,11 +147,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles using (stream) { - return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken); + return ConvertSubtitles(stream, info, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); } } - private async Task<(Stream Stream, string Format)> GetSubtitleStream( + private async Task<(Stream Stream, SubtitleInfo Info)> GetSubtitleStream( MediaSourceInfo mediaSource, MediaStream subtitleStream, CancellationToken cancellationToken) @@ -170,7 +160,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false); - return (stream, fileInfo.Format); + return (stream, fileInfo); } private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken) @@ -190,10 +180,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles await using (stream.ConfigureAwait(false)) { - using var reader = new StreamReader(stream, detected.Encoding); - var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + using var reader = new StreamReader(stream, detected.Encoding); + var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - return new MemoryStream(Encoding.UTF8.GetBytes(text)); + return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } } @@ -212,19 +202,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream); var outputFormat = GetExtractableSubtitleFormat(subtitleStream); - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension) + ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUID Id, e.g. Live TV stream)."); return new SubtitleInfo() { Path = outputPath, Protocol = MediaProtocol.File, Format = outputFormat, - IsExternal = false + IsExternal = MediaStream.IsVobSubFormat(outputFormat) }; } - var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path) - .TrimStart('.'); + var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec).TrimStart('.'); // Handle PGS subtitles as raw streams for the client to render if (MediaStream.IsPgsFormat(currentFormat)) @@ -242,7 +232,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!_subtitleParser.SupportsFileExtension(currentFormat)) { // Convert - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt"); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt") + ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUID Id, e.g. Live TV stream)."); await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); @@ -265,13 +256,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles }; } - private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value) + private bool TryGetWriter(string format, [NotNullWhen(true)] out Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat? value) { ArgumentException.ThrowIfNullOrEmpty(format); if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) { - value = new AssWriter(); + value = new AdvancedSubStationAlpha(); return true; } @@ -281,27 +272,29 @@ namespace MediaBrowser.MediaEncoding.Subtitles return true; } - if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase) + || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase)) { - value = new SrtWriter(); + value = new SubRip(); return true; } if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)) { - value = new SsaWriter(); + value = new SubStationAlpha(); return true; } - if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase) + || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase)) { - value = new VttWriter(); + value = new WebVTT(); return true; } if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase)) { - value = new TtmlWriter(); + value = new TimedText10(); return true; } @@ -309,7 +302,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles return false; } - private ISubtitleWriter GetWriter(string format) + private Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat GetWriter(string format) { if (TryGetWriter(format, out var writer)) { @@ -473,6 +466,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles { return subtitleStream.Codec; } + else if (MediaStream.IsVobSubFormat(subtitleStream.Codec)) + { + return "mks"; + } else { return "srt"; @@ -486,6 +483,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles { return "sup"; } + 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 { return GetExtractableSubtitleFormat(subtitleStream); @@ -498,7 +500,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); } /// <inheritdoc /> @@ -514,12 +517,17 @@ 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)) { continue; } var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); + if (outputPath is null) + { + continue; + } var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false); @@ -591,7 +599,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles } var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); + if (outputPath is null) + { + continue; + } + 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); if (streamIndex == -1) @@ -605,9 +620,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); } @@ -636,7 +652,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles } var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); + if (outputPath is null) + { + continue; + } + 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); if (streamIndex == -1) @@ -650,18 +673,17 @@ 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); } - 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); } private async Task ExtractSubtitlesForFile( @@ -968,7 +990,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) + private string? GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) { return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension); } @@ -981,9 +1003,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { - path = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec); - await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken) - .ConfigureAwait(false); + var cachePath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec); + if (cachePath is not null) + { + path = cachePath; + await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken) + .ConfigureAwait(false); + } } var result = await DetectCharset(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); @@ -1007,20 +1033,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles switch (protocol) { case MediaProtocol.Http: - { - using var stream = await _httpClientFactory - .CreateClient(NamedClient.Default) - .GetStreamAsync(new Uri(path), cancellationToken) - .ConfigureAwait(false); + { + using var stream = await _httpClientFactory + .CreateClient(NamedClient.Default) + .GetStreamAsync(new Uri(path), cancellationToken) + .ConfigureAwait(false); - return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); - } + return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); + } case MediaProtocol.File: - { - return await CharsetDetector.DetectFromFileAsync(path, cancellationToken) - .ConfigureAwait(false); - } + { + return await CharsetDetector.DetectFromFileAsync(path, cancellationToken) + .ConfigureAwait(false); + } default: throw new ArgumentOutOfRangeException(nameof(protocol), protocol, "Unsupported protocol"); |
