aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAttila Szakacs <szakacs.attila96@gmail.com>2024-03-03 21:33:54 +0100
committerGitHub <noreply@github.com>2024-03-03 13:33:54 -0700
commit8d40d431e8e5b067a535e564362b902480a13259 (patch)
tree031c8dfc187c255cd997726c57a29ed5f92bccbe
parentf7f3ad9eb792a02ba1815c8a316e02f9ed89fe85 (diff)
Extract and cache all media attachments in bulk (#11029)
Similar to https://github.com/jellyfin/jellyfin/pull/10884 --- Jellyfin clients need fonts for subtitles, and each font is a separate attachment, which causes a lot of re-reads of the file. Certain contents, like anime in a lot of cases, contain 50-80 different attachments. Spawning 80 ffmpeg processes at the same time on the same file might cause swapping on slower HDDs and can bring disk subsystem to a crawl. (For more info, see https://github.com/jellyfin/jellyfin/3215) This change helps a lot in this scenario. Signed-off-by: Attila Szakacs <szakacs.attila96@gmail.com>
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs158
1 files changed, 157 insertions, 1 deletions
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index ff91a60a7..a97cca6b8 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -9,6 +9,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
+using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
@@ -230,6 +231,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
MediaAttachment mediaAttachment,
CancellationToken cancellationToken)
{
+ await CacheAllAttachments(mediaPath, inputFile, mediaSource, cancellationToken).ConfigureAwait(false);
+
var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, mediaAttachment.Index);
await ExtractAttachment(inputFile, mediaSource, mediaAttachment.Index, outputPath, cancellationToken)
.ConfigureAwait(false);
@@ -237,6 +240,159 @@ namespace MediaBrowser.MediaEncoding.Attachments
return outputPath;
}
+ private async Task CacheAllAttachments(
+ string mediaPath,
+ string inputFile,
+ MediaSourceInfo mediaSource,
+ CancellationToken cancellationToken)
+ {
+ var outputFileLocks = new List<AsyncKeyedLockReleaser<string>>();
+ var extractableAttachmentIds = new List<int>();
+
+ try
+ {
+ foreach (var attachment in mediaSource.MediaAttachments)
+ {
+ var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, attachment.Index);
+
+ var @outputFileLock = _semaphoreLocks.GetOrAdd(outputPath);
+ await @outputFileLock.SemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ if (File.Exists(outputPath))
+ {
+ @outputFileLock.Dispose();
+ continue;
+ }
+
+ outputFileLocks.Add(@outputFileLock);
+ extractableAttachmentIds.Add(attachment.Index);
+ }
+
+ if (extractableAttachmentIds.Count > 0)
+ {
+ await CacheAllAttachmentsInternal(mediaPath, inputFile, mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Unable to cache media attachments for File:{File}", mediaPath);
+ }
+ finally
+ {
+ foreach (var @outputFileLock in outputFileLocks)
+ {
+ @outputFileLock.Dispose();
+ }
+ }
+ }
+
+ private async Task CacheAllAttachmentsInternal(
+ string mediaPath,
+ string inputFile,
+ MediaSourceInfo mediaSource,
+ List<int> extractableAttachmentIds,
+ CancellationToken cancellationToken)
+ {
+ var outputPaths = new List<string>();
+ var processArgs = string.Empty;
+
+ foreach (var attachmentId in extractableAttachmentIds)
+ {
+ var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, attachmentId);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid."));
+
+ outputPaths.Add(outputPath);
+ processArgs += string.Format(
+ CultureInfo.InvariantCulture,
+ " -dump_attachment:{0} \"{1}\"",
+ attachmentId,
+ EncodingUtils.NormalizePath(outputPath));
+ }
+
+ processArgs += string.Format(
+ CultureInfo.InvariantCulture,
+ " -i \"{0}\" -t 0 -f null null",
+ inputFile);
+
+ int exitCode;
+
+ using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ Arguments = processArgs,
+ FileName = _mediaEncoder.EncoderPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
+ {
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ process.Start();
+
+ try
+ {
+ await process.WaitForExitAsync(cancellationToken).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 media attachment due to failure: {Path}", outputPath);
+ _fileSystem.DeleteFile(outputPath);
+ }
+ catch (FileNotFoundException)
+ {
+ // ffmpeg failed, so it is normal that one or more expected output files do not exist.
+ // There is no need to log anything for the user here.
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting extracted media attachment {Path}", outputPath);
+ }
+ }
+ }
+ else
+ {
+ foreach (var outputPath in outputPaths)
+ {
+ if (!File.Exists(outputPath))
+ {
+ _logger.LogError("ffmpeg media attachment extraction failed for {InputPath} to {OutputPath}", inputFile, outputPath);
+ failed = true;
+ continue;
+ }
+
+ _logger.LogInformation("ffmpeg media attachment extraction completed for {InputPath} to {OutputPath}", inputFile, outputPath);
+ }
+ }
+
+ if (failed)
+ {
+ throw new FfmpegException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg media attachment extraction failed for {0}", inputFile));
+ }
+ }
+
private async Task ExtractAttachment(
string inputFile,
MediaSourceInfo mediaSource,