< Summary - Jellyfin

Information
Class: MediaBrowser.MediaEncoding.Encoder.MediaEncoder
Assembly: MediaBrowser.MediaEncoding
File(s): /srv/git/jellyfin/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
Line coverage
12%
Covered lines: 82
Uncovered lines: 557
Coverable lines: 639
Total lines: 1482
Line coverage: 12.8%
Branch coverage
6%
Covered branches: 18
Total branches: 286
Branch coverage: 6.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 AM Line coverage: 8.7% (53/606) Branch coverage: 7.7% (21/270) Total lines: 13985/20/2026 - 12:15:44 AM Line coverage: 8.7% (53/606) Branch coverage: 5.5% (15/270) Total lines: 13985/24/2026 - 12:15:27 AM Line coverage: 8.8% (54/609) Branch coverage: 5.5% (15/272) Total lines: 14047/18/2026 - 12:15:19 AM Line coverage: 8.8% (54/610) Branch coverage: 5.5% (15/272) Total lines: 14067/21/2026 - 12:16:33 AM Line coverage: 8.7% (54/618) Branch coverage: 5.3% (15/280) Total lines: 142510/29/2026 - 12:17:11 AM Line coverage: 12.8% (82/639) Branch coverage: 6.2% (18/286) Total lines: 1482 5/16/2026 - 12:15:55 AM Line coverage: 8.7% (53/606) Branch coverage: 7.7% (21/270) Total lines: 13985/20/2026 - 12:15:44 AM Line coverage: 8.7% (53/606) Branch coverage: 5.5% (15/270) Total lines: 13985/24/2026 - 12:15:27 AM Line coverage: 8.8% (54/609) Branch coverage: 5.5% (15/272) Total lines: 14047/18/2026 - 12:15:19 AM Line coverage: 8.8% (54/610) Branch coverage: 5.5% (15/272) Total lines: 14067/21/2026 - 12:16:33 AM Line coverage: 8.7% (54/618) Branch coverage: 5.3% (15/280) Total lines: 142510/29/2026 - 12:17:11 AM Line coverage: 12.8% (82/639) Branch coverage: 6.2% (18/286) Total lines: 1482

Coverage delta

