mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-06-03 21:54:26 -04: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
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -9,6 +9,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AsyncKeyedLock;
|
using AsyncKeyedLock;
|
||||||
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@ -230,6 +231,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||||||
MediaAttachment mediaAttachment,
|
MediaAttachment mediaAttachment,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
await CacheAllAttachments(mediaPath, inputFile, mediaSource, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, mediaAttachment.Index);
|
var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, mediaAttachment.Index);
|
||||||
await ExtractAttachment(inputFile, mediaSource, mediaAttachment.Index, outputPath, cancellationToken)
|
await ExtractAttachment(inputFile, mediaSource, mediaAttachment.Index, outputPath, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
@ -237,6 +240,159 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||||||
return outputPath;
|
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(
|
private async Task ExtractAttachment(
|
||||||
string inputFile,
|
string inputFile,
|
||||||
MediaSourceInfo mediaSource,
|
MediaSourceInfo mediaSource,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user