diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs')
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 480 |
1 files changed, 377 insertions, 103 deletions
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 262772959..60a2d39e5 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -1,135 +1,386 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; -using MediaBrowser.Model.Diagnostics; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder { public class EncoderValidator { + private static readonly string[] _requiredDecoders = new[] + { + "h264", + "hevc", + "mpeg2video", + "mpeg4", + "msmpeg4", + "dts", + "ac3", + "aac", + "mp3", + "flac", + "h264_qsv", + "hevc_qsv", + "mpeg2_qsv", + "vc1_qsv", + "vp8_qsv", + "vp9_qsv", + "h264_cuvid", + "hevc_cuvid", + "mpeg2_cuvid", + "vc1_cuvid", + "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" + }; + + private static readonly string[] _requiredEncoders = new[] + { + "libx264", + "libx265", + "mpeg4", + "msmpeg4", + "libvpx", + "libvpx-vp9", + "aac", + "libfdk_aac", + "ac3", + "libmp3lame", + "libopus", + "libvorbis", + "flac", + "srt", + "h264_amf", + "hevc_amf", + "h264_qsv", + "hevc_qsv", + "h264_nvenc", + "hevc_nvenc", + "h264_vaapi", + "hevc_vaapi", + "h264_omx", + "hevc_omx", + "h264_v4l2m2m", + "h264_videotoolbox", + "hevc_videotoolbox" + }; + + private static readonly string[] _requiredFilters = new[] + { + "scale_cuda", + "yadif_cuda", + "hwupload_cuda", + "overlay_cuda", + "tonemap_cuda", + "tonemap_opencl", + "tonemap_vaapi", + }; + + 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" } } + }; + + // 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> + { + { "libavutil", new Version(56, 14) }, + { "libavcodec", new Version(58, 18) }, + { "libavformat", new Version(58, 12) }, + { "libavdevice", new Version(58, 3) }, + { "libavfilter", new Version(7, 16) }, + { "libswscale", new Version(5, 1) }, + { "libswresample", new Version(3, 1) }, + { "libpostproc", new Version(55, 1) } + }; + private readonly ILogger _logger; - private readonly IProcessFactory _processFactory; - public EncoderValidator(ILogger logger, IProcessFactory processFactory) + private readonly string _encoderPath; + + public EncoderValidator(ILogger logger, string encoderPath) { _logger = logger; - _processFactory = processFactory; + _encoderPath = encoderPath; } - public (IEnumerable<string> decoders, IEnumerable<string> encoders) Validate(string encoderPath) + private enum Codec { - _logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath); - - var decoders = GetCodecs(encoderPath, Codec.Decoder); - var encoders = GetCodecs(encoderPath, Codec.Encoder); + Encoder, + Decoder + } - _logger.LogInformation("Encoder validation complete"); + // When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions + public static Version MinVersion { get; } = new Version(4, 0); - return (decoders, encoders); - } + public static Version? MaxVersion { get; } = null; - public bool ValidateVersion(string encoderAppPath, bool logOutput) + public bool ValidateVersion() { - string output = null; + string output; try { - output = GetProcessOutput(encoderAppPath, "-version"); + output = GetProcessOutput(_encoderPath, "-version"); } catch (Exception ex) { - if (logOutput) - { - _logger.LogError(ex, "Error validating encoder"); - } + _logger.LogError(ex, "Error validating encoder"); + return false; } if (string.IsNullOrWhiteSpace(output)) { + _logger.LogError("FFmpeg validation: The process returned no result"); return false; } _logger.LogDebug("ffmpeg output: {Output}", output); - if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) + return ValidateVersionInternal(output); + } + + internal bool ValidateVersionInternal(string versionOutput) + { + if (versionOutput.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) { + _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported"); return false; } - output = " " + output + " "; + // Work out what the version under test is + var version = GetFFmpegVersionInternal(versionOutput); - for (var i = 2013; i <= 2015; i++) + _logger.LogInformation("Found ffmpeg version {Version}", version != null ? version.ToString() : "unknown"); + + if (version == null) { - var yearString = i.ToString(CultureInfo.InvariantCulture); - if (output.IndexOf(" " + yearString + " ", StringComparison.OrdinalIgnoreCase) != -1) + if (MaxVersion != null) // Version is unknown + { + if (MinVersion == MaxVersion) + { + _logger.LogWarning("FFmpeg validation: We recommend version {MinVersion}", MinVersion); + } + else + { + _logger.LogWarning("FFmpeg validation: We recommend a minimum of {MinVersion} and maximum of {MaxVersion}", MinVersion, MaxVersion); + } + } + else { - return false; + _logger.LogWarning("FFmpeg validation: We recommend minimum version {MinVersion}", MinVersion); } + + return false; + } + else 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 + { + _logger.LogWarning("FFmpeg validation: The maximum recommended version is {MaxVersion}", MaxVersion); + return false; } return true; } - private static readonly string[] requiredDecoders = new[] - { - "mpeg2video", - "h264_qsv", - "hevc_qsv", - "mpeg2_qsv", - "vc1_qsv", - "h264_cuvid", - "hevc_cuvid", - "dts", - "ac3", - "aac", - "mp3", - "h264", - "hevc" - }; - - private static readonly string[] requiredEncoders = new[] - { - "libx264", - "libx265", - "mpeg4", - "msmpeg4", - "libvpx", - "libvpx-vp9", - "aac", - "libmp3lame", - "libopus", - "libvorbis", - "srt", - "h264_nvenc", - "hevc_nvenc", - "h264_qsv", - "hevc_qsv", - "h264_omx", - "hevc_omx", - "h264_vaapi", - "hevc_vaapi", - "ac3" - }; + public IEnumerable<string> GetDecoders() => GetCodecs(Codec.Decoder); - private enum Codec + public IEnumerable<string> GetEncoders() => GetCodecs(Codec.Encoder); + + public IEnumerable<string> GetHwaccels() => GetHwaccelTypes(); + + public IEnumerable<string> GetFilters() => GetFFmpegFilters(); + + public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption(); + + public Version? GetFFmpegVersion() { - Encoder, - Decoder + string output; + try + { + output = GetProcessOutput(_encoderPath, "-version"); + } + 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 + /// to parse. If this is not available, then we try to match known library versions to FFmpeg versions. + /// If that fails then we test the libraries to determine if they're newer than our minimum versions. + /// </summary> + /// <param name="output">The output from "ffmpeg -version".</param> + /// <returns>The FFmpeg version.</returns> + 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]+\.?)+)"); + + if (match.Success) + { + if (Version.TryParse(match.Groups[1].Value, out var result)) + { + return result; + } + } + + var versionMap = GetFFmpegLibraryVersions(output); + + var allVersionsValidated = true; + + foreach (var minimumVersion in _ffmpegMinimumLibraryVersions) + { + if (versionMap.TryGetValue(minimumVersion.Key, out var foundVersion)) + { + if (foundVersion >= minimumVersion.Value) + { + _logger.LogInformation("Found {Library} version {FoundVersion} ({MinimumVersion})", minimumVersion.Key, foundVersion, minimumVersion.Value); + } + else + { + _logger.LogWarning("Found {Library} version {FoundVersion} lower than recommended version {MinimumVersion}", minimumVersion.Key, foundVersion, minimumVersion.Value); + allVersionsValidated = false; + } + } + else + { + _logger.LogError("{Library} version not found", minimumVersion.Key); + allVersionsValidated = false; + } + } + + return allVersionsValidated ? MinVersion : null; } - private IEnumerable<string> GetCodecs(string encoderAppPath, Codec codec) + /// <summary> + /// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output + /// and condenses them on to one line. Output format is "name1=major.minor,name2=major.minor,etc.". + /// </summary> + /// <param name="output">The 'ffmpeg -version' output.</param> + /// <returns>The library names and major.minor version numbers.</returns> + private static IReadOnlyDictionary<string, Version> GetFFmpegLibraryVersions(string output) + { + 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)) + { + var version = new Version( + int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture), + int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture)); + + map.Add(match.Groups["name"].Value, version); + } + + return map; + } + + private IEnumerable<string> GetHwaccelTypes() + { + string? output = null; + try + { + output = GetProcessOutput(_encoderPath, "-hwaccels"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting available hwaccel types"); + } + + if (string.IsNullOrWhiteSpace(output)) + { + return Enumerable.Empty<string>(); + } + + var found = output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList(); + _logger.LogInformation("Available hwaccel types: {Types}", found); + + return found; + } + + public bool CheckFilterWithOption(string filter, string option) + { + if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option)) + { + return false; + } + + string output; + try + { + output = GetProcessOutput(_encoderPath, "-h filter=" + filter); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting the given filter"); + return false; + } + + if (output.Contains("Filter " + filter, StringComparison.Ordinal)) + { + return output.Contains(option, StringComparison.Ordinal); + } + + _logger.LogWarning("Filter: {Name} with option {Option} is not available", filter, option); + + return false; + } + + private IEnumerable<string> GetCodecs(Codec codec) { string codecstr = codec == Codec.Encoder ? "encoders" : "decoders"; - string output = null; + string output; try { - output = GetProcessOutput(encoderAppPath, "-" + codecstr); + output = GetProcessOutput(_encoderPath, "-" + codecstr); } catch (Exception ex) { _logger.LogError(ex, "Error detecting available {Codec}", codecstr); + return Enumerable.Empty<string>(); } if (string.IsNullOrWhiteSpace(output)) @@ -137,7 +388,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return Enumerable.Empty<string>(); } - var required = codec == Codec.Encoder ? requiredEncoders : requiredDecoders; + var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders; var found = Regex .Matches(output, @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$", RegexOptions.Multiline) @@ -150,47 +401,70 @@ namespace MediaBrowser.MediaEncoding.Encoder return found; } - private string GetProcessOutput(string path, string arguments) + private IEnumerable<string> GetFFmpegFilters() { - IProcess process = _processFactory.Create(new ProcessOptions + string output; + try { - CreateNoWindow = true, - UseShellExecute = false, - FileName = path, - Arguments = arguments, - IsHidden = true, - ErrorDialog = false, - RedirectStandardOutput = true, - // ffmpeg uses stderr to log info, don't show this - RedirectStandardError = true - }); - - _logger.LogDebug("Running {Path} {Arguments}", path, arguments); + output = GetProcessOutput(_encoderPath, "-filters"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting available filters"); + return Enumerable.Empty<string>(); + } - using (process) + if (string.IsNullOrWhiteSpace(output)) { - process.Start(); + return Enumerable.Empty<string>(); + } + + var found = Regex + .Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline) + .Cast<Match>() + .Select(x => x.Groups["filter"].Value) + .Where(x => _requiredFilters.Contains(x)); + + _logger.LogInformation("Available filters: {Filters}", found); - try + 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) { - return process.StandardOutput.ReadToEnd(); + dict.Add(i, CheckFilterWithOption(val[0], val[1])); } - catch - { - _logger.LogWarning("Killing process {Path} {Arguments}", path, arguments); + } - // Hate having to do this - try - { - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing process"); - } + return dict; + } - throw; + private string GetProcessOutput(string path, string arguments) + { + using (var process = new Process() + { + StartInfo = new ProcessStartInfo(path, arguments) + { + CreateNoWindow = true, + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + RedirectStandardOutput = true, + // ffmpeg uses stderr to log info, don't show this + RedirectStandardError = true } + }) + { + _logger.LogDebug("Running {Path} {Arguments}", path, arguments); + + process.Start(); + + return process.StandardOutput.ReadToEnd(); } } } |