Coverage delta 5 -5

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%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(...)100%210%
GetExtraArguments(...)44.44%261870.58%
GetInputArgument(...)100%210%
GetInputArgument(...)0%620%
GetExternalSubtitleInputArgument(...)100%210%
GetMediaInfoInternal()0%506220%
ExtractAudioImage(...)100%210%
ExtractVideoImage(...)100%210%
ExtractVideoImage(...)100%210%
ExtractImage()0%620%
GetImageResolutionParameter()0%132110%
ExtractImageInternal()0%3306570%
ExtractVideoImagesOnIntervalAccelerated()0%3660600%
ExtractVideoImagesOnIntervalInternal()0%812280%
GetTimeParameter(...)100%210%
GetTimeParameter(...)100%210%
StartProcess(...)0%620%
StopProcess(...)0%620%
StopProcesses()50%4475%
EscapeSubtitleFilterPath(...)100%210%
Dispose()100%11100%
Dispose(...)50%22100%
ConvertImage(...)100%210%
GetPrimaryPlaylistVobFiles(...)0%4260%
GetPrimaryPlaylistM2tsFiles(...)100%210%
GetInputPathArgument(...)100%210%
GetInputPathArgument(...)0%4260%
GenerateConcatConfig(...)0%4260%
CanExtractSubtitles(...)100%210%
.ctor(...)100%11100%
get_HasExited()100%11100%
get_ExitCode()100%11100%
ReadExitState()33.33%14640%
OnProcessExited(...)100%1175%
Untrack()100%11100%
Dispose()50%2288.88%

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;
 11using System.Text.Json;
 12using System.Text.RegularExpressions;
 13using System.Threading;
 14using System.Threading.Tasks;
 15using AsyncKeyedLock;
 16using Jellyfin.Data.Enums;
 17using Jellyfin.Extensions;
 18using Jellyfin.Extensions.Json;
 19using Jellyfin.Extensions.Json.Converters;
 20using MediaBrowser.Common;
 21using MediaBrowser.Common.Configuration;
 22using MediaBrowser.Common.Extensions;
 23using MediaBrowser.Controller.Configuration;
 24using MediaBrowser.Controller.Extensions;
 25using MediaBrowser.Controller.MediaEncoding;
 26using MediaBrowser.MediaEncoding.Probing;
 27using MediaBrowser.Model.Configuration;
 28using MediaBrowser.Model.Dlna;
 29using MediaBrowser.Model.Drawing;
 30using MediaBrowser.Model.Dto;
 31using MediaBrowser.Model.Entities;
 32using MediaBrowser.Model.Globalization;
 33using MediaBrowser.Model.IO;
 34using MediaBrowser.Model.MediaInfo;
 35using Microsoft.Extensions.Configuration;
 36using Microsoft.Extensions.Logging;
 37
 38namespace MediaBrowser.MediaEncoding.Encoder
 39{
 40    /// <summary>
 41    /// Class MediaEncoder.
 42    /// </summary>
 43    public partial class MediaEncoder : IMediaEncoder, IDisposable
 44    {
 45        /// <summary>
 46        /// The default SDR image extraction timeout in milliseconds.
 47        /// </summary>
 48        internal const int DefaultSdrImageExtractionTimeout = 10000;
 49
 50        /// <summary>
 51        /// The default HDR image extraction timeout in milliseconds.
 52        /// </summary>
 53        internal const int DefaultHdrImageExtractionTimeout = 20000;
 54
 55        private readonly ILogger<MediaEncoder> _logger;
 56        private readonly IServerConfigurationManager _configurationManager;
 57        private readonly IFileSystem _fileSystem;
 58        private readonly ILocalizationManager _localization;
 59        private readonly IBlurayExaminer _blurayExaminer;
 60        private readonly IConfiguration _config;
 61        private readonly IServerConfigurationManager _serverConfig;
 62        private readonly string _startupOptionFFmpegPath;
 63
 64        private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
 65
 3166        private readonly Lock _runningProcessesLock = new();
 3167        private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
 68
 69        // MediaEncoder is registered as a Singleton
 70        private readonly JsonSerializerOptions _jsonSerializerOptions;
 71
 3172        private List<string> _encoders = new List<string>();
 3173        private List<string> _decoders = new List<string>();
 3174        private List<string> _hwaccels = new List<string>();
 3175        private List<string> _filters = new List<string>();
 3176        private IDictionary<FilterOptionType, bool> _filtersWithOption = new Dictionary<FilterOptionType, bool>();
 3177        private IDictionary<BitStreamFilterOptionType, bool> _bitStreamFiltersWithOption = new Dictionary<BitStreamFilte
 78
 79        private bool _isPkeyPauseSupported = false;
 80        private bool _isLowPriorityHwDecodeSupported = false;
 81        private bool _proberSupportsFirstVideoFrame = false;
 82
 83        private bool _isVaapiDeviceAmd = false;
 84        private bool _isVaapiDeviceInteliHD = false;
 85        private bool _isVaapiDeviceInteli965 = false;
 86        private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
 87        private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
 88
 3189        private bool _canSetProcessPriority = true;
 90
 91        private bool _isVideoToolboxAv1DecodeAvailable = false;
 92
 093        private static string[] _vulkanImageDrmFmtModifierExts =
 094        {
 095            "VK_EXT_image_drm_format_modifier",
 096        };
 97
 098        private static string[] _vulkanExternalMemoryDmaBufExts =
 099        {
 0100            "VK_KHR_external_memory_fd",
 0101            "VK_EXT_external_memory_dma_buf",
 0102            "VK_KHR_external_semaphore_fd",
 0103            "VK_EXT_external_memory_host"
 0104        };
 105
 106        private Version _ffmpegVersion = null;
 31107        private string _ffmpegPath = string.Empty;
 108        private string _ffprobePath;
 109        private int _threads;
 110
 111        public MediaEncoder(
 112            ILogger<MediaEncoder> logger,
 113            IServerConfigurationManager configurationManager,
 114            IFileSystem fileSystem,
 115            IBlurayExaminer blurayExaminer,
 116            ILocalizationManager localization,
 117            IConfiguration config,
 118            IServerConfigurationManager serverConfig)
 119        {
 31120            _logger = logger;
 31121            _configurationManager = configurationManager;
 31122            _fileSystem = fileSystem;
 31123            _blurayExaminer = blurayExaminer;
 31124            _localization = localization;
 31125            _config = config;
 31126            _serverConfig = serverConfig;
 31127            _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathK
 128
 31129            _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
 31130            _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
 131
 132            // Although the type is not nullable, this might still be null during unit tests
 31133            var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
 31134            if (semaphoreCount < 1)
 135            {
 31136                semaphoreCount = Environment.ProcessorCount;
 137            }
 138
 31139            _thumbnailResourcePool = new(semaphoreCount);
 31140        }
 141
 142        /// <inheritdoc />
 0143        public string EncoderPath => _ffmpegPath;
 144
 145        /// <inheritdoc />
 0146        public string ProbePath => _ffprobePath;
 147
 148        /// <inheritdoc />
 0149        public Version EncoderVersion => _ffmpegVersion;
 150
 151        /// <inheritdoc />
 0152        public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
 153
 154        /// <inheritdoc />
 0155        public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
 156
 157        /// <inheritdoc />
 0158        public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
 159
 160        /// <inheritdoc />
 0161        public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
 162
 163        /// <inheritdoc />
 0164        public bool IsVaapiDeviceSupportVulkanDrmModifier => _isVaapiDeviceSupportVulkanDrmModifier;
 165
 166        /// <inheritdoc />
 0167        public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
 168
 0169        public bool IsVideoToolboxAv1DecodeAvailable => _isVideoToolboxAv1DecodeAvailable;
 170
 171        [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
 172        private static partial Regex FfprobePathRegex();
 173
 174        /// <summary>
 175        /// Run at startup to validate ffmpeg.
 176        /// Sets global variables FFmpegPath.
 177        /// Precedence is: CLI/Env var > Config > $PATH.
 178        /// </summary>
 179        /// <returns>bool indicates whether a valid ffmpeg is found.</returns>
 180        public bool SetFFmpegPath()
 181        {
 27182            var skipValidation = _config.GetFFmpegSkipValidation();
 27183            if (skipValidation)
 184            {
 27185                _logger.LogWarning("FFmpeg: Skipping FFmpeg Validation due to FFmpeg:novalidation set to true");
 27186                return true;
 187            }
 188
 189            // 1) Check if the --ffmpeg CLI switch has been given
 0190            var ffmpegPath = _startupOptionFFmpegPath;
 0191            string ffmpegPathSetMethodText = "command line or environment variable";
 0192            if (string.IsNullOrEmpty(ffmpegPath))
 193            {
 194                // 2) Custom path stored in config/encoding xml file under tag <EncoderAppPath> should be used as a fall
 0195                ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
 0196                ffmpegPathSetMethodText = "encoding.xml config file";
 0197                if (string.IsNullOrEmpty(ffmpegPath))
 198                {
 199                    // 3) Check "ffmpeg"
 0200                    ffmpegPath = "ffmpeg";
 0201                    ffmpegPathSetMethodText = "system $PATH";
 202                }
 203            }
 204
 0205            if (!ValidatePath(ffmpegPath))
 206            {
 0207                _ffmpegPath = null;
 0208                _logger.LogError("FFmpeg: Path set by {FfmpegPathSetMethodText} is invalid", ffmpegPathSetMethodText);
 0209                return false;
 210            }
 211
 212            // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
 0213            var options = _configurationManager.GetEncodingOptions();
 0214            options.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
 0215            _configurationManager.SaveConfiguration("encoding", options);
 216
 217            // Only if mpeg path is set, try and set path to probe
 0218            if (_ffmpegPath is not null)
 219            {
 220                // Determine a probe path from the mpeg path
 0221                _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, "ffprobe$1");
 222
 223                // Interrogate to understand what coders are supported
 0224                var validator = new EncoderValidator(_logger, _ffmpegPath);
 225
 0226                SetAvailableDecoders(validator.GetDecoders());
 0227                SetAvailableEncoders(validator.GetEncoders());
 0228                SetAvailableFilters(validator.GetFilters());
 0229                SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
 0230                SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption());
 0231                SetAvailableHwaccels(validator.GetHwaccels());
 0232                SetMediaEncoderVersion(validator);
 233
 0234                _threads = EncodingHelper.GetNumberOfThreads(null, options, null);
 235
 0236                _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p      pause transcoding", _ffmpegVersion);
 0237                _isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
 0238                _proberSupportsFirstVideoFrame = validator.CheckSupportedProberOption("only_first_vframe", _ffprobePath)
 239
 240                // Check the Vaapi device vendor
 0241                if (OperatingSystem.IsLinux()
 0242                    && SupportsHwaccel("vaapi")
 0243                    && !string.IsNullOrEmpty(options.VaapiDevice)
 0244                    && options.HardwareAccelerationType == HardwareAccelerationType.vaapi)
 245                {
 0246                    _isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevic
 0247                    _isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDev
 0248                    _isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiD
 0249                    _isVaapiDeviceSupportVulkanDrmModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.Vaapi
 0250                    _isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiD
 251
 0252                    if (_isVaapiDeviceAmd)
 253                    {
 0254                        _logger.LogInformation("VAAPI device {RenderNodePath} is AMD GPU", options.VaapiDevice);
 255                    }
 0256                    else if (_isVaapiDeviceInteliHD)
 257                    {
 0258                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (iHD)", options.VaapiDevice);
 259                    }
 0260                    else if (_isVaapiDeviceInteli965)
 261                    {
 0262                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice)
 263                    }
 264
 0265                    if (_isVaapiDeviceSupportVulkanDrmModifier)
 266                    {
 0267                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM modifier", options.Vaa
 268                    }
 269
 0270                    if (_isVaapiDeviceSupportVulkanDrmInterop)
 271                    {
 0272                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.Vaap
 273                    }
 274                }
 275
 276                // Check if VideoToolbox supports AV1 decode
 0277                if (OperatingSystem.IsMacOS() && SupportsHwaccel("videotoolbox"))
 278                {
 0279                    _isVideoToolboxAv1DecodeAvailable = validator.CheckIsVideoToolboxAv1DecodeAvailable();
 280                }
 281            }
 282
 0283            _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
 0284            return !string.IsNullOrWhiteSpace(ffmpegPath);
 285        }
 286
 287        /// <summary>
 288        /// Validates the supplied FQPN to ensure it is a ffmpeg utility.
 289        /// If checks pass, global variable FFmpegPath is updated.
 290        /// </summary>
 291        /// <param name="path">FQPN to test.</param>
 292        /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
 293        private bool ValidatePath(string path)
 294        {
 0295            if (string.IsNullOrEmpty(path))
 296            {
 0297                return false;
 298            }
 299
 0300            bool rc = new EncoderValidator(_logger, path).ValidateVersion();
 0301            if (!rc)
 302            {
 0303                _logger.LogError("FFmpeg: Failed version check: {Path}", path);
 0304                return false;
 305            }
 306
 0307            _ffmpegPath = path;
 0308            return true;
 309        }
 310
 311        private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false)
 312        {
 313            try
 314            {
 0315                var files = _fileSystem.GetFilePaths(path, recursive);
 316
 0317                return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringCom
 0318                                                    && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.Ordi
 319            }
 0320            catch (Exception)
 321            {
 322                // Trap all exceptions, like DirNotExists, and return null
 0323                return null;
 324            }
 0325        }
 326
 327        public void SetAvailableEncoders(IEnumerable<string> list)
 328        {
 0329            _encoders = list.ToList();
 0330        }
 331
 332        public void SetAvailableDecoders(IEnumerable<string> list)
 333        {
 0334            _decoders = list.ToList();
 0335        }
 336
 337        public void SetAvailableHwaccels(IEnumerable<string> list)
 338        {
 0339            _hwaccels = list.ToList();
 0340        }
 341
 342        public void SetAvailableFilters(IEnumerable<string> list)
 343        {
 0344            _filters = list.ToList();
 0345        }
 346
 347        public void SetAvailableFiltersWithOption(IDictionary<FilterOptionType, bool> dict)
 348        {
 0349            _filtersWithOption = dict;
 0350        }
 351
 352        public void SetAvailableBitStreamFiltersWithOption(IDictionary<BitStreamFilterOptionType, bool> dict)
 353        {
 0354            _bitStreamFiltersWithOption = dict;
 0355        }
 356
 357        public void SetMediaEncoderVersion(EncoderValidator validator)
 358        {
 0359            _ffmpegVersion = validator.GetFFmpegVersion();
 0360        }
 361
 362        /// <inheritdoc />
 363        public bool SupportsEncoder(string encoder)
 364        {
 0365            return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
 366        }
 367
 368        /// <inheritdoc />
 369        public bool SupportsDecoder(string decoder)
 370        {
 0371            return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase);
 372        }
 373
 374        /// <inheritdoc />
 375        public bool SupportsHwaccel(string hwaccel)
 376        {
 0377            return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
 378        }
 379
 380        /// <inheritdoc />
 381        public bool SupportsFilter(string filter)
 382        {
 0383            return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
 384        }
 385
 386        /// <inheritdoc />
 387        public bool SupportsFilterWithOption(FilterOptionType option)
 388        {
 0389            return _filtersWithOption.TryGetValue(option, out var val) && val;
 390        }
 391
 392        public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option)
 393        {
 0394            return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val;
 395        }
 396
 397        public bool CanEncodeToAudioCodec(string codec)
 398        {
 0399            if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
 400            {
 0401                codec = "libopus";
 402            }
 0403            else if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
 404            {
 0405                codec = "libmp3lame";
 406            }
 407
 0408            return SupportsEncoder(codec);
 409        }
 410
 411        public bool CanEncodeToSubtitleCodec(string codec)
 412        {
 413            // TODO
 0414            return true;
 415        }
 416
 417        /// <inheritdoc />
 418        public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
 419        {
 0420            var extractChapters = request.ExtractChapters;
 0421            var extraArgs = GetExtraArguments(request);
 422
 0423            return GetMediaInfoInternal(
 0424                GetInputArgument(request.MediaSource.Path, request.MediaSource),
 0425                request.MediaSource.Path,
 0426                request.MediaSource.Protocol,
 0427                extractChapters,
 0428                extraArgs,
 0429                request.MediaType == DlnaProfileType.Audio,
 0430                request.MediaSource.VideoType,
 0431                cancellationToken);
 432        }
 433
 434        internal string GetExtraArguments(MediaInfoRequest request)
 435        {
 1436            var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
 1437            var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
 1438            var analyzeDuration = string.Empty;
 1439            var extraArgs = string.Empty;
 440
 1441            if (request.MediaSource.AnalyzeDurationMs > 0)
 442            {
 0443                analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000);
 444            }
 1445            else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
 446            {
 0447                analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
 448            }
 449
 1450            if (!string.IsNullOrEmpty(analyzeDuration))
 451            {
 0452                extraArgs = analyzeDuration;
 453            }
 454
 1455            if (!string.IsNullOrEmpty(ffmpegProbeSize))
 456            {
 0457                extraArgs += " -probesize " + ffmpegProbeSize;
 458            }
 459
 1460            if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
 461            {
 1462                extraArgs += $" -user_agent \"{userAgent}\"";
 463            }
 464
 1465            if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
 466            {
 0467                extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
 468            }
 469
 1470            return extraArgs;
 471        }
 472
 473        /// <inheritdoc />
 474        public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource)
 475        {
 0476            return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol);
 477        }
 478
 479        /// <inheritdoc />
 480        public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
 481        {
 0482            var prefix = "file";
 0483            if (mediaSource.IsoType == IsoType.BluRay)
 484            {
 0485                prefix = "bluray";
 486            }
 487
 0488            return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol);
 489        }
 490
 491        /// <inheritdoc />
 492        public string GetExternalSubtitleInputArgument(string inputFile)
 493        {
 494            const string Prefix = "file";
 495
 0496            return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File);
 497        }
 498
 499        /// <summary>
 500        /// Gets the media info internal.
 501        /// </summary>
 502        /// <returns>Task{MediaInfoResult}.</returns>
 503        private async Task<MediaInfo> GetMediaInfoInternal(
 504            string inputPath,
 505            string primaryPath,
 506            MediaProtocol protocol,
 507            bool extractChapters,
 508            string probeSizeArgument,
 509            bool isAudio,
 510            VideoType? videoType,
 511            CancellationToken cancellationToken)
 512        {
 0513            var args = extractChapters
 0514                ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
 0515                : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
 516
 0517            if (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame)
 518            {
 0519                args += " -show_frames -only_first_vframe";
 520            }
 521
 0522            args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
 523
 0524            var process = new Process
 0525            {
 0526                StartInfo = new ProcessStartInfo
 0527                {
 0528                    CreateNoWindow = true,
 0529                    UseShellExecute = false,
 0530
 0531                    // Must consume both or ffmpeg may hang due to deadlocks.
 0532                    StandardOutputEncoding = Encoding.UTF8,
 0533                    RedirectStandardOutput = true,
 0534
 0535                    FileName = _ffprobePath,
 0536                    Arguments = args,
 0537
 0538                    WindowStyle = ProcessWindowStyle.Hidden,
 0539                    ErrorDialog = false,
 0540                },
 0541                EnableRaisingEvents = true
 0542            };
 543
 0544            _logger.LogDebug("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
 545
 0546            var memoryStream = new MemoryStream();
 0547            await using (memoryStream.ConfigureAwait(false))
 0548            using (var processWrapper = new ProcessWrapper(process, this))
 549            {
 0550                StartProcess(processWrapper);
 0551                using var reader = process.StandardOutput;
 0552                await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
 0553                memoryStream.Seek(0, SeekOrigin.Begin);
 554                InternalMediaInfoResult result;
 555                try
 556                {
 0557                    result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
 0558                                        memoryStream,
 0559                                        _jsonSerializerOptions,
 0560                                        cancellationToken).ConfigureAwait(false);
 0561                }
 0562                catch
 563                {
 0564                    StopProcess(processWrapper, 100);
 565
 0566                    throw;
 567                }
 568
 0569                if (result is null || (result.Streams is null && result.Format is null))
 570                {
 0571                    throw new FfmpegException("ffprobe failed - streams and format are both null.");
 572                }
 573
 0574                if (result.Streams is not null)
 575                {
 576                    // Normalize aspect ratio if invalid
 0577                    foreach (var stream in result.Streams)
 578                    {
 0579                        if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 580                        {
 0581                            stream.DisplayAspectRatio = string.Empty;
 582                        }
 583
 0584                        if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 585                        {
 0586                            stream.SampleAspectRatio = string.Empty;
 587                        }
 588                    }
 589                }
 590
 0591                return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primar
 592            }
 0593        }
 594
 595        /// <inheritdoc />
 596        public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
 597        {
 0598            var mediaSource = new MediaSourceInfo
 0599            {
 0600                Protocol = MediaProtocol.File
 0601            };
 602
 0603            return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, canc
 604        }
 605
 606        /// <inheritdoc />
 607        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 608        {
 0609            return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, Image
 610        }
 611
 612        /// <inheritdoc />
 613        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 614        {
 0615            return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, tar
 616        }
 617
 618        private async Task<string> ExtractImage(
 619            string inputFile,
 620            string container,
 621            MediaStream videoStream,
 622            int? imageStreamIndex,
 623            MediaSourceInfo mediaSource,
 624            bool isAudio,
 625            Video3DFormat? threedFormat,
 626            TimeSpan? offset,
 627            ImageFormat? targetFormat,
 628            CancellationToken cancellationToken)
 629        {
 0630            var inputArgument = GetInputPathArgument(inputFile, mediaSource);
 631
 0632            if (!isAudio)
 633            {
 634                try
 635                {
 0636                    return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFor
 637                }
 0638                catch (ArgumentException)
 639                {
 0640                    throw;
 641                }
 0642                catch (Exception ex)
 643                {
 0644                    _logger.LogWarning(ex, "I-frame image extraction failed, will attempt standard way. Input: {Argument
 0645                }
 646            }
 647
 0648            return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, off
 0649        }
 650
 651        private string GetImageResolutionParameter()
 652        {
 0653            var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
 0654            {
 0655                ImageResolution.P144 => "256x144",
 0656                ImageResolution.P240 => "426x240",
 0657                ImageResolution.P360 => "640x360",
 0658                ImageResolution.P480 => "854x480",
 0659                ImageResolution.P720 => "1280x720",
 0660                ImageResolution.P1080 => "1920x1080",
 0661                ImageResolution.P1440 => "2560x1440",
 0662                ImageResolution.P2160 => "3840x2160",
 0663                _ => string.Empty
 0664            };
 665
 0666            if (!string.IsNullOrEmpty(imageResolutionParameter))
 667            {
 0668                imageResolutionParameter = " -s " + imageResolutionParameter;
 669            }
 670
 0671            return imageResolutionParameter;
 672        }
 673
 674        private async Task<string> ExtractImageInternal(
 675            string inputPath,
 676            string container,
 677            MediaStream videoStream,
 678            int? imageStreamIndex,
 679            Video3DFormat? threedFormat,
 680            TimeSpan? offset,
 681            bool useIFrame,
 682            ImageFormat? targetFormat,
 683            bool isAudio,
 684            CancellationToken cancellationToken)
 685        {
 0686            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 687
 0688            var useTradeoff = _config.GetFFmpegImgExtractPerfTradeoff();
 689
 0690            var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
 691
 0692            var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ou
 0693            Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
 694
 695            // deint -> scale -> thumbnail -> tonemap.
 696            // put the SW tonemap right after the thumbnail to do it only once to reduce cpu usage.
 0697            var filters = new List<string>();
 698
 699            // deinterlace using bwdif algorithm for video stream.
 0700            if (videoStream is not null && videoStream.IsInterlaced)
 701            {
 0702                filters.Add("bwdif=0:-1:0");
 703            }
 704
 705            // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the cor
 706            // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex
 0707            var scaler = threedFormat switch
 0708            {
 0709                // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may 
 0710                Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min
 0711                // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
 0712                Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw
 0713                // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may
 0714                Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):m
 0715                // ftab crop height in half, set the display aspect,crop out any black bars we may have made
 0716                Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(
 0717                _ => "scale=round(iw*sar/2)*2:round(ih/2)*2"
 0718            };
 719
 0720            filters.Add(scaler);
 721
 722            // Use ffmpeg to sample N frames and pick the best thumbnail. Have a fall back just in case.
 0723            var enableThumbnail = !useTradeoff && useIFrame && !string.Equals("wtv", container, StringComparison.Ordinal
 0724            if (enableThumbnail)
 725            {
 0726                filters.Add("thumbnail=n=24");
 727            }
 728
 729            // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
 730            // Only enable Dolby Vision tonemap when tonemapx is available
 0731            var enableHdrExtraction = false;
 732
 0733            if (videoStream?.VideoRange == VideoRange.HDR)
 734            {
 0735                if (SupportsFilter("tonemapx"))
 736                {
 0737                    var peak = videoStream.VideoRangeType == VideoRangeType.DOVI ? "400" : "100";
 0738                    enableHdrExtraction = true;
 0739                    filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p:ran
 740                }
 0741                else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
 742                {
 0743                    enableHdrExtraction = true;
 0744                    filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:p
 745                }
 746            }
 747
 0748            var vf = string.Join(',', filters);
 0749            var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.Invariant
 0750            var args = string.Format(
 0751                CultureInfo.InvariantCulture,
 0752                "-i {0}{1} -threads {2} -v quiet -vframes 1 -vf {3}{4}{5} -f image2 \"{6}\"",
 0753                inputPath,
 0754                mapArg,
 0755                _threads,
 0756                vf,
 0757                isAudio ? string.Empty : GetImageResolutionParameter(),
 0758                EncodingHelper.GetVideoSyncOption("-1", EncoderVersion), // auto decide fps mode
 0759                tempExtractPath);
 760
 0761            if (offset.HasValue)
 762            {
 0763                args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
 764            }
 765
 766            // The mpegts demuxer cannot seek to keyframes, so we have to let the
 767            // decoder discard non-keyframes, which may contain corrupted images.
 0768            var seekMpegTs = offset.HasValue && string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
 0769            if (useIFrame && (useTradeoff || seekMpegTs))
 770            {
 0771                args = "-skip_frame nokey " + args;
 772            }
 773
 0774            if (!string.IsNullOrWhiteSpace(container))
 775            {
 0776                var inputFormat = EncodingHelper.GetInputFormat(container);
 0777                if (!string.IsNullOrWhiteSpace(inputFormat))
 778                {
 0779                    args = "-f " + inputFormat + " " + args;
 780                }
 781            }
 782
 0783            var process = new Process
 0784            {
 0785                StartInfo = new ProcessStartInfo
 0786                {
 0787                    CreateNoWindow = true,
 0788                    UseShellExecute = false,
 0789                    FileName = _ffmpegPath,
 0790                    Arguments = args,
 0791                    WindowStyle = ProcessWindowStyle.Hidden,
 0792                    ErrorDialog = false,
 0793                },
 0794                EnableRaisingEvents = true
 0795            };
 796
 0797            _logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Argum
 798
 0799            using (var processWrapper = new ProcessWrapper(process, this))
 800            {
 0801                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 802                {
 0803                    StartProcess(processWrapper);
 804
 0805                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 0806                    if (timeoutMs <= 0)
 807                    {
 0808                        timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTi
 809                    }
 810
 811                    try
 812                    {
 0813                        await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 0814                    }
 0815                    catch (OperationCanceledException ex)
 816                    {
 0817                        process.Kill(true);
 0818                        throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction t
 819                    }
 0820                }
 821
 0822                var file = _fileSystem.GetFileInfo(tempExtractPath);
 823
 0824                if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
 825                {
 0826                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 827                }
 828
 0829                return tempExtractPath;
 830            }
 0831        }
 832
 833        /// <inheritdoc />
 834        public async Task<string> ExtractVideoImagesOnIntervalAccelerated(
 835            string inputFile,
 836            string container,
 837            MediaSourceInfo mediaSource,
 838            MediaStream imageStream,
 839            int maxWidth,
 840            TimeSpan interval,
 841            bool allowHwAccel,
 842            bool enableHwEncoding,
 843            int? threads,
 844            int? qualityScale,
 845            ProcessPriorityClass? priority,
 846            bool enableKeyFrameOnlyExtraction,
 847            EncodingHelper encodingHelper,
 848            CancellationToken cancellationToken)
 849        {
 0850            var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
 0851            threads ??= _threads;
 852
 0853            if (allowHwAccel && enableKeyFrameOnlyExtraction)
 854            {
 0855                var hardwareAccelerationType = options.HardwareAccelerationType;
 0856                var supportsKeyFrameOnly = (hardwareAccelerationType == HardwareAccelerationType.nvenc && options.Enable
 0857                                           || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSyst
 0858                                           || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.Prefe
 0859                                           || hardwareAccelerationType == HardwareAccelerationType.vaapi
 0860                                           || hardwareAccelerationType == HardwareAccelerationType.videotoolbox
 0861                                           || hardwareAccelerationType == HardwareAccelerationType.rkmpp;
 0862                if (!supportsKeyFrameOnly)
 863                {
 864                    // Disable hardware acceleration when the hardware decoder does not support keyframe only mode.
 0865                    allowHwAccel = false;
 0866                    options = new EncodingOptions();
 867                }
 868            }
 869
 870            // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
 871            // Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
 0872            if (!allowHwAccel)
 873            {
 0874                options.EnableHardwareEncoding = false;
 0875                options.HardwareAccelerationType = HardwareAccelerationType.none;
 0876                options.EnableTonemapping = false;
 877            }
 878
 0879            if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.Asp
 880            {
 881                // For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimen
 0882                var darParts = imageStream.AspectRatio.Split(':');
 0883                var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], Cultu
 884                // When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
 885                // Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, 
 0886                var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05
 0887                if (shouldResetHeight)
 888                {
 889                    // SAR = DAR * Height / Width
 890                    // RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
 0891                    imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
 892                }
 893            }
 894
 0895            var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.To
 0896            var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
 0897            {
 0898                IsVideoRequest = true,  // must be true for InputVideoHwaccelArgs to return non-empty value
 0899                MediaSource = mediaSource,
 0900                VideoStream = imageStream,
 0901                BaseRequest = baseRequest,  // GetVideoProcessingFilterParam errors if null
 0902                MediaPath = inputFile,
 0903                OutputVideoCodec = "mjpeg"
 0904            };
 0905            var vidEncoder = enableHwEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideo
 906
 907            // Get input and filter arguments
 0908            var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
 0909            if (string.IsNullOrWhiteSpace(inputArg))
 910            {
 0911                throw new InvalidOperationException("EncodingHelper returned empty input arguments.");
 912            }
 913
 0914            if (!allowHwAccel)
 915            {
 0916                inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, 
 917            }
 918
 0919            if (options.HardwareAccelerationType == HardwareAccelerationType.videotoolbox && _isLowPriorityHwDecodeSuppo
 920            {
 921                // VideoToolbox supports low priority decoding, which is useful for trickplay
 0922                inputArg = "-hwaccel_flags +low_priority " + inputArg;
 923            }
 924
 925            // Force the video stream, otherwise ffmpeg may pick a cover image.
 0926            var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, imageStream);
 0927            if (streamIndex < 0)
 928            {
 0929                throw new InvalidOperationException($"Unable to locate requested stream {imageStream.Title}");
 930            }
 931
 0932            inputArg += " -map 0:" + streamIndex;
 933
 0934            var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
 0935            if (string.IsNullOrWhiteSpace(filterParam))
 936            {
 0937                throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
 938            }
 939
 940            // Normalize invalid PTS from containers for non keyframe only mode
 0941            if (!enableKeyFrameOnlyExtraction)
 942            {
 0943                var fpsFilterIndex = filterParam.IndexOf("fps=", StringComparison.Ordinal);
 0944                if (fpsFilterIndex >= 0)
 945                {
 0946                    var inputFrameRate = (imageStream.ReferenceFrameRate.HasValue && imageStream.ReferenceFrameRate > 0)
 0947                        ? imageStream.ReferenceFrameRate.Value : 30;
 948
 0949                    var setPtsFilter = string.Create(CultureInfo.InvariantCulture, $"setpts=N/{inputFrameRate:F3}/TB,");
 950
 0951                    filterParam = filterParam.Insert(fpsFilterIndex, setPtsFilter);
 952                }
 953                else
 954                {
 0955                    throw new InvalidOperationException("EncodingHelper returned invalid filter parameters.");
 956                }
 957            }
 958
 959            try
 960            {
 0961                return await ExtractVideoImagesOnIntervalInternal(
 0962                    (enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg,
 0963                    filterParam,
 0964                    vidEncoder,
 0965                    threads,
 0966                    qualityScale,
 0967                    priority,
 0968                    cancellationToken).ConfigureAwait(false);
 969            }
 0970            catch (FfmpegException ex)
 971            {
 0972                if (!enableKeyFrameOnlyExtraction)
 973                {
 0974                    throw;
 975                }
 976
 0977                _logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFil
 0978            }
 979
 0980            return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, 
 0981        }
 982
 983        private async Task<string> ExtractVideoImagesOnIntervalInternal(
 984            string inputArg,
 985            string filterParam,
 986            string vidEncoder,
 987            int? outputThreads,
 988            int? qualityScale,
 989            ProcessPriorityClass? priority,
 990            CancellationToken cancellationToken)
 991        {
 0992            if (string.IsNullOrWhiteSpace(inputArg))
 993            {
 0994                throw new InvalidOperationException("Empty or invalid input argument.");
 995            }
 996
 997            // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
 998            // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
 0999            var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
 01000            var encoderQualityOption = "-qscale:v ";
 1001
 01002            if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
 01003                || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
 1004            {
 1005                // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
 01006                encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
 01007                encoderQualityOption = "-global_quality:v ";
 1008            }
 1009
 01010            if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
 1011            {
 1012                // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qs
 01013                encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
 1014            }
 1015
 01016            if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
 1017            {
 1018                // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qsca
 01019                encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
 01020                encoderQualityOption = "-qp_init:v ";
 1021            }
 1022
 1023            // Output arguments
 01024            var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToSt
 01025            Directory.CreateDirectory(targetDirectory);
 01026            var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
 1027
 1028            // Final command arguments
 01029            var args = string.Format(
 01030                CultureInfo.InvariantCulture,
 01031                "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"",
 01032                inputArg,
 01033                filterParam,
 01034                outputThreads.GetValueOrDefault(_threads),
 01035                vidEncoder,
 01036                encoderQualityOption + encoderQuality + " ",
 01037                vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : stri
 01038                EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim() + " ", // passthrough timestamp
 01039                "image2",
 01040                outputPath);
 1041
 1042            // Start ffmpeg process
 01043            var process = new Process
 01044            {
 01045                StartInfo = new ProcessStartInfo
 01046                {
 01047                    CreateNoWindow = true,
 01048                    UseShellExecute = false,
 01049                    FileName = _ffmpegPath,
 01050                    Arguments = args,
 01051                    WindowStyle = ProcessWindowStyle.Hidden,
 01052                    ErrorDialog = false,
 01053                },
 01054                EnableRaisingEvents = true
 01055            };
 1056
 01057            var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, 
 01058            _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
 1059
 01060            using (var processWrapper = new ProcessWrapper(process, this))
 1061            {
 01062                bool ranToCompletion = false;
 1063
 01064                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 1065                {
 01066                    StartProcess(processWrapper);
 1067
 1068                    // Set process priority
 01069                    if (priority.HasValue)
 1070                    {
 1071                        try
 1072                        {
 01073                            processWrapper.Process.PriorityClass = priority.Value;
 01074                        }
 01075                        catch (Exception ex)
 1076                        {
 01077                            _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", prior
 01078                        }
 1079                    }
 1080
 1081                    // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
 1082                    // but we still need to detect if the process hangs.
 1083                    // Making the assumption that as long as new jpegs are showing up, everything is good.
 1084
 01085                    bool isResponsive = true;
 01086                    int lastCount = 0;
 01087                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 01088                    timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
 1089
 01090                    while (isResponsive && !cancellationToken.IsCancellationRequested)
 1091                    {
 1092                        try
 1093                        {
 01094                            await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 1095
 01096                            ranToCompletion = true;
 01097                            break;
 1098                        }
 01099                        catch (OperationCanceledException)
 1100                        {
 1101                            // We don't actually expect the process to be finished in one timeout span, just that one im
 01102                        }
 1103
 01104                        var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
 1105
 01106                        isResponsive = jpegCount > lastCount;
 01107                        lastCount = jpegCount;
 1108                    }
 1109
 01110                    if (!ranToCompletion)
 1111                    {
 01112                        if (!isResponsive)
 1113                        {
 01114                            _logger.LogInformation("Trickplay process unresponsive.");
 1115                        }
 1116
 01117                        _logger.LogInformation("Stopping trickplay extraction.");
 01118                        StopProcess(processWrapper, 1000);
 1119                    }
 01120                }
 1121
 01122                if (!ranToCompletion || processWrapper.ExitCode != 0)
 1123                {
 1124                    // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed 
 1125                    // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed
 1126                    try
 1127                    {
 01128                        Directory.Delete(targetDirectory, true);
 01129                    }
 01130                    catch (Exception e)
 1131                    {
 01132                        _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory)
 01133                    }
 1134
 01135                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 1136                }
 1137
 01138                return targetDirectory;
 1139            }
 01140        }
 1141
 1142        public string GetTimeParameter(long ticks)
 1143        {
 01144            var time = TimeSpan.FromTicks(ticks);
 1145
 01146            return GetTimeParameter(time);
 1147        }
 1148
 1149        public string GetTimeParameter(TimeSpan time)
 1150        {
 01151            return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
 1152        }
 1153
 1154        private void StartProcess(ProcessWrapper process)
 1155        {
 01156            process.Process.Start();
 1157
 01158            if (_canSetProcessPriority)
 1159            {
 1160                try
 1161                {
 01162                    process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
 01163                }
 01164                catch (InvalidOperationException)
 1165                {
 1166                    // The process finished before its priority could be lowered. That says nothing
 1167                    // about whether the platform allows it, so keep the capability for the next one.
 01168                }
 01169                catch (Exception ex)
 1170                {
 01171                    _canSetProcessPriority = false;
 01172                    _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}. Further
 01173                }
 1174            }
 1175
 1176            lock (_runningProcessesLock)
 1177            {
 01178                _runningProcesses.Add(process);
 01179            }
 01180        }
 1181
 1182        private void StopProcess(ProcessWrapper process, int waitTimeMs)
 1183        {
 1184            try
 1185            {
 01186                if (process.Process.WaitForExit(waitTimeMs))
 1187                {
 01188                    return;
 1189                }
 1190
 01191                _logger.LogInformation("Killing ffmpeg process");
 1192
 01193                process.Process.Kill();
 01194            }
 01195            catch (InvalidOperationException)
 1196            {
 1197                // The process has already exited or
 1198                // there is no process associated with this Process object.
 01199            }
 01200            catch (Exception ex)
 1201            {
 01202                _logger.LogError(ex, "Error killing process");
 01203            }
 01204        }
 1205
 1206        private void StopProcesses()
 271207        {
 1208            List<ProcessWrapper> processes;
 1209            lock (_runningProcessesLock)
 1210            {
 271211                processes = _runningProcesses.ToList();
 271212                _runningProcesses.Clear();
 271213            }
 1214
 541215            foreach (var process in processes)
 1216            {
 01217                if (!process.HasExited)
 1218                {
 01219                    StopProcess(process, 500);
 1220                }
 1221            }
 271222        }
 1223
 1224        public string EscapeSubtitleFilterPath(string path)
 1225        {
 1226            // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
 1227            // We need to double escape
 1228
 01229            return path
 01230                .Replace('\\', '/')
 01231                .Replace(":", "\\:", StringComparison.Ordinal)
 01232                .Replace("'", @"'\\\''", StringComparison.Ordinal)
 01233                .Replace("\"", "\\\"", StringComparison.Ordinal);
 1234        }
 1235
 1236        /// <inheritdoc />
 1237        public void Dispose()
 1238        {
 271239            Dispose(true);
 271240            GC.SuppressFinalize(this);
 271241        }
 1242
 1243        /// <summary>
 1244        /// Releases unmanaged and - optionally - managed resources.
 1245        /// </summary>
 1246        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 1247        protected virtual void Dispose(bool dispose)
 1248        {
 271249            if (dispose)
 1250            {
 271251                StopProcesses();
 271252                _thumbnailResourcePool.Dispose();
 1253            }
 271254        }
 1255
 1256        /// <inheritdoc />
 1257        public Task ConvertImage(string inputPath, string outputPath)
 1258        {
 01259            throw new NotImplementedException();
 1260        }
 1261
 1262        /// <inheritdoc />
 1263        public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
 1264        {
 1265            // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VO
 01266            var allVobs = _fileSystem.GetFiles(path, true)
 01267                .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
 01268                .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
 01269                .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
 01270                .OrderBy(i => i.FullName)
 01271                .ToList();
 1272
 01273            if (titleNumber.HasValue)
 1274            {
 01275                var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
 01276                var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
 1277
 01278                if (vobs.Count > 0)
 1279                {
 01280                    return vobs.Select(i => i.FullName).ToList();
 1281                }
 1282
 01283                _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
 1284            }
 1285
 1286            // Check for multiple big titles (> 900 MB)
 01287            var titles = allVobs
 01288                .Where(vob => vob.Length >= 900 * 1024 * 1024)
 01289                .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
 01290                .Distinct()
 01291                .ToList();
 1292
 1293            // Fall back to first title if no big title is found
 01294            if (titles.Count == 0)
 1295            {
 01296                titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
 1297            }
 1298
 1299            // Aggregate all .vob files of the titles
 01300            return allVobs
 01301                .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToStr
 01302                .Select(i => i.FullName)
 01303                .Order()
 01304                .ToList();
 1305        }
 1306
 1307        /// <inheritdoc />
 1308        public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
 01309            => _blurayExaminer.GetDiscInfo(path).Files;
 1310
 1311        /// <inheritdoc />
 1312        public string GetInputPathArgument(EncodingJobInfo state)
 01313            => GetInputPathArgument(state.MediaPath, state.MediaSource);
 1314
 1315        /// <inheritdoc />
 1316        public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
 1317        {
 01318            return mediaSource.VideoType switch
 01319            {
 01320                VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource),
 01321                VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource),
 01322                _ => GetInputArgument(path, mediaSource)
 01323            };
 1324        }
 1325
 1326        /// <inheritdoc />
 1327        public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
 1328        {
 1329            // Get all playable files
 1330            IReadOnlyList<string> files;
 01331            var videoType = source.VideoType;
 01332            if (videoType == VideoType.Dvd)
 1333            {
 01334                files = GetPrimaryPlaylistVobFiles(source.Path, null);
 1335            }
 01336            else if (videoType == VideoType.BluRay)
 1337            {
 01338                files = GetPrimaryPlaylistM2tsFiles(source.Path);
 1339            }
 1340            else
 1341            {
 01342                return;
 1343            }
 1344
 1345            // Generate concat configuration entries for each file and write to file
 01346            Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
 01347            using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
 01348            foreach (var path in files)
 1349            {
 01350                var mediaInfoResult = GetMediaInfo(
 01351                    new MediaInfoRequest
 01352                    {
 01353                        MediaType = DlnaProfileType.Video,
 01354                        MediaSource = new MediaSourceInfo
 01355                        {
 01356                            Path = path,
 01357                            Protocol = MediaProtocol.File,
 01358                            VideoType = videoType
 01359                        }
 01360                    },
 01361                    CancellationToken.None).GetAwaiter().GetResult();
 1362
 01363                var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
 1364
 1365                // Add file path stanza to concat configuration
 01366                sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
 1367
 1368                // Add duration stanza to concat configuration
 01369                sw.WriteLine("duration {0}", duration);
 1370            }
 01371        }
 1372
 1373        public bool CanExtractSubtitles(string codec)
 1374        {
 01375            return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction;
 1376        }
 1377
 1378        internal sealed class ProcessWrapper : IDisposable
 1379        {
 1380            private readonly MediaEncoder _mediaEncoder;
 1381
 1382            // The exit event is raised on the thread pool, so it writes the state below while the
 1383            // caller that started the process is reading it.
 31384            private readonly Lock _exitLock = new();
 1385
 1386            private bool _disposed = false;
 1387
 1388            private bool _hasExited;
 1389
 1390            private int? _exitCode;
 1391
 1392            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
 1393            {
 1394                Process = process;
 31395                _mediaEncoder = mediaEncoder;
 31396                Process.Exited += OnProcessExited;
 31397            }
 1398
 1399            public Process Process { get; }
 1400
 1401            // The exit event can lag behind the wait that returned, so ask the process rather than
 1402            // report one that has exited as still running.
 31403            public bool HasExited => ReadExitState().HasExited;
 1404
 1405            // As above: rather than report no exit code for a process that has one.
 41406            public int? ExitCode => ReadExitState().ExitCode;
 1407
 1408            private (bool HasExited, int? ExitCode) ReadExitState()
 71409            {
 1410                lock (_exitLock)
 1411                {
 71412                    if (!_hasExited && !_disposed)
 1413                    {
 1414                        try
 1415                        {
 01416                            if (Process.HasExited)
 1417                            {
 01418                                _hasExited = true;
 01419                                _exitCode = Process.ExitCode;
 1420                            }
 01421                        }
 01422                        catch (InvalidOperationException)
 1423                        {
 1424                            // No process is associated with this object, or it was disposed from
 1425                            // under us - ObjectDisposedException derives from this one.
 01426                        }
 1427                    }
 1428
 71429                    return (_hasExited, _exitCode);
 1430                }
 71431            }
 1432
 1433            private void OnProcessExited(object sender, EventArgs e)
 1434            {
 31435                var process = (Process)sender;
 1436
 1437                lock (_exitLock)
 1438                {
 31439                    _hasExited = true;
 1440
 1441                    try
 1442                    {
 31443                        _exitCode = process.ExitCode;
 31444                    }
 01445                    catch
 1446                    {
 01447                    }
 1448                }
 1449
 1450                // Only stop tracking it. The caller that started the process still holds it to read
 1451                // its output and its exit code, so disposing it here handed whoever was quickest to
 1452                // exit - an ffprobe on a file it rejects outright - an ObjectDisposedException.
 31453                Untrack();
 31454            }
 1455
 1456            private void Untrack()
 61457            {
 1458                lock (_mediaEncoder._runningProcessesLock)
 1459                {
 61460                    _mediaEncoder._runningProcesses.Remove(this);
 61461                }
 61462            }
 1463
 1464            public void Dispose()
 31465            {
 1466                lock (_exitLock)
 1467                {
 31468                    if (_disposed)
 1469                    {
 01470                        return;
 1471                    }
 1472
 31473                    _disposed = true;
 31474                }
 1475
 31476                Process.Exited -= OnProcessExited;
 31477                Untrack();
 31478                Process.Dispose();
 31479            }
 1480        }
 1481    }
 1482}

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)
GetMediaInfoInternal()
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)
ExtractImage()
GetImageResolutionParameter()
ExtractImageInternal()
ExtractVideoImagesOnIntervalAccelerated()
ExtractVideoImagesOnIntervalInternal()
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)
get_HasExited()
get_ExitCode()
ReadExitState()
OnProcessExited(System.Object,System.EventArgs)
Untrack()
Dispose()