< 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: 555
Coverable lines: 609
Total lines: 1404
Line coverage: 8.8%
Branch coverage
5%
Covered branches: 15
Total branches: 272
Branch coverage: 5.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/13/2026 - 12:11:21 AM Line coverage: 16.5% (53/321) Branch coverage: 18.5% (21/113) Total lines: 13994/12/2026 - 12:13:54 AM Line coverage: 16.5% (53/321) Branch coverage: 18.5% (21/113) Total lines: 13984/19/2026 - 12:14:27 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: 1404 2/13/2026 - 12:11:21 AM Line coverage: 16.5% (53/321) Branch coverage: 18.5% (21/113) Total lines: 13994/12/2026 - 12:13:54 AM Line coverage: 16.5% (53/321) Branch coverage: 18.5% (21/113) Total lines: 13984/19/2026 - 12:14:27 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: 1404

Coverage delta

Coverage delta 11 -11

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%2550500%
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.Json;
 11using System.Text.RegularExpressions;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using AsyncKeyedLock;
 15using Jellyfin.Data.Enums;
 16using Jellyfin.Extensions;
 17using Jellyfin.Extensions.Json;
 18using Jellyfin.Extensions.Json.Converters;
 19using MediaBrowser.Common;
 20using MediaBrowser.Common.Configuration;
 21using MediaBrowser.Common.Extensions;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Extensions;
 24using MediaBrowser.Controller.MediaEncoding;
 25using MediaBrowser.MediaEncoding.Probing;
 26using MediaBrowser.Model.Configuration;
 27using MediaBrowser.Model.Dlna;
 28using MediaBrowser.Model.Drawing;
 29using MediaBrowser.Model.Dto;
 30using MediaBrowser.Model.Entities;
 31using MediaBrowser.Model.Globalization;
 32using MediaBrowser.Model.IO;
 33using MediaBrowser.Model.MediaInfo;
 34using Microsoft.Extensions.Configuration;
 35using Microsoft.Extensions.Logging;
 36
 37namespace MediaBrowser.MediaEncoding.Encoder
 38{
 39    /// <summary>
 40    /// Class MediaEncoder.
 41    /// </summary>
 42    public partial class MediaEncoder : IMediaEncoder, IDisposable
 43    {
 44        /// <summary>
 45        /// The default SDR image extraction timeout in milliseconds.
 46        /// </summary>
 47        internal const int DefaultSdrImageExtractionTimeout = 10000;
 48
 49        /// <summary>
 50        /// The default HDR image extraction timeout in milliseconds.
 51        /// </summary>
 52        internal const int DefaultHdrImageExtractionTimeout = 20000;
 53
 54        private readonly ILogger<MediaEncoder> _logger;
 55        private readonly IServerConfigurationManager _configurationManager;
 56        private readonly IFileSystem _fileSystem;
 57        private readonly ILocalizationManager _localization;
 58        private readonly IBlurayExaminer _blurayExaminer;
 59        private readonly IConfiguration _config;
 60        private readonly IServerConfigurationManager _serverConfig;
 61        private readonly string _startupOptionFFmpegPath;
 62
 63        private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
 64
 2265        private readonly Lock _runningProcessesLock = new();
 2266        private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
 67
 68        // MediaEncoder is registered as a Singleton
 69        private readonly JsonSerializerOptions _jsonSerializerOptions;
 70
 2271        private List<string> _encoders = new List<string>();
 2272        private List<string> _decoders = new List<string>();
 2273        private List<string> _hwaccels = new List<string>();
 2274        private List<string> _filters = new List<string>();
 2275        private IDictionary<FilterOptionType, bool> _filtersWithOption = new Dictionary<FilterOptionType, bool>();
 2276        private IDictionary<BitStreamFilterOptionType, bool> _bitStreamFiltersWithOption = new Dictionary<BitStreamFilte
 77
 78        private bool _isPkeyPauseSupported = false;
 79        private bool _isLowPriorityHwDecodeSupported = false;
 80        private bool _proberSupportsFirstVideoFrame = false;
 81
 82        private bool _isVaapiDeviceAmd = false;
 83        private bool _isVaapiDeviceInteliHD = false;
 84        private bool _isVaapiDeviceInteli965 = false;
 85        private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
 86        private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
 87
 2288        private bool _canSetProcessPriority = true;
 89
 90        private bool _isVideoToolboxAv1DecodeAvailable = false;
 91
 092        private static string[] _vulkanImageDrmFmtModifierExts =
 093        {
 094            "VK_EXT_image_drm_format_modifier",
 095        };
 96
 097        private static string[] _vulkanExternalMemoryDmaBufExts =
 098        {
 099            "VK_KHR_external_memory_fd",
 0100            "VK_EXT_external_memory_dma_buf",
 0101            "VK_KHR_external_semaphore_fd",
 0102            "VK_EXT_external_memory_host"
 0103        };
 104
 105        private Version _ffmpegVersion = null;
 22106        private string _ffmpegPath = string.Empty;
 107        private string _ffprobePath;
 108        private int _threads;
 109
 110        public MediaEncoder(
 111            ILogger<MediaEncoder> logger,
 112            IServerConfigurationManager configurationManager,
 113            IFileSystem fileSystem,
 114            IBlurayExaminer blurayExaminer,
 115            ILocalizationManager localization,
 116            IConfiguration config,
 117            IServerConfigurationManager serverConfig)
 118        {
 22119            _logger = logger;
 22120            _configurationManager = configurationManager;
 22121            _fileSystem = fileSystem;
 22122            _blurayExaminer = blurayExaminer;
 22123            _localization = localization;
 22124            _config = config;
 22125            _serverConfig = serverConfig;
 22126            _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathK
 127
 22128            _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
 22129            _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
 130
 131            // Although the type is not nullable, this might still be null during unit tests
 22132            var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
 22133            if (semaphoreCount < 1)
 134            {
 22135                semaphoreCount = Environment.ProcessorCount;
 136            }
 137
 22138            _thumbnailResourcePool = new(semaphoreCount);
 22139        }
 140
 141        /// <inheritdoc />
 0142        public string EncoderPath => _ffmpegPath;
 143
 144        /// <inheritdoc />
 0145        public string ProbePath => _ffprobePath;
 146
 147        /// <inheritdoc />
 0148        public Version EncoderVersion => _ffmpegVersion;
 149
 150        /// <inheritdoc />
 0151        public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
 152
 153        /// <inheritdoc />
 0154        public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
 155
 156        /// <inheritdoc />
 0157        public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
 158
 159        /// <inheritdoc />
 0160        public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
 161
 162        /// <inheritdoc />
 0163        public bool IsVaapiDeviceSupportVulkanDrmModifier => _isVaapiDeviceSupportVulkanDrmModifier;
 164
 165        /// <inheritdoc />
 0166        public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
 167
 0168        public bool IsVideoToolboxAv1DecodeAvailable => _isVideoToolboxAv1DecodeAvailable;
 169
 170        [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
 171        private static partial Regex FfprobePathRegex();
 172
 173        /// <summary>
 174        /// Run at startup to validate ffmpeg.
 175        /// Sets global variables FFmpegPath.
 176        /// Precedence is: CLI/Env var > Config > $PATH.
 177        /// </summary>
 178        /// <returns>bool indicates whether a valid ffmpeg is found.</returns>
 179        public bool SetFFmpegPath()
 180        {
 21181            var skipValidation = _config.GetFFmpegSkipValidation();
 21182            if (skipValidation)
 183            {
 21184                _logger.LogWarning("FFmpeg: Skipping FFmpeg Validation due to FFmpeg:novalidation set to true");
 21185                return true;
 186            }
 187
 188            // 1) Check if the --ffmpeg CLI switch has been given
 0189            var ffmpegPath = _startupOptionFFmpegPath;
 0190            string ffmpegPathSetMethodText = "command line or environment variable";
 0191            if (string.IsNullOrEmpty(ffmpegPath))
 192            {
 193                // 2) Custom path stored in config/encoding xml file under tag <EncoderAppPath> should be used as a fall
 0194                ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
 0195                ffmpegPathSetMethodText = "encoding.xml config file";
 0196                if (string.IsNullOrEmpty(ffmpegPath))
 197                {
 198                    // 3) Check "ffmpeg"
 0199                    ffmpegPath = "ffmpeg";
 0200                    ffmpegPathSetMethodText = "system $PATH";
 201                }
 202            }
 203
 0204            if (!ValidatePath(ffmpegPath))
 205            {
 0206                _ffmpegPath = null;
 0207                _logger.LogError("FFmpeg: Path set by {FfmpegPathSetMethodText} is invalid", ffmpegPathSetMethodText);
 0208                return false;
 209            }
 210
 211            // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
 0212            var options = _configurationManager.GetEncodingOptions();
 0213            options.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
 0214            _configurationManager.SaveConfiguration("encoding", options);
 215
 216            // Only if mpeg path is set, try and set path to probe
 0217            if (_ffmpegPath is not null)
 218            {
 219                // Determine a probe path from the mpeg path
 0220                _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, "ffprobe$1");
 221
 222                // Interrogate to understand what coders are supported
 0223                var validator = new EncoderValidator(_logger, _ffmpegPath);
 224
 0225                SetAvailableDecoders(validator.GetDecoders());
 0226                SetAvailableEncoders(validator.GetEncoders());
 0227                SetAvailableFilters(validator.GetFilters());
 0228                SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
 0229                SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption());
 0230                SetAvailableHwaccels(validator.GetHwaccels());
 0231                SetMediaEncoderVersion(validator);
 232
 0233                _threads = EncodingHelper.GetNumberOfThreads(null, options, null);
 234
 0235                _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p      pause transcoding", _ffmpegVersion);
 0236                _isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
 0237                _proberSupportsFirstVideoFrame = validator.CheckSupportedProberOption("only_first_vframe", _ffprobePath)
 238
 239                // Check the Vaapi device vendor
 0240                if (OperatingSystem.IsLinux()
 0241                    && SupportsHwaccel("vaapi")
 0242                    && !string.IsNullOrEmpty(options.VaapiDevice)
 0243                    && options.HardwareAccelerationType == HardwareAccelerationType.vaapi)
 244                {
 0245                    _isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevic
 0246                    _isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDev
 0247                    _isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiD
 0248                    _isVaapiDeviceSupportVulkanDrmModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.Vaapi
 0249                    _isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiD
 250
 0251                    if (_isVaapiDeviceAmd)
 252                    {
 0253                        _logger.LogInformation("VAAPI device {RenderNodePath} is AMD GPU", options.VaapiDevice);
 254                    }
 0255                    else if (_isVaapiDeviceInteliHD)
 256                    {
 0257                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (iHD)", options.VaapiDevice);
 258                    }
 0259                    else if (_isVaapiDeviceInteli965)
 260                    {
 0261                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice)
 262                    }
 263
 0264                    if (_isVaapiDeviceSupportVulkanDrmModifier)
 265                    {
 0266                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM modifier", options.Vaa
 267                    }
 268
 0269                    if (_isVaapiDeviceSupportVulkanDrmInterop)
 270                    {
 0271                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.Vaap
 272                    }
 273                }
 274
 275                // Check if VideoToolbox supports AV1 decode
 0276                if (OperatingSystem.IsMacOS() && SupportsHwaccel("videotoolbox"))
 277                {
 0278                    _isVideoToolboxAv1DecodeAvailable = validator.CheckIsVideoToolboxAv1DecodeAvailable();
 279                }
 280            }
 281
 0282            _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
 0283            return !string.IsNullOrWhiteSpace(ffmpegPath);
 284        }
 285
 286        /// <summary>
 287        /// Validates the supplied FQPN to ensure it is a ffmpeg utility.
 288        /// If checks pass, global variable FFmpegPath is updated.
 289        /// </summary>
 290        /// <param name="path">FQPN to test.</param>
 291        /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
 292        private bool ValidatePath(string path)
 293        {
 0294            if (string.IsNullOrEmpty(path))
 295            {
 0296                return false;
 297            }
 298
 0299            bool rc = new EncoderValidator(_logger, path).ValidateVersion();
 0300            if (!rc)
 301            {
 0302                _logger.LogError("FFmpeg: Failed version check: {Path}", path);
 0303                return false;
 304            }
 305
 0306            _ffmpegPath = path;
 0307            return true;
 308        }
 309
 310        private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false)
 311        {
 312            try
 313            {
 0314                var files = _fileSystem.GetFilePaths(path, recursive);
 315
 0316                return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringCom
 0317                                                    && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.Ordi
 318            }
 0319            catch (Exception)
 320            {
 321                // Trap all exceptions, like DirNotExists, and return null
 0322                return null;
 323            }
 0324        }
 325
 326        public void SetAvailableEncoders(IEnumerable<string> list)
 327        {
 0328            _encoders = list.ToList();
 0329        }
 330
 331        public void SetAvailableDecoders(IEnumerable<string> list)
 332        {
 0333            _decoders = list.ToList();
 0334        }
 335
 336        public void SetAvailableHwaccels(IEnumerable<string> list)
 337        {
 0338            _hwaccels = list.ToList();
 0339        }
 340
 341        public void SetAvailableFilters(IEnumerable<string> list)
 342        {
 0343            _filters = list.ToList();
 0344        }
 345
 346        public void SetAvailableFiltersWithOption(IDictionary<FilterOptionType, bool> dict)
 347        {
 0348            _filtersWithOption = dict;
 0349        }
 350
 351        public void SetAvailableBitStreamFiltersWithOption(IDictionary<BitStreamFilterOptionType, bool> dict)
 352        {
 0353            _bitStreamFiltersWithOption = dict;
 0354        }
 355
 356        public void SetMediaEncoderVersion(EncoderValidator validator)
 357        {
 0358            _ffmpegVersion = validator.GetFFmpegVersion();
 0359        }
 360
 361        /// <inheritdoc />
 362        public bool SupportsEncoder(string encoder)
 363        {
 0364            return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
 365        }
 366
 367        /// <inheritdoc />
 368        public bool SupportsDecoder(string decoder)
 369        {
 0370            return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase);
 371        }
 372
 373        /// <inheritdoc />
 374        public bool SupportsHwaccel(string hwaccel)
 375        {
 0376            return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
 377        }
 378
 379        /// <inheritdoc />
 380        public bool SupportsFilter(string filter)
 381        {
 0382            return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
 383        }
 384
 385        /// <inheritdoc />
 386        public bool SupportsFilterWithOption(FilterOptionType option)
 387        {
 0388            return _filtersWithOption.TryGetValue(option, out var val) && val;
 389        }
 390
 391        public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option)
 392        {
 0393            return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val;
 394        }
 395
 396        public bool CanEncodeToAudioCodec(string codec)
 397        {
 0398            if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
 399            {
 0400                codec = "libopus";
 401            }
 0402            else if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
 403            {
 0404                codec = "libmp3lame";
 405            }
 406
 0407            return SupportsEncoder(codec);
 408        }
 409
 410        public bool CanEncodeToSubtitleCodec(string codec)
 411        {
 412            // TODO
 0413            return true;
 414        }
 415
 416        /// <inheritdoc />
 417        public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
 418        {
 0419            var extractChapters = request.ExtractChapters;
 0420            var extraArgs = GetExtraArguments(request);
 421
 0422            return GetMediaInfoInternal(
 0423                GetInputArgument(request.MediaSource.Path, request.MediaSource),
 0424                request.MediaSource.Path,
 0425                request.MediaSource.Protocol,
 0426                extractChapters,
 0427                extraArgs,
 0428                request.MediaType == DlnaProfileType.Audio,
 0429                request.MediaSource.VideoType,
 0430                cancellationToken);
 431        }
 432
 433        internal string GetExtraArguments(MediaInfoRequest request)
 434        {
 1435            var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
 1436            var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
 1437            var analyzeDuration = string.Empty;
 1438            var extraArgs = string.Empty;
 439
 1440            if (request.MediaSource.AnalyzeDurationMs > 0)
 441            {
 0442                analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000);
 443            }
 1444            else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
 445            {
 0446                analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
 447            }
 448
 1449            if (!string.IsNullOrEmpty(analyzeDuration))
 450            {
 0451                extraArgs = analyzeDuration;
 452            }
 453
 1454            if (!string.IsNullOrEmpty(ffmpegProbeSize))
 455            {
 0456                extraArgs += " -probesize " + ffmpegProbeSize;
 457            }
 458
 1459            if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
 460            {
 1461                extraArgs += $" -user_agent \"{userAgent}\"";
 462            }
 463
 1464            if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
 465            {
 0466                extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
 467            }
 468
 1469            return extraArgs;
 470        }
 471
 472        /// <inheritdoc />
 473        public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource)
 474        {
 0475            return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol);
 476        }
 477
 478        /// <inheritdoc />
 479        public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
 480        {
 0481            var prefix = "file";
 0482            if (mediaSource.IsoType == IsoType.BluRay)
 483            {
 0484                prefix = "bluray";
 485            }
 486
 0487            return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol);
 488        }
 489
 490        /// <inheritdoc />
 491        public string GetExternalSubtitleInputArgument(string inputFile)
 492        {
 493            const string Prefix = "file";
 494
 0495            return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File);
 496        }
 497
 498        /// <summary>
 499        /// Gets the media info internal.
 500        /// </summary>
 501        /// <returns>Task{MediaInfoResult}.</returns>
 502        private async Task<MediaInfo> GetMediaInfoInternal(
 503            string inputPath,
 504            string primaryPath,
 505            MediaProtocol protocol,
 506            bool extractChapters,
 507            string probeSizeArgument,
 508            bool isAudio,
 509            VideoType? videoType,
 510            CancellationToken cancellationToken)
 511        {
 0512            var args = extractChapters
 0513                ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
 0514                : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
 515
 0516            if (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame)
 517            {
 0518                args += " -show_frames -only_first_vframe";
 519            }
 520
 0521            args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
 522
 0523            var process = new Process
 0524            {
 0525                StartInfo = new ProcessStartInfo
 0526                {
 0527                    CreateNoWindow = true,
 0528                    UseShellExecute = false,
 0529
 0530                    // Must consume both or ffmpeg may hang due to deadlocks.
 0531                    RedirectStandardOutput = true,
 0532
 0533                    FileName = _ffprobePath,
 0534                    Arguments = args,
 0535
 0536                    WindowStyle = ProcessWindowStyle.Hidden,
 0537                    ErrorDialog = false,
 0538                },
 0539                EnableRaisingEvents = true
 0540            };
 541
 0542            _logger.LogDebug("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
 543
 0544            var memoryStream = new MemoryStream();
 0545            await using (memoryStream.ConfigureAwait(false))
 0546            using (var processWrapper = new ProcessWrapper(process, this))
 547            {
 0548                StartProcess(processWrapper);
 0549                using var reader = process.StandardOutput;
 0550                await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
 0551                memoryStream.Seek(0, SeekOrigin.Begin);
 552                InternalMediaInfoResult result;
 553                try
 554                {
 0555                    result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
 0556                                        memoryStream,
 0557                                        _jsonSerializerOptions,
 0558                                        cancellationToken).ConfigureAwait(false);
 0559                }
 0560                catch
 561                {
 0562                    StopProcess(processWrapper, 100);
 563
 0564                    throw;
 565                }
 566
 0567                if (result is null || (result.Streams is null && result.Format is null))
 568                {
 0569                    throw new FfmpegException("ffprobe failed - streams and format are both null.");
 570                }
 571
 0572                if (result.Streams is not null)
 573                {
 574                    // Normalize aspect ratio if invalid
 0575                    foreach (var stream in result.Streams)
 576                    {
 0577                        if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 578                        {
 0579                            stream.DisplayAspectRatio = string.Empty;
 580                        }
 581
 0582                        if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 583                        {
 0584                            stream.SampleAspectRatio = string.Empty;
 585                        }
 586                    }
 587                }
 588
 0589                return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primar
 590            }
 0591        }
 592
 593        /// <inheritdoc />
 594        public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
 595        {
 0596            var mediaSource = new MediaSourceInfo
 0597            {
 0598                Protocol = MediaProtocol.File
 0599            };
 600
 0601            return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, canc
 602        }
 603
 604        /// <inheritdoc />
 605        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 606        {
 0607            return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, Image
 608        }
 609
 610        /// <inheritdoc />
 611        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 612        {
 0613            return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, tar
 614        }
 615
 616        private async Task<string> ExtractImage(
 617            string inputFile,
 618            string container,
 619            MediaStream videoStream,
 620            int? imageStreamIndex,
 621            MediaSourceInfo mediaSource,
 622            bool isAudio,
 623            Video3DFormat? threedFormat,
 624            TimeSpan? offset,
 625            ImageFormat? targetFormat,
 626            CancellationToken cancellationToken)
 627        {
 0628            var inputArgument = GetInputPathArgument(inputFile, mediaSource);
 629
 0630            if (!isAudio)
 631            {
 632                try
 633                {
 0634                    return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFor
 635                }
 0636                catch (ArgumentException)
 637                {
 0638                    throw;
 639                }
 0640                catch (Exception ex)
 641                {
 0642                    _logger.LogWarning(ex, "I-frame image extraction failed, will attempt standard way. Input: {Argument
 0643                }
 644            }
 645
 0646            return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, off
 0647        }
 648
 649        private string GetImageResolutionParameter()
 650        {
 0651            var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
 0652            {
 0653                ImageResolution.P144 => "256x144",
 0654                ImageResolution.P240 => "426x240",
 0655                ImageResolution.P360 => "640x360",
 0656                ImageResolution.P480 => "854x480",
 0657                ImageResolution.P720 => "1280x720",
 0658                ImageResolution.P1080 => "1920x1080",
 0659                ImageResolution.P1440 => "2560x1440",
 0660                ImageResolution.P2160 => "3840x2160",
 0661                _ => string.Empty
 0662            };
 663
 0664            if (!string.IsNullOrEmpty(imageResolutionParameter))
 665            {
 0666                imageResolutionParameter = " -s " + imageResolutionParameter;
 667            }
 668
 0669            return imageResolutionParameter;
 670        }
 671
 672        private async Task<string> ExtractImageInternal(
 673            string inputPath,
 674            string container,
 675            MediaStream videoStream,
 676            int? imageStreamIndex,
 677            Video3DFormat? threedFormat,
 678            TimeSpan? offset,
 679            bool useIFrame,
 680            ImageFormat? targetFormat,
 681            bool isAudio,
 682            CancellationToken cancellationToken)
 683        {
 0684            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 685
 0686            var useTradeoff = _config.GetFFmpegImgExtractPerfTradeoff();
 687
 0688            var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
 689
 0690            var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ou
 0691            Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
 692
 693            // deint -> scale -> thumbnail -> tonemap.
 694            // put the SW tonemap right after the thumbnail to do it only once to reduce cpu usage.
 0695            var filters = new List<string>();
 696
 697            // deinterlace using bwdif algorithm for video stream.
 0698            if (videoStream is not null && videoStream.IsInterlaced)
 699            {
 0700                filters.Add("bwdif=0:-1:0");
 701            }
 702
 703            // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the cor
 704            // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex
 0705            var scaler = threedFormat switch
 0706            {
 0707                // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may 
 0708                Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min
 0709                // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
 0710                Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw
 0711                // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may
 0712                Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):
 0713                // ftab crop height in half, set the display aspect,crop out any black bars we may have made
 0714                Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(
 0715                _ => "scale=round(iw*sar/2)*2:round(ih/2)*2"
 0716            };
 717
 0718            filters.Add(scaler);
 719
 720            // Use ffmpeg to sample N frames and pick the best thumbnail. Have a fall back just in case.
 0721            var enableThumbnail = !useTradeoff && useIFrame && !string.Equals("wtv", container, StringComparison.Ordinal
 0722            if (enableThumbnail)
 723            {
 0724                filters.Add("thumbnail=n=24");
 725            }
 726
 727            // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
 728            // Only enable Dolby Vision tonemap when tonemapx is available
 0729            var enableHdrExtraction = false;
 730
 0731            if (videoStream?.VideoRange == VideoRange.HDR)
 732            {
 0733                if (SupportsFilter("tonemapx"))
 734                {
 0735                    var peak = videoStream.VideoRangeType == VideoRangeType.DOVI ? "400" : "100";
 0736                    enableHdrExtraction = true;
 0737                    filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p:ran
 738                }
 0739                else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
 740                {
 0741                    enableHdrExtraction = true;
 0742                    filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:p
 743                }
 744            }
 745
 0746            var vf = string.Join(',', filters);
 0747            var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.Invariant
 0748            var args = string.Format(
 0749                CultureInfo.InvariantCulture,
 0750                "-i {0}{1} -threads {2} -v quiet -vframes 1 -vf {3}{4}{5} -f image2 \"{6}\"",
 0751                inputPath,
 0752                mapArg,
 0753                _threads,
 0754                vf,
 0755                isAudio ? string.Empty : GetImageResolutionParameter(),
 0756                EncodingHelper.GetVideoSyncOption("-1", EncoderVersion), // auto decide fps mode
 0757                tempExtractPath);
 758
 0759            if (offset.HasValue)
 760            {
 0761                args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
 762            }
 763
 764            // The mpegts demuxer cannot seek to keyframes, so we have to let the
 765            // decoder discard non-keyframes, which may contain corrupted images.
 0766            var seekMpegTs = offset.HasValue && string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
 0767            if (useIFrame && (useTradeoff || seekMpegTs))
 768            {
 0769                args = "-skip_frame nokey " + args;
 770            }
 771
 0772            if (!string.IsNullOrWhiteSpace(container))
 773            {
 0774                var inputFormat = EncodingHelper.GetInputFormat(container);
 0775                if (!string.IsNullOrWhiteSpace(inputFormat))
 776                {
 0777                    args = "-f " + inputFormat + " " + args;
 778                }
 779            }
 780
 0781            var process = new Process
 0782            {
 0783                StartInfo = new ProcessStartInfo
 0784                {
 0785                    CreateNoWindow = true,
 0786                    UseShellExecute = false,
 0787                    FileName = _ffmpegPath,
 0788                    Arguments = args,
 0789                    WindowStyle = ProcessWindowStyle.Hidden,
 0790                    ErrorDialog = false,
 0791                },
 0792                EnableRaisingEvents = true
 0793            };
 794
 0795            _logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Argum
 796
 0797            using (var processWrapper = new ProcessWrapper(process, this))
 798            {
 0799                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 800                {
 0801                    StartProcess(processWrapper);
 802
 0803                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 0804                    if (timeoutMs <= 0)
 805                    {
 0806                        timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTi
 807                    }
 808
 809                    try
 810                    {
 0811                        await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 0812                    }
 0813                    catch (OperationCanceledException ex)
 814                    {
 0815                        process.Kill(true);
 0816                        throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction t
 817                    }
 0818                }
 819
 0820                var file = _fileSystem.GetFileInfo(tempExtractPath);
 821
 0822                if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
 823                {
 0824                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 825                }
 826
 0827                return tempExtractPath;
 828            }
 0829        }
 830
 831        /// <inheritdoc />
 832        public async Task<string> ExtractVideoImagesOnIntervalAccelerated(
 833            string inputFile,
 834            string container,
 835            MediaSourceInfo mediaSource,
 836            MediaStream imageStream,
 837            int maxWidth,
 838            TimeSpan interval,
 839            bool allowHwAccel,
 840            bool enableHwEncoding,
 841            int? threads,
 842            int? qualityScale,
 843            ProcessPriorityClass? priority,
 844            bool enableKeyFrameOnlyExtraction,
 845            EncodingHelper encodingHelper,
 846            CancellationToken cancellationToken)
 847        {
 0848            var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
 0849            threads ??= _threads;
 850
 0851            if (allowHwAccel && enableKeyFrameOnlyExtraction)
 852            {
 0853                var hardwareAccelerationType = options.HardwareAccelerationType;
 0854                var supportsKeyFrameOnly = (hardwareAccelerationType == HardwareAccelerationType.nvenc && options.Enable
 0855                                           || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSyst
 0856                                           || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.Prefe
 0857                                           || hardwareAccelerationType == HardwareAccelerationType.vaapi
 0858                                           || hardwareAccelerationType == HardwareAccelerationType.videotoolbox
 0859                                           || hardwareAccelerationType == HardwareAccelerationType.rkmpp;
 0860                if (!supportsKeyFrameOnly)
 861                {
 862                    // Disable hardware acceleration when the hardware decoder does not support keyframe only mode.
 0863                    allowHwAccel = false;
 0864                    options = new EncodingOptions();
 865                }
 866            }
 867
 868            // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
 869            // Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
 0870            if (!allowHwAccel)
 871            {
 0872                options.EnableHardwareEncoding = false;
 0873                options.HardwareAccelerationType = HardwareAccelerationType.none;
 0874                options.EnableTonemapping = false;
 875            }
 876
 0877            if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.Asp
 878            {
 879                // For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimen
 0880                var darParts = imageStream.AspectRatio.Split(':');
 0881                var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], Cultu
 882                // When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
 883                // Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, 
 0884                var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05
 0885                if (shouldResetHeight)
 886                {
 887                    // SAR = DAR * Height / Width
 888                    // RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
 0889                    imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
 890                }
 891            }
 892
 0893            var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.To
 0894            var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
 0895            {
 0896                IsVideoRequest = true,  // must be true for InputVideoHwaccelArgs to return non-empty value
 0897                MediaSource = mediaSource,
 0898                VideoStream = imageStream,
 0899                BaseRequest = baseRequest,  // GetVideoProcessingFilterParam errors if null
 0900                MediaPath = inputFile,
 0901                OutputVideoCodec = "mjpeg"
 0902            };
 0903            var vidEncoder = enableHwEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideo
 904
 905            // Get input and filter arguments
 0906            var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
 0907            if (string.IsNullOrWhiteSpace(inputArg))
 908            {
 0909                throw new InvalidOperationException("EncodingHelper returned empty input arguments.");
 910            }
 911
 0912            if (!allowHwAccel)
 913            {
 0914                inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, 
 915            }
 916
 0917            if (options.HardwareAccelerationType == HardwareAccelerationType.videotoolbox && _isLowPriorityHwDecodeSuppo
 918            {
 919                // VideoToolbox supports low priority decoding, which is useful for trickplay
 0920                inputArg = "-hwaccel_flags +low_priority " + inputArg;
 921            }
 922
 0923            var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
 0924            if (string.IsNullOrWhiteSpace(filterParam))
 925            {
 0926                throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
 927            }
 928
 929            try
 930            {
 0931                return await ExtractVideoImagesOnIntervalInternal(
 0932                    (enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg,
 0933                    filterParam,
 0934                    vidEncoder,
 0935                    threads,
 0936                    qualityScale,
 0937                    priority,
 0938                    cancellationToken).ConfigureAwait(false);
 939            }
 0940            catch (FfmpegException ex)
 941            {
 0942                if (!enableKeyFrameOnlyExtraction)
 943                {
 0944                    throw;
 945                }
 946
 0947                _logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFil
 0948            }
 949
 0950            return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, 
 0951        }
 952
 953        private async Task<string> ExtractVideoImagesOnIntervalInternal(
 954            string inputArg,
 955            string filterParam,
 956            string vidEncoder,
 957            int? outputThreads,
 958            int? qualityScale,
 959            ProcessPriorityClass? priority,
 960            CancellationToken cancellationToken)
 961        {
 0962            if (string.IsNullOrWhiteSpace(inputArg))
 963            {
 0964                throw new InvalidOperationException("Empty or invalid input argument.");
 965            }
 966
 967            // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
 968            // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
 0969            var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
 0970            var encoderQualityOption = "-qscale:v ";
 971
 0972            if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
 0973                || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
 974            {
 975                // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
 0976                encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
 0977                encoderQualityOption = "-global_quality:v ";
 978            }
 979
 0980            if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
 981            {
 982                // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qs
 0983                encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
 984            }
 985
 0986            if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
 987            {
 988                // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qsca
 0989                encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
 0990                encoderQualityOption = "-qp_init:v ";
 991            }
 992
 993            // Output arguments
 0994            var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToSt
 0995            Directory.CreateDirectory(targetDirectory);
 0996            var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
 997
 998            // Final command arguments
 0999            var args = string.Format(
 01000                CultureInfo.InvariantCulture,
 01001                "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"",
 01002                inputArg,
 01003                filterParam,
 01004                outputThreads.GetValueOrDefault(_threads),
 01005                vidEncoder,
 01006                encoderQualityOption + encoderQuality + " ",
 01007                vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : stri
 01008                EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim() + " ", // passthrough timestamp
 01009                "image2",
 01010                outputPath);
 1011
 1012            // Start ffmpeg process
 01013            var process = new Process
 01014            {
 01015                StartInfo = new ProcessStartInfo
 01016                {
 01017                    CreateNoWindow = true,
 01018                    UseShellExecute = false,
 01019                    FileName = _ffmpegPath,
 01020                    Arguments = args,
 01021                    WindowStyle = ProcessWindowStyle.Hidden,
 01022                    ErrorDialog = false,
 01023                },
 01024                EnableRaisingEvents = true
 01025            };
 1026
 01027            var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, 
 01028            _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
 1029
 01030            using (var processWrapper = new ProcessWrapper(process, this))
 1031            {
 01032                bool ranToCompletion = false;
 1033
 01034                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 1035                {
 01036                    StartProcess(processWrapper);
 1037
 1038                    // Set process priority
 01039                    if (priority.HasValue)
 1040                    {
 1041                        try
 1042                        {
 01043                            processWrapper.Process.PriorityClass = priority.Value;
 01044                        }
 01045                        catch (Exception ex)
 1046                        {
 01047                            _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", prior
 01048                        }
 1049                    }
 1050
 1051                    // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
 1052                    // but we still need to detect if the process hangs.
 1053                    // Making the assumption that as long as new jpegs are showing up, everything is good.
 1054
 01055                    bool isResponsive = true;
 01056                    int lastCount = 0;
 01057                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 01058                    timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
 1059
 01060                    while (isResponsive && !cancellationToken.IsCancellationRequested)
 1061                    {
 1062                        try
 1063                        {
 01064                            await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 1065
 01066                            ranToCompletion = true;
 01067                            break;
 1068                        }
 01069                        catch (OperationCanceledException)
 1070                        {
 1071                            // We don't actually expect the process to be finished in one timeout span, just that one im
 01072                        }
 1073
 01074                        var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
 1075
 01076                        isResponsive = jpegCount > lastCount;
 01077                        lastCount = jpegCount;
 1078                    }
 1079
 01080                    if (!ranToCompletion)
 1081                    {
 01082                        if (!isResponsive)
 1083                        {
 01084                            _logger.LogInformation("Trickplay process unresponsive.");
 1085                        }
 1086
 01087                        _logger.LogInformation("Stopping trickplay extraction.");
 01088                        StopProcess(processWrapper, 1000);
 1089                    }
 01090                }
 1091
 01092                if (!ranToCompletion || processWrapper.ExitCode != 0)
 1093                {
 1094                    // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed 
 1095                    // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed
 1096                    try
 1097                    {
 01098                        Directory.Delete(targetDirectory, true);
 01099                    }
 01100                    catch (Exception e)
 1101                    {
 01102                        _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory)
 01103                    }
 1104
 01105                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 1106                }
 1107
 01108                return targetDirectory;
 1109            }
 01110        }
 1111
 1112        public string GetTimeParameter(long ticks)
 1113        {
 01114            var time = TimeSpan.FromTicks(ticks);
 1115
 01116            return GetTimeParameter(time);
 1117        }
 1118
 1119        public string GetTimeParameter(TimeSpan time)
 1120        {
 01121            return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
 1122        }
 1123
 1124        private void StartProcess(ProcessWrapper process)
 1125        {
 01126            process.Process.Start();
 1127
 01128            if (_canSetProcessPriority)
 1129            {
 1130                try
 1131                {
 01132                    process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
 01133                }
 01134                catch (Exception ex)
 1135                {
 01136                    _canSetProcessPriority = false;
 01137                    _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}. Further
 01138                }
 1139            }
 1140
 1141            lock (_runningProcessesLock)
 1142            {
 01143                _runningProcesses.Add(process);
 01144            }
 01145        }
 1146
 1147        private void StopProcess(ProcessWrapper process, int waitTimeMs)
 1148        {
 1149            try
 1150            {
 01151                if (process.Process.WaitForExit(waitTimeMs))
 1152                {
 01153                    return;
 1154                }
 1155
 01156                _logger.LogInformation("Killing ffmpeg process");
 1157
 01158                process.Process.Kill();
 01159            }
 01160            catch (InvalidOperationException)
 1161            {
 1162                // The process has already exited or
 1163                // there is no process associated with this Process object.
 01164            }
 01165            catch (Exception ex)
 1166            {
 01167                _logger.LogError(ex, "Error killing process");
 01168            }
 01169        }
 1170
 1171        private void StopProcesses()
 211172        {
 1173            List<ProcessWrapper> processes;
 1174            lock (_runningProcessesLock)
 1175            {
 211176                processes = _runningProcesses.ToList();
 211177                _runningProcesses.Clear();
 211178            }
 1179
 421180            foreach (var process in processes)
 1181            {
 01182                if (!process.HasExited)
 1183                {
 01184                    StopProcess(process, 500);
 1185                }
 1186            }
 211187        }
 1188
 1189        public string EscapeSubtitleFilterPath(string path)
 1190        {
 1191            // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
 1192            // We need to double escape
 1193
 01194            return path
 01195                .Replace('\\', '/')
 01196                .Replace(":", "\\:", StringComparison.Ordinal)
 01197                .Replace("'", @"'\\\''", StringComparison.Ordinal)
 01198                .Replace("\"", "\\\"", StringComparison.Ordinal);
 1199        }
 1200
 1201        /// <inheritdoc />
 1202        public void Dispose()
 1203        {
 211204            Dispose(true);
 211205            GC.SuppressFinalize(this);
 211206        }
 1207
 1208        /// <summary>
 1209        /// Releases unmanaged and - optionally - managed resources.
 1210        /// </summary>
 1211        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 1212        protected virtual void Dispose(bool dispose)
 1213        {
 211214            if (dispose)
 1215            {
 211216                StopProcesses();
 211217                _thumbnailResourcePool.Dispose();
 1218            }
 211219        }
 1220
 1221        /// <inheritdoc />
 1222        public Task ConvertImage(string inputPath, string outputPath)
 1223        {
 01224            throw new NotImplementedException();
 1225        }
 1226
 1227        /// <inheritdoc />
 1228        public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
 1229        {
 1230            // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VO
 01231            var allVobs = _fileSystem.GetFiles(path, true)
 01232                .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
 01233                .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
 01234                .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
 01235                .OrderBy(i => i.FullName)
 01236                .ToList();
 1237
 01238            if (titleNumber.HasValue)
 1239            {
 01240                var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
 01241                var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
 1242
 01243                if (vobs.Count > 0)
 1244                {
 01245                    return vobs.Select(i => i.FullName).ToList();
 1246                }
 1247
 01248                _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
 1249            }
 1250
 1251            // Check for multiple big titles (> 900 MB)
 01252            var titles = allVobs
 01253                .Where(vob => vob.Length >= 900 * 1024 * 1024)
 01254                .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
 01255                .Distinct()
 01256                .ToList();
 1257
 1258            // Fall back to first title if no big title is found
 01259            if (titles.Count == 0)
 1260            {
 01261                titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
 1262            }
 1263
 1264            // Aggregate all .vob files of the titles
 01265            return allVobs
 01266                .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToStr
 01267                .Select(i => i.FullName)
 01268                .Order()
 01269                .ToList();
 1270        }
 1271
 1272        /// <inheritdoc />
 1273        public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
 01274            => _blurayExaminer.GetDiscInfo(path).Files;
 1275
 1276        /// <inheritdoc />
 1277        public string GetInputPathArgument(EncodingJobInfo state)
 01278            => GetInputPathArgument(state.MediaPath, state.MediaSource);
 1279
 1280        /// <inheritdoc />
 1281        public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
 1282        {
 01283            return mediaSource.VideoType switch
 01284            {
 01285                VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource),
 01286                VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource),
 01287                _ => GetInputArgument(path, mediaSource)
 01288            };
 1289        }
 1290
 1291        /// <inheritdoc />
 1292        public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
 1293        {
 1294            // Get all playable files
 1295            IReadOnlyList<string> files;
 01296            var videoType = source.VideoType;
 01297            if (videoType == VideoType.Dvd)
 1298            {
 01299                files = GetPrimaryPlaylistVobFiles(source.Path, null);
 1300            }
 01301            else if (videoType == VideoType.BluRay)
 1302            {
 01303                files = GetPrimaryPlaylistM2tsFiles(source.Path);
 1304            }
 1305            else
 1306            {
 01307                return;
 1308            }
 1309
 1310            // Generate concat configuration entries for each file and write to file
 01311            Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
 01312            using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
 01313            foreach (var path in files)
 1314            {
 01315                var mediaInfoResult = GetMediaInfo(
 01316                    new MediaInfoRequest
 01317                    {
 01318                        MediaType = DlnaProfileType.Video,
 01319                        MediaSource = new MediaSourceInfo
 01320                        {
 01321                            Path = path,
 01322                            Protocol = MediaProtocol.File,
 01323                            VideoType = videoType
 01324                        }
 01325                    },
 01326                    CancellationToken.None).GetAwaiter().GetResult();
 1327
 01328                var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
 1329
 1330                // Add file path stanza to concat configuration
 01331                sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
 1332
 1333                // Add duration stanza to concat configuration
 01334                sw.WriteLine("duration {0}", duration);
 1335            }
 01336        }
 1337
 1338        public bool CanExtractSubtitles(string codec)
 1339        {
 01340            return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction;
 1341        }
 1342
 1343        private sealed class ProcessWrapper : IDisposable
 1344        {
 1345            private readonly MediaEncoder _mediaEncoder;
 1346
 1347            private bool _disposed = false;
 1348
 1349            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
 1350            {
 1351                Process = process;
 01352                _mediaEncoder = mediaEncoder;
 01353                Process.Exited += OnProcessExited;
 01354            }
 1355
 1356            public Process Process { get; }
 1357
 1358            public bool HasExited { get; private set; }
 1359
 1360            public int? ExitCode { get; private set; }
 1361
 1362            private void OnProcessExited(object sender, EventArgs e)
 1363            {
 01364                var process = (Process)sender;
 1365
 01366                HasExited = true;
 1367
 1368                try
 1369                {
 01370                    ExitCode = process.ExitCode;
 01371                }
 01372                catch
 1373                {
 01374                }
 1375
 01376                DisposeProcess(process);
 01377            }
 1378
 1379            private void DisposeProcess(Process process)
 01380            {
 1381                lock (_mediaEncoder._runningProcessesLock)
 1382                {
 01383                    _mediaEncoder._runningProcesses.Remove(this);
 01384                }
 1385
 01386                process.Dispose();
 01387            }
 1388
 1389            public void Dispose()
 1390            {
 01391                if (!_disposed)
 1392                {
 01393                    if (Process is not null)
 1394                    {
 01395                        Process.Exited -= OnProcessExited;
 01396                        DisposeProcess(Process);
 1397                    }
 1398                }
 1399
 01400                _disposed = true;
 01401            }
 1402        }
 1403    }
 1404}

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()