< 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: 204
Uncovered lines: 179
Coverable lines: 383
Total lines: 699
Line coverage: 53.2%
Branch coverage
28%
Covered branches: 23
Total branches: 82
Branch coverage: 28%
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.Runtime.Versioning;
 9using System.Text.RegularExpressions;
 10using MediaBrowser.Controller.MediaEncoding;
 11using Microsoft.Extensions.Logging;
 12
 13namespace MediaBrowser.MediaEncoding.Encoder
 14{
 15    public partial class EncoderValidator
 16    {
 117        private static readonly string[] _requiredDecoders =
 118        [
 119            "h264",
 120            "hevc",
 121            "vp8",
 122            "libvpx",
 123            "vp9",
 124            "libvpx-vp9",
 125            "av1",
 126            "libdav1d",
 127            "mpeg2video",
 128            "mpeg4",
 129            "msmpeg4",
 130            "dca",
 131            "ac3",
 132            "ac4",
 133            "aac",
 134            "mp3",
 135            "flac",
 136            "truehd",
 137            "h264_qsv",
 138            "hevc_qsv",
 139            "mpeg2_qsv",
 140            "vc1_qsv",
 141            "vp8_qsv",
 142            "vp9_qsv",
 143            "av1_qsv",
 144            "h264_cuvid",
 145            "hevc_cuvid",
 146            "mpeg2_cuvid",
 147            "vc1_cuvid",
 148            "mpeg4_cuvid",
 149            "vp8_cuvid",
 150            "vp9_cuvid",
 151            "av1_cuvid",
 152            "h264_rkmpp",
 153            "hevc_rkmpp",
 154            "mpeg1_rkmpp",
 155            "mpeg2_rkmpp",
 156            "mpeg4_rkmpp",
 157            "vp8_rkmpp",
 158            "vp9_rkmpp",
 159            "av1_rkmpp"
 160        ];
 61
 162        private static readonly string[] _requiredEncoders =
 163        [
 164            "libx264",
 165            "libx265",
 166            "libsvtav1",
 167            "aac",
 168            "aac_at",
 169            "libfdk_aac",
 170            "ac3",
 171            "alac",
 172            "dca",
 173            "libmp3lame",
 174            "libopus",
 175            "libvorbis",
 176            "flac",
 177            "truehd",
 178            "srt",
 179            "h264_amf",
 180            "hevc_amf",
 181            "av1_amf",
 182            "h264_qsv",
 183            "hevc_qsv",
 184            "mjpeg_qsv",
 185            "av1_qsv",
 186            "h264_nvenc",
 187            "hevc_nvenc",
 188            "av1_nvenc",
 189            "h264_vaapi",
 190            "hevc_vaapi",
 191            "av1_vaapi",
 192            "mjpeg_vaapi",
 193            "h264_v4l2m2m",
 194            "h264_videotoolbox",
 195            "hevc_videotoolbox",
 196            "mjpeg_videotoolbox",
 197            "h264_rkmpp",
 198            "hevc_rkmpp",
 199            "mjpeg_rkmpp"
 1100        ];
 101
 1102        private static readonly string[] _requiredFilters =
 1103        [
 1104            // sw
 1105            "alphasrc",
 1106            "zscale",
 1107            "tonemapx",
 1108            // qsv
 1109            "scale_qsv",
 1110            "vpp_qsv",
 1111            "deinterlace_qsv",
 1112            "overlay_qsv",
 1113            // cuda
 1114            "scale_cuda",
 1115            "yadif_cuda",
 1116            "bwdif_cuda",
 1117            "tonemap_cuda",
 1118            "overlay_cuda",
 1119            "transpose_cuda",
 1120            "hwupload_cuda",
 1121            // opencl
 1122            "scale_opencl",
 1123            "tonemap_opencl",
 1124            "overlay_opencl",
 1125            "transpose_opencl",
 1126            "yadif_opencl",
 1127            "bwdif_opencl",
 1128            // vaapi
 1129            "scale_vaapi",
 1130            "deinterlace_vaapi",
 1131            "tonemap_vaapi",
 1132            "procamp_vaapi",
 1133            "overlay_vaapi",
 1134            "transpose_vaapi",
 1135            "hwupload_vaapi",
 1136            // vulkan
 1137            "libplacebo",
 1138            "scale_vulkan",
 1139            "overlay_vulkan",
 1140            "transpose_vulkan",
 1141            "flip_vulkan",
 1142            // videotoolbox
 1143            "yadif_videotoolbox",
 1144            "bwdif_videotoolbox",
 1145            "scale_vt",
 1146            "transpose_vt",
 1147            "overlay_videotoolbox",
 1148            "tonemap_videotoolbox",
 1149            // rkrga
 1150            "scale_rkrga",
 1151            "vpp_rkrga",
 1152            "overlay_rkrga"
 1153        ];
 154
 1155        private static readonly Dictionary<FilterOptionType, (string, string)> _filterOptionsDict = new Dictionary<Filte
 1156        {
 1157            { FilterOptionType.ScaleCudaFormat, ("scale_cuda", "format") },
 1158            { FilterOptionType.TonemapCudaName, ("tonemap_cuda", "GPU accelerated HDR to SDR tonemapping") },
 1159            { FilterOptionType.TonemapOpenclBt2390, ("tonemap_opencl", "bt2390") },
 1160            { FilterOptionType.OverlayOpenclFrameSync, ("overlay_opencl", "Action to take when encountering EOF from sec
 1161            { FilterOptionType.OverlayVaapiFrameSync, ("overlay_vaapi", "Action to take when encountering EOF from secon
 1162            { FilterOptionType.OverlayVulkanFrameSync, ("overlay_vulkan", "Action to take when encountering EOF from sec
 1163            { FilterOptionType.TransposeOpenclReversal, ("transpose_opencl", "rotate by half-turn") },
 1164            { FilterOptionType.OverlayOpenclAlphaFormat, ("overlay_opencl", "alpha_format") },
 1165            { FilterOptionType.OverlayCudaAlphaFormat, ("overlay_cuda", "alpha_format") }
 1166        };
 167
 1168        private static readonly Dictionary<BitStreamFilterOptionType, (string, string)> _bsfOptionsDict = new Dictionary
 1169        {
 1170            { BitStreamFilterOptionType.HevcMetadataRemoveDovi, ("hevc_metadata", "remove_dovi") },
 1171            { BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus, ("hevc_metadata", "remove_hdr10plus") },
 1172            { BitStreamFilterOptionType.Av1MetadataRemoveDovi, ("av1_metadata", "remove_dovi") },
 1173            { BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus, ("av1_metadata", "remove_hdr10plus") },
 1174            { BitStreamFilterOptionType.DoviRpuStrip, ("dovi_rpu", "strip") }
 1175        };
 176
 177        // These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version ta
 178        // Refers to the versions in https://ffmpeg.org/download.html
 1179        private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Versi
 1180        {
 1181            { "libavutil", new Version(56, 70) },
 1182            { "libavcodec", new Version(58, 134) },
 1183            { "libavformat", new Version(58, 76) },
 1184            { "libavdevice", new Version(58, 13) },
 1185            { "libavfilter", new Version(7, 110) },
 1186            { "libswscale", new Version(5, 9) },
 1187            { "libswresample", new Version(3, 9) },
 1188            { "libpostproc", new Version(55, 9) }
 1189        };
 190
 191        private readonly ILogger _logger;
 192
 193        private readonly string _encoderPath;
 194
 16195        private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
 196
 197        public EncoderValidator(ILogger logger, string encoderPath)
 198        {
 16199            _logger = logger;
 16200            _encoderPath = encoderPath;
 16201        }
 202
 203        private enum Codec
 204        {
 205            Encoder,
 206            Decoder
 207        }
 208
 209        // When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions
 1210        public static Version MinVersion { get; } = new Version(4, 4);
 211
 1212        public static Version? MaxVersion { get; } = null;
 213
 214        [GeneratedRegex(@"^ffmpeg version n?((?:[0-9]+\.?)+)")]
 215        private static partial Regex FfmpegVersionRegex();
 216
 217        [GeneratedRegex(@"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))", RegexOptions.Multiline)]
 218        private static partial Regex LibraryRegex();
 219
 220        public bool ValidateVersion()
 221        {
 222            string output;
 223            try
 224            {
 0225                output = GetProcessOutput(_encoderPath, "-version", false, null);
 0226            }
 0227            catch (Exception ex)
 228            {
 0229                _logger.LogError(ex, "Error validating encoder");
 0230                return false;
 231            }
 232
 0233            if (string.IsNullOrWhiteSpace(output))
 234            {
 0235                _logger.LogError("FFmpeg validation: The process returned no result");
 0236                return false;
 237            }
 238
 0239            _logger.LogDebug("ffmpeg output: {Output}", output);
 240
 0241            return ValidateVersionInternal(output);
 0242        }
 243
 244        internal bool ValidateVersionInternal(string versionOutput)
 245        {
 8246            if (versionOutput.Contains("Libav developers", StringComparison.OrdinalIgnoreCase))
 247            {
 0248                _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
 0249                return false;
 250            }
 251
 252            // Work out what the version under test is
 8253            var version = GetFFmpegVersionInternal(versionOutput);
 254
 8255            _logger.LogInformation("Found ffmpeg version {Version}", version is not null ? version.ToString() : "unknown
 256
 8257            if (version is null)
 258            {
 1259                if (MaxVersion is not null) // Version is unknown
 260                {
 0261                    if (MinVersion == MaxVersion)
 262                    {
 0263                        _logger.LogWarning("FFmpeg validation: We recommend version {MinVersion}", MinVersion);
 264                    }
 265                    else
 266                    {
 0267                        _logger.LogWarning("FFmpeg validation: We recommend a minimum of {MinVersion} and maximum of {Ma
 268                    }
 269                }
 270                else
 271                {
 1272                    _logger.LogWarning("FFmpeg validation: We recommend minimum version {MinVersion}", MinVersion);
 273                }
 274
 1275                return false;
 276            }
 277
 7278            if (version < MinVersion) // Version is below what we recommend
 279            {
 1280                _logger.LogWarning("FFmpeg validation: The minimum recommended version is {MinVersion}", MinVersion);
 1281                return false;
 282            }
 283
 6284            if (MaxVersion is not null && version > MaxVersion) // Version is above what we recommend
 285            {
 0286                _logger.LogWarning("FFmpeg validation: The maximum recommended version is {MaxVersion}", MaxVersion);
 0287                return false;
 288            }
 289
 6290            return true;
 291        }
 292
 0293        public IEnumerable<string> GetDecoders() => GetCodecs(Codec.Decoder);
 294
 0295        public IEnumerable<string> GetEncoders() => GetCodecs(Codec.Encoder);
 296
 0297        public IEnumerable<string> GetHwaccels() => GetHwaccelTypes();
 298
 0299        public IEnumerable<string> GetFilters() => GetFFmpegFilters();
 300
 0301        public IDictionary<FilterOptionType, bool> GetFiltersWithOption() => _filterOptionsDict
 0302            .ToDictionary(item => item.Key, item => CheckFilterWithOption(item.Value.Item1, item.Value.Item2));
 303
 0304        public IDictionary<BitStreamFilterOptionType, bool> GetBitStreamFiltersWithOption() => _bsfOptionsDict
 0305            .ToDictionary(item => item.Key, item => CheckBitStreamFilterWithOption(item.Value.Item1, item.Value.Item2));
 306
 307        public Version? GetFFmpegVersion()
 308        {
 309            string output;
 310            try
 311            {
 0312                output = GetProcessOutput(_encoderPath, "-version", false, null);
 0313            }
 0314            catch (Exception ex)
 315            {
 0316                _logger.LogError(ex, "Error validating encoder");
 0317                return null;
 318            }
 319
 0320            if (string.IsNullOrWhiteSpace(output))
 321            {
 0322                _logger.LogError("FFmpeg validation: The process returned no result");
 0323                return null;
 324            }
 325
 0326            _logger.LogDebug("ffmpeg output: {Output}", output);
 327
 0328            return GetFFmpegVersionInternal(output);
 0329        }
 330
 331        /// <summary>
 332        /// Using the output from "ffmpeg -version" work out the FFmpeg version.
 333        /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
 334        /// to parse. If this is not available, then we try to match known library versions to FFmpeg versions.
 335        /// If that fails then we test the libraries to determine if they're newer than our minimum versions.
 336        /// </summary>
 337        /// <param name="output">The output from "ffmpeg -version".</param>
 338        /// <returns>The FFmpeg version.</returns>
 339        internal Version? GetFFmpegVersionInternal(string output)
 340        {
 341            // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
 16342            var match = FfmpegVersionRegex().Match(output);
 343
 16344            if (match.Success)
 345            {
 12346                if (Version.TryParse(match.Groups[1].ValueSpan, out var result))
 347                {
 12348                    return result;
 349                }
 350            }
 351
 4352            var versionMap = GetFFmpegLibraryVersions(output);
 353
 4354            var allVersionsValidated = true;
 355
 72356            foreach (var minimumVersion in _ffmpegMinimumLibraryVersions)
 357            {
 32358                if (versionMap.TryGetValue(minimumVersion.Key, out var foundVersion))
 359                {
 32360                    if (foundVersion >= minimumVersion.Value)
 361                    {
 16362                        _logger.LogInformation("Found {Library} version {FoundVersion} ({MinimumVersion})", minimumVersi
 363                    }
 364                    else
 365                    {
 16366                        _logger.LogWarning("Found {Library} version {FoundVersion} lower than recommended version {Minim
 16367                        allVersionsValidated = false;
 368                    }
 369                }
 370                else
 371                {
 0372                    _logger.LogError("{Library} version not found", minimumVersion.Key);
 0373                    allVersionsValidated = false;
 374                }
 375            }
 376
 4377            return allVersionsValidated ? MinVersion : null;
 378        }
 379
 380        /// <summary>
 381        /// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output
 382        /// and condenses them on to one line.  Output format is "name1=major.minor,name2=major.minor,etc.".
 383        /// </summary>
 384        /// <param name="output">The 'ffmpeg -version' output.</param>
 385        /// <returns>The library names and major.minor version numbers.</returns>
 386        private static Dictionary<string, Version> GetFFmpegLibraryVersions(string output)
 387        {
 4388            var map = new Dictionary<string, Version>();
 389
 72390            foreach (Match match in LibraryRegex().Matches(output))
 391            {
 32392                var version = new Version(
 32393                    int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture),
 32394                    int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture));
 395
 32396                map.Add(match.Groups["name"].Value, version);
 397            }
 398
 4399            return map;
 400        }
 401
 402        public bool CheckVaapiDeviceByDriverName(string driverName, string renderNodePath)
 403        {
 0404            if (!OperatingSystem.IsLinux())
 405            {
 0406                return false;
 407            }
 408
 0409            if (string.IsNullOrEmpty(driverName) || string.IsNullOrEmpty(renderNodePath))
 410            {
 0411                return false;
 412            }
 413
 414            try
 415            {
 0416                var output = GetProcessOutput(_encoderPath, "-v verbose -hide_banner -init_hw_device vaapi=va:" + render
 0417                return output.Contains(driverName, StringComparison.Ordinal);
 418            }
 0419            catch (Exception ex)
 420            {
 0421                _logger.LogError(ex, "Error detecting the given vaapi render node path");
 0422                return false;
 423            }
 0424        }
 425
 426        public bool CheckVulkanDrmDeviceByExtensionName(string renderNodePath, string[] vulkanExtensions)
 427        {
 0428            if (!OperatingSystem.IsLinux())
 429            {
 0430                return false;
 431            }
 432
 0433            if (string.IsNullOrEmpty(renderNodePath))
 434            {
 0435                return false;
 436            }
 437
 438            try
 439            {
 0440                var command = "-v verbose -hide_banner -init_hw_device drm=dr:" + renderNodePath + " -init_hw_device vul
 0441                var output = GetProcessOutput(_encoderPath, command, true, null);
 0442                foreach (string ext in vulkanExtensions)
 443                {
 0444                    if (!output.Contains(ext, StringComparison.Ordinal))
 445                    {
 0446                        return false;
 447                    }
 448                }
 449
 0450                return true;
 451            }
 0452            catch (Exception ex)
 453            {
 0454                _logger.LogError(ex, "Error detecting the given drm render node path");
 0455                return false;
 456            }
 0457        }
 458
 459        [SupportedOSPlatform("macos")]
 460        public bool CheckIsVideoToolboxAv1DecodeAvailable()
 461        {
 0462            return ApplePlatformHelper.HasAv1HardwareAccel(_logger);
 463        }
 464
 465        private IEnumerable<string> GetHwaccelTypes()
 466        {
 0467            string? output = null;
 468            try
 469            {
 0470                output = GetProcessOutput(_encoderPath, "-hwaccels", false, null);
 0471            }
 0472            catch (Exception ex)
 473            {
 0474                _logger.LogError(ex, "Error detecting available hwaccel types");
 0475            }
 476
 0477            if (string.IsNullOrWhiteSpace(output))
 478            {
 0479                return [];
 480            }
 481
 0482            var found = output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList();
 0483            _logger.LogInformation("Available hwaccel types: {Types}", found);
 484
 0485            return found;
 486        }
 487
 488        public bool CheckFilterWithOption(string filter, string option)
 489        {
 0490            if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
 491            {
 0492                return false;
 493            }
 494
 495            string output;
 496            try
 497            {
 0498                output = GetProcessOutput(_encoderPath, "-h filter=" + filter, false, null);
 0499            }
 0500            catch (Exception ex)
 501            {
 0502                _logger.LogError(ex, "Error detecting the given filter");
 0503                return false;
 504            }
 505
 0506            if (output.Contains("Filter " + filter, StringComparison.Ordinal))
 507            {
 0508                return output.Contains(option, StringComparison.Ordinal);
 509            }
 510
 0511            _logger.LogWarning("Filter: {Name} with option {Option} is not available", filter, option);
 512
 0513            return false;
 0514        }
 515
 516        public bool CheckBitStreamFilterWithOption(string filter, string option)
 517        {
 0518            if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
 519            {
 0520                return false;
 521            }
 522
 523            string output;
 524            try
 525            {
 0526                output = GetProcessOutput(_encoderPath, "-h bsf=" + filter, false, null);
 0527            }
 0528            catch (Exception ex)
 529            {
 0530                _logger.LogError(ex, "Error detecting the given bit stream filter");
 0531                return false;
 532            }
 533
 0534            if (output.Contains("Bit stream filter " + filter, StringComparison.Ordinal))
 535            {
 0536                return output.Contains(option, StringComparison.Ordinal);
 537            }
 538
 0539            _logger.LogWarning("Bit stream filter: {Name} with option {Option} is not available", filter, option);
 540
 0541            return false;
 0542        }
 543
 544        public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
 545        {
 0546            if (string.IsNullOrEmpty(keyDesc))
 547            {
 0548                return false;
 549            }
 550
 551            string output;
 552            try
 553            {
 554                // With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
 0555                var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
 0556                output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -"
 0557            }
 0558            catch (Exception ex)
 559            {
 0560                _logger.LogError(ex, "Error checking supported runtime key");
 0561                return false;
 562            }
 563
 0564            return output.Contains(keyDesc, StringComparison.Ordinal);
 0565        }
 566
 567        public bool CheckSupportedHwaccelFlag(string flag)
 568        {
 0569            return !string.IsNullOrEmpty(flag) && GetProcessExitCode(_encoderPath, $"-loglevel quiet -hwaccel_flags +{fl
 570        }
 571
 572        public bool CheckSupportedProberOption(string option, string proberPath)
 573        {
 0574            return !string.IsNullOrEmpty(option) && GetProcessExitCode(proberPath, $"-loglevel quiet -f lavfi -i nullsrc
 575        }
 576
 577        private IEnumerable<string> GetCodecs(Codec codec)
 578        {
 0579            string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
 580            string output;
 581            try
 582            {
 0583                output = GetProcessOutput(_encoderPath, "-" + codecstr, false, null);
 0584            }
 0585            catch (Exception ex)
 586            {
 0587                _logger.LogError(ex, "Error detecting available {Codec}", codecstr);
 0588                return [];
 589            }
 590
 0591            if (string.IsNullOrWhiteSpace(output))
 592            {
 0593                return [];
 594            }
 595
 0596            var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders;
 597
 0598            var found = CodecRegex()
 0599                .Matches(output)
 0600                .Select(x => x.Groups["codec"].Value)
 0601                .Where(x => required.Contains(x));
 602
 0603            _logger.LogInformation("Available {Codec}: {Codecs}", codecstr, found);
 604
 0605            return found;
 0606        }
 607
 608        private IEnumerable<string> GetFFmpegFilters()
 609        {
 610            string output;
 611            try
 612            {
 0613                output = GetProcessOutput(_encoderPath, "-filters", false, null);
 0614            }
 0615            catch (Exception ex)
 616            {
 0617                _logger.LogError(ex, "Error detecting available filters");
 0618                return [];
 619            }
 620
 0621            if (string.IsNullOrWhiteSpace(output))
 622            {
 0623                return [];
 624            }
 625
 0626            var found = FilterRegex()
 0627                .Matches(output)
 0628                .Select(x => x.Groups["filter"].Value)
 0629                .Where(x => _requiredFilters.Contains(x));
 630
 0631            _logger.LogInformation("Available filters: {Filters}", found);
 632
 0633            return found;
 0634        }
 635
 636        private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
 637        {
 0638            var redirectStandardIn = !string.IsNullOrEmpty(testKey);
 0639            using (var process = new Process
 0640            {
 0641                StartInfo = new ProcessStartInfo(path, arguments)
 0642                {
 0643                    CreateNoWindow = true,
 0644                    UseShellExecute = false,
 0645                    WindowStyle = ProcessWindowStyle.Hidden,
 0646                    ErrorDialog = false,
 0647                    RedirectStandardInput = redirectStandardIn,
 0648                    RedirectStandardOutput = true,
 0649                    RedirectStandardError = true
 0650                }
 0651            })
 652            {
 0653                _logger.LogDebug("Running {Path} {Arguments}", path, arguments);
 654
 0655                process.Start();
 656
 0657                if (redirectStandardIn)
 658                {
 0659                    using var writer = process.StandardInput;
 0660                    writer.Write(testKey);
 661                }
 662
 0663                using var reader = readStdErr ? process.StandardError : process.StandardOutput;
 0664                return reader.ReadToEnd();
 665            }
 0666        }
 667
 668        private bool GetProcessExitCode(string path, string arguments)
 669        {
 0670            using var process = new Process();
 0671            process.StartInfo = new ProcessStartInfo(path, arguments)
 0672            {
 0673                CreateNoWindow = true,
 0674                UseShellExecute = false,
 0675                WindowStyle = ProcessWindowStyle.Hidden,
 0676                ErrorDialog = false
 0677            };
 0678            _logger.LogDebug("Running {Path} {Arguments}", path, arguments);
 679
 680            try
 681            {
 0682                process.Start();
 0683                process.WaitForExit();
 0684                return process.ExitCode == 0;
 685            }
 0686            catch (Exception ex)
 687            {
 0688                _logger.LogError("Running {Path} {Arguments} failed with exception {Exception}", path, arguments, ex.Mes
 0689                return false;
 690            }
 0691        }
 692
 693        [GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
 694        private static partial Regex CodecRegex();
 695
 696        [GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
 697        private static partial Regex FilterRegex();
 698    }
 699}