From 8fea819b5152f6a38febb9435df18c2fa26d3273 Mon Sep 17 00:00:00 2001 From: Attila Szakacs Date: Thu, 18 Jan 2024 16:38:47 +0100 Subject: [PATCH 1/2] Extract all subtitle streams simultaneously Extracting a subtitle stream is a disk I/O bottlenecked operation as ffmpeg has to read through the whole file, but usually there is nothing CPU intensive to do. If a file has multiple subtitle streams, and we want to extract more of them, extracting them one-by-one results in reading the whole file again and again. However ffmpeg can extract multiple streams at once. We can optimize this by extracting the subtitle streams all at once when only one of them gets queried, then we will have all of them cached for later use. It is useful for people switching subtitles during playback. It is even more useful for people who extract all the subtitle streams in advance, for example with the "Subtitle Extract" plugin. In this case we reduce the extraction time significantly based on the number of subtitle streams in the files, which can be 5-10 in many cases. Signed-off-by: Attila Szakacs --- .../Subtitles/SubtitleEncoder.cs | 221 +++++++++++++++--- 1 file changed, 194 insertions(+), 27 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 459d854bf1..0e66565ed0 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -194,36 +195,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { - string outputFormat; - string outputCodec; + await ExtractAllTextSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); - if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) - || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) - || string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase)) - { - // Extract - outputCodec = "copy"; - outputFormat = subtitleStream.Codec; - } - else if (string.Equals(subtitleStream.Codec, "subrip", StringComparison.OrdinalIgnoreCase)) - { - // Extract - outputCodec = "copy"; - outputFormat = "srt"; - } - else - { - // Extract - outputCodec = "srt"; - outputFormat = "srt"; - } - - // Extract + var outputFormat = GetTextSubtitleFormat(subtitleStream); var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat); - await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, cancellationToken) - .ConfigureAwait(false); - return new SubtitleInfo() { Path = outputPath, @@ -467,6 +443,197 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath); } + private string GetTextSubtitleFormat(MediaStream subtitleStream) + { + if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) + || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)) + { + return subtitleStream.Codec; + } + else + { + return "srt"; + } + } + + private bool IsCodecCopyable(string codec) + { + return string.Equals(codec, "ass", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Extracts all text subtitles. + /// + /// The mediaSource. + /// The cancellation token. + /// Task. + private async Task ExtractAllTextSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) + { + var semaphores = new List { }; + var extractableStreams = new List { }; + + try + { + var subtitleStreams = mediaSource.MediaStreams + .Where(stream => stream.IsTextSubtitleStream && stream.SupportsExternalStream); + + foreach (var subtitleStream in subtitleStreams) + { + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream)); + + var semaphore = GetLock(outputPath); + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + if (File.Exists(outputPath)) + { + semaphore.Release(); + continue; + } + + semaphores.Add(semaphore); + extractableStreams.Add(subtitleStream); + } + + if (extractableStreams.Count > 0) + { + await ExtractAllTextSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Unable to get streams for File:{File}", mediaSource.Path); + } + finally + { + foreach (var semaphore in semaphores) + { + semaphore.Release(); + } + } + } + + private async Task ExtractAllTextSubtitlesInternal( + MediaSourceInfo mediaSource, + List subtitleStreams, + CancellationToken cancellationToken) + { + var inputPath = mediaSource.Path; + var outputPaths = new List { }; + var args = string.Format( + CultureInfo.InvariantCulture, + "-i {0} -copyts", + inputPath); + + foreach (var subtitleStream in subtitleStreams) + { + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream)); + var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; + + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid.")); + + outputPaths.Add(outputPath); + args += string.Format( + CultureInfo.InvariantCulture, + " -map 0:{0} -an -vn -c:s {1} \"{2}\"", + subtitleStream.Index, + outputCodec, + outputPath); + } + + int exitCode; + + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _mediaEncoder.EncoderPath, + Arguments = args, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) + { + _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); + + try + { + process.Start(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting ffmpeg"); + + throw; + } + + try + { + await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; + } + } + + var failed = false; + + if (exitCode == -1) + { + failed = true; + + foreach (var outputPath in outputPaths) + { + try + { + _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); + } + } + } + else + { + foreach (var outputPath in outputPaths) + { + if (!File.Exists(outputPath)) + { + _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); + failed = true; + } + else + { + if (outputPath.EndsWith("ass", StringComparison.OrdinalIgnoreCase)) + { + await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false); + } + + _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); + } + } + } + + if (failed) + { + throw new FfmpegException( + string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0}", inputPath)); + } + } + /// /// Extracts the text subtitle. /// From ce81e2aeab942538a7d5640b7ad88a50398b10d5 Mon Sep 17 00:00:00 2001 From: Attila Szakacs Date: Thu, 18 Jan 2024 17:00:00 +0100 Subject: [PATCH 2/2] Add alltilla to CONTRIBUTORS.md Signed-off-by: Attila Szakacs --- CONTRIBUTORS.md | 1 + .../Subtitles/SubtitleEncoder.cs | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 457f59e0f6..5dcb6daa39 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -4,6 +4,7 @@ - [97carmine](https://github.com/97carmine) - [Abbe98](https://github.com/Abbe98) - [agrenott](https://github.com/agrenott) + - [alltilla](https://github.com/alltilla) - [AndreCarvalho](https://github.com/AndreCarvalho) - [anthonylavado](https://github.com/anthonylavado) - [Artiume](https://github.com/Artiume) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 0e66565ed0..8fd1f9fc1e 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -472,8 +472,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// Task. private async Task ExtractAllTextSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) { - var semaphores = new List { }; - var extractableStreams = new List { }; + var semaphores = new List(); + var extractableStreams = new List(); try { @@ -498,9 +498,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles } if (extractableStreams.Count > 0) - { - await ExtractAllTextSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); - } + { + await ExtractAllTextSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); + } } catch (Exception ex) { @@ -521,7 +521,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles CancellationToken cancellationToken) { var inputPath = mediaSource.Path; - var outputPaths = new List { }; + var outputPaths = new List(); var args = string.Format( CultureInfo.InvariantCulture, "-i {0} -copyts", @@ -531,6 +531,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles { var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream)); var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; + var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); + + if (streamIndex == -1) + { + _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", inputPath, subtitleStream.Index); + continue; + } Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid.")); @@ -538,7 +545,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles args += string.Format( CultureInfo.InvariantCulture, " -map 0:{0} -an -vn -c:s {1} \"{2}\"", - subtitleStream.Index, + streamIndex, outputCodec, outputPath); } @@ -614,16 +621,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); failed = true; + continue; } - else - { - if (outputPath.EndsWith("ass", StringComparison.OrdinalIgnoreCase)) - { - await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false); - } - _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); + if (outputPath.EndsWith("ass", StringComparison.OrdinalIgnoreCase)) + { + await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false); } + + _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); } }