aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.MediaEncoding/Encoder
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.MediaEncoding/Encoder')
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs87
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs178
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs426
4 files changed, 510 insertions, 183 deletions
diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs
new file mode 100644
index 0000000000..a8ff58b091
--- /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 ae0284e3ab..68d6d215b2 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",
@@ -27,6 +29,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"msmpeg4",
"dca",
"ac3",
+ "ac4",
"aac",
"mp3",
"flac",
@@ -54,21 +57,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
"vp8_rkmpp",
"vp9_rkmpp",
"av1_rkmpp"
- };
+ ];
- private static readonly string[] _requiredEncoders = new[]
- {
+ private static readonly string[] _requiredEncoders =
+ [
"libx264",
"libx265",
"libsvtav1",
- "mpeg4",
- "msmpeg4",
- "libvpx",
- "libvpx-vp9",
"aac",
"aac_at",
"libfdk_aac",
"ac3",
+ "alac",
"dca",
"libmp3lame",
"libopus",
@@ -81,6 +81,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"av1_amf",
"h264_qsv",
"hevc_qsv",
+ "mjpeg_qsv",
"av1_qsv",
"h264_nvenc",
"hevc_nvenc",
@@ -88,18 +89,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
"h264_vaapi",
"hevc_vaapi",
"av1_vaapi",
+ "mjpeg_vaapi",
"h264_v4l2m2m",
"h264_videotoolbox",
"hevc_videotoolbox",
+ "mjpeg_videotoolbox",
"h264_rkmpp",
- "hevc_rkmpp"
- };
+ "hevc_rkmpp",
+ "mjpeg_rkmpp"
+ ];
- private static readonly string[] _requiredFilters = new[]
- {
+ private static readonly string[] _requiredFilters =
+ [
// sw
"alphasrc",
"zscale",
+ "tonemapx",
// qsv
"scale_qsv",
"vpp_qsv",
@@ -108,43 +113,65 @@ namespace MediaBrowser.MediaEncoding.Encoder
// cuda
"scale_cuda",
"yadif_cuda",
+ "bwdif_cuda",
"tonemap_cuda",
"overlay_cuda",
+ "transpose_cuda",
"hwupload_cuda",
// opencl
"scale_opencl",
"tonemap_opencl",
"overlay_opencl",
+ "transpose_opencl",
+ "yadif_opencl",
+ "bwdif_opencl",
// vaapi
"scale_vaapi",
"deinterlace_vaapi",
"tonemap_vaapi",
"procamp_vaapi",
"overlay_vaapi",
+ "transpose_vaapi",
"hwupload_vaapi",
// vulkan
"libplacebo",
"scale_vulkan",
"overlay_vulkan",
+ "transpose_vulkan",
+ "flip_vulkan",
// videotoolbox
"yadif_videotoolbox",
+ "bwdif_videotoolbox",
"scale_vt",
+ "transpose_vt",
"overlay_videotoolbox",
"tonemap_videotoolbox",
// rkrga
"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", "Output format (default \"same\")" } },
- { 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" } }
+ { 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
@@ -165,6 +192,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly string _encoderPath;
+ private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
+
public EncoderValidator(ILogger logger, string encoderPath)
{
_logger = logger;
@@ -269,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()
{
@@ -423,6 +456,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ [SupportedOSPlatform("macos")]
+ public bool CheckIsVideoToolboxAv1DecodeAvailable()
+ {
+ return ApplePlatformHelper.HasAv1HardwareAccel(_logger);
+ }
+
private IEnumerable<string> GetHwaccelTypes()
{
string? output = null;
@@ -437,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;
@@ -474,7 +513,35 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
- public bool CheckSupportedRuntimeKey(string keyDesc)
+ 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))
{
@@ -484,7 +551,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output;
try
{
- output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?");
+ // With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
+ var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
+ output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -", true, "?");
}
catch (Exception ex)
{
@@ -495,6 +564,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
return output.Contains(keyDesc, StringComparison.Ordinal);
}
+ public bool CheckSupportedHwaccelFlag(string flag)
+ {
+ 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";
@@ -506,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;
@@ -536,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()
@@ -554,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);
@@ -600,10 +665,35 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ private bool GetProcessExitCode(string path, string arguments)
+ {
+ using var process = new Process();
+ process.StartInfo = new ProcessStartInfo(path, arguments)
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ };
+ _logger.LogDebug("Running {Path} {Arguments}", path, arguments);
+
+ try
+ {
+ process.Start();
+ process.WaitForExit();
+ return process.ExitCode == 0;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("Running {Path} {Arguments} failed with exception {Exception}", path, arguments, ex.Message);
+ return false;
+ }
+ }
+
[GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
private static partial Regex CodecRegex();
- [GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
+ [GeneratedRegex("^\\s\\S{2,3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
private static partial Regex FilterRegex();
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index c5f500e76f..2daeac7343 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -42,7 +42,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// If there's more than one we'll need to use the concat command
if (inputFiles.Count > 1)
{
- var files = string.Join("|", inputFiles.Select(NormalizePath));
+ var files = string.Join('|', inputFiles.Select(NormalizePath));
return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 3df4442056..770965cab3 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -12,6 +12,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
@@ -30,10 +31,8 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
-using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
-using static Nikse.SubtitleEdit.Core.Common.IfoParser;
namespace MediaBrowser.MediaEncoding.Encoder
{
@@ -63,7 +62,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
- private readonly object _runningProcessesLock = new object();
+ private readonly Lock _runningProcessesLock = new();
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
// MediaEncoder is registered as a Singleton
@@ -73,15 +72,26 @@ 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;
private bool _isVaapiDeviceInteli965 = false;
+ private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
+ private bool _isVideoToolboxAv1DecodeAvailable = false;
+
+ private static string[] _vulkanImageDrmFmtModifierExts =
+ {
+ "VK_EXT_image_drm_format_modifier",
+ };
+
private static string[] _vulkanExternalMemoryDmaBufExts =
{
"VK_KHR_external_memory_fd",
@@ -116,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);
}
@@ -142,34 +158,52 @@ namespace MediaBrowser.MediaEncoding.Encoder
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
/// <inheritdoc />
+ public bool IsVaapiDeviceSupportVulkanDrmModifier => _isVaapiDeviceSupportVulkanDrmModifier;
+
+ /// <inheritdoc />
public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
+ public bool IsVideoToolboxAv1DecodeAvailable => _isVideoToolboxAv1DecodeAvailable;
+
[GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
private static partial Regex FfprobePathRegex();
/// <summary>
- /// Run at startup or if the user removes a Custom path from transcode page.
+ /// Run at startup to validate ffmpeg.
/// Sets global variables FFmpegPath.
- /// Precedence is: Config > CLI > $PATH.
+ /// Precedence is: CLI/Env var > Config > $PATH.
/// </summary>
- public void SetFFmpegPath()
+ /// <returns>bool indicates whether a valid ffmpeg is found.</returns>
+ public bool SetFFmpegPath()
{
+ var skipValidation = _config.GetFFmpegSkipValidation();
+ if (skipValidation)
+ {
+ _logger.LogWarning("FFmpeg: Skipping FFmpeg Validation due to FFmpeg:novalidation set to true");
+ return true;
+ }
+
// 1) Check if the --ffmpeg CLI switch has been given
var ffmpegPath = _startupOptionFFmpegPath;
+ string ffmpegPathSetMethodText = "command line or environment variable";
if (string.IsNullOrEmpty(ffmpegPath))
{
// 2) Custom path stored in config/encoding xml file under tag <EncoderAppPath> should be used as a fallback
ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
+ ffmpegPathSetMethodText = "encoding.xml config file";
if (string.IsNullOrEmpty(ffmpegPath))
{
// 3) Check "ffmpeg"
ffmpegPath = "ffmpeg";
+ ffmpegPathSetMethodText = "system $PATH";
}
}
if (!ValidatePath(ffmpegPath))
{
_ffmpegPath = null;
+ _logger.LogError("FFmpeg: Path set by {FfmpegPathSetMethodText} is invalid", ffmpegPathSetMethodText);
+ return false;
}
// Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
@@ -190,22 +224,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
SetAvailableEncoders(validator.GetEncoders());
SetAvailableFilters(validator.GetFilters());
SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
+ SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption());
SetAvailableHwaccels(validator.GetHwaccels());
SetMediaEncoderVersion(validator);
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
- _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
+ _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()
&& SupportsHwaccel("vaapi")
&& !string.IsNullOrEmpty(options.VaapiDevice)
- && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ && options.HardwareAccelerationType == HardwareAccelerationType.vaapi)
{
_isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice);
_isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice);
_isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice);
+ _isVaapiDeviceSupportVulkanDrmModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanImageDrmFmtModifierExts);
_isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanExternalMemoryDmaBufExts);
if (_isVaapiDeviceAmd)
@@ -221,73 +259,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice);
}
+ if (_isVaapiDeviceSupportVulkanDrmModifier)
+ {
+ _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM modifier", options.VaapiDevice);
+ }
+
if (_isVaapiDeviceSupportVulkanDrmInterop)
{
_logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.VaapiDevice);
}
}
- }
-
- _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
- }
-
- /// <summary>
- /// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use.
- /// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="pathType">The path type.</param>
- public void UpdateEncoderPath(string path, string pathType)
- {
- var config = _configurationManager.GetEncodingOptions();
-
- // Filesystem may not be case insensitive, but EncoderAppPathDisplay should always point to a valid file?
- if (string.IsNullOrEmpty(config.EncoderAppPath)
- && string.Equals(config.EncoderAppPathDisplay, path, StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Existing ffmpeg path is empty and the new path is the same as {EncoderAppPathDisplay}. Skipping", nameof(config.EncoderAppPathDisplay));
- return;
- }
-
- string newPath;
-
- _logger.LogInformation("Attempting to update encoder path to {Path}. pathType: {PathType}", path ?? string.Empty, pathType ?? string.Empty);
-
- if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase))
- {
- throw new ArgumentException("Unexpected pathType value");
- }
-
- if (string.IsNullOrWhiteSpace(path))
- {
- // User had cleared the custom path in UI
- newPath = string.Empty;
- }
- else
- {
- if (Directory.Exists(path))
- {
- // Given path is directory, so resolve down to filename
- newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
- }
- else
- {
- newPath = path;
- }
- if (!new EncoderValidator(_logger, newPath).ValidateVersion())
+ // Check if VideoToolbox supports AV1 decode
+ if (OperatingSystem.IsMacOS() && SupportsHwaccel("videotoolbox"))
{
- throw new ResourceNotFoundException();
+ _isVideoToolboxAv1DecodeAvailable = validator.CheckIsVideoToolboxAv1DecodeAvailable();
}
}
- // Write the new ffmpeg path to the xml as <EncoderAppPath>
- // This ensures its not lost on next startup
- config.EncoderAppPath = newPath;
- _configurationManager.SaveConfiguration("encoding", config);
-
- // Trigger SetFFmpegPath so we validate the new path and setup probe path
- SetFFmpegPath();
+ _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
+ return !string.IsNullOrWhiteSpace(ffmpegPath);
}
/// <summary>
@@ -306,7 +297,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
bool rc = new EncoderValidator(_logger, path).ValidateVersion();
if (!rc)
{
- _logger.LogWarning("FFmpeg: Failed version check: {Path}", path);
+ _logger.LogError("FFmpeg: Failed version check: {Path}", path);
return false;
}
@@ -350,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();
@@ -387,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)
@@ -458,9 +454,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
extraArgs += " -probesize " + ffmpegProbeSize;
}
- if (request.MediaSource.RequiredHttpHeaders.TryGetValue("user_agent", out var userAgent))
+ if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
{
- extraArgs += " -user_agent " + userAgent;
+ extraArgs += $" -user_agent \"{userAgent}\"";
+ }
+
+ if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
+ {
+ extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
}
return extraArgs;
@@ -509,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 (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame)
+ {
+ args += " -show_frames -only_first_vframe";
+ }
+
args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
var process = new Process
@@ -530,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))
@@ -616,13 +623,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
ImageFormat? targetFormat,
CancellationToken cancellationToken)
{
- var inputArgument = GetInputArgument(inputFile, mediaSource);
+ var inputArgument = GetInputPathArgument(inputFile, mediaSource);
if (!isAudio)
{
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, false, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -630,11 +637,11 @@ 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);
}
}
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, isAudio, cancellationToken).ConfigureAwait(false);
}
private string GetImageResolutionParameter()
@@ -660,10 +667,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
return imageResolutionParameter;
}
- private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken)
+ private async Task<string> ExtractImageInternal(
+ string inputPath,
+ string container,
+ MediaStream videoStream,
+ int? imageStreamIndex,
+ Video3DFormat? threedFormat,
+ TimeSpan? offset,
+ bool useIFrame,
+ ImageFormat? targetFormat,
+ bool isAudio,
+ CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(inputPath);
+ var useTradeoff = _config.GetFFmpegImgExtractPerfTradeoff();
+
var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
@@ -696,36 +715,58 @@ 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.
- var enableThumbnail = useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase);
+ // 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 HDR10/HLG video stream only when the zscale filter is available.
+ // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
+ // Only enable Dolby Vision tonemap when tonemapx is available
var enableHdrExtraction = false;
- if ((string.Equals(videoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoStream?.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
- && SupportsFilter("zscale"))
+ if (videoStream?.VideoRange == VideoRange.HDR)
{
- 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");
+ if (SupportsFilter("tonemapx"))
+ {
+ 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: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: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, 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;
}
+ // 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;
+ }
+
if (!string.IsNullOrWhiteSpace(container))
{
var inputFormat = EncodingHelper.GetInputFormat(container);
@@ -753,8 +794,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
using (var processWrapper = new ProcessWrapper(process, this))
{
- bool ranToCompletion;
-
using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
StartProcess(processWrapper);
@@ -768,22 +807,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
- ranToCompletion = true;
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException ex)
{
process.Kill(true);
- ranToCompletion = false;
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction timed out for {0} after {1}ms", inputPath, timeoutMs), ex);
}
}
- var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
var file = _fileSystem.GetFileInfo(tempExtractPath);
- if (exitCode == -1 || !file.Exists || file.Length == 0)
+ if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
{
- _logger.LogError("ffmpeg image extraction failed for {Path}", inputPath);
-
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath));
}
@@ -792,7 +827,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
/// <inheritdoc />
- public Task<string> ExtractVideoImagesOnIntervalAccelerated(
+ public async Task<string> ExtractVideoImagesOnIntervalAccelerated(
string inputFile,
string container,
MediaSourceInfo mediaSource,
@@ -800,24 +835,59 @@ namespace MediaBrowser.MediaEncoding.Encoder
int maxWidth,
TimeSpan interval,
bool allowHwAccel,
+ bool enableHwEncoding,
int? threads,
int? qualityScale,
ProcessPriorityClass? priority,
+ bool enableKeyFrameOnlyExtraction,
EncodingHelper encodingHelper,
CancellationToken cancellationToken)
{
var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
threads ??= _threads;
+ if (allowHwAccel && enableKeyFrameOnlyExtraction)
+ {
+ var hardwareAccelerationType = options.HardwareAccelerationType;
+ var supportsKeyFrameOnly = (hardwareAccelerationType == HardwareAccelerationType.nvenc && options.EnableEnhancedNvdecDecoder)
+ || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSystem.IsWindows())
+ || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.PreferSystemNativeHwDecoder)
+ || hardwareAccelerationType == HardwareAccelerationType.vaapi
+ || hardwareAccelerationType == HardwareAccelerationType.videotoolbox
+ || hardwareAccelerationType == HardwareAccelerationType.rkmpp;
+ if (!supportsKeyFrameOnly)
+ {
+ // Disable hardware acceleration when the hardware decoder does not support keyframe only mode.
+ allowHwAccel = false;
+ options = new EncodingOptions();
+ }
+ }
+
// A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
// Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
if (!allowHwAccel)
{
options.EnableHardwareEncoding = false;
- options.HardwareAccelerationType = string.Empty;
+ options.HardwareAccelerationType = HardwareAccelerationType.none;
options.EnableTonemapping = false;
}
+ if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.AspectRatio))
+ {
+ // For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimensions
+ var darParts = imageStream.AspectRatio.Split(':');
+ var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], CultureInfo.InvariantCulture));
+ // When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
+ // Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, but we really can do little about it.
+ var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05;
+ if (shouldResetHeight)
+ {
+ // SAR = DAR * Height / Width
+ // RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
+ imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
+ }
+ }
+
var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
{
@@ -828,7 +898,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
MediaPath = inputFile,
OutputVideoCodec = "mjpeg"
};
- var vidEncoder = options.AllowMjpegEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec;
+ var vidEncoder = enableHwEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec;
// Get input and filter arguments
var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
@@ -842,13 +912,40 @@ namespace MediaBrowser.MediaEncoding.Encoder
inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled
}
- var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim();
+ if (options.HardwareAccelerationType == HardwareAccelerationType.videotoolbox && _isLowPriorityHwDecodeSupported)
+ {
+ // VideoToolbox supports low priority decoding, which is useful for trickplay
+ inputArg = "-hwaccel_flags +low_priority " + inputArg;
+ }
+
+ var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
if (string.IsNullOrWhiteSpace(filterParam))
{
throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
}
- return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken);
+ try
+ {
+ return await ExtractVideoImagesOnIntervalInternal(
+ (enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg,
+ filterParam,
+ vidEncoder,
+ threads,
+ qualityScale,
+ priority,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (FfmpegException ex)
+ {
+ if (!enableKeyFrameOnlyExtraction)
+ {
+ throw;
+ }
+
+ _logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFile}", inputFile);
+ }
+
+ return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken).ConfigureAwait(false);
}
private async Task<string> ExtractVideoImagesOnIntervalInternal(
@@ -865,6 +962,32 @@ namespace MediaBrowser.MediaEncoding.Encoder
throw new InvalidOperationException("Empty or invalid input argument.");
}
+ // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
+ // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
+ var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
+ var encoderQualityOption = "-qscale:v ";
+
+ if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
+ || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
+ encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
+ encoderQualityOption = "-global_quality:v ";
+ }
+
+ if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qscale
+ encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
+ }
+
+ if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
+ {
+ // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qscale
+ encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
+ encoderQualityOption = "-qp_init:v ";
+ }
+
// Output arguments
var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(targetDirectory);
@@ -873,12 +996,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Final command arguments
var args = string.Format(
CultureInfo.InvariantCulture,
- "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}-f {5} \"{6}\"",
+ "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"",
inputArg,
filterParam,
outputThreads.GetValueOrDefault(_threads),
vidEncoder,
- qualityScale.HasValue ? "-qscale:v " + qualityScale.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
+ encoderQualityOption + encoderQuality + " ",
+ vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : string.Empty, // allow_sw fallback for some intel macs
+ EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim() + " ", // passthrough timestamp
"image2",
outputPath);
@@ -930,7 +1055,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
- while (isResponsive)
+ while (isResponsive && !cancellationToken.IsCancellationRequested)
{
try
{
@@ -944,8 +1069,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
// We don't actually expect the process to be finished in one timeout span, just that one image has been generated.
}
- cancellationToken.ThrowIfCancellationRequested();
-
var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
isResponsive = jpegCount > lastCount;
@@ -954,16 +1077,28 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (!ranToCompletion)
{
- _logger.LogInformation("Stopping trickplay extraction due to process inactivity.");
+ if (!isResponsive)
+ {
+ _logger.LogInformation("Trickplay process unresponsive.");
+ }
+
+ _logger.LogInformation("Stopping trickplay extraction.");
StopProcess(processWrapper, 1000);
}
}
- var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
-
- if (exitCode == -1)
+ if (!ranToCompletion || processWrapper.ExitCode != 0)
{
- _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
+ // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed ffmpeg process is not possible for caller.
+ // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed.
+ try
+ {
+ Directory.Delete(targetDirectory, true);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory);
+ }
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
}
@@ -988,6 +1123,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
process.Process.Start();
+ try
+ {
+ process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName);
+ }
+
lock (_runningProcessesLock)
{
_runningProcesses.Add(process);
@@ -1020,14 +1164,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)
{
@@ -1041,7 +1185,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
// https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
// We need to double escape
- return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", @"'\\\''", StringComparison.Ordinal);
+ return path
+ .Replace('\\', '/')
+ .Replace(":", "\\:", StringComparison.Ordinal)
+ .Replace("'", @"'\\\''", StringComparison.Ordinal)
+ .Replace("\"", "\\\"", StringComparison.Ordinal);
}
/// <inheritdoc />
@@ -1117,19 +1265,21 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
- {
- // Get all playable .m2ts files
- var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
+ => _blurayExaminer.GetDiscInfo(path).Files;
- // Get all files from the BDMV/STREAMING directory
- var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM"));
+ /// <inheritdoc />
+ public string GetInputPathArgument(EncodingJobInfo state)
+ => GetInputPathArgument(state.MediaPath, state.MediaSource);
- // Only return playable local .m2ts files
- return directoryFiles
- .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
- .Select(f => f.FullName)
- .Order()
- .ToList();
+ /// <inheritdoc />
+ public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
+ {
+ return mediaSource.VideoType switch
+ {
+ VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource),
+ VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource),
+ _ => GetInputArgument(path, mediaSource)
+ };
}
/// <inheritdoc />
@@ -1152,7 +1302,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
// Generate concat configuration entries for each file and write to file
- using StreamWriter sw = new StreamWriter(concatFilePath);
+ Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
+ using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
foreach (var path in files)
{
var mediaInfoResult = GetMediaInfo(
@@ -1171,7 +1322,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
// Add file path stanza to concat configuration
- sw.WriteLine("file '{0}'", path);
+ sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
// Add duration stanza to concat configuration
sw.WriteLine("duration {0}", duration);
@@ -1180,8 +1331,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
public bool CanExtractSubtitles(string codec)
{
- // TODO is there ever a case when a subtitle can't be extracted??
- return true;
+ return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction;
}
private sealed class ProcessWrapper : IDisposable