< Summary - Jellyfin

Information
Class: MediaBrowser.MediaEncoding.Encoder.MediaEncoder
Assembly: MediaBrowser.MediaEncoding
File(s): /srv/git/jellyfin/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
Line coverage
8%
Covered lines: 54
Uncovered lines: 564
Coverable lines: 618
Total lines: 1425
Line coverage: 8.7%
Branch coverage
5%
Covered branches: 15
Total branches: 280
Branch coverage: 5.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/1/2026 - 12:13:05 AM Line coverage: 8.7% (53/606) Branch coverage: 7.7% (21/272) Total lines: 13985/4/2026 - 12:15:16 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: 1425 5/1/2026 - 12:13:05 AM Line coverage: 8.7% (53/606) Branch coverage: 7.7% (21/272) Total lines: 13985/4/2026 - 12:15:16 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: 1425

Coverage delta

Coverage delta 3 -3

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%3422580%
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%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;
 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
 2366        private readonly Lock _runningProcessesLock = new();
 2367        private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
 68
 69        // MediaEncoder is registered as a Singleton
 70        private readonly JsonSerializerOptions _jsonSerializerOptions;
 71
 2372        private List<string> _encoders = new List<string>();
 2373        private List<string> _decoders = new List<string>();
 2374        private List<string> _hwaccels = new List<string>();
 2375        private List<string> _filters = new List<string>();
 2376        private IDictionary<FilterOptionType, bool> _filtersWithOption = new Dictionary<FilterOptionType, bool>();
 2377        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
 2389        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;
 23107        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        {
 23120            _logger = logger;
 23121            _configurationManager = configurationManager;
 23122            _fileSystem = fileSystem;
 23123            _blurayExaminer = blurayExaminer;
 23124            _localization = localization;
 23125            _config = config;
 23126            _serverConfig = serverConfig;
 23127            _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathK
 128
 23129            _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
 23130            _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
 131
 132            // Although the type is not nullable, this might still be null during unit tests
 23133            var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
 23134            if (semaphoreCount < 1)
 135            {
 23136                semaphoreCount = Environment.ProcessorCount;
 137            }
 138
 23139            _thumbnailResourcePool = new(semaphoreCount);
 23140        }
 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        {
 22182            var skipValidation = _config.GetFFmpegSkipValidation();
 22183            if (skipValidation)
 184            {
 22185                _logger.LogWarning("FFmpeg: Skipping FFmpeg Validation due to FFmpeg:novalidation set to true");
 22186                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):
 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
 0925            var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
 0926            if (string.IsNullOrWhiteSpace(filterParam))
 927            {
 0928                throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
 929            }
 930
 931            // Normalize invalid PTS from containers for non keyframe only mode
 0932            if (!enableKeyFrameOnlyExtraction)
 933            {
 0934                var fpsFilterIndex = filterParam.IndexOf("fps=", StringComparison.Ordinal);
 0935                if (fpsFilterIndex >= 0)
 936                {
 0937                    var inputFrameRate = (imageStream.ReferenceFrameRate.HasValue && imageStream.ReferenceFrameRate > 0)
 0938                        ? imageStream.ReferenceFrameRate.Value : 30;
 939
 0940                    var setPtsFilter = string.Create(CultureInfo.InvariantCulture, $"setpts=N/{inputFrameRate:F3}/TB,");
 941
 0942                    filterParam = filterParam.Insert(fpsFilterIndex, setPtsFilter);
 943                }
 944                else
 945                {
 0946                    throw new InvalidOperationException("EncodingHelper returned invalid filter parameters.");
 947                }
 948            }
 949
 950            try
 951            {
 0952                return await ExtractVideoImagesOnIntervalInternal(
 0953                    (enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg,
 0954                    filterParam,
 0955                    vidEncoder,
 0956                    threads,
 0957                    qualityScale,
 0958                    priority,
 0959                    cancellationToken).ConfigureAwait(false);
 960            }
 0961            catch (FfmpegException ex)
 962            {
 0963                if (!enableKeyFrameOnlyExtraction)
 964                {
 0965                    throw;
 966                }
 967
 0968                _logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFil
 0969            }
 970
 0971            return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, 
 0972        }
 973
 974        private async Task<string> ExtractVideoImagesOnIntervalInternal(
 975            string inputArg,
 976            string filterParam,
 977            string vidEncoder,
 978            int? outputThreads,
 979            int? qualityScale,
 980            ProcessPriorityClass? priority,
 981            CancellationToken cancellationToken)
 982        {
 0983            if (string.IsNullOrWhiteSpace(inputArg))
 984            {
 0985                throw new InvalidOperationException("Empty or invalid input argument.");
 986            }
 987
 988            // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
 989            // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
 0990            var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
 0991            var encoderQualityOption = "-qscale:v ";
 992
 0993            if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
 0994                || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
 995            {
 996                // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
 0997                encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
 0998                encoderQualityOption = "-global_quality:v ";
 999            }
 1000
 01001            if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
 1002            {
 1003                // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qs
 01004                encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
 1005            }
 1006
 01007            if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
 1008            {
 1009                // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qsca
 01010                encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
 01011                encoderQualityOption = "-qp_init:v ";
 1012            }
 1013
 1014            // Output arguments
 01015            var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToSt
 01016            Directory.CreateDirectory(targetDirectory);
 01017            var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
 1018
 1019            // Final command arguments
 01020            var args = string.Format(
 01021                CultureInfo.InvariantCulture,
 01022                "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"",
 01023                inputArg,
 01024                filterParam,
 01025                outputThreads.GetValueOrDefault(_threads),
 01026                vidEncoder,
 01027                encoderQualityOption + encoderQuality + " ",
 01028                vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : stri
 01029                EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim() + " ", // passthrough timestamp
 01030                "image2",
 01031                outputPath);
 1032
 1033            // Start ffmpeg process
 01034            var process = new Process
 01035            {
 01036                StartInfo = new ProcessStartInfo
 01037                {
 01038                    CreateNoWindow = true,
 01039                    UseShellExecute = false,
 01040                    FileName = _ffmpegPath,
 01041                    Arguments = args,
 01042                    WindowStyle = ProcessWindowStyle.Hidden,
 01043                    ErrorDialog = false,
 01044                },
 01045                EnableRaisingEvents = true
 01046            };
 1047
 01048            var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, 
 01049            _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
 1050
 01051            using (var processWrapper = new ProcessWrapper(process, this))
 1052            {
 01053                bool ranToCompletion = false;
 1054
 01055                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 1056                {
 01057                    StartProcess(processWrapper);
 1058
 1059                    // Set process priority
 01060                    if (priority.HasValue)
 1061                    {
 1062                        try
 1063                        {
 01064                            processWrapper.Process.PriorityClass = priority.Value;
 01065                        }
 01066                        catch (Exception ex)
 1067                        {
 01068                            _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", prior
 01069                        }
 1070                    }
 1071
 1072                    // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
 1073                    // but we still need to detect if the process hangs.
 1074                    // Making the assumption that as long as new jpegs are showing up, everything is good.
 1075
 01076                    bool isResponsive = true;
 01077                    int lastCount = 0;
 01078                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 01079                    timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
 1080
 01081                    while (isResponsive && !cancellationToken.IsCancellationRequested)
 1082                    {
 1083                        try
 1084                        {
 01085                            await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 1086
 01087                            ranToCompletion = true;
 01088                            break;
 1089                        }
 01090                        catch (OperationCanceledException)
 1091                        {
 1092                            // We don't actually expect the process to be finished in one timeout span, just that one im
 01093                        }
 1094
 01095                        var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
 1096
 01097                        isResponsive = jpegCount > lastCount;
 01098                        lastCount = jpegCount;
 1099                    }
 1100
 01101                    if (!ranToCompletion)
 1102                    {
 01103                        if (!isResponsive)
 1104                        {
 01105                            _logger.LogInformation("Trickplay process unresponsive.");
 1106                        }
 1107
 01108                        _logger.LogInformation("Stopping trickplay extraction.");
 01109                        StopProcess(processWrapper, 1000);
 1110                    }
 01111                }
 1112
 01113                if (!ranToCompletion || processWrapper.ExitCode != 0)
 1114                {
 1115                    // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed 
 1116                    // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed
 1117                    try
 1118                    {
 01119                        Directory.Delete(targetDirectory, true);
 01120                    }
 01121                    catch (Exception e)
 1122                    {
 01123                        _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory)
 01124                    }
 1125
 01126                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 1127                }
 1128
 01129                return targetDirectory;
 1130            }
 01131        }
 1132
 1133        public string GetTimeParameter(long ticks)
 1134        {
 01135            var time = TimeSpan.FromTicks(ticks);
 1136
 01137            return GetTimeParameter(time);
 1138        }
 1139
 1140        public string GetTimeParameter(TimeSpan time)
 1141        {
 01142            return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
 1143        }
 1144
 1145        private void StartProcess(ProcessWrapper process)
 1146        {
 01147            process.Process.Start();
 1148
 01149            if (_canSetProcessPriority)
 1150            {
 1151                try
 1152                {
 01153                    process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
 01154                }
 01155                catch (Exception ex)
 1156                {
 01157                    _canSetProcessPriority = false;
 01158                    _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}. Further
 01159                }
 1160            }
 1161
 1162            lock (_runningProcessesLock)
 1163            {
 01164                _runningProcesses.Add(process);
 01165            }
 01166        }
 1167
 1168        private void StopProcess(ProcessWrapper process, int waitTimeMs)
 1169        {
 1170            try
 1171            {
 01172                if (process.Process.WaitForExit(waitTimeMs))
 1173                {
 01174                    return;
 1175                }
 1176
 01177                _logger.LogInformation("Killing ffmpeg process");
 1178
 01179                process.Process.Kill();
 01180            }
 01181            catch (InvalidOperationException)
 1182            {
 1183                // The process has already exited or
 1184                // there is no process associated with this Process object.
 01185            }
 01186            catch (Exception ex)
 1187            {
 01188                _logger.LogError(ex, "Error killing process");
 01189            }
 01190        }
 1191
 1192        private void StopProcesses()
 221193        {
 1194            List<ProcessWrapper> processes;
 1195            lock (_runningProcessesLock)
 1196            {
 221197                processes = _runningProcesses.ToList();
 221198                _runningProcesses.Clear();
 221199            }
 1200
 441201            foreach (var process in processes)
 1202            {
 01203                if (!process.HasExited)
 1204                {
 01205                    StopProcess(process, 500);
 1206                }
 1207            }
 221208        }
 1209
 1210        public string EscapeSubtitleFilterPath(string path)
 1211        {
 1212            // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
 1213            // We need to double escape
 1214
 01215            return path
 01216                .Replace('\\', '/')
 01217                .Replace(":", "\\:", StringComparison.Ordinal)
 01218                .Replace("'", @"'\\\''", StringComparison.Ordinal)
 01219                .Replace("\"", "\\\"", StringComparison.Ordinal);
 1220        }
 1221
 1222        /// <inheritdoc />
 1223        public void Dispose()
 1224        {
 221225            Dispose(true);
 221226            GC.SuppressFinalize(this);
 221227        }
 1228
 1229        /// <summary>
 1230        /// Releases unmanaged and - optionally - managed resources.
 1231        /// </summary>
 1232        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 1233        protected virtual void Dispose(bool dispose)
 1234        {
 221235            if (dispose)
 1236            {
 221237                StopProcesses();
 221238                _thumbnailResourcePool.Dispose();
 1239            }
 221240        }
 1241
 1242        /// <inheritdoc />
 1243        public Task ConvertImage(string inputPath, string outputPath)
 1244        {
 01245            throw new NotImplementedException();
 1246        }
 1247
 1248        /// <inheritdoc />
 1249        public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
 1250        {
 1251            // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VO
 01252            var allVobs = _fileSystem.GetFiles(path, true)
 01253                .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
 01254                .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
 01255                .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
 01256                .OrderBy(i => i.FullName)
 01257                .ToList();
 1258
 01259            if (titleNumber.HasValue)
 1260            {
 01261                var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
 01262                var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
 1263
 01264                if (vobs.Count > 0)
 1265                {
 01266                    return vobs.Select(i => i.FullName).ToList();
 1267                }
 1268
 01269                _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
 1270            }
 1271
 1272            // Check for multiple big titles (> 900 MB)
 01273            var titles = allVobs
 01274                .Where(vob => vob.Length >= 900 * 1024 * 1024)
 01275                .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
 01276                .Distinct()
 01277                .ToList();
 1278
 1279            // Fall back to first title if no big title is found
 01280            if (titles.Count == 0)
 1281            {
 01282                titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
 1283            }
 1284
 1285            // Aggregate all .vob files of the titles
 01286            return allVobs
 01287                .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToStr
 01288                .Select(i => i.FullName)
 01289                .Order()
 01290                .ToList();
 1291        }
 1292
 1293        /// <inheritdoc />
 1294        public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
 01295            => _blurayExaminer.GetDiscInfo(path).Files;
 1296
 1297        /// <inheritdoc />
 1298        public string GetInputPathArgument(EncodingJobInfo state)
 01299            => GetInputPathArgument(state.MediaPath, state.MediaSource);
 1300
 1301        /// <inheritdoc />
 1302        public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
 1303        {
 01304            return mediaSource.VideoType switch
 01305            {
 01306                VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource),
 01307                VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource),
 01308                _ => GetInputArgument(path, mediaSource)
 01309            };
 1310        }
 1311
 1312        /// <inheritdoc />
 1313        public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
 1314        {
 1315            // Get all playable files
 1316            IReadOnlyList<string> files;
 01317            var videoType = source.VideoType;
 01318            if (videoType == VideoType.Dvd)
 1319            {
 01320                files = GetPrimaryPlaylistVobFiles(source.Path, null);
 1321            }
 01322            else if (videoType == VideoType.BluRay)
 1323            {
 01324                files = GetPrimaryPlaylistM2tsFiles(source.Path);
 1325            }
 1326            else
 1327            {
 01328                return;
 1329            }
 1330
 1331            // Generate concat configuration entries for each file and write to file
 01332            Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
 01333            using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
 01334            foreach (var path in files)
 1335            {
 01336                var mediaInfoResult = GetMediaInfo(
 01337                    new MediaInfoRequest
 01338                    {
 01339                        MediaType = DlnaProfileType.Video,
 01340                        MediaSource = new MediaSourceInfo
 01341                        {
 01342                            Path = path,
 01343                            Protocol = MediaProtocol.File,
 01344                            VideoType = videoType
 01345                        }
 01346                    },
 01347                    CancellationToken.None).GetAwaiter().GetResult();
 1348
 01349                var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
 1350
 1351                // Add file path stanza to concat configuration
 01352                sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
 1353
 1354                // Add duration stanza to concat configuration
 01355                sw.WriteLine("duration {0}", duration);
 1356            }
 01357        }
 1358
 1359        public bool CanExtractSubtitles(string codec)
 1360        {
 01361            return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction;
 1362        }
 1363
 1364        private sealed class ProcessWrapper : IDisposable
 1365        {
 1366            private readonly MediaEncoder _mediaEncoder;
 1367
 1368            private bool _disposed = false;
 1369
 1370            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
 1371            {
 1372                Process = process;
 01373                _mediaEncoder = mediaEncoder;
 01374                Process.Exited += OnProcessExited;
 01375            }
 1376
 1377            public Process Process { get; }
 1378
 1379            public bool HasExited { get; private set; }
 1380
 1381            public int? ExitCode { get; private set; }
 1382
 1383            private void OnProcessExited(object sender, EventArgs e)
 1384            {
 01385                var process = (Process)sender;
 1386
 01387                HasExited = true;
 1388
 1389                try
 1390                {
 01391                    ExitCode = process.ExitCode;
 01392                }
 01393                catch
 1394                {
 01395                }
 1396
 01397                DisposeProcess(process);
 01398            }
 1399
 1400            private void DisposeProcess(Process process)
 01401            {
 1402                lock (_mediaEncoder._runningProcessesLock)
 1403                {
 01404                    _mediaEncoder._runningProcesses.Remove(this);
 01405                }
 1406
 01407                process.Dispose();
 01408            }
 1409
 1410            public void Dispose()
 1411            {
 01412                if (!_disposed)
 1413                {
 01414                    if (Process is not null)
 1415                    {
 01416                        Process.Exited -= OnProcessExited;
 01417                        DisposeProcess(Process);
 1418                    }
 1419                }
 1420
 01421                _disposed = true;
 01422            }
 1423        }
 1424    }
 1425}

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)
OnProcessExited(System.Object,System.EventArgs)
DisposeProcess(System.Diagnostics.Process)
Dispose()