diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs')
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 307 |
1 files changed, 252 insertions, 55 deletions
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f782e65bd..db119ce5c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -10,28 +10,34 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder { - public class EncoderValidator + public partial class EncoderValidator { - private const string DefaultEncoderPath = "ffmpeg"; - private static readonly string[] _requiredDecoders = new[] { "h264", "hevc", + "vp8", + "libvpx", + "vp9", + "libvpx-vp9", + "av1", + "libdav1d", "mpeg2video", "mpeg4", "msmpeg4", - "dts", + "dca", "ac3", "aac", "mp3", "flac", + "truehd", "h264_qsv", "hevc_qsv", "mpeg2_qsv", "vc1_qsv", "vp8_qsv", "vp9_qsv", + "av1_qsv", "h264_cuvid", "hevc_cuvid", "mpeg2_cuvid", @@ -39,56 +45,92 @@ namespace MediaBrowser.MediaEncoding.Encoder "mpeg4_cuvid", "vp8_cuvid", "vp9_cuvid", - "h264_mmal", - "mpeg2_mmal", - "mpeg4_mmal", - "vc1_mmal", - "h264_mediacodec", - "hevc_mediacodec", - "mpeg2_mediacodec", - "mpeg4_mediacodec", - "vp8_mediacodec", - "vp9_mediacodec", - "h264_opencl", - "hevc_opencl", - "mpeg2_opencl", - "mpeg4_opencl", - "vp8_opencl", - "vp9_opencl", - "vc1_opencl" + "av1_cuvid" }; private static readonly string[] _requiredEncoders = new[] { "libx264", "libx265", + "libsvtav1", "mpeg4", "msmpeg4", "libvpx", "libvpx-vp9", "aac", + "aac_at", "libfdk_aac", "ac3", + "dca", "libmp3lame", "libopus", "libvorbis", "flac", + "truehd", "srt", "h264_amf", "hevc_amf", + "av1_amf", "h264_qsv", "hevc_qsv", + "av1_qsv", "h264_nvenc", "hevc_nvenc", + "av1_nvenc", "h264_vaapi", "hevc_vaapi", - "h264_omx", - "hevc_omx", + "av1_vaapi", "h264_v4l2m2m", "h264_videotoolbox", "hevc_videotoolbox" }; + private static readonly string[] _requiredFilters = new[] + { + // sw + "alphasrc", + "zscale", + // qsv + "scale_qsv", + "vpp_qsv", + "deinterlace_qsv", + "overlay_qsv", + // cuda + "scale_cuda", + "yadif_cuda", + "tonemap_cuda", + "overlay_cuda", + "hwupload_cuda", + // opencl + "scale_opencl", + "tonemap_opencl", + "overlay_opencl", + // vaapi + "scale_vaapi", + "deinterlace_vaapi", + "tonemap_vaapi", + "procamp_vaapi", + "overlay_vaapi", + "hwupload_vaapi", + // vulkan + "libplacebo", + "scale_vulkan", + "overlay_vulkan", + "hwupload_vaapi", + // videotoolbox + "yadif_videotoolbox" + }; + + private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, 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" } } + }; + // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below private static readonly IReadOnlyDictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version> { @@ -106,7 +148,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly string _encoderPath; - public EncoderValidator(ILogger logger, string encoderPath = DefaultEncoderPath) + public EncoderValidator(ILogger logger, string encoderPath) { _logger = logger; _encoderPath = encoderPath; @@ -123,12 +165,18 @@ namespace MediaBrowser.MediaEncoding.Encoder public static Version? MaxVersion { get; } = null; + [GeneratedRegex(@"^ffmpeg version n?((?:[0-9]+\.?)+)")] + private static partial Regex FfmpegVersionRegex(); + + [GeneratedRegex(@"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))", RegexOptions.Multiline)] + private static partial Regex LibraryRegex(); + public bool ValidateVersion() { string output; try { - output = GetProcessOutput(_encoderPath, "-version"); + output = GetProcessOutput(_encoderPath, "-version", false, null); } catch (Exception ex) { @@ -156,13 +204,13 @@ namespace MediaBrowser.MediaEncoding.Encoder } // Work out what the version under test is - var version = GetFFmpegVersion(versionOutput); + var version = GetFFmpegVersionInternal(versionOutput); - _logger.LogInformation("Found ffmpeg version {Version}", version != null ? version.ToString() : "unknown"); + _logger.LogInformation("Found ffmpeg version {Version}", version is not null ? version.ToString() : "unknown"); - if (version == null) + if (version is null) { - if (MaxVersion != null) // Version is unknown + if (MaxVersion is not null) // Version is unknown { if (MinVersion == MaxVersion) { @@ -180,12 +228,14 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } - else if (version < MinVersion) // Version is below what we recommend + + if (version < MinVersion) // Version is below what we recommend { _logger.LogWarning("FFmpeg validation: The minimum recommended version is {MinVersion}", MinVersion); return false; } - else if (MaxVersion != null && version > MaxVersion) // Version is above what we recommend + + if (MaxVersion is not null && version > MaxVersion) // Version is above what we recommend { _logger.LogWarning("FFmpeg validation: The maximum recommended version is {MaxVersion}", MaxVersion); return false; @@ -200,6 +250,34 @@ namespace MediaBrowser.MediaEncoding.Encoder public IEnumerable<string> GetHwaccels() => GetHwaccelTypes(); + public IEnumerable<string> GetFilters() => GetFFmpegFilters(); + + public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption(); + + public Version? GetFFmpegVersion() + { + string output; + try + { + output = GetProcessOutput(_encoderPath, "-version", false, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error validating encoder"); + return null; + } + + if (string.IsNullOrWhiteSpace(output)) + { + _logger.LogError("FFmpeg validation: The process returned no result"); + return null; + } + + _logger.LogDebug("ffmpeg output: {Output}", output); + + return GetFFmpegVersionInternal(output); + } + /// <summary> /// Using the output from "ffmpeg -version" work out the FFmpeg version. /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy @@ -208,14 +286,14 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="output">The output from "ffmpeg -version".</param> /// <returns>The FFmpeg version.</returns> - internal Version? GetFFmpegVersion(string output) + internal Version? GetFFmpegVersionInternal(string output) { // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output - var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)"); + var match = FfmpegVersionRegex().Match(output); if (match.Success) { - if (Version.TryParse(match.Groups[1].Value, out var result)) + if (Version.TryParse(match.Groups[1].ValueSpan, out var result)) { return result; } @@ -259,14 +337,11 @@ namespace MediaBrowser.MediaEncoding.Encoder { var map = new Dictionary<string, Version>(); - foreach (Match match in Regex.Matches( - output, - @"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))", - RegexOptions.Multiline)) + foreach (Match match in LibraryRegex().Matches(output)) { var version = new Version( - int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture), - int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture)); + int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture), + int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture)); map.Add(match.Groups["name"].Value, version); } @@ -274,12 +349,69 @@ namespace MediaBrowser.MediaEncoding.Encoder return map; } + public bool CheckVaapiDeviceByDriverName(string driverName, string renderNodePath) + { + if (!OperatingSystem.IsLinux()) + { + return false; + } + + if (string.IsNullOrEmpty(driverName) || string.IsNullOrEmpty(renderNodePath)) + { + return false; + } + + try + { + var output = GetProcessOutput(_encoderPath, "-v verbose -hide_banner -init_hw_device vaapi=va:" + renderNodePath, true, null); + return output.Contains(driverName, StringComparison.Ordinal); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting the given vaapi render node path"); + return false; + } + } + + public bool CheckVulkanDrmDeviceByExtensionName(string renderNodePath, string[] vulkanExtensions) + { + if (!OperatingSystem.IsLinux()) + { + return false; + } + + if (string.IsNullOrEmpty(renderNodePath)) + { + return false; + } + + try + { + var command = "-v verbose -hide_banner -init_hw_device drm=dr:" + renderNodePath + " -init_hw_device vulkan=vk@dr"; + var output = GetProcessOutput(_encoderPath, command, true, null); + foreach (string ext in vulkanExtensions) + { + if (!output.Contains(ext, StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting the given drm render node path"); + return false; + } + } + private IEnumerable<string> GetHwaccelTypes() { string? output = null; try { - output = GetProcessOutput(_encoderPath, "-hwaccels"); + output = GetProcessOutput(_encoderPath, "-hwaccels", false, null); } catch (Exception ex) { @@ -297,9 +429,9 @@ namespace MediaBrowser.MediaEncoding.Encoder return found; } - public bool CheckFilter(string filter, string option) + public bool CheckFilterWithOption(string filter, string option) { - if (string.IsNullOrEmpty(filter)) + if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option)) { return false; } @@ -307,7 +439,7 @@ namespace MediaBrowser.MediaEncoding.Encoder string output; try { - output = GetProcessOutput(_encoderPath, "-h filter=" + filter); + output = GetProcessOutput(_encoderPath, "-h filter=" + filter, false, null); } catch (Exception ex) { @@ -317,11 +449,6 @@ namespace MediaBrowser.MediaEncoding.Encoder if (output.Contains("Filter " + filter, StringComparison.Ordinal)) { - if (string.IsNullOrEmpty(option)) - { - return true; - } - return output.Contains(option, StringComparison.Ordinal); } @@ -330,13 +457,34 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } + public bool CheckSupportedRuntimeKey(string keyDesc) + { + if (string.IsNullOrEmpty(keyDesc)) + { + return false; + } + + string output; + try + { + output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error checking supported runtime key"); + return false; + } + + return output.Contains(keyDesc, StringComparison.Ordinal); + } + private IEnumerable<string> GetCodecs(Codec codec) { string codecstr = codec == Codec.Encoder ? "encoders" : "decoders"; string output; try { - output = GetProcessOutput(_encoderPath, "-" + codecstr); + output = GetProcessOutput(_encoderPath, "-" + codecstr, false, null); } catch (Exception ex) { @@ -353,7 +501,6 @@ namespace MediaBrowser.MediaEncoding.Encoder var found = Regex .Matches(output, @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$", RegexOptions.Multiline) - .Cast<Match>() .Select(x => x.Groups["codec"].Value) .Where(x => required.Contains(x)); @@ -362,9 +509,52 @@ namespace MediaBrowser.MediaEncoding.Encoder return found; } - private string GetProcessOutput(string path, string arguments) + private IEnumerable<string> GetFFmpegFilters() { - using (var process = new Process() + string output; + try + { + output = GetProcessOutput(_encoderPath, "-filters", false, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting available filters"); + return Enumerable.Empty<string>(); + } + + if (string.IsNullOrWhiteSpace(output)) + { + return Enumerable.Empty<string>(); + } + + var found = Regex + .Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline) + .Select(x => x.Groups["filter"].Value) + .Where(x => _requiredFilters.Contains(x)); + + _logger.LogInformation("Available filters: {Filters}", found); + + return found; + } + + private IDictionary<int, bool> GetFFmpegFiltersWithOption() + { + IDictionary<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); + using (var process = new Process { StartInfo = new ProcessStartInfo(path, arguments) { @@ -372,8 +562,8 @@ namespace MediaBrowser.MediaEncoding.Encoder UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false, + RedirectStandardInput = redirectStandardIn, RedirectStandardOutput = true, - // ffmpeg uses stderr to log info, don't show this RedirectStandardError = true } }) @@ -382,7 +572,14 @@ namespace MediaBrowser.MediaEncoding.Encoder process.Start(); - return process.StandardOutput.ReadToEnd(); + if (redirectStandardIn) + { + using var writer = process.StandardInput; + writer.Write(testKey); + } + + using var reader = readStdErr ? process.StandardError : process.StandardOutput; + return reader.ReadToEnd(); } } } |
