mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
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>
This commit is contained in:
parent
f7f3ad9eb7
commit
8d40d431e8
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user