diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs')
| -rw-r--r-- | MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 182 |
1 files changed, 133 insertions, 49 deletions
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 8d237473a3..67e323177b 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(); - - try - { - var trackInfo = _subtitleParser.Parse(stream, inputFormat); + var subtitle = Subtitle.Parse(stream, Path.GetExtension(inputInfo.Path)); - FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); + FilterEvents(subtitle, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); - var writer = GetWriter(outputFormat); + var formatter = GetWriter(outputFormat); - writer.Write(trackInfo, ms, cancellationToken); - ms.Position = 0; - } - catch - { - ms.Dispose(); - throw; - } + var text = formatter.ToText(subtitle, "untitled"); + var bytes = Encoding.UTF8.GetBytes(text); - 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) @@ -224,7 +214,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles }; } - var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path).TrimStart('.'); + // Normalize ffmpeg codec names to the file extensions the parser is keyed on + var currentFormat = NormalizeCodecToParserExtension((Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec).TrimStart('.')); // Handle PGS subtitles as raw streams for the client to render if (MediaStream.IsPgsFormat(currentFormat)) @@ -266,13 +257,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; } @@ -282,27 +273,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; } @@ -310,7 +303,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)) { @@ -332,13 +325,91 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) { - if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) + if (!IsCachedSubtitleFresh(outputPath, subtitleStream.Path)) { await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); } } } + // ffmpeg codec names don't always match the file extensions the subtitle parser is keyed on. + private static string NormalizeCodecToParserExtension(string codecOrExtension) + { + return codecOrExtension switch + { + "subrip" => "srt", + "webvtt" => "vtt", + _ => codecOrExtension + }; + } + + // Records "this cache was built from this exact source revision" in a sidecar file next to the cache: "<sizeBytes>:<mtimeTicks>" + private static string GetCacheMetaPath(string cachePath) => cachePath + ".meta"; + + private static string FormatCacheMeta(long length, DateTime lastWriteUtc) + => string.Create(CultureInfo.InvariantCulture, $"{length}:{lastWriteUtc.Ticks}"); + + private bool IsCachedSubtitleFresh(string cachePath, string? sourcePath) + { + if (!File.Exists(cachePath)) + { + return false; + } + + var cacheInfo = _fileSystem.GetFileInfo(cachePath); + if (cacheInfo.Length == 0) + { + return false; + } + + if (string.IsNullOrEmpty(sourcePath) || !File.Exists(sourcePath)) + { + return true; + } + + var metaPath = GetCacheMetaPath(cachePath); + if (!File.Exists(metaPath)) + { + // Pre-existing cache from before metadata tracking - regenerate so we can record the source state. + return false; + } + + try + { + var sourceInfo = _fileSystem.GetFileInfo(sourcePath); + var expected = FormatCacheMeta(sourceInfo.Length, sourceInfo.LastWriteTimeUtc); + var actual = File.ReadAllText(metaPath); + return string.Equals(expected, actual, StringComparison.Ordinal); + } + catch (IOException) + { + return false; + } + } + + private void WriteCacheMeta(string cachePath, string? sourcePath) + { + if (string.IsNullOrEmpty(sourcePath)) + { + return; + } + + try + { + var sourceInfo = _fileSystem.GetFileInfo(sourcePath); + if (!sourceInfo.Exists) + { + return; + } + + File.WriteAllText(GetCacheMetaPath(cachePath), FormatCacheMeta(sourceInfo.Length, sourceInfo.LastWriteTimeUtc)); + } + catch (IOException ex) + { + _logger.LogWarning(ex, "Failed to record subtitle cache metadata for {CachePath}", cachePath); + } + } + /// <summary> /// Converts the text subtitle to SRT internal. /// </summary> @@ -383,7 +454,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles CreateNoWindow = true, UseShellExecute = false, FileName = _mediaEncoder.EncoderPath, - Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), + Arguments = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false }, @@ -463,6 +534,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false); + WriteCacheMeta(outputPath, inputPath); + _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath); } @@ -539,7 +612,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false); - if (File.Exists(outputPath) && _fileSystem.GetFileInfo(outputPath).Length > 0) + var sourcePath = string.IsNullOrEmpty(subtitleStream.Path) ? mediaSource.Path : subtitleStream.Path; + if (IsCachedSubtitleFresh(outputPath, sourcePath)) { releaser.Dispose(); continue; @@ -596,7 +670,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputPaths = new List<string>(); var args = string.Format( CultureInfo.InvariantCulture, - "-i {0}", + "-y -i {0}", inputPath); foreach (var subtitleStream in subtitleStreams) @@ -636,6 +710,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles } await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false); + + foreach (var outputPath in outputPaths) + { + WriteCacheMeta(outputPath, mksFile); + } } } @@ -691,6 +770,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (outputPaths.Count > 0) { await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false); + + foreach (var outputPath in outputPaths) + { + WriteCacheMeta(outputPath, mediaSource.Path); + } } } |
