< Summary - Jellyfin

Information
Class: MediaBrowser.MediaEncoding.Encoder.EncoderValidator
Assembly: MediaBrowser.MediaEncoding
File(s): /srv/git/jellyfin/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
Line coverage
53%
Covered lines: 192
Uncovered lines: 167
Coverable lines: 359
Total lines: 655
Line coverage: 53.4%
Branch coverage
28%
Covered branches: 23
Total branches: 80
Branch coverage: 28.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

File(s)

/srv/git/jellyfin/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.Globalization;
 7using System.Linq;
 8using System.Text.RegularExpressions;
 9using Microsoft.Extensions.Logging;
 10
 11namespace MediaBrowser.MediaEncoding.Encoder
 12{
 13    public partial class EncoderValidator
 14    {
 115        private static readonly string[] _requiredDecoders = new[]
 116        {
 117            "h264",
 118            "hevc",
 119            "vp8",
 120            "libvpx",
 121            "vp9",
 122            "libvpx-vp9",
 123            "av1",
 124            "libdav1d",
 125            "mpeg2video",
 126            "mpeg4",
 127            "msmpeg4",
 128            "dca",
 129            "ac3",
 130            "ac4",
 131            "aac",
 132            "mp3",
 133            "flac",
 134            "truehd",
 135            "h264_qsv",
 136            "hevc_qsv",
 137            "mpeg2_qsv",
 138            "vc1_qsv",
 139            "vp8_qsv",
 140            "vp9_qsv",
 141            "av1_qsv",
 142            "h264_cuvid",
 143            "hevc_cuvid",
 144            "mpeg2_cuvid",
 145            "vc1_cuvid",
 146            "mpeg4_cuvid",
 147            "vp8_cuvid",
 148            "vp9_cuvid",
 149            "av1_cuvid",
 150            "h264_rkmpp",
 151            "hevc_rkmpp",
 152            "mpeg1_rkmpp",
 153            "mpeg2_rkmpp",
 154            "mpeg4_rkmpp",
 155            "vp8_rkmpp",
 156            "vp9_rkmpp",
 157            "av1_rkmpp"
 158        };
 59
 160        private static readonly string[] _requiredEncoders = new[]
 161        {
 162            "libx264",
 163            "libx265",
 164            "libsvtav1",
 165            "aac",
 166            "aac_at",
 167            "libfdk_aac",
 168            "ac3",
 169            "alac",
 170            "dca",
 171            "libmp3lame",
 172            "libopus",
 173            "libvorbis",
 174            "flac",
 175            "truehd",
 176            "srt",
 177            "h264_amf",
 178            "hevc_amf",
 179            "av1_amf",
 180            "h264_qsv",
 181            "hevc_qsv",
 182            "mjpeg_qsv",
 183            "av1_qsv",
 184            "h264_nvenc",
 185            "hevc_nvenc",
 186            "av1_nvenc",
 187            "h264_vaapi",
 188            "hevc_vaapi",
 189            "av1_vaapi",
 190            "mjpeg_vaapi",
 191            "h264_v4l2m2m",
 192            "h264_videotoolbox",
 193            "hevc_videotoolbox",
 194            "mjpeg_videotoolbox",
 195            "h264_rkmpp",
 196            "hevc_rkmpp",
 197            "mjpeg_rkmpp"
 198        };
 99
 1100        private static readonly string[] _requiredFilters = new[]
 1101        {
 1102            // sw
 1103            "alphasrc",
 1104            "zscale",
 1105            "tonemapx",
 1106            // qsv
 1107            "scale_qsv",
 1108            "vpp_qsv",
 1109            "deinterlace_qsv",
 1110            "overlay_qsv",
 1111            // cuda
 1112            "scale_cuda",
 1113            "yadif_cuda",
 1114            "bwdif_cuda",
 1115            "tonemap_cuda",
 1116            "overlay_cuda",
 1117            "transpose_cuda",
 1118            "hwupload_cuda",
 1119            // opencl
 1120            "scale_opencl",
 1121            "tonemap_opencl",
 1122            "overlay_opencl",
 1123            "transpose_opencl",
 1124            // vaapi
 1125            "scale_vaapi",
 1126            "deinterlace_vaapi",
 1127            "tonemap_vaapi",
 1128            "procamp_vaapi",
 1129            "overlay_vaapi",
 1130            "transpose_vaapi",
 1131            "hwupload_vaapi",
 1132            // vulkan
 1133            "libplacebo",
 1134            "scale_vulkan",
 1135            "overlay_vulkan",
 1136            "transpose_vulkan",
 1137            "flip_vulkan",
 1138            // videotoolbox
 1139            "yadif_videotoolbox",
 1140            "bwdif_videotoolbox",
 1141            "scale_vt",
 1142            "transpose_vt",
 1143            "overlay_videotoolbox",
 1144            "tonemap_videotoolbox",
 1145            // rkrga
 1146            "scale_rkrga",
 1147            "vpp_rkrga",
 1148            "overlay_rkrga"
 1149        };
 150
 1151        private static readonly Dictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
 1152        {
 1153            { 0, new string[] { "scale_cuda", "Output format (default \"same\")" } },
 1154            { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } },
 1155            { 2, new string[] { "tonemap_opencl", "bt2390" } },
 1156            { 3, new string[] { "overlay_opencl", "Action to take when encountering EOF from secondary input" } },
 1157            { 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } },
 1158            { 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } },
 1159            { 6, new string[] { "transpose_opencl", "rotate by half-turn" } }
 1160        };
 161
 162        // These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version ta
 163        // Refers to the versions in https://ffmpeg.org/download.html
 1164        private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Versi
 1165        {
 1166            { "libavutil", new Version(56, 70) },
 1167            { "libavcodec", new Version(58, 134) },
 1168            { "libavformat", new Version(58, 76) },
 1169            { "libavdevice", new Version(58, 13) },
 1170            { "libavfilter", new Version(7, 110) },
 1171            { "libswscale", new Version(5, 9) },
 1172            { "libswresample", new Version(3, 9) },
 1173            { "libpostproc", new Version(55, 9) }
 1174        };
 175
 176        private readonly ILogger _logger;
 177
 178        private readonly string _encoderPath;
 179
 16180        private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
 181
 182        public EncoderValidator(ILogger logger, string encoderPath)
 183        {
 16184            _logger = logger;
 16185            _encoderPath = encoderPath;
 16186        }
 187
 188        private enum Codec
 189        {
 190            Encoder,
 191            Decoder
 192        }
 193
 194        // When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions
 1195        public static Version MinVersion { get; } = new Version(4, 4);
 196
 1197        public static Version? MaxVersion { get; } = null;
 198
 199        [GeneratedRegex(@"^ffmpeg version n?((?:[0-9]+\.?)+)")]
 200        private static partial Regex FfmpegVersionRegex();
 201
 202        [GeneratedRegex(@"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))", RegexOptions.Multiline)]
 203        private static partial Regex LibraryRegex();
 204
 205        public bool ValidateVersion()
 206        {
 207            string output;
 208            try
 209            {
 0210                output = GetProcessOutput(_encoderPath, "-version", false, null);
 0211            }
 0212            catch (Exception ex)
 213            {
 0214                _logger.LogError(ex, "Error validating encoder");
 0215                return false;
 216            }
 217
 0218            if (string.IsNullOrWhiteSpace(output))
 219            {
 0220                _logger.LogError("FFmpeg validation: The process returned no result");
 0221                return false;
 222            }
 223
 0224            _logger.LogDebug("ffmpeg output: {Output}", output);
 225
 0226            return ValidateVersionInternal(output);
 0227        }
 228
 229        internal bool ValidateVersionInternal(string versionOutput)
 230        {
 8231            if (versionOutput.Contains("Libav developers", StringComparison.OrdinalIgnoreCase))
 232            {
 0233                _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
 0234                return false;
 235            }
 236
 237            // Work out what the version under test is
 8238            var version = GetFFmpegVersionInternal(versionOutput);
 239
 8240            _logger.LogInformation("Found ffmpeg version {Version}", version is not null ? version.ToString() : "unknown
 241
 8242            if (version is null)
 243            {
 1244                if (MaxVersion is not null) // Version is unknown
 245                {
 0246                    if (MinVersion == MaxVersion)
 247                    {
 0248                        _logger.LogWarning("FFmpeg validation: We recommend version {MinVersion}", MinVersion);
 249                    }
 250                    else
 251                    {
 0252                        _logger.LogWarning("FFmpeg validation: We recommend a minimum of {MinVersion} and maximum of {Ma
 253                    }
 254                }
 255                else
 256                {
 1257                    _logger.LogWarning("FFmpeg validation: We recommend minimum version {MinVersion}", MinVersion);
 258                }
 259
 1260                return false;
 261            }
 262
 7263            if (version < MinVersion) // Version is below what we recommend
 264            {
 1265                _logger.LogWarning("FFmpeg validation: The minimum recommended version is {MinVersion}", MinVersion);
 1266                return false;
 267            }
 268
 6269            if (MaxVersion is not null && version > MaxVersion) // Version is above what we recommend
 270            {
 0271                _logger.LogWarning("FFmpeg validation: The maximum recommended version is {MaxVersion}", MaxVersion);
 0272                return false;
 273            }
 274
 6275            return true;
 276        }
 277
 0278        public IEnumerable<string> GetDecoders() => GetCodecs(Codec.Decoder);
 279
 0280        public IEnumerable<string> GetEncoders() => GetCodecs(Codec.Encoder);
 281
 0282        public IEnumerable<string> GetHwaccels() => GetHwaccelTypes();
 283
 0284        public IEnumerable<string> GetFilters() => GetFFmpegFilters();
 285
 0286        public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption();
 287
 288        public Version? GetFFmpegVersion()
 289        {
 290            string output;
 291            try
 292            {
 0293                output = GetProcessOutput(_encoderPath, "-version", false, null);
 0294            }
 0295            catch (Exception ex)
 296            {
 0297                _logger.LogError(ex, "Error validating encoder");
 0298                return null;
 299            }
 300
 0301            if (string.IsNullOrWhiteSpace(output))
 302            {
 0303                _logger.LogError("FFmpeg validation: The process returned no result");
 0304                return null;
 305            }
 306
 0307            _logger.LogDebug("ffmpeg output: {Output}", output);
 308
 0309            return GetFFmpegVersionInternal(output);
 0310        }
 311
 312        /// <summary>
 313        /// Using the output from "ffmpeg -version" work out the FFmpeg version.
 314        /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
 315        /// to parse. If this is not available, then we try to match known library versions to FFmpeg versions.
 316        /// If that fails then we test the libraries to determine if they're newer than our minimum versions.
 317        /// </summary>
 318        /// <param name="output">The output from "ffmpeg -version".</param>
 319        /// <returns>The FFmpeg version.</returns>
 320        internal Version? GetFFmpegVersionInternal(string output)
 321        {
 322            // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
 16323            var match = FfmpegVersionRegex().Match(output);
 324
 16325            if (match.Success)
 326            {
 12327                if (Version.TryParse(match.Groups[1].ValueSpan, out var result))
 328                {
 12329                    return result;
 330                }
 331            }
 332
 4333            var versionMap = GetFFmpegLibraryVersions(output);
 334
 4335            var allVersionsValidated = true;
 336
 72337            foreach (var minimumVersion in _ffmpegMinimumLibraryVersions)
 338            {
 32339                if (versionMap.TryGetValue(minimumVersion.Key, out var foundVersion))
 340                {
 32341                    if (foundVersion >= minimumVersion.Value)
 342                    {
 16343                        _logger.LogInformation("Found {Library} version {FoundVersion} ({MinimumVersion})", minimumVersi
 344                    }
 345                    else
 346                    {
 16347                        _logger.LogWarning("Found {Library} version {FoundVersion} lower than recommended version {Minim
 16348                        allVersionsValidated = false;
 349                    }
 350                }
 351                else
 352                {
 0353                    _logger.LogError("{Library} version not found", minimumVersion.Key);
 0354                    allVersionsValidated = false;
 355                }
 356            }
 357
 4358            return allVersionsValidated ? MinVersion : null;
 359        }
 360
 361        /// <summary>
 362        /// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output
 363        /// and condenses them on to one line.  Output format is "name1=major.minor,name2=major.minor,etc.".
 364        /// </summary>
 365        /// <param name="output">The 'ffmpeg -version' output.</param>
 366        /// <returns>The library names and major.minor version numbers.</returns>
 367        private static Dictionary<string, Version> GetFFmpegLibraryVersions(string output)
 368        {
 4369            var map = new Dictionary<string, Version>();
 370
 72371            foreach (Match match in LibraryRegex().Matches(output))
 372            {
 32373                var version = new Version(
 32374                    int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture),
 32375                    int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture));
 376
 32377                map.Add(match.Groups["name"].Value, version);
 378            }
 379
 4380            return map;
 381        }
 382
 383        public bool CheckVaapiDeviceByDriverName(string driverName, string renderNodePath)
 384        {
 0385            if (!OperatingSystem.IsLinux())
 386            {
 0387                return false;
 388            }
 389
 0390            if (string.IsNullOrEmpty(driverName) || string.IsNullOrEmpty(renderNodePath))
 391            {
 0392                return false;
 393            }
 394
 395            try
 396            {
 0397                var output = GetProcessOutput(_encoderPath, "-v verbose -hide_banner -init_hw_device vaapi=va:" + render
 0398                return output.Contains(driverName, StringComparison.Ordinal);
 399            }
 0400            catch (Exception ex)
 401            {
 0402                _logger.LogError(ex, "Error detecting the given vaapi render node path");
 0403                return false;
 404            }
 0405        }
 406
 407        public bool CheckVulkanDrmDeviceByExtensionName(string renderNodePath, string[] vulkanExtensions)
 408        {
 0409            if (!OperatingSystem.IsLinux())
 410            {
 0411                return false;
 412            }
 413
 0414            if (string.IsNullOrEmpty(renderNodePath))
 415            {
 0416                return false;
 417            }
 418
 419            try
 420            {
 0421                var command = "-v verbose -hide_banner -init_hw_device drm=dr:" + renderNodePath + " -init_hw_device vul
 0422                var output = GetProcessOutput(_encoderPath, command, true, null);
 0423                foreach (string ext in vulkanExtensions)
 424                {
 0425                    if (!output.Contains(ext, StringComparison.Ordinal))
 426                    {
 0427                        return false;
 428                    }
 429                }
 430
 0431                return true;
 432            }
 0433            catch (Exception ex)
 434            {
 0435                _logger.LogError(ex, "Error detecting the given drm render node path");
 0436                return false;
 437            }
 0438        }
 439
 440        private IEnumerable<string> GetHwaccelTypes()
 441        {
 0442            string? output = null;
 443            try
 444            {
 0445                output = GetProcessOutput(_encoderPath, "-hwaccels", false, null);
 0446            }
 0447            catch (Exception ex)
 448            {
 0449                _logger.LogError(ex, "Error detecting available hwaccel types");
 0450            }
 451
 0452            if (string.IsNullOrWhiteSpace(output))
 453            {
 0454                return Enumerable.Empty<string>();
 455            }
 456
 0457            var found = output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct(
 0458            _logger.LogInformation("Available hwaccel types: {Types}", found);
 459
 0460            return found;
 461        }
 462
 463        public bool CheckFilterWithOption(string filter, string option)
 464        {
 0465            if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
 466            {
 0467                return false;
 468            }
 469
 470            string output;
 471            try
 472            {
 0473                output = GetProcessOutput(_encoderPath, "-h filter=" + filter, false, null);
 0474            }
 0475            catch (Exception ex)
 476            {
 0477                _logger.LogError(ex, "Error detecting the given filter");
 0478                return false;
 479            }
 480
 0481            if (output.Contains("Filter " + filter, StringComparison.Ordinal))
 482            {
 0483                return output.Contains(option, StringComparison.Ordinal);
 484            }
 485
 0486            _logger.LogWarning("Filter: {Name} with option {Option} is not available", filter, option);
 487
 0488            return false;
 0489        }
 490
 491        public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
 492        {
 0493            if (string.IsNullOrEmpty(keyDesc))
 494            {
 0495                return false;
 496            }
 497
 498            string output;
 499            try
 500            {
 501                // With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
 0502                var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
 0503                output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -"
 0504            }
 0505            catch (Exception ex)
 506            {
 0507                _logger.LogError(ex, "Error checking supported runtime key");
 0508                return false;
 509            }
 510
 0511            return output.Contains(keyDesc, StringComparison.Ordinal);
 0512        }
 513
 514        public bool CheckSupportedHwaccelFlag(string flag)
 515        {
 0516            return !string.IsNullOrEmpty(flag) && GetProcessExitCode(_encoderPath, $"-loglevel quiet -hwaccel_flags +{fl
 517        }
 518
 519        private IEnumerable<string> GetCodecs(Codec codec)
 520        {
 0521            string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
 522            string output;
 523            try
 524            {
 0525                output = GetProcessOutput(_encoderPath, "-" + codecstr, false, null);
 0526            }
 0527            catch (Exception ex)
 528            {
 0529                _logger.LogError(ex, "Error detecting available {Codec}", codecstr);
 0530                return Enumerable.Empty<string>();
 531            }
 532
 0533            if (string.IsNullOrWhiteSpace(output))
 534            {
 0535                return Enumerable.Empty<string>();
 536            }
 537
 0538            var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders;
 539
 0540            var found = CodecRegex()
 0541                .Matches(output)
 0542                .Select(x => x.Groups["codec"].Value)
 0543                .Where(x => required.Contains(x));
 544
 0545            _logger.LogInformation("Available {Codec}: {Codecs}", codecstr, found);
 546
 0547            return found;
 0548        }
 549
 550        private IEnumerable<string> GetFFmpegFilters()
 551        {
 552            string output;
 553            try
 554            {
 0555                output = GetProcessOutput(_encoderPath, "-filters", false, null);
 0556            }
 0557            catch (Exception ex)
 558            {
 0559                _logger.LogError(ex, "Error detecting available filters");
 0560                return Enumerable.Empty<string>();
 561            }
 562
 0563            if (string.IsNullOrWhiteSpace(output))
 564            {
 0565                return Enumerable.Empty<string>();
 566            }
 567
 0568            var found = FilterRegex()
 0569                .Matches(output)
 0570                .Select(x => x.Groups["filter"].Value)
 0571                .Where(x => _requiredFilters.Contains(x));
 572
 0573            _logger.LogInformation("Available filters: {Filters}", found);
 574
 0575            return found;
 0576        }
 577
 578        private Dictionary<int, bool> GetFFmpegFiltersWithOption()
 579        {
 0580            Dictionary<int, bool> dict = new Dictionary<int, bool>();
 0581            for (int i = 0; i < _filterOptionsDict.Count; i++)
 582            {
 0583                if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2)
 584                {
 0585                    dict.Add(i, CheckFilterWithOption(val[0], val[1]));
 586                }
 587            }
 588
 0589            return dict;
 590        }
 591
 592        private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
 593        {
 0594            var redirectStandardIn = !string.IsNullOrEmpty(testKey);
 0595            using (var process = new Process
 0596            {
 0597                StartInfo = new ProcessStartInfo(path, arguments)
 0598                {
 0599                    CreateNoWindow = true,
 0600                    UseShellExecute = false,
 0601                    WindowStyle = ProcessWindowStyle.Hidden,
 0602                    ErrorDialog = false,
 0603                    RedirectStandardInput = redirectStandardIn,
 0604                    RedirectStandardOutput = true,
 0605                    RedirectStandardError = true
 0606                }
 0607            })
 608            {
 0609                _logger.LogDebug("Running {Path} {Arguments}", path, arguments);
 610
 0611                process.Start();
 612
 0613                if (redirectStandardIn)
 614                {
 0615                    using var writer = process.StandardInput;
 0616                    writer.Write(testKey);
 617                }
 618
 0619                using var reader = readStdErr ? process.StandardError : process.StandardOutput;
 0620                return reader.ReadToEnd();
 621            }
 0622        }
 623
 624        private bool GetProcessExitCode(string path, string arguments)
 625        {
 0626            using var process = new Process();
 0627            process.StartInfo = new ProcessStartInfo(path, arguments)
 0628            {
 0629                CreateNoWindow = true,
 0630                UseShellExecute = false,
 0631                WindowStyle = ProcessWindowStyle.Hidden,
 0632                ErrorDialog = false
 0633            };
 0634            _logger.LogDebug("Running {Path} {Arguments}", path, arguments);
 635
 636            try
 637            {
 0638                process.Start();
 0639                process.WaitForExit();
 0640                return process.ExitCode == 0;
 641            }
 0642            catch (Exception ex)
 643            {
 0644                _logger.LogError("Running {Path} {Arguments} failed with exception {Exception}", path, arguments, ex.Mes
 0645                return false;
 646            }
 0647        }
 648
 649        [GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
 650        private static partial Regex CodecRegex();
 651
 652        [GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
 653        private static partial Regex FilterRegex();
 654    }
 655}