< Summary - Jellyfin

Information
Class: MediaBrowser.MediaEncoding.Encoder.MediaEncoder
Assembly: MediaBrowser.MediaEncoding
File(s): /srv/git/jellyfin/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
Line coverage
16%
Covered lines: 53
Uncovered lines: 264
Coverable lines: 317
Total lines: 1391
Line coverage: 16.7%
Branch coverage
18%
Covered branches: 21
Total branches: 113
Branch coverage: 18.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)83.33%66100%
.cctor()100%210%
get_EncoderPath()100%210%
get_ProbePath()100%210%
get_EncoderVersion()100%210%
get_IsPkeyPauseSupported()100%210%
get_IsVaapiDeviceAmd()100%210%
get_IsVaapiDeviceInteliHD()100%210%
get_IsVaapiDeviceInteli965()100%210%
get_IsVaapiDeviceSupportVulkanDrmModifier()100%210%
get_IsVaapiDeviceSupportVulkanDrmInterop()100%210%
get_IsVideoToolboxAv1DecodeAvailable()100%210%
SetFFmpegPath()2.77%1074367.14%
ValidatePath(...)0%2040%
GetEncoderPathFromDirectory(...)100%210%
SetAvailableEncoders(...)100%210%
SetAvailableDecoders(...)100%210%
SetAvailableHwaccels(...)100%210%
SetAvailableFilters(...)100%210%
SetAvailableFiltersWithOption(...)100%210%
SetAvailableBitStreamFiltersWithOption(...)100%210%
SetMediaEncoderVersion(...)100%210%
SupportsEncoder(...)100%210%
SupportsDecoder(...)100%210%
SupportsHwaccel(...)100%210%
SupportsFilter(...)100%210%
SupportsFilterWithOption(...)100%210%
SupportsBitStreamFilterWithOption(...)100%210%
CanEncodeToAudioCodec(...)0%2040%
CanEncodeToSubtitleCodec(...)100%210%
GetMediaInfo(...)0%620%
GetExtraArguments(...)61.11%261870.58%
GetInputArgument(...)100%210%
GetInputArgument(...)0%620%
GetExternalSubtitleInputArgument(...)100%210%
ExtractAudioImage(...)100%210%
ExtractVideoImage(...)100%210%
ExtractVideoImage(...)100%210%
GetImageResolutionParameter()0%132110%
GetTimeParameter(...)100%210%
GetTimeParameter(...)100%210%
StartProcess(...)100%210%
StopProcess(...)0%620%
StopProcesses()50%4475%
EscapeSubtitleFilterPath(...)100%210%
Dispose()100%11100%
Dispose(...)100%22100%
ConvertImage(...)100%210%
GetPrimaryPlaylistVobFiles(...)0%4260%
GetPrimaryPlaylistM2tsFiles(...)100%210%
GetInputPathArgument(...)100%210%
GetInputPathArgument(...)0%4260%
GenerateConcatConfig(...)0%4260%
CanExtractSubtitles(...)100%210%
.ctor(...)100%210%
OnProcessExited(...)100%210%
DisposeProcess(...)100%210%
Dispose()0%2040%

File(s)

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

#LineLine coverage
 1#nullable disable
 2#pragma warning disable CS1591
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Text.Json;
 11using System.Text.RegularExpressions;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using AsyncKeyedLock;
 15using Jellyfin.Data.Enums;
 16using Jellyfin.Extensions;
 17using Jellyfin.Extensions.Json;
 18using Jellyfin.Extensions.Json.Converters;
 19using MediaBrowser.Common;
 20using MediaBrowser.Common.Configuration;
 21using MediaBrowser.Common.Extensions;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Extensions;
 24using MediaBrowser.Controller.MediaEncoding;
 25using MediaBrowser.MediaEncoding.Probing;
 26using MediaBrowser.Model.Configuration;
 27using MediaBrowser.Model.Dlna;
 28using MediaBrowser.Model.Drawing;
 29using MediaBrowser.Model.Dto;
 30using MediaBrowser.Model.Entities;
 31using MediaBrowser.Model.Globalization;
 32using MediaBrowser.Model.IO;
 33using MediaBrowser.Model.MediaInfo;
 34using Microsoft.Extensions.Configuration;
 35using Microsoft.Extensions.Logging;
 36
 37namespace MediaBrowser.MediaEncoding.Encoder
 38{
 39    /// <summary>
 40    /// Class MediaEncoder.
 41    /// </summary>
 42    public partial class MediaEncoder : IMediaEncoder, IDisposable
 43    {
 44        /// <summary>
 45        /// The default SDR image extraction timeout in milliseconds.
 46        /// </summary>
 47        internal const int DefaultSdrImageExtractionTimeout = 10000;
 48
 49        /// <summary>
 50        /// The default HDR image extraction timeout in milliseconds.
 51        /// </summary>
 52        internal const int DefaultHdrImageExtractionTimeout = 20000;
 53
 54        private readonly ILogger<MediaEncoder> _logger;
 55        private readonly IServerConfigurationManager _configurationManager;
 56        private readonly IFileSystem _fileSystem;
 57        private readonly ILocalizationManager _localization;
 58        private readonly IBlurayExaminer _blurayExaminer;
 59        private readonly IConfiguration _config;
 60        private readonly IServerConfigurationManager _serverConfig;
 61        private readonly string _startupOptionFFmpegPath;
 62
 63        private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
 64
 2265        private readonly Lock _runningProcessesLock = new();
 2266        private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
 67
 68        // MediaEncoder is registered as a Singleton
 69        private readonly JsonSerializerOptions _jsonSerializerOptions;
 70
 2271        private List<string> _encoders = new List<string>();
 2272        private List<string> _decoders = new List<string>();
 2273        private List<string> _hwaccels = new List<string>();
 2274        private List<string> _filters = new List<string>();
 2275        private IDictionary<FilterOptionType, bool> _filtersWithOption = new Dictionary<FilterOptionType, bool>();
 2276        private IDictionary<BitStreamFilterOptionType, bool> _bitStreamFiltersWithOption = new Dictionary<BitStreamFilte
 77
 78        private bool _isPkeyPauseSupported = false;
 79        private bool _isLowPriorityHwDecodeSupported = false;
 80        private bool _proberSupportsFirstVideoFrame = false;
 81
 82        private bool _isVaapiDeviceAmd = false;
 83        private bool _isVaapiDeviceInteliHD = false;
 84        private bool _isVaapiDeviceInteli965 = false;
 85        private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
 86        private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
 87
 88        private bool _isVideoToolboxAv1DecodeAvailable = false;
 89
 090        private static string[] _vulkanImageDrmFmtModifierExts =
 091        {
 092            "VK_EXT_image_drm_format_modifier",
 093        };
 94
 095        private static string[] _vulkanExternalMemoryDmaBufExts =
 096        {
 097            "VK_KHR_external_memory_fd",
 098            "VK_EXT_external_memory_dma_buf",
 099            "VK_KHR_external_semaphore_fd",
 0100            "VK_EXT_external_memory_host"
 0101        };
 102
 103        private Version _ffmpegVersion = null;
 22104        private string _ffmpegPath = string.Empty;
 105        private string _ffprobePath;
 106        private int _threads;
 107
 108        public MediaEncoder(
 109            ILogger<MediaEncoder> logger,
 110            IServerConfigurationManager configurationManager,
 111            IFileSystem fileSystem,
 112            IBlurayExaminer blurayExaminer,
 113            ILocalizationManager localization,
 114            IConfiguration config,
 115            IServerConfigurationManager serverConfig)
 116        {
 22117            _logger = logger;
 22118            _configurationManager = configurationManager;
 22119            _fileSystem = fileSystem;
 22120            _blurayExaminer = blurayExaminer;
 22121            _localization = localization;
 22122            _config = config;
 22123            _serverConfig = serverConfig;
 22124            _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathK
 125
 22126            _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
 22127            _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
 128
 129            // Although the type is not nullable, this might still be null during unit tests
 22130            var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
 22131            if (semaphoreCount < 1)
 132            {
 22133                semaphoreCount = Environment.ProcessorCount;
 134            }
 135
 22136            _thumbnailResourcePool = new(semaphoreCount);
 22137        }
 138
 139        /// <inheritdoc />
 0140        public string EncoderPath => _ffmpegPath;
 141
 142        /// <inheritdoc />
 0143        public string ProbePath => _ffprobePath;
 144
 145        /// <inheritdoc />
 0146        public Version EncoderVersion => _ffmpegVersion;
 147
 148        /// <inheritdoc />
 0149        public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
 150
 151        /// <inheritdoc />
 0152        public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
 153
 154        /// <inheritdoc />
 0155        public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
 156
 157        /// <inheritdoc />
 0158        public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
 159
 160        /// <inheritdoc />
 0161        public bool IsVaapiDeviceSupportVulkanDrmModifier => _isVaapiDeviceSupportVulkanDrmModifier;
 162
 163        /// <inheritdoc />
 0164        public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
 165
 0166        public bool IsVideoToolboxAv1DecodeAvailable => _isVideoToolboxAv1DecodeAvailable;
 167
 168        [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
 169        private static partial Regex FfprobePathRegex();
 170
 171        /// <summary>
 172        /// Run at startup to validate ffmpeg.
 173        /// Sets global variables FFmpegPath.
 174        /// Precedence is: CLI/Env var > Config > $PATH.
 175        /// </summary>
 176        /// <returns>bool indicates whether a valid ffmpeg is found.</returns>
 177        public bool SetFFmpegPath()
 178        {
 21179            var skipValidation = _config.GetFFmpegSkipValidation();
 21180            if (skipValidation)
 181            {
 21182                _logger.LogWarning("FFmpeg: Skipping FFmpeg Validation due to FFmpeg:novalidation set to true");
 21183                return true;
 184            }
 185
 186            // 1) Check if the --ffmpeg CLI switch has been given
 0187            var ffmpegPath = _startupOptionFFmpegPath;
 0188            string ffmpegPathSetMethodText = "command line or environment variable";
 0189            if (string.IsNullOrEmpty(ffmpegPath))
 190            {
 191                // 2) Custom path stored in config/encoding xml file under tag <EncoderAppPath> should be used as a fall
 0192                ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
 0193                ffmpegPathSetMethodText = "encoding.xml config file";
 0194                if (string.IsNullOrEmpty(ffmpegPath))
 195                {
 196                    // 3) Check "ffmpeg"
 0197                    ffmpegPath = "ffmpeg";
 0198                    ffmpegPathSetMethodText = "system $PATH";
 199                }
 200            }
 201
 0202            if (!ValidatePath(ffmpegPath))
 203            {
 0204                _ffmpegPath = null;
 0205                _logger.LogError("FFmpeg: Path set by {FfmpegPathSetMethodText} is invalid", ffmpegPathSetMethodText);
 0206                return false;
 207            }
 208
 209            // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
 0210            var options = _configurationManager.GetEncodingOptions();
 0211            options.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
 0212            _configurationManager.SaveConfiguration("encoding", options);
 213
 214            // Only if mpeg path is set, try and set path to probe
 0215            if (_ffmpegPath is not null)
 216            {
 217                // Determine a probe path from the mpeg path
 0218                _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, "ffprobe$1");
 219
 220                // Interrogate to understand what coders are supported
 0221                var validator = new EncoderValidator(_logger, _ffmpegPath);
 222
 0223                SetAvailableDecoders(validator.GetDecoders());
 0224                SetAvailableEncoders(validator.GetEncoders());
 0225                SetAvailableFilters(validator.GetFilters());
 0226                SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
 0227                SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption());
 0228                SetAvailableHwaccels(validator.GetHwaccels());
 0229                SetMediaEncoderVersion(validator);
 230
 0231                _threads = EncodingHelper.GetNumberOfThreads(null, options, null);
 232
 0233                _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p      pause transcoding", _ffmpegVersion);
 0234                _isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
 0235                _proberSupportsFirstVideoFrame = validator.CheckSupportedProberOption("only_first_vframe", _ffprobePath)
 236
 237                // Check the Vaapi device vendor
 0238                if (OperatingSystem.IsLinux()
 0239                    && SupportsHwaccel("vaapi")
 0240                    && !string.IsNullOrEmpty(options.VaapiDevice)
 0241                    && options.HardwareAccelerationType == HardwareAccelerationType.vaapi)
 242                {
 0243                    _isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevic
 0244                    _isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDev
 0245                    _isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiD
 0246                    _isVaapiDeviceSupportVulkanDrmModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.Vaapi
 0247                    _isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiD
 248
 0249                    if (_isVaapiDeviceAmd)
 250                    {
 0251                        _logger.LogInformation("VAAPI device {RenderNodePath} is AMD GPU", options.VaapiDevice);
 252                    }
 0253                    else if (_isVaapiDeviceInteliHD)
 254                    {
 0255                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (iHD)", options.VaapiDevice);
 256                    }
 0257                    else if (_isVaapiDeviceInteli965)
 258                    {
 0259                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice)
 260                    }
 261
 0262                    if (_isVaapiDeviceSupportVulkanDrmModifier)
 263                    {
 0264                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM modifier", options.Vaa
 265                    }
 266
 0267                    if (_isVaapiDeviceSupportVulkanDrmInterop)
 268                    {
 0269                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.Vaap
 270                    }
 271                }
 272
 273                // Check if VideoToolbox supports AV1 decode
 0274                if (OperatingSystem.IsMacOS() && SupportsHwaccel("videotoolbox"))
 275                {
 0276                    _isVideoToolboxAv1DecodeAvailable = validator.CheckIsVideoToolboxAv1DecodeAvailable();
 277                }
 278            }
 279
 0280            _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
 0281            return !string.IsNullOrWhiteSpace(ffmpegPath);
 282        }
 283
 284        /// <summary>
 285        /// Validates the supplied FQPN to ensure it is a ffmpeg utility.
 286        /// If checks pass, global variable FFmpegPath is updated.
 287        /// </summary>
 288        /// <param name="path">FQPN to test.</param>
 289        /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
 290        private bool ValidatePath(string path)
 291        {
 0292            if (string.IsNullOrEmpty(path))
 293            {
 0294                return false;
 295            }
 296
 0297            bool rc = new EncoderValidator(_logger, path).ValidateVersion();
 0298            if (!rc)
 299            {
 0300                _logger.LogError("FFmpeg: Failed version check: {Path}", path);
 0301                return false;
 302            }
 303
 0304            _ffmpegPath = path;
 0305            return true;
 306        }
 307
 308        private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false)
 309        {
 310            try
 311            {
 0312                var files = _fileSystem.GetFilePaths(path, recursive);
 313
 0314                return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringCom
 0315                                                    && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.Ordi
 316            }
 0317            catch (Exception)
 318            {
 319                // Trap all exceptions, like DirNotExists, and return null
 0320                return null;
 321            }
 0322        }
 323
 324        public void SetAvailableEncoders(IEnumerable<string> list)
 325        {
 0326            _encoders = list.ToList();
 0327        }
 328
 329        public void SetAvailableDecoders(IEnumerable<string> list)
 330        {
 0331            _decoders = list.ToList();
 0332        }
 333
 334        public void SetAvailableHwaccels(IEnumerable<string> list)
 335        {
 0336            _hwaccels = list.ToList();
 0337        }
 338
 339        public void SetAvailableFilters(IEnumerable<string> list)
 340        {
 0341            _filters = list.ToList();
 0342        }
 343
 344        public void SetAvailableFiltersWithOption(IDictionary<FilterOptionType, bool> dict)
 345        {
 0346            _filtersWithOption = dict;
 0347        }
 348
 349        public void SetAvailableBitStreamFiltersWithOption(IDictionary<BitStreamFilterOptionType, bool> dict)
 350        {
 0351            _bitStreamFiltersWithOption = dict;
 0352        }
 353
 354        public void SetMediaEncoderVersion(EncoderValidator validator)
 355        {
 0356            _ffmpegVersion = validator.GetFFmpegVersion();
 0357        }
 358
 359        /// <inheritdoc />
 360        public bool SupportsEncoder(string encoder)
 361        {
 0362            return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
 363        }
 364
 365        /// <inheritdoc />
 366        public bool SupportsDecoder(string decoder)
 367        {
 0368            return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase);
 369        }
 370
 371        /// <inheritdoc />
 372        public bool SupportsHwaccel(string hwaccel)
 373        {
 0374            return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
 375        }
 376
 377        /// <inheritdoc />
 378        public bool SupportsFilter(string filter)
 379        {
 0380            return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
 381        }
 382
 383        /// <inheritdoc />
 384        public bool SupportsFilterWithOption(FilterOptionType option)
 385        {
 0386            return _filtersWithOption.TryGetValue(option, out var val) && val;
 387        }
 388
 389        public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option)
 390        {
 0391            return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val;
 392        }
 393
 394        public bool CanEncodeToAudioCodec(string codec)
 395        {
 0396            if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
 397            {
 0398                codec = "libopus";
 399            }
 0400            else if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
 401            {
 0402                codec = "libmp3lame";
 403            }
 404
 0405            return SupportsEncoder(codec);
 406        }
 407
 408        public bool CanEncodeToSubtitleCodec(string codec)
 409        {
 410            // TODO
 0411            return true;
 412        }
 413
 414        /// <inheritdoc />
 415        public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
 416        {
 0417            var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
 0418            var extraArgs = GetExtraArguments(request);
 419
 0420            return GetMediaInfoInternal(
 0421                GetInputArgument(request.MediaSource.Path, request.MediaSource),
 0422                request.MediaSource.Path,
 0423                request.MediaSource.Protocol,
 0424                extractChapters,
 0425                extraArgs,
 0426                request.MediaType == DlnaProfileType.Audio,
 0427                request.MediaSource.VideoType,
 0428                cancellationToken);
 429        }
 430
 431        internal string GetExtraArguments(MediaInfoRequest request)
 432        {
 1433            var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
 1434            var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
 1435            var analyzeDuration = string.Empty;
 1436            var extraArgs = string.Empty;
 437
 1438            if (request.MediaSource.AnalyzeDurationMs > 0)
 439            {
 0440                analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000);
 441            }
 1442            else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
 443            {
 0444                analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
 445            }
 446
 1447            if (!string.IsNullOrEmpty(analyzeDuration))
 448            {
 0449                extraArgs = analyzeDuration;
 450            }
 451
 1452            if (!string.IsNullOrEmpty(ffmpegProbeSize))
 453            {
 0454                extraArgs += " -probesize " + ffmpegProbeSize;
 455            }
 456
 1457            if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
 458            {
 1459                extraArgs += $" -user_agent \"{userAgent}\"";
 460            }
 461
 1462            if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
 463            {
 0464                extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
 465            }
 466
 1467            return extraArgs;
 468        }
 469
 470        /// <inheritdoc />
 471        public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource)
 472        {
 0473            return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol);
 474        }
 475
 476        /// <inheritdoc />
 477        public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
 478        {
 0479            var prefix = "file";
 0480            if (mediaSource.IsoType == IsoType.BluRay)
 481            {
 0482                prefix = "bluray";
 483            }
 484
 0485            return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol);
 486        }
 487
 488        /// <inheritdoc />
 489        public string GetExternalSubtitleInputArgument(string inputFile)
 490        {
 491            const string Prefix = "file";
 492
 0493            return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File);
 494        }
 495
 496        /// <summary>
 497        /// Gets the media info internal.
 498        /// </summary>
 499        /// <returns>Task{MediaInfoResult}.</returns>
 500        private async Task<MediaInfo> GetMediaInfoInternal(
 501            string inputPath,
 502            string primaryPath,
 503            MediaProtocol protocol,
 504            bool extractChapters,
 505            string probeSizeArgument,
 506            bool isAudio,
 507            VideoType? videoType,
 508            CancellationToken cancellationToken)
 509        {
 510            var args = extractChapters
 511                ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
 512                : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
 513
 514            if (_proberSupportsFirstVideoFrame)
 515            {
 516                args += " -show_frames -only_first_vframe";
 517            }
 518
 519            args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
 520
 521            var process = new Process
 522            {
 523                StartInfo = new ProcessStartInfo
 524                {
 525                    CreateNoWindow = true,
 526                    UseShellExecute = false,
 527
 528                    // Must consume both or ffmpeg may hang due to deadlocks.
 529                    RedirectStandardOutput = true,
 530
 531                    FileName = _ffprobePath,
 532                    Arguments = args,
 533
 534                    WindowStyle = ProcessWindowStyle.Hidden,
 535                    ErrorDialog = false,
 536                },
 537                EnableRaisingEvents = true
 538            };
 539
 540            _logger.LogDebug("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
 541
 542            var memoryStream = new MemoryStream();
 543            await using (memoryStream.ConfigureAwait(false))
 544            using (var processWrapper = new ProcessWrapper(process, this))
 545            {
 546                StartProcess(processWrapper);
 547                using var reader = process.StandardOutput;
 548                await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
 549                memoryStream.Seek(0, SeekOrigin.Begin);
 550                InternalMediaInfoResult result;
 551                try
 552                {
 553                    result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
 554                                        memoryStream,
 555                                        _jsonSerializerOptions,
 556                                        cancellationToken).ConfigureAwait(false);
 557                }
 558                catch
 559                {
 560                    StopProcess(processWrapper, 100);
 561
 562                    throw;
 563                }
 564
 565                if (result is null || (result.Streams is null && result.Format is null))
 566                {
 567                    throw new FfmpegException("ffprobe failed - streams and format are both null.");
 568                }
 569
 570                if (result.Streams is not null)
 571                {
 572                    // Normalize aspect ratio if invalid
 573                    foreach (var stream in result.Streams)
 574                    {
 575                        if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 576                        {
 577                            stream.DisplayAspectRatio = string.Empty;
 578                        }
 579
 580                        if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 581                        {
 582                            stream.SampleAspectRatio = string.Empty;
 583                        }
 584                    }
 585                }
 586
 587                return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primar
 588            }
 589        }
 590
 591        /// <inheritdoc />
 592        public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
 593        {
 0594            var mediaSource = new MediaSourceInfo
 0595            {
 0596                Protocol = MediaProtocol.File
 0597            };
 598
 0599            return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, canc
 600        }
 601
 602        /// <inheritdoc />
 603        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 604        {
 0605            return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, Image
 606        }
 607
 608        /// <inheritdoc />
 609        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 610        {
 0611            return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, tar
 612        }
 613
 614        private async Task<string> ExtractImage(
 615            string inputFile,
 616            string container,
 617            MediaStream videoStream,
 618            int? imageStreamIndex,
 619            MediaSourceInfo mediaSource,
 620            bool isAudio,
 621            Video3DFormat? threedFormat,
 622            TimeSpan? offset,
 623            ImageFormat? targetFormat,
 624            CancellationToken cancellationToken)
 625        {
 626            var inputArgument = GetInputPathArgument(inputFile, mediaSource);
 627
 628            if (!isAudio)
 629            {
 630                try
 631                {
 632                    return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFor
 633                }
 634                catch (ArgumentException)
 635                {
 636                    throw;
 637                }
 638                catch (Exception ex)
 639                {
 640                    _logger.LogWarning(ex, "I-frame image extraction failed, will attempt standard way. Input: {Argument
 641                }
 642            }
 643
 644            return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, off
 645        }
 646
 647        private string GetImageResolutionParameter()
 648        {
 0649            var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
 0650            {
 0651                ImageResolution.P144 => "256x144",
 0652                ImageResolution.P240 => "426x240",
 0653                ImageResolution.P360 => "640x360",
 0654                ImageResolution.P480 => "854x480",
 0655                ImageResolution.P720 => "1280x720",
 0656                ImageResolution.P1080 => "1920x1080",
 0657                ImageResolution.P1440 => "2560x1440",
 0658                ImageResolution.P2160 => "3840x2160",
 0659                _ => string.Empty
 0660            };
 661
 0662            if (!string.IsNullOrEmpty(imageResolutionParameter))
 663            {
 0664                imageResolutionParameter = " -s " + imageResolutionParameter;
 665            }
 666
 0667            return imageResolutionParameter;
 668        }
 669
 670        private async Task<string> ExtractImageInternal(
 671            string inputPath,
 672            string container,
 673            MediaStream videoStream,
 674            int? imageStreamIndex,
 675            Video3DFormat? threedFormat,
 676            TimeSpan? offset,
 677            bool useIFrame,
 678            ImageFormat? targetFormat,
 679            bool isAudio,
 680            CancellationToken cancellationToken)
 681        {
 682            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 683
 684            var useTradeoff = _config.GetFFmpegImgExtractPerfTradeoff();
 685
 686            var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
 687
 688            var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ou
 689            Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
 690
 691            // deint -> scale -> thumbnail -> tonemap.
 692            // put the SW tonemap right after the thumbnail to do it only once to reduce cpu usage.
 693            var filters = new List<string>();
 694
 695            // deinterlace using bwdif algorithm for video stream.
 696            if (videoStream is not null && videoStream.IsInterlaced)
 697            {
 698                filters.Add("bwdif=0:-1:0");
 699            }
 700
 701            // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the cor
 702            // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex
 703            var scaler = threedFormat switch
 704            {
 705                // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may 
 706                Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min
 707                // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
 708                Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw
 709                // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may
 710                Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):
 711                // ftab crop height in half, set the display aspect,crop out any black bars we may have made
 712                Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(
 713                _ => "scale=round(iw*sar/2)*2:round(ih/2)*2"
 714            };
 715
 716            filters.Add(scaler);
 717
 718            // Use ffmpeg to sample N frames and pick the best thumbnail. Have a fall back just in case.
 719            var enableThumbnail = !useTradeoff && useIFrame && !string.Equals("wtv", container, StringComparison.Ordinal
 720            if (enableThumbnail)
 721            {
 722                filters.Add("thumbnail=n=24");
 723            }
 724
 725            // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
 726            // Only enable Dolby Vision tonemap when tonemapx is available
 727            var enableHdrExtraction = false;
 728
 729            if (videoStream?.VideoRange == VideoRange.HDR)
 730            {
 731                if (SupportsFilter("tonemapx"))
 732                {
 733                    var peak = videoStream.VideoRangeType == VideoRangeType.DOVI ? "400" : "100";
 734                    enableHdrExtraction = true;
 735                    filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p:ran
 736                }
 737                else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
 738                {
 739                    enableHdrExtraction = true;
 740                    filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:p
 741                }
 742            }
 743
 744            var vf = string.Join(',', filters);
 745            var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.Invariant
 746            var args = string.Format(
 747                CultureInfo.InvariantCulture,
 748                "-i {0}{1} -threads {2} -v quiet -vframes 1 -vf {3}{4}{5} -f image2 \"{6}\"",
 749                inputPath,
 750                mapArg,
 751                _threads,
 752                vf,
 753                isAudio ? string.Empty : GetImageResolutionParameter(),
 754                EncodingHelper.GetVideoSyncOption("-1", EncoderVersion), // auto decide fps mode
 755                tempExtractPath);
 756
 757            if (offset.HasValue)
 758            {
 759                args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
 760            }
 761
 762            // The mpegts demuxer cannot seek to keyframes, so we have to let the
 763            // decoder discard non-keyframes, which may contain corrupted images.
 764            var seekMpegTs = offset.HasValue && string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
 765            if (useIFrame && (useTradeoff || seekMpegTs))
 766            {
 767                args = "-skip_frame nokey " + args;
 768            }
 769
 770            if (!string.IsNullOrWhiteSpace(container))
 771            {
 772                var inputFormat = EncodingHelper.GetInputFormat(container);
 773                if (!string.IsNullOrWhiteSpace(inputFormat))
 774                {
 775                    args = "-f " + inputFormat + " " + args;
 776                }
 777            }
 778
 779            var process = new Process
 780            {
 781                StartInfo = new ProcessStartInfo
 782                {
 783                    CreateNoWindow = true,
 784                    UseShellExecute = false,
 785                    FileName = _ffmpegPath,
 786                    Arguments = args,
 787                    WindowStyle = ProcessWindowStyle.Hidden,
 788                    ErrorDialog = false,
 789                },
 790                EnableRaisingEvents = true
 791            };
 792
 793            _logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Argum
 794
 795            using (var processWrapper = new ProcessWrapper(process, this))
 796            {
 797                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 798                {
 799                    StartProcess(processWrapper);
 800
 801                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 802                    if (timeoutMs <= 0)
 803                    {
 804                        timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTi
 805                    }
 806
 807                    try
 808                    {
 809                        await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 810                    }
 811                    catch (OperationCanceledException ex)
 812                    {
 813                        process.Kill(true);
 814                        throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction t
 815                    }
 816                }
 817
 818                var file = _fileSystem.GetFileInfo(tempExtractPath);
 819
 820                if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
 821                {
 822                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 823                }
 824
 825                return tempExtractPath;
 826            }
 827        }
 828
 829        /// <inheritdoc />
 830        public async Task<string> ExtractVideoImagesOnIntervalAccelerated(
 831            string inputFile,
 832            string container,
 833            MediaSourceInfo mediaSource,
 834            MediaStream imageStream,
 835            int maxWidth,
 836            TimeSpan interval,
 837            bool allowHwAccel,
 838            bool enableHwEncoding,
 839            int? threads,
 840            int? qualityScale,
 841            ProcessPriorityClass? priority,
 842            bool enableKeyFrameOnlyExtraction,
 843            EncodingHelper encodingHelper,
 844            CancellationToken cancellationToken)
 845        {
 846            var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
 847            threads ??= _threads;
 848
 849            if (allowHwAccel && enableKeyFrameOnlyExtraction)
 850            {
 851                var hardwareAccelerationType = options.HardwareAccelerationType;
 852                var supportsKeyFrameOnly = (hardwareAccelerationType == HardwareAccelerationType.nvenc && options.Enable
 853                                           || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSyst
 854                                           || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.Prefe
 855                                           || hardwareAccelerationType == HardwareAccelerationType.vaapi
 856                                           || hardwareAccelerationType == HardwareAccelerationType.videotoolbox
 857                                           || hardwareAccelerationType == HardwareAccelerationType.rkmpp;
 858                if (!supportsKeyFrameOnly)
 859                {
 860                    // Disable hardware acceleration when the hardware decoder does not support keyframe only mode.
 861                    allowHwAccel = false;
 862                    options = new EncodingOptions();
 863                }
 864            }
 865
 866            // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
 867            // Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
 868            if (!allowHwAccel)
 869            {
 870                options.EnableHardwareEncoding = false;
 871                options.HardwareAccelerationType = HardwareAccelerationType.none;
 872                options.EnableTonemapping = false;
 873            }
 874
 875            if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.Asp
 876            {
 877                // For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimen
 878                var darParts = imageStream.AspectRatio.Split(':');
 879                var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], Cultu
 880                // When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
 881                // Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, 
 882                var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05
 883                if (shouldResetHeight)
 884                {
 885                    // SAR = DAR * Height / Width
 886                    // RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
 887                    imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
 888                }
 889            }
 890
 891            var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.To
 892            var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
 893            {
 894                IsVideoRequest = true,  // must be true for InputVideoHwaccelArgs to return non-empty value
 895                MediaSource = mediaSource,
 896                VideoStream = imageStream,
 897                BaseRequest = baseRequest,  // GetVideoProcessingFilterParam errors if null
 898                MediaPath = inputFile,
 899                OutputVideoCodec = "mjpeg"
 900            };
 901            var vidEncoder = enableHwEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideo
 902
 903            // Get input and filter arguments
 904            var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
 905            if (string.IsNullOrWhiteSpace(inputArg))
 906            {
 907                throw new InvalidOperationException("EncodingHelper returned empty input arguments.");
 908            }
 909
 910            if (!allowHwAccel)
 911            {
 912                inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, 
 913            }
 914
 915            if (options.HardwareAccelerationType == HardwareAccelerationType.videotoolbox && _isLowPriorityHwDecodeSuppo
 916            {
 917                // VideoToolbox supports low priority decoding, which is useful for trickplay
 918                inputArg = "-hwaccel_flags +low_priority " + inputArg;
 919            }
 920
 921            var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
 922            if (string.IsNullOrWhiteSpace(filterParam))
 923            {
 924                throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
 925            }
 926
 927            try
 928            {
 929                return await ExtractVideoImagesOnIntervalInternal(
 930                    (enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg,
 931                    filterParam,
 932                    vidEncoder,
 933                    threads,
 934                    qualityScale,
 935                    priority,
 936                    cancellationToken).ConfigureAwait(false);
 937            }
 938            catch (FfmpegException ex)
 939            {
 940                if (!enableKeyFrameOnlyExtraction)
 941                {
 942                    throw;
 943                }
 944
 945                _logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFil
 946            }
 947
 948            return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, 
 949        }
 950
 951        private async Task<string> ExtractVideoImagesOnIntervalInternal(
 952            string inputArg,
 953            string filterParam,
 954            string vidEncoder,
 955            int? outputThreads,
 956            int? qualityScale,
 957            ProcessPriorityClass? priority,
 958            CancellationToken cancellationToken)
 959        {
 960            if (string.IsNullOrWhiteSpace(inputArg))
 961            {
 962                throw new InvalidOperationException("Empty or invalid input argument.");
 963            }
 964
 965            // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
 966            // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
 967            var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
 968            var encoderQualityOption = "-qscale:v ";
 969
 970            if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
 971                || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
 972            {
 973                // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
 974                encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
 975                encoderQualityOption = "-global_quality:v ";
 976            }
 977
 978            if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
 979            {
 980                // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qs
 981                encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
 982            }
 983
 984            if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
 985            {
 986                // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qsca
 987                encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
 988                encoderQualityOption = "-qp_init:v ";
 989            }
 990
 991            // Output arguments
 992            var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToSt
 993            Directory.CreateDirectory(targetDirectory);
 994            var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
 995
 996            // Final command arguments
 997            var args = string.Format(
 998                CultureInfo.InvariantCulture,
 999                "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"",
 1000                inputArg,
 1001                filterParam,
 1002                outputThreads.GetValueOrDefault(_threads),
 1003                vidEncoder,
 1004                encoderQualityOption + encoderQuality + " ",
 1005                vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : stri
 1006                EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim() + " ", // passthrough timestamp
 1007                "image2",
 1008                outputPath);
 1009
 1010            // Start ffmpeg process
 1011            var process = new Process
 1012            {
 1013                StartInfo = new ProcessStartInfo
 1014                {
 1015                    CreateNoWindow = true,
 1016                    UseShellExecute = false,
 1017                    FileName = _ffmpegPath,
 1018                    Arguments = args,
 1019                    WindowStyle = ProcessWindowStyle.Hidden,
 1020                    ErrorDialog = false,
 1021                },
 1022                EnableRaisingEvents = true
 1023            };
 1024
 1025            var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, 
 1026            _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
 1027
 1028            using (var processWrapper = new ProcessWrapper(process, this))
 1029            {
 1030                bool ranToCompletion = false;
 1031
 1032                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 1033                {
 1034                    StartProcess(processWrapper);
 1035
 1036                    // Set process priority
 1037                    if (priority.HasValue)
 1038                    {
 1039                        try
 1040                        {
 1041                            processWrapper.Process.PriorityClass = priority.Value;
 1042                        }
 1043                        catch (Exception ex)
 1044                        {
 1045                            _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", prior
 1046                        }
 1047                    }
 1048
 1049                    // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
 1050                    // but we still need to detect if the process hangs.
 1051                    // Making the assumption that as long as new jpegs are showing up, everything is good.
 1052
 1053                    bool isResponsive = true;
 1054                    int lastCount = 0;
 1055                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 1056                    timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
 1057
 1058                    while (isResponsive && !cancellationToken.IsCancellationRequested)
 1059                    {
 1060                        try
 1061                        {
 1062                            await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 1063
 1064                            ranToCompletion = true;
 1065                            break;
 1066                        }
 1067                        catch (OperationCanceledException)
 1068                        {
 1069                            // We don't actually expect the process to be finished in one timeout span, just that one im
 1070                        }
 1071
 1072                        var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
 1073
 1074                        isResponsive = jpegCount > lastCount;
 1075                        lastCount = jpegCount;
 1076                    }
 1077
 1078                    if (!ranToCompletion)
 1079                    {
 1080                        if (!isResponsive)
 1081                        {
 1082                            _logger.LogInformation("Trickplay process unresponsive.");
 1083                        }
 1084
 1085                        _logger.LogInformation("Stopping trickplay extraction.");
 1086                        StopProcess(processWrapper, 1000);
 1087                    }
 1088                }
 1089
 1090                if (!ranToCompletion || processWrapper.ExitCode != 0)
 1091                {
 1092                    // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed 
 1093                    // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed
 1094                    try
 1095                    {
 1096                        Directory.Delete(targetDirectory, true);
 1097                    }
 1098                    catch (Exception e)
 1099                    {
 1100                        _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory)
 1101                    }
 1102
 1103                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 1104                }
 1105
 1106                return targetDirectory;
 1107            }
 1108        }
 1109
 1110        public string GetTimeParameter(long ticks)
 1111        {
 01112            var time = TimeSpan.FromTicks(ticks);
 1113
 01114            return GetTimeParameter(time);
 1115        }
 1116
 1117        public string GetTimeParameter(TimeSpan time)
 1118        {
 01119            return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
 1120        }
 1121
 1122        private void StartProcess(ProcessWrapper process)
 1123        {
 01124            process.Process.Start();
 01125            process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
 1126
 1127            lock (_runningProcessesLock)
 1128            {
 01129                _runningProcesses.Add(process);
 01130            }
 01131        }
 1132
 1133        private void StopProcess(ProcessWrapper process, int waitTimeMs)
 1134        {
 1135            try
 1136            {
 01137                if (process.Process.WaitForExit(waitTimeMs))
 1138                {
 01139                    return;
 1140                }
 1141
 01142                _logger.LogInformation("Killing ffmpeg process");
 1143
 01144                process.Process.Kill();
 01145            }
 01146            catch (InvalidOperationException)
 1147            {
 1148                // The process has already exited or
 1149                // there is no process associated with this Process object.
 01150            }
 01151            catch (Exception ex)
 1152            {
 01153                _logger.LogError(ex, "Error killing process");
 01154            }
 01155        }
 1156
 1157        private void StopProcesses()
 211158        {
 1159            List<ProcessWrapper> processes;
 1160            lock (_runningProcessesLock)
 1161            {
 211162                processes = _runningProcesses.ToList();
 211163                _runningProcesses.Clear();
 211164            }
 1165
 421166            foreach (var process in processes)
 1167            {
 01168                if (!process.HasExited)
 1169                {
 01170                    StopProcess(process, 500);
 1171                }
 1172            }
 211173        }
 1174
 1175        public string EscapeSubtitleFilterPath(string path)
 1176        {
 1177            // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
 1178            // We need to double escape
 1179
 01180            return path
 01181                .Replace('\\', '/')
 01182                .Replace(":", "\\:", StringComparison.Ordinal)
 01183                .Replace("'", @"'\\\''", StringComparison.Ordinal)
 01184                .Replace("\"", "\\\"", StringComparison.Ordinal);
 1185        }
 1186
 1187        /// <inheritdoc />
 1188        public void Dispose()
 1189        {
 211190            Dispose(true);
 211191            GC.SuppressFinalize(this);
 211192        }
 1193
 1194        /// <summary>
 1195        /// Releases unmanaged and - optionally - managed resources.
 1196        /// </summary>
 1197        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 1198        protected virtual void Dispose(bool dispose)
 1199        {
 211200            if (dispose)
 1201            {
 211202                StopProcesses();
 211203                _thumbnailResourcePool.Dispose();
 1204            }
 211205        }
 1206
 1207        /// <inheritdoc />
 1208        public Task ConvertImage(string inputPath, string outputPath)
 1209        {
 01210            throw new NotImplementedException();
 1211        }
 1212
 1213        /// <inheritdoc />
 1214        public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
 1215        {
 1216            // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VO
 01217            var allVobs = _fileSystem.GetFiles(path, true)
 01218                .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
 01219                .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
 01220                .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
 01221                .OrderBy(i => i.FullName)
 01222                .ToList();
 1223
 01224            if (titleNumber.HasValue)
 1225            {
 01226                var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
 01227                var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
 1228
 01229                if (vobs.Count > 0)
 1230                {
 01231                    return vobs.Select(i => i.FullName).ToList();
 1232                }
 1233
 01234                _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
 1235            }
 1236
 1237            // Check for multiple big titles (> 900 MB)
 01238            var titles = allVobs
 01239                .Where(vob => vob.Length >= 900 * 1024 * 1024)
 01240                .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
 01241                .Distinct()
 01242                .ToList();
 1243
 1244            // Fall back to first title if no big title is found
 01245            if (titles.Count == 0)
 1246            {
 01247                titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
 1248            }
 1249
 1250            // Aggregate all .vob files of the titles
 01251            return allVobs
 01252                .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToStr
 01253                .Select(i => i.FullName)
 01254                .Order()
 01255                .ToList();
 1256        }
 1257
 1258        /// <inheritdoc />
 1259        public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
 01260            => _blurayExaminer.GetDiscInfo(path).Files;
 1261
 1262        /// <inheritdoc />
 1263        public string GetInputPathArgument(EncodingJobInfo state)
 01264            => GetInputPathArgument(state.MediaPath, state.MediaSource);
 1265
 1266        /// <inheritdoc />
 1267        public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
 1268        {
 01269            return mediaSource.VideoType switch
 01270            {
 01271                VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource),
 01272                VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource),
 01273                _ => GetInputArgument(path, mediaSource)
 01274            };
 1275        }
 1276
 1277        /// <inheritdoc />
 1278        public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
 1279        {
 1280            // Get all playable files
 1281            IReadOnlyList<string> files;
 01282            var videoType = source.VideoType;
 01283            if (videoType == VideoType.Dvd)
 1284            {
 01285                files = GetPrimaryPlaylistVobFiles(source.Path, null);
 1286            }
 01287            else if (videoType == VideoType.BluRay)
 1288            {
 01289                files = GetPrimaryPlaylistM2tsFiles(source.Path);
 1290            }
 1291            else
 1292            {
 01293                return;
 1294            }
 1295
 1296            // Generate concat configuration entries for each file and write to file
 01297            Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
 01298            using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
 01299            foreach (var path in files)
 1300            {
 01301                var mediaInfoResult = GetMediaInfo(
 01302                    new MediaInfoRequest
 01303                    {
 01304                        MediaType = DlnaProfileType.Video,
 01305                        MediaSource = new MediaSourceInfo
 01306                        {
 01307                            Path = path,
 01308                            Protocol = MediaProtocol.File,
 01309                            VideoType = videoType
 01310                        }
 01311                    },
 01312                    CancellationToken.None).GetAwaiter().GetResult();
 1313
 01314                var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
 1315
 1316                // Add file path stanza to concat configuration
 01317                sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
 1318
 1319                // Add duration stanza to concat configuration
 01320                sw.WriteLine("duration {0}", duration);
 1321            }
 01322        }
 1323
 1324        public bool CanExtractSubtitles(string codec)
 1325        {
 1326            // TODO is there ever a case when a subtitle can't be extracted??
 01327            return true;
 1328        }
 1329
 1330        private sealed class ProcessWrapper : IDisposable
 1331        {
 1332            private readonly MediaEncoder _mediaEncoder;
 1333
 1334            private bool _disposed = false;
 1335
 1336            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
 1337            {
 1338                Process = process;
 01339                _mediaEncoder = mediaEncoder;
 01340                Process.Exited += OnProcessExited;
 01341            }
 1342
 1343            public Process Process { get; }
 1344
 1345            public bool HasExited { get; private set; }
 1346
 1347            public int? ExitCode { get; private set; }
 1348
 1349            private void OnProcessExited(object sender, EventArgs e)
 1350            {
 01351                var process = (Process)sender;
 1352
 01353                HasExited = true;
 1354
 1355                try
 1356                {
 01357                    ExitCode = process.ExitCode;
 01358                }
 01359                catch
 1360                {
 01361                }
 1362
 01363                DisposeProcess(process);
 01364            }
 1365
 1366            private void DisposeProcess(Process process)
 01367            {
 1368                lock (_mediaEncoder._runningProcessesLock)
 1369                {
 01370                    _mediaEncoder._runningProcesses.Remove(this);
 01371                }
 1372
 01373                process.Dispose();
 01374            }
 1375
 1376            public void Dispose()
 1377            {
 01378                if (!_disposed)
 1379                {
 01380                    if (Process is not null)
 1381                    {
 01382                        Process.Exited -= OnProcessExited;
 01383                        DisposeProcess(Process);
 1384                    }
 1385                }
 1386
 01387                _disposed = true;
 01388            }
 1389        }
 1390    }
 1391}

