aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.MediaEncoding
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.MediaEncoding')
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs416
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs87
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs118
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs83
-rw-r--r--MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs7
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs184
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs16
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs50
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs37
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs33
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs23
13 files changed, 624 insertions, 440 deletions
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 431fc0b17..48a0654bb 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,7 +1,4 @@
-#pragma warning disable CS1591
-
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -9,28 +6,27 @@ 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;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Attachments
{
+ /// <inheritdoc cref="IAttachmentExtractor"/>
public sealed class AttachmentExtractor : IAttachmentExtractor, IDisposable
{
private readonly ILogger<AttachmentExtractor> _logger;
- private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IPathManager _pathManager;
private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
{
@@ -38,18 +34,26 @@ namespace MediaBrowser.MediaEncoding.Attachments
o.PoolInitialFill = 1;
});
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttachmentExtractor"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger{AttachmentExtractor}"/>.</param>
+ /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
+ /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
+ /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
+ /// <param name="pathManager">The <see cref="IPathManager"/>.</param>
public AttachmentExtractor(
ILogger<AttachmentExtractor> logger,
- IApplicationPaths appPaths,
IFileSystem fileSystem,
IMediaEncoder mediaEncoder,
- IMediaSourceManager mediaSourceManager)
+ IMediaSourceManager mediaSourceManager,
+ IPathManager pathManager)
{
_logger = logger;
- _appPaths = appPaths;
_fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_mediaSourceManager = mediaSourceManager;
+ _pathManager = pathManager;
}
/// <inheritdoc />
@@ -77,350 +81,177 @@ namespace MediaBrowser.MediaEncoding.Attachments
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}");
}
+ if (string.Equals(mediaAttachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ResourceNotFoundException($"Attachment with stream index {attachmentStreamIndex} can't be extracted for MediaSource {mediaSourceId}");
+ }
+
var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken)
.ConfigureAwait(false);
return (mediaAttachment, attachmentStream);
}
+ /// <inheritdoc />
public async Task ExtractAllAttachments(
string inputFile,
MediaSourceInfo mediaSource,
- string outputPath,
CancellationToken cancellationToken)
{
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
- if (shouldExtractOneByOne)
- {
- var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
- foreach (var i in attachmentIndexes)
- {
- var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
- await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false);
- }
- }
- else
+ if (shouldExtractOneByOne && !inputFile.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ foreach (var attachment in mediaSource.MediaAttachments)
{
- if (!Directory.Exists(outputPath))
+ if (!string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
{
- await ExtractAllAttachmentsInternal(
- _mediaEncoder.GetInputArgument(inputFile, mediaSource),
- outputPath,
- false,
- cancellationToken).ConfigureAwait(false);
+ await ExtractAttachment(inputFile, mediaSource, attachment, cancellationToken).ConfigureAwait(false);
}
}
}
- }
-
- public async Task ExtractAllAttachmentsExternal(
- string inputArgument,
- string id,
- string outputPath,
- CancellationToken cancellationToken)
- {
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ else
{
- if (!File.Exists(Path.Join(outputPath, id)))
- {
- await ExtractAllAttachmentsInternal(
- inputArgument,
- outputPath,
- true,
- cancellationToken).ConfigureAwait(false);
-
- if (Directory.Exists(outputPath))
- {
- File.Create(Path.Join(outputPath, id));
- }
- }
+ await ExtractAllAttachmentsInternal(
+ inputFile,
+ mediaSource,
+ false,
+ cancellationToken).ConfigureAwait(false);
}
}
private async Task ExtractAllAttachmentsInternal(
- string inputPath,
- string outputPath,
+ string inputFile,
+ MediaSourceInfo mediaSource,
bool isExternal,
CancellationToken cancellationToken)
{
- ArgumentException.ThrowIfNullOrEmpty(inputPath);
- ArgumentException.ThrowIfNullOrEmpty(outputPath);
-
- Directory.CreateDirectory(outputPath);
-
- var processArgs = string.Format(
- CultureInfo.InvariantCulture,
- "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
- inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
- inputPath);
+ var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
- int exitCode;
+ ArgumentException.ThrowIfNullOrEmpty(inputPath);
- using (var process = new Process
- {
- StartInfo = new ProcessStartInfo
- {
- Arguments = processArgs,
- FileName = _mediaEncoder.EncoderPath,
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden,
- WorkingDirectory = outputPath,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- })
+ var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
{
- _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
-
- process.Start();
-
- try
+ var directory = Directory.CreateDirectory(outputFolder);
+ var fileNames = directory.GetFiles("*", SearchOption.TopDirectoryOnly).Select(f => f.Name).ToHashSet();
+ var missingFiles = mediaSource.MediaAttachments.Where(a => a.FileName is not null && !fileNames.Contains(a.FileName) && !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase));
+ if (!missingFiles.Any())
{
- await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
- exitCode = process.ExitCode;
- }
- catch (OperationCanceledException)
- {
- process.Kill(true);
- exitCode = -1;
+ // Skip extraction if all files already exist
+ return;
}
- }
- var failed = false;
+ var processArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
+ inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
+ inputPath);
- if (exitCode != 0)
- {
- if (isExternal && exitCode == 1)
- {
- // ffmpeg returns exitCode 1 because there is no video or audio stream
- // this can be ignored
- }
- else
+ int exitCode;
+
+ using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ Arguments = processArgs,
+ FileName = _mediaEncoder.EncoderPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ WorkingDirectory = outputFolder,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
{
- failed = true;
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ process.Start();
- _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputPath, exitCode);
try
{
- Directory.Delete(outputPath);
+ await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ exitCode = process.ExitCode;
}
- catch (IOException ex)
+ catch (OperationCanceledException)
{
- _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputPath);
+ process.Kill(true);
+ exitCode = -1;
}
}
- }
- else if (!Directory.Exists(outputPath))
- {
- failed = true;
- }
-
- if (failed)
- {
- _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
- throw new InvalidOperationException(
- string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
- }
-
- _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
- }
-
- private async Task<Stream> GetAttachmentStream(
- MediaSourceInfo mediaSource,
- MediaAttachment mediaAttachment,
- CancellationToken cancellationToken)
- {
- var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false);
- return AsyncFile.OpenRead(attachmentPath);
- }
-
- private async Task<string> GetReadableFile(
- string mediaPath,
- string inputFile,
- MediaSourceInfo mediaSource,
- 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);
-
- return outputPath;
- }
-
- private async Task CacheAllAttachments(
- string mediaPath,
- string inputFile,
- MediaSourceInfo mediaSource,
- CancellationToken cancellationToken)
- {
- var outputFileLocks = new List<IDisposable>();
- var extractableAttachmentIds = new List<int>();
+ var failed = false;
- try
- {
- foreach (var attachment in mediaSource.MediaAttachments)
+ if (exitCode != 0)
{
- var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, attachment.Index);
-
- var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
-
- if (File.Exists(outputPath))
+ if (isExternal && exitCode == 1)
{
- releaser.Dispose();
- continue;
+ // ffmpeg returns exitCode 1 because there is no video or audio stream
+ // this can be ignored
}
-
- outputFileLocks.Add(releaser);
- extractableAttachmentIds.Add(attachment.Index);
- }
-
- if (extractableAttachmentIds.Count > 0)
- {
- await CacheAllAttachmentsInternal(mediaPath, _mediaEncoder.GetInputArgument(inputFile, mediaSource), mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
- }
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Unable to cache media attachments for File:{File}", mediaPath);
- }
- finally
- {
- outputFileLocks.ForEach(x => x.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
+ else
{
- 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();
+ failed = true;
- try
- {
- await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
- exitCode = process.ExitCode;
+ _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputFolder, exitCode);
+ try
+ {
+ Directory.Delete(outputFolder);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputFolder);
+ }
+ }
}
- catch (OperationCanceledException)
+ else if (!Directory.Exists(outputFolder))
{
- process.Kill(true);
- exitCode = -1;
+ failed = true;
}
- }
- var failed = false;
-
- if (exitCode == -1)
- {
- failed = true;
-
- foreach (var outputPath in outputPaths)
+ if (failed)
{
- 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.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputFolder);
- _logger.LogInformation("ffmpeg media attachment extraction completed for {InputPath} to {OutputPath}", inputFile, outputPath);
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputFolder));
}
- }
- if (failed)
- {
- throw new FfmpegException(
- string.Format(CultureInfo.InvariantCulture, "ffmpeg media attachment extraction failed for {0}", inputFile));
+ _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputFolder);
}
}
- private async Task ExtractAttachment(
+ private async Task<Stream> GetAttachmentStream(
+ MediaSourceInfo mediaSource,
+ MediaAttachment mediaAttachment,
+ CancellationToken cancellationToken)
+ {
+ var attachmentPath = await ExtractAttachment(mediaSource.Path, mediaSource, mediaAttachment, cancellationToken)
+ .ConfigureAwait(false);
+ return AsyncFile.OpenRead(attachmentPath);
+ }
+
+ private async Task<string> ExtractAttachment(
string inputFile,
MediaSourceInfo mediaSource,
- int attachmentStreamIndex,
- string outputPath,
+ MediaAttachment mediaAttachment,
CancellationToken cancellationToken)
{
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ var attachmentFolderPath = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ using (await _semaphoreLocks.LockAsync(attachmentFolderPath, cancellationToken).ConfigureAwait(false))
{
- if (!File.Exists(outputPath))
+ var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture));
+ if (!File.Exists(attachmentPath))
{
await ExtractAttachmentInternal(
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
- attachmentStreamIndex,
- outputPath,
+ mediaAttachment.Index,
+ attachmentPath,
cancellationToken).ConfigureAwait(false);
}
+
+ return attachmentPath;
}
}
@@ -510,23 +341,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
_logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
}
- private string GetAttachmentCachePath(string mediaPath, MediaSourceInfo mediaSource, int attachmentStreamIndex)
- {
- string filename;
- if (mediaSource.Protocol == MediaProtocol.File)
- {
- var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
- filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
- }
- else
- {
- filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
- }
-
- var prefix = filename.AsSpan(0, 1);
- return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
- }
-
/// <inheritdoc />
public void Dispose()
{
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index fca17d4c0..7c0be5a9f 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -84,7 +84,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
/// Gets the files matching a pattern.
/// </summary>
/// <param name="searchPattern">The search pattern.</param>
- /// <returns>All files of the directory matchign the search pattern.</returns>
+ /// <returns>All files of the directory matching the search pattern.</returns>
public IFileInfo[] GetFiles(string searchPattern)
{
return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
@@ -96,8 +96,8 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
/// Gets the files matching a pattern and search options.
/// </summary>
/// <param name="searchPattern">The search pattern.</param>
- /// <param name="searchOption">The search optin.</param>
- /// <returns>All files of the directory matchign the search pattern and options.</returns>
+ /// <param name="searchOption">The search option.</param>
+ /// <returns>All files of the directory matching the search pattern and options.</returns>
public IFileInfo[] GetFiles(string searchPattern, SearchOption searchOption)
{
return _fileSystem.GetFiles(
diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs
new file mode 100644
index 000000000..a8ff58b09
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs
@@ -0,0 +1,87 @@
+#pragma warning disable CA1031
+
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.MediaEncoding.Encoder;
+
+/// <summary>
+/// Helper class for Apple platform specific operations.
+/// </summary>
+[SupportedOSPlatform("macos")]
+public static class ApplePlatformHelper
+{
+ private static readonly string[] _av1DecodeBlacklistedCpuClass = ["M1", "M2"];
+
+ private static string GetSysctlValue(ReadOnlySpan<byte> name)
+ {
+ IntPtr length = IntPtr.Zero;
+ // Get length of the value
+ int osStatus = SysctlByName(name, IntPtr.Zero, ref length, IntPtr.Zero, 0);
+
+ if (osStatus != 0)
+ {
+ throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}");
+ }
+
+ IntPtr buffer = Marshal.AllocHGlobal(length.ToInt32());
+ try
+ {
+ osStatus = SysctlByName(name, buffer, ref length, IntPtr.Zero, 0);
+ if (osStatus != 0)
+ {
+ throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}");
+ }
+
+ return Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+ }
+
+ private static int SysctlByName(ReadOnlySpan<byte> name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen)
+ {
+ return NativeMethods.SysctlByName(name.ToArray(), oldp, ref oldlenp, newp, newlen);
+ }
+
+ /// <summary>
+ /// Check if the current system has hardware acceleration for AV1 decoding.
+ /// </summary>
+ /// <param name="logger">The logger used for error logging.</param>
+ /// <returns>Boolean indicates the hwaccel support.</returns>
+ public static bool HasAv1HardwareAccel(ILogger logger)
+ {
+ if (!RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64))
+ {
+ return false;
+ }
+
+ try
+ {
+ string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string"u8);
+ return !_av1DecodeBlacklistedCpuClass.Any(blacklistedCpuClass => cpuBrandString.Contains(blacklistedCpuClass, StringComparison.OrdinalIgnoreCase));
+ }
+ catch (NotSupportedException e)
+ {
+ logger.LogError("Error getting CPU brand string: {Message}", e.Message);
+ }
+ catch (Exception e)
+ {
+ logger.LogError("Unknown error occured: {Exception}", e);
+ }
+
+ return false;
+ }
+
+ private static class NativeMethods
+ {
+ [DllImport("libc", EntryPoint = "sysctlbyname", SetLastError = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ internal static extern int SysctlByName(byte[] name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen);
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 23d9ca7ef..f4e8c39c1 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -5,15 +5,17 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
+using System.Runtime.Versioning;
using System.Text.RegularExpressions;
+using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Encoder
{
public partial class EncoderValidator
{
- private static readonly string[] _requiredDecoders = new[]
- {
+ private static readonly string[] _requiredDecoders =
+ [
"h264",
"hevc",
"vp8",
@@ -55,10 +57,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
"vp8_rkmpp",
"vp9_rkmpp",
"av1_rkmpp"
- };
+ ];
- private static readonly string[] _requiredEncoders = new[]
- {
+ private static readonly string[] _requiredEncoders =
+ [
"libx264",
"libx265",
"libsvtav1",
@@ -95,10 +97,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
"h264_rkmpp",
"hevc_rkmpp",
"mjpeg_rkmpp"
- };
+ ];
- private static readonly string[] _requiredFilters = new[]
- {
+ private static readonly string[] _requiredFilters =
+ [
// sw
"alphasrc",
"zscale",
@@ -121,6 +123,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
"tonemap_opencl",
"overlay_opencl",
"transpose_opencl",
+ "yadif_opencl",
+ "bwdif_opencl",
// vaapi
"scale_vaapi",
"deinterlace_vaapi",
@@ -146,17 +150,28 @@ namespace MediaBrowser.MediaEncoding.Encoder
"scale_rkrga",
"vpp_rkrga",
"overlay_rkrga"
+ ];
+
+ private static readonly Dictionary<FilterOptionType, (string, string)> _filterOptionsDict = new Dictionary<FilterOptionType, (string, string)>
+ {
+ { FilterOptionType.ScaleCudaFormat, ("scale_cuda", "format") },
+ { FilterOptionType.TonemapCudaName, ("tonemap_cuda", "GPU accelerated HDR to SDR tonemapping") },
+ { FilterOptionType.TonemapOpenclBt2390, ("tonemap_opencl", "bt2390") },
+ { FilterOptionType.OverlayOpenclFrameSync, ("overlay_opencl", "Action to take when encountering EOF from secondary input") },
+ { FilterOptionType.OverlayVaapiFrameSync, ("overlay_vaapi", "Action to take when encountering EOF from secondary input") },
+ { FilterOptionType.OverlayVulkanFrameSync, ("overlay_vulkan", "Action to take when encountering EOF from secondary input") },
+ { FilterOptionType.TransposeOpenclReversal, ("transpose_opencl", "rotate by half-turn") },
+ { FilterOptionType.OverlayOpenclAlphaFormat, ("overlay_opencl", "alpha_format") },
+ { FilterOptionType.OverlayCudaAlphaFormat, ("overlay_cuda", "alpha_format") }
};
- private static readonly Dictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
+ private static readonly Dictionary<BitStreamFilterOptionType, (string, string)> _bsfOptionsDict = new Dictionary<BitStreamFilterOptionType, (string, string)>
{
- { 0, new string[] { "scale_cuda", "format" } },
- { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } },
- { 2, new string[] { "tonemap_opencl", "bt2390" } },
- { 3, new string[] { "overlay_opencl", "Action to take when encountering EOF from secondary input" } },
- { 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } },
- { 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } },
- { 6, new string[] { "transpose_opencl", "rotate by half-turn" } }
+ { BitStreamFilterOptionType.HevcMetadataRemoveDovi, ("hevc_metadata", "remove_dovi") },
+ { BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus, ("hevc_metadata", "remove_hdr10plus") },
+ { BitStreamFilterOptionType.Av1MetadataRemoveDovi, ("av1_metadata", "remove_dovi") },
+ { BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus, ("av1_metadata", "remove_hdr10plus") },
+ { BitStreamFilterOptionType.DoviRpuStrip, ("dovi_rpu", "strip") }
};
// These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below
@@ -283,7 +298,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
public IEnumerable<string> GetFilters() => GetFFmpegFilters();
- public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption();
+ public IDictionary<FilterOptionType, bool> GetFiltersWithOption() => _filterOptionsDict
+ .ToDictionary(item => item.Key, item => CheckFilterWithOption(item.Value.Item1, item.Value.Item2));
+
+ public IDictionary<BitStreamFilterOptionType, bool> GetBitStreamFiltersWithOption() => _bsfOptionsDict
+ .ToDictionary(item => item.Key, item => CheckBitStreamFilterWithOption(item.Value.Item1, item.Value.Item2));
public Version? GetFFmpegVersion()
{
@@ -437,6 +456,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ [SupportedOSPlatform("macos")]
+ public bool CheckIsVideoToolboxAv1DecodeAvailable()
+ {
+ return ApplePlatformHelper.HasAv1HardwareAccel(_logger);
+ }
+
private IEnumerable<string> GetHwaccelTypes()
{
string? output = null;
@@ -451,10 +476,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (string.IsNullOrWhiteSpace(output))
{
- return Enumerable.Empty<string>();
+ return [];
}
- var found = output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList();
+ var found = output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList();
_logger.LogInformation("Available hwaccel types: {Types}", found);
return found;
@@ -488,6 +513,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
+ public bool CheckBitStreamFilterWithOption(string filter, string option)
+ {
+ if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
+ {
+ return false;
+ }
+
+ string output;
+ try
+ {
+ output = GetProcessOutput(_encoderPath, "-h bsf=" + filter, false, null);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error detecting the given bit stream filter");
+ return false;
+ }
+
+ if (output.Contains("Bit stream filter " + filter, StringComparison.Ordinal))
+ {
+ return output.Contains(option, StringComparison.Ordinal);
+ }
+
+ _logger.LogWarning("Bit stream filter: {Name} with option {Option} is not available", filter, option);
+
+ return false;
+ }
+
public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
{
if (string.IsNullOrEmpty(keyDesc))
@@ -516,6 +569,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
return !string.IsNullOrEmpty(flag) && GetProcessExitCode(_encoderPath, $"-loglevel quiet -hwaccel_flags +{flag} -hide_banner -f lavfi -i nullsrc=s=1x1:d=100 -f null -");
}
+ public bool CheckSupportedProberOption(string option, string proberPath)
+ {
+ return !string.IsNullOrEmpty(option) && GetProcessExitCode(proberPath, $"-loglevel quiet -f lavfi -i nullsrc=s=1x1:d=1 -{option}");
+ }
+
private IEnumerable<string> GetCodecs(Codec codec)
{
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
@@ -527,12 +585,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
catch (Exception ex)
{
_logger.LogError(ex, "Error detecting available {Codec}", codecstr);
- return Enumerable.Empty<string>();
+ return [];
}
if (string.IsNullOrWhiteSpace(output))
{
- return Enumerable.Empty<string>();
+ return [];
}
var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders;
@@ -557,12 +615,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
catch (Exception ex)
{
_logger.LogError(ex, "Error detecting available filters");
- return Enumerable.Empty<string>();
+ return [];
}
if (string.IsNullOrWhiteSpace(output))
{
- return Enumerable.Empty<string>();
+ return [];
}
var found = FilterRegex()
@@ -575,20 +633,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
return found;
}
- private Dictionary<int, bool> GetFFmpegFiltersWithOption()
- {
- Dictionary<int, bool> dict = new Dictionary<int, bool>();
- for (int i = 0; i < _filterOptionsDict.Count; i++)
- {
- if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2)
- {
- dict.Add(i, CheckFilterWithOption(val[0], val[1]));
- }
- }
-
- return dict;
- }
-
private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
{
var redirectStandardIn = !string.IsNullOrEmpty(testKey);
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index e084bda27..2eb647e26 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -72,10 +72,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
private List<string> _decoders = new List<string>();
private List<string> _hwaccels = new List<string>();
private List<string> _filters = new List<string>();
- private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
+ private IDictionary<FilterOptionType, bool> _filtersWithOption = new Dictionary<FilterOptionType, bool>();
+ private IDictionary<BitStreamFilterOptionType, bool> _bitStreamFiltersWithOption = new Dictionary<BitStreamFilterOptionType, bool>();
private bool _isPkeyPauseSupported = false;
private bool _isLowPriorityHwDecodeSupported = false;
+ private bool _proberSupportsFirstVideoFrame = false;
private bool _isVaapiDeviceAmd = false;
private bool _isVaapiDeviceInteliHD = false;
@@ -83,6 +85,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
+ private bool _isVideoToolboxAv1DecodeAvailable = false;
+
private static string[] _vulkanImageDrmFmtModifierExts =
{
"VK_EXT_image_drm_format_modifier",
@@ -122,7 +126,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
- var semaphoreCount = 2 * Environment.ProcessorCount;
+ // Although the type is not nullable, this might still be null during unit tests
+ var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
+ if (semaphoreCount < 1)
+ {
+ semaphoreCount = Environment.ProcessorCount;
+ }
+
_thumbnailResourcePool = new(semaphoreCount);
}
@@ -153,6 +163,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
+ public bool IsVideoToolboxAv1DecodeAvailable => _isVideoToolboxAv1DecodeAvailable;
+
[GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
private static partial Regex FfprobePathRegex();
@@ -212,6 +224,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
SetAvailableEncoders(validator.GetEncoders());
SetAvailableFilters(validator.GetFilters());
SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
+ SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption());
SetAvailableHwaccels(validator.GetHwaccels());
SetMediaEncoderVersion(validator);
@@ -219,6 +232,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
_isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
+ _proberSupportsFirstVideoFrame = validator.CheckSupportedProberOption("only_first_vframe", _ffprobePath);
// Check the Vaapi device vendor
if (OperatingSystem.IsLinux()
@@ -255,6 +269,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.VaapiDevice);
}
}
+
+ // Check if VideoToolbox supports AV1 decode
+ if (OperatingSystem.IsMacOS() && SupportsHwaccel("videotoolbox"))
+ {
+ _isVideoToolboxAv1DecodeAvailable = validator.CheckIsVideoToolboxAv1DecodeAvailable();
+ }
}
_logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
@@ -321,11 +341,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
_filters = list.ToList();
}
- public void SetAvailableFiltersWithOption(IDictionary<int, bool> dict)
+ public void SetAvailableFiltersWithOption(IDictionary<FilterOptionType, bool> dict)
{
_filtersWithOption = dict;
}
+ public void SetAvailableBitStreamFiltersWithOption(IDictionary<BitStreamFilterOptionType, bool> dict)
+ {
+ _bitStreamFiltersWithOption = dict;
+ }
+
public void SetMediaEncoderVersion(EncoderValidator validator)
{
_ffmpegVersion = validator.GetFFmpegVersion();
@@ -358,12 +383,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public bool SupportsFilterWithOption(FilterOptionType option)
{
- if (_filtersWithOption.TryGetValue((int)option, out var val))
- {
- return val;
- }
+ return _filtersWithOption.TryGetValue(option, out var val) && val;
+ }
- return false;
+ public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option)
+ {
+ return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val;
}
public bool CanEncodeToAudioCodec(string codec)
@@ -485,6 +510,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
var args = extractChapters
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
+
+ if (_proberSupportsFirstVideoFrame)
+ {
+ args += " -show_frames -only_first_vframe";
+ }
+
args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
var process = new Process
@@ -506,7 +537,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
EnableRaisingEvents = true
};
- _logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
+ _logger.LogDebug("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
var memoryStream = new MemoryStream();
await using (memoryStream.ConfigureAwait(false))
@@ -606,7 +637,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
catch (Exception ex)
{
- _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument);
+ _logger.LogWarning(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument);
}
}
@@ -684,13 +715,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
filters.Add(scaler);
- // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
- // mpegts need larger batch size otherwise the corrupted thumbnail will be created. Larger batch size will lower the processing speed.
+ // Use ffmpeg to sample N frames and pick the best thumbnail. Have a fall back just in case.
var enableThumbnail = !useTradeoff && useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase);
if (enableThumbnail)
{
- var useLargerBatchSize = string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
- filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24"));
+ filters.Add("thumbnail=n=24");
}
// Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
@@ -703,25 +732,37 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
var peak = videoStream.VideoRangeType == VideoRangeType.DOVI ? "400" : "100";
enableHdrExtraction = true;
- filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p");
+ filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p:range=full");
}
else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
{
enableHdrExtraction = true;
- filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p");
+ filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709:out_range=full,format=yuv420p");
}
}
var vf = string.Join(',', filters);
var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
- var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, isAudio ? string.Empty : GetImageResolutionParameter());
+ var args = string.Format(
+ CultureInfo.InvariantCulture,
+ "-i {0}{1} -threads {2} -v quiet -vframes 1 -vf {3}{4}{5} -f image2 \"{6}\"",
+ inputPath,
+ mapArg,
+ _threads,
+ vf,
+ isAudio ? string.Empty : GetImageResolutionParameter(),
+ EncodingHelper.GetVideoSyncOption("-1", EncoderVersion), // auto decide fps mode
+ tempExtractPath);
if (offset.HasValue)
{
args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
}
- if (useIFrame && useTradeoff)
+ // The mpegts demuxer cannot seek to keyframes, so we have to let the
+ // decoder discard non-keyframes, which may contain corrupted images.
+ var seekMpegTs = offset.HasValue && string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
+ if (useIFrame && (useTradeoff || seekMpegTs))
{
args = "-skip_frame nokey " + args;
}
@@ -1101,14 +1142,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
private void StopProcesses()
{
- List<ProcessWrapper> proceses;
+ List<ProcessWrapper> processes;
lock (_runningProcessesLock)
{
- proceses = _runningProcesses.ToList();
+ processes = _runningProcesses.ToList();
_runningProcesses.Clear();
}
- foreach (var process in proceses)
+ foreach (var process in processes)
{
if (!process.HasExited)
{
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index 1b5b5262a..6f51e1a6a 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (result.Streams is not null)
{
- // Convert all dictionaries to case insensitive
+ // Convert all dictionaries to case-insensitive
foreach (var stream in result.Streams)
{
if (stream.Tags is not null)
@@ -70,7 +70,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
/// <summary>
- /// Converts a dictionary to case insensitive.
+ /// Converts a dictionary to case-insensitive.
/// </summary>
/// <param name="dict">The dict.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
index d4d153b08..53eea64db 100644
--- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
+++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
@@ -30,5 +30,12 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <value>The chapters.</value>
[JsonPropertyName("chapters")]
public IReadOnlyList<MediaChapter> Chapters { get; set; }
+
+ /// <summary>
+ /// Gets or sets the frames.
+ /// </summary>
+ /// <value>The streams.</value>
+ [JsonPropertyName("frames")]
+ public IReadOnlyList<MediaFrameInfo> Frames { get; set; }
}
}
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs
new file mode 100644
index 000000000..bed4368ed
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs
@@ -0,0 +1,184 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.MediaEncoding.Probing;
+
+/// <summary>
+/// Class MediaFrameInfo.
+/// </summary>
+public class MediaFrameInfo
+{
+ /// <summary>
+ /// Gets or sets the media type.
+ /// </summary>
+ [JsonPropertyName("media_type")]
+ public string? MediaType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the StreamIndex.
+ /// </summary>
+ [JsonPropertyName("stream_index")]
+ public int? StreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the KeyFrame.
+ /// </summary>
+ [JsonPropertyName("key_frame")]
+ public int? KeyFrame { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Pts.
+ /// </summary>
+ [JsonPropertyName("pts")]
+ public long? Pts { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PtsTime.
+ /// </summary>
+ [JsonPropertyName("pts_time")]
+ public string? PtsTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the BestEffortTimestamp.
+ /// </summary>
+ [JsonPropertyName("best_effort_timestamp")]
+ public long BestEffortTimestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the BestEffortTimestampTime.
+ /// </summary>
+ [JsonPropertyName("best_effort_timestamp_time")]
+ public string? BestEffortTimestampTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Duration.
+ /// </summary>
+ [JsonPropertyName("duration")]
+ public int Duration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the DurationTime.
+ /// </summary>
+ [JsonPropertyName("duration_time")]
+ public string? DurationTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PktPos.
+ /// </summary>
+ [JsonPropertyName("pkt_pos")]
+ public string? PktPos { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PktSize.
+ /// </summary>
+ [JsonPropertyName("pkt_size")]
+ public string? PktSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Width.
+ /// </summary>
+ [JsonPropertyName("width")]
+ public int? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Height.
+ /// </summary>
+ [JsonPropertyName("height")]
+ public int? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the CropTop.
+ /// </summary>
+ [JsonPropertyName("crop_top")]
+ public int? CropTop { get; set; }
+
+ /// <summary>
+ /// Gets or sets the CropBottom.
+ /// </summary>
+ [JsonPropertyName("crop_bottom")]
+ public int? CropBottom { get; set; }
+
+ /// <summary>
+ /// Gets or sets the CropLeft.
+ /// </summary>
+ [JsonPropertyName("crop_left")]
+ public int? CropLeft { get; set; }
+
+ /// <summary>
+ /// Gets or sets the CropRight.
+ /// </summary>
+ [JsonPropertyName("crop_right")]
+ public int? CropRight { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PixFmt.
+ /// </summary>
+ [JsonPropertyName("pix_fmt")]
+ public string? PixFmt { get; set; }
+
+ /// <summary>
+ /// Gets or sets the SampleAspectRatio.
+ /// </summary>
+ [JsonPropertyName("sample_aspect_ratio")]
+ public string? SampleAspectRatio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PictType.
+ /// </summary>
+ [JsonPropertyName("pict_type")]
+ public string? PictType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the InterlacedFrame.
+ /// </summary>
+ [JsonPropertyName("interlaced_frame")]
+ public int? InterlacedFrame { get; set; }
+
+ /// <summary>
+ /// Gets or sets the TopFieldFirst.
+ /// </summary>
+ [JsonPropertyName("top_field_first")]
+ public int? TopFieldFirst { get; set; }
+
+ /// <summary>
+ /// Gets or sets the RepeatPict.
+ /// </summary>
+ [JsonPropertyName("repeat_pict")]
+ public int? RepeatPict { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ColorRange.
+ /// </summary>
+ [JsonPropertyName("color_range")]
+ public string? ColorRange { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ColorSpace.
+ /// </summary>
+ [JsonPropertyName("color_space")]
+ public string? ColorSpace { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ColorPrimaries.
+ /// </summary>
+ [JsonPropertyName("color_primaries")]
+ public string? ColorPrimaries { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ColorTransfer.
+ /// </summary>
+ [JsonPropertyName("color_transfer")]
+ public string? ColorTransfer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ChromaLocation.
+ /// </summary>
+ [JsonPropertyName("chroma_location")]
+ public string? ChromaLocation { get; set; }
+
+ /// <summary>
+ /// Gets or sets the SideDataList.
+ /// </summary>
+ [JsonPropertyName("side_data_list")]
+ public IReadOnlyList<MediaFrameSideDataInfo>? SideDataList { get; set; }
+}
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs
new file mode 100644
index 000000000..3f7dd9a69
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.MediaEncoding.Probing;
+
+/// <summary>
+/// Class MediaFrameSideDataInfo.
+/// Currently only records the SideDataType for HDR10+ detection.
+/// </summary>
+public class MediaFrameSideDataInfo
+{
+ /// <summary>
+ /// Gets or sets the SideDataType.
+ /// </summary>
+ [JsonPropertyName("side_data_type")]
+ public string? SideDataType { get; set; }
+}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index c730f4cda..5784deacd 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Xml;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -104,8 +105,9 @@ namespace MediaBrowser.MediaEncoding.Probing
SetSize(data, info);
var internalStreams = data.Streams ?? Array.Empty<MediaStreamInfo>();
+ var internalFrames = data.Frames ?? Array.Empty<MediaFrameInfo>();
- info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format))
+ info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format, internalFrames))
.Where(i => i is not null)
// Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them
.Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec))
@@ -531,42 +533,44 @@ namespace MediaBrowser.MediaEncoding.Probing
private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info)
{
List<BaseItemPerson> peoples = new List<BaseItemPerson>();
+ var distinctPairs = pairs.Select(p => p.Value)
+ .Where(i => !string.IsNullOrWhiteSpace(i))
+ .Trimmed()
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+
if (string.Equals(key, "studio", StringComparison.OrdinalIgnoreCase))
{
- info.Studios = pairs.Select(p => p.Value)
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray();
+ info.Studios = distinctPairs.ToArray();
}
else if (string.Equals(key, "screenwriters", StringComparison.OrdinalIgnoreCase))
{
- foreach (var pair in pairs)
+ foreach (var pair in distinctPairs)
{
peoples.Add(new BaseItemPerson
{
- Name = pair.Value,
+ Name = pair,
Type = PersonKind.Writer
});
}
}
else if (string.Equals(key, "producers", StringComparison.OrdinalIgnoreCase))
{
- foreach (var pair in pairs)
+ foreach (var pair in distinctPairs)
{
peoples.Add(new BaseItemPerson
{
- Name = pair.Value,
+ Name = pair,
Type = PersonKind.Producer
});
}
}
else if (string.Equals(key, "directors", StringComparison.OrdinalIgnoreCase))
{
- foreach (var pair in pairs)
+ foreach (var pair in distinctPairs)
{
peoples.Add(new BaseItemPerson
{
- Name = pair.Value,
+ Name = pair,
Type = PersonKind.Director
});
}
@@ -591,10 +595,10 @@ namespace MediaBrowser.MediaEncoding.Probing
switch (reader.Name)
{
case "key":
- name = reader.ReadElementContentAsString();
+ name = reader.ReadNormalizedString();
break;
case "string":
- value = reader.ReadElementContentAsString();
+ value = reader.ReadNormalizedString();
break;
default:
reader.Skip();
@@ -607,8 +611,8 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
- if (string.IsNullOrWhiteSpace(name)
- || string.IsNullOrWhiteSpace(value))
+ if (string.IsNullOrEmpty(name)
+ || string.IsNullOrEmpty(value))
{
return null;
}
@@ -682,8 +686,9 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <param name="isAudio">if set to <c>true</c> [is info].</param>
/// <param name="streamInfo">The stream info.</param>
/// <param name="formatInfo">The format info.</param>
+ /// <param name="frameInfoList">The frame info.</param>
/// <returns>MediaStream.</returns>
- private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
+ private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList<MediaFrameInfo> frameInfoList)
{
// These are mp4 chapters
if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase))
@@ -901,6 +906,15 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
}
+
+ var frameInfo = frameInfoList?.FirstOrDefault(i => i.StreamIndex == stream.Index);
+ if (frameInfo?.SideDataList != null)
+ {
+ if (frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase)))
+ {
+ stream.Hdr10PlusPresentFlag = true;
+ }
+ }
}
else if (streamInfo.CodecType == CodecType.Data)
{
@@ -951,7 +965,7 @@ namespace MediaBrowser.MediaEncoding.Probing
// Get average bitrate info from tag "NUMBER_OF_BYTES" and "DURATION" if possible.
var durationInSeconds = GetRuntimeSecondsFromTags(streamInfo);
var bytes = GetNumberOfBytesFromTags(streamInfo);
- if (durationInSeconds is not null && bytes is not null)
+ if (durationInSeconds is not null && durationInSeconds.Value >= 1 && bytes is not null)
{
bps = Convert.ToInt32(bytes * 8 / durationInSeconds, CultureInfo.InvariantCulture);
if (bps > 0)
@@ -1453,7 +1467,7 @@ namespace MediaBrowser.MediaEncoding.Probing
var genres = new List<string>(info.Genres);
foreach (var genre in Split(genreVal, true))
{
- if (string.IsNullOrWhiteSpace(genre))
+ if (string.IsNullOrEmpty(genre))
{
continue;
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index a79d801fb..d060b247d 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public class SubtitleEditParser : ISubtitleParser
{
private readonly ILogger<SubtitleEditParser> _logger;
- private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
+ private readonly Dictionary<string, List<Type>> _subtitleFormatTypes;
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
@@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
{
_logger = logger;
- _subtitleFormats = GetSubtitleFormats()
- .Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
- .GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
- .ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
+ _subtitleFormatTypes = GetSubtitleFormatTypes();
}
/// <inheritdoc />
@@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitle = new Subtitle();
var lines = stream.ReadAllLines().ToList();
- if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
+ if (!_subtitleFormatTypes.TryGetValue(fileExtension, out var subtitleFormatTypesForExtension))
{
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
}
- foreach (var subtitleFormat in subtitleFormats)
+ foreach (var subtitleFormatType in subtitleFormatTypesForExtension)
{
+ var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!;
_logger.LogDebug(
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
fileExtension,
@@ -97,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <inheritdoc />
public bool SupportsFileExtension(string fileExtension)
- => _subtitleFormats.ContainsKey(fileExtension);
+ => _subtitleFormatTypes.ContainsKey(fileExtension);
- private List<SubtitleFormat> GetSubtitleFormats()
+ private Dictionary<string, List<Type>> GetSubtitleFormatTypes()
{
- var subtitleFormats = new List<SubtitleFormat>();
+ var subtitleFormatTypes = new Dictionary<string, List<Type>>(StringComparer.OrdinalIgnoreCase);
var assembly = typeof(SubtitleFormat).Assembly;
foreach (var type in assembly.GetTypes())
@@ -113,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
- // It shouldn't be null, but the exception is caught if it is
- var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
- subtitleFormats.Add(subtitleFormat);
+ var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!;
+ var extension = tempInstance.Extension.TrimStart('.');
+ if (!string.IsNullOrEmpty(extension))
+ {
+ // Store only the type, we will instantiate from it later
+ if (!subtitleFormatTypes.TryGetValue(extension, out var subtitleFormatTypesForExtension))
+ {
+ subtitleFormatTypes[extension] = [type];
+ }
+ else
+ {
+ subtitleFormatTypesForExtension.Add(type);
+ }
+ }
}
catch (Exception ex)
{
@@ -123,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- return subtitleFormats;
+ return subtitleFormatTypes;
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index a731d4785..777e33587 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -13,10 +13,10 @@ using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
@@ -31,12 +31,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
{
private readonly ILogger<SubtitleEncoder> _logger;
- private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly ISubtitleParser _subtitleParser;
+ private readonly IPathManager _pathManager;
/// <summary>
/// The _semaphoreLocks.
@@ -49,24 +49,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public SubtitleEncoder(
ILogger<SubtitleEncoder> logger,
- IApplicationPaths appPaths,
IFileSystem fileSystem,
IMediaEncoder mediaEncoder,
IHttpClientFactory httpClientFactory,
IMediaSourceManager mediaSourceManager,
- ISubtitleParser subtitleParser)
+ ISubtitleParser subtitleParser,
+ IPathManager pathManager)
{
_logger = logger;
- _appPaths = appPaths;
_fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_httpClientFactory = httpClientFactory;
_mediaSourceManager = mediaSourceManager;
_subtitleParser = subtitleParser;
+ _pathManager = pathManager;
}
- private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
-
private MemoryStream ConvertSubtitles(
Stream stream,
string inputFormat,
@@ -830,26 +828,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
{
- if (mediaSource.Protocol == MediaProtocol.File)
- {
- var ticksParam = string.Empty;
-
- var date = _fileSystem.GetLastWriteTimeUtc(mediaSource.Path);
-
- ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
-
- var prefix = filename.Slice(0, 1);
-
- return Path.Join(SubtitleCachePath, prefix, filename);
- }
- else
- {
- ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
-
- var prefix = filename.Slice(0, 1);
-
- return Path.Join(SubtitleCachePath, prefix, filename);
- }
+ return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension);
}
/// <inheritdoc />
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index 57557d55c..0cda803d6 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -10,7 +10,8 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
-using Jellyfin.Data.Enums;
+using Jellyfin.Data;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@@ -241,14 +242,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
{
- try
- {
- await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
- }
+ await _sessionManager.CloseLiveStreamIfNeededAsync(job.LiveStreamId, job.PlaySessionId).ConfigureAwait(false);
}
}
@@ -404,24 +398,19 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
// If subtitles get burned in fonts may need to be extracted from the media file
if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{
- var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
{
var concatPath = Path.Join(_appPaths.CachePath, "concat", state.MediaSource.Id + ".concat");
- await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
+ await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false);
}
else
{
- await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
+ await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false);
}
if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
{
- string subtitlePath = state.SubtitleStream.Path;
- string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
- string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-
- await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
+ await _attachmentExtractor.ExtractAllAttachments(state.SubtitleStream.Path, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false);
}
}