diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding/Encoder')
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs | 87 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 53 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 52 |
3 files changed, 186 insertions, 6 deletions
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..d28cd70ef 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -5,7 +5,9 @@ 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 @@ -159,6 +161,15 @@ namespace MediaBrowser.MediaEncoding.Encoder { 6, new string[] { "transpose_opencl", "rotate by half-turn" } } }; + private static readonly Dictionary<BitStreamFilterOptionType, (string, string)> _bsfOptionsDict = new Dictionary<BitStreamFilterOptionType, (string, string)> + { + { 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 // Refers to the versions in https://ffmpeg.org/download.html private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version> @@ -285,6 +296,9 @@ namespace MediaBrowser.MediaEncoding.Encoder public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption(); + public IDictionary<BitStreamFilterOptionType, bool> GetBitStreamFiltersWithOption() => _bsfOptionsDict + .ToDictionary(item => item.Key, item => CheckBitStreamFilterWithOption(item.Value.Item1, item.Value.Item2)); + public Version? GetFFmpegVersion() { string output; @@ -437,6 +451,12 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + [SupportedOSPlatform("macos")] + public bool CheckIsVideoToolboxAv1DecodeAvailable() + { + return ApplePlatformHelper.HasAv1HardwareAccel(_logger); + } + private IEnumerable<string> GetHwaccelTypes() { string? output = null; @@ -488,6 +508,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 +564,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"; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 14cf869f9..39c0bfed4 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -73,9 +73,11 @@ namespace MediaBrowser.MediaEncoding.Encoder 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<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", @@ -159,6 +163,8 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <inheritdoc /> public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop; + public bool IsVideoToolboxAv1DecodeAvailable => _isVideoToolboxAv1DecodeAvailable; + [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")] private static partial Regex FfprobePathRegex(); @@ -218,6 +224,7 @@ namespace MediaBrowser.MediaEncoding.Encoder SetAvailableEncoders(validator.GetEncoders()); SetAvailableFilters(validator.GetFilters()); SetAvailableFiltersWithOption(validator.GetFiltersWithOption()); + SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption()); SetAvailableHwaccels(validator.GetHwaccels()); SetMediaEncoderVersion(validator); @@ -225,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() @@ -261,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); @@ -332,6 +346,11 @@ namespace MediaBrowser.MediaEncoding.Encoder _filtersWithOption = dict; } + public void SetAvailableBitStreamFiltersWithOption(IDictionary<BitStreamFilterOptionType, bool> dict) + { + _bitStreamFiltersWithOption = dict; + } + public void SetMediaEncoderVersion(EncoderValidator validator) { _ffmpegVersion = validator.GetFFmpegVersion(); @@ -372,6 +391,11 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } + public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option) + { + return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val; + } + public bool CanEncodeToAudioCodec(string codec) { if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase)) @@ -491,6 +515,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 @@ -690,13 +720,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. @@ -720,14 +748,26 @@ namespace MediaBrowser.MediaEncoding.Encoder 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("0", EncoderVersion).Trim(), // passthrough timestamp + 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; } |