Methods/Properties

.ctor(Microsoft.Extensions.Logging.ILogger`1<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Model.MediaInfo.IBlurayExaminer,MediaBrowser.Model.Globalization.ILocalizationManager,Microsoft.Extensions.Configuration.IConfiguration,MediaBrowser.Controller.Configuration.IServerConfigurationManager)
.cctor()
get_EncoderPath()
get_ProbePath()
get_EncoderVersion()
get_IsPkeyPauseSupported()
get_IsVaapiDeviceAmd()
get_IsVaapiDeviceInteliHD()
get_IsVaapiDeviceInteli965()
get_IsVaapiDeviceSupportVulkanDrmModifier()
get_IsVaapiDeviceSupportVulkanDrmInterop()
get_IsVideoToolboxAv1DecodeAvailable()
SetFFmpegPath()
ValidatePath(System.String)
GetEncoderPathFromDirectory(System.String,System.String,System.Boolean)
SetAvailableEncoders(System.Collections.Generic.IEnumerable`1<System.String>)
SetAvailableDecoders(System.Collections.Generic.IEnumerable`1<System.String>)
SetAvailableHwaccels(System.Collections.Generic.IEnumerable`1<System.String>)
SetAvailableFilters(System.Collections.Generic.IEnumerable`1<System.String>)
SetAvailableFiltersWithOption(System.Collections.Generic.IDictionary`2<MediaBrowser.Controller.MediaEncoding.FilterOptionType,System.Boolean>)
SetAvailableBitStreamFiltersWithOption(System.Collections.Generic.IDictionary`2<MediaBrowser.Controller.MediaEncoding.BitStreamFilterOptionType,System.Boolean>)
SetMediaEncoderVersion(MediaBrowser.MediaEncoding.Encoder.EncoderValidator)
SupportsEncoder(System.String)
SupportsDecoder(System.String)
SupportsHwaccel(System.String)
SupportsFilter(System.String)
SupportsFilterWithOption(MediaBrowser.Controller.MediaEncoding.FilterOptionType)
SupportsBitStreamFilterWithOption(MediaBrowser.Controller.MediaEncoding.BitStreamFilterOptionType)
CanEncodeToAudioCodec(System.String)
CanEncodeToSubtitleCodec(System.String)
GetMediaInfo(MediaBrowser.Controller.MediaEncoding.MediaInfoRequest,System.Threading.CancellationToken)
GetExtraArguments(MediaBrowser.Controller.MediaEncoding.MediaInfoRequest)
GetInputArgument(System.Collections.Generic.IReadOnlyList`1<System.String>,MediaBrowser.Model.Dto.MediaSourceInfo)
GetInputArgument(System.String,MediaBrowser.Model.Dto.MediaSourceInfo)
GetExternalSubtitleInputArgument(System.String)
ExtractAudioImage(System.String,System.Nullable`1<System.Int32>,System.Threading.CancellationToken)
ExtractVideoImage(System.String,System.String,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,System.Nullable`1<MediaBrowser.Model.Entities.Video3DFormat>,System.Nullable`1<System.TimeSpan>,System.Threading.CancellationToken)
ExtractVideoImage(System.String,System.String,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,System.Nullable`1<System.Int32>,System.Nullable`1<MediaBrowser.Model.Drawing.ImageFormat>,System.Threading.CancellationToken)
GetImageResolutionParameter()
GetTimeParameter(System.Int64)
GetTimeParameter(System.TimeSpan)
StartProcess(MediaBrowser.MediaEncoding.Encoder.MediaEncoder/ProcessWrapper)
StopProcess(MediaBrowser.MediaEncoding.Encoder.MediaEncoder/ProcessWrapper,System.Int32)
StopProcesses()
EscapeSubtitleFilterPath(System.String)
Dispose()
Dispose(System.Boolean)
ConvertImage(System.String,System.String)
GetPrimaryPlaylistVobFiles(System.String,System.Nullable`1<System.UInt32>)
GetPrimaryPlaylistM2tsFiles(System.String)
GetInputPathArgument(MediaBrowser.Controller.MediaEncoding.EncodingJobInfo)
GetInputPathArgument(System.String,MediaBrowser.Model.Dto.MediaSourceInfo)
GenerateConcatConfig(MediaBrowser.Model.Dto.MediaSourceInfo,System.String)
CanExtractSubtitles(System.String)
.ctor(System.Diagnostics.Process,MediaBrowser.MediaEncoding.Encoder.MediaEncoder)
OnProcessExited(System.Object,System.EventArgs)
DisposeProcess(System.Diagnostics.Process)
Dispose()