< 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: 53
Uncovered lines: 553
Coverable lines: 606
Total lines: 1398
Line coverage: 8.7%
Branch coverage
7%
Covered branches: 21
Total branches: 270
Branch coverage: 7.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 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: 1398 1/23/2026 - 12:11:06 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: 1398

Coverage delta

Coverage delta 11 -11

Metrics

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

File(s)

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

#LineLine coverage
 1#nullable disable
 2#pragma warning disable CS1591
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Text.Json;
 11using System.Text.RegularExpressions;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using AsyncKeyedLock;
 15using Jellyfin.Data.Enums;
 16using Jellyfin.Extensions;
 17using Jellyfin.Extensions.Json;
 18using Jellyfin.Extensions.Json.Converters;
 19using MediaBrowser.Common;
 20using MediaBrowser.Common.Configuration;
 21using MediaBrowser.Common.Extensions;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Extensions;
 24using MediaBrowser.Controller.MediaEncoding;
 25using MediaBrowser.MediaEncoding.Probing;
 26using MediaBrowser.Model.Configuration;
 27using MediaBrowser.Model.Dlna;
 28using MediaBrowser.Model.Drawing;
 29using MediaBrowser.Model.Dto;
 30using MediaBrowser.Model.Entities;
 31using MediaBrowser.Model.Globalization;
 32using MediaBrowser.Model.IO;
 33using MediaBrowser.Model.MediaInfo;
 34using Microsoft.Extensions.Configuration;
 35using Microsoft.Extensions.Logging;
 36
 37namespace MediaBrowser.MediaEncoding.Encoder
 38{
 39    /// <summary>
 40    /// Class MediaEncoder.
 41    /// </summary>
 42    public partial class MediaEncoder : IMediaEncoder, IDisposable
 43    {
 44        /// <summary>
 45        /// The default SDR image extraction timeout in milliseconds.
 46        /// </summary>
 47        internal const int DefaultSdrImageExtractionTimeout = 10000;
 48
 49        /// <summary>
 50        /// The default HDR image extraction timeout in milliseconds.
 51        /// </summary>
 52        internal const int DefaultHdrImageExtractionTimeout = 20000;
 53
 54        private readonly ILogger<MediaEncoder> _logger;
 55        private readonly IServerConfigurationManager _configurationManager;
 56        private readonly IFileSystem _fileSystem;
 57        private readonly ILocalizationManager _localization;
 58        private readonly IBlurayExaminer _blurayExaminer;
 59        private readonly IConfiguration _config;
 60        private readonly IServerConfigurationManager _serverConfig;
 61        private readonly string _startupOptionFFmpegPath;
 62
 63        private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
 64
 2265        private readonly Lock _runningProcessesLock = new();
 2266        private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
 67
 68        // MediaEncoder is registered as a Singleton
 69        private readonly JsonSerializerOptions _jsonSerializerOptions;
 70
 2271        private List<string> _encoders = new List<string>();
 2272        private List<string> _decoders = new List<string>();
 2273        private List<string> _hwaccels = new List<string>();
 2274        private List<string> _filters = new List<string>();
 2275        private IDictionary<FilterOptionType, bool> _filtersWithOption = new Dictionary<FilterOptionType, bool>();
 2276        private IDictionary<BitStreamFilterOptionType, bool> _bitStreamFiltersWithOption = new Dictionary<BitStreamFilte
 77
 78        private bool _isPkeyPauseSupported = false;
 79        private bool _isLowPriorityHwDecodeSupported = false;
 80        private bool _proberSupportsFirstVideoFrame = false;
 81
 82        private bool _isVaapiDeviceAmd = false;
 83        private bool _isVaapiDeviceInteliHD = false;
 84        private bool _isVaapiDeviceInteli965 = false;
 85        private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
 86        private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
 87
 88        private bool _isVideoToolboxAv1DecodeAvailable = false;
 89
 090        private static string[] _vulkanImageDrmFmtModifierExts =
 091        {
 092            "VK_EXT_image_drm_format_modifier",
 093        };
 94
 095        private static string[] _vulkanExternalMemoryDmaBufExts =
 096        {
 097            "VK_KHR_external_memory_fd",
 098            "VK_EXT_external_memory_dma_buf",
 099            "VK_KHR_external_semaphore_fd",
 0100            "VK_EXT_external_memory_host"
 0101        };
 102
 103        private Version _ffmpegVersion = null;
 22104        private string _ffmpegPath = string.Empty;
 105        private string _ffprobePath;
 106        private int _threads;
 107
 108        public MediaEncoder(
 109            ILogger<MediaEncoder> logger,
 110            IServerConfigurationManager configurationManager,
 111            IFileSystem fileSystem,
 112            IBlurayExaminer blurayExaminer,
 113            ILocalizationManager localization,
 114            IConfiguration config,
 115            IServerConfigurationManager serverConfig)
 116        {
 22117            _logger = logger;
 22118            _configurationManager = configurationManager;
 22119            _fileSystem = fileSystem;
 22120            _blurayExaminer = blurayExaminer;
 22121            _localization = localization;
 22122            _config = config;
 22123            _serverConfig = serverConfig;
 22124            _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathK
 125
 22126            _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
 22127            _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
 128
 129            // Although the type is not nullable, this might still be null during unit tests
 22130            var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
 22131            if (semaphoreCount < 1)
 132            {
 22133                semaphoreCount = Environment.ProcessorCount;
 134            }
 135
 22136            _thumbnailResourcePool = new(semaphoreCount);
 22137        }
 138
 139        /// <inheritdoc />
 0140        public string EncoderPath => _ffmpegPath;
 141
 142        /// <inheritdoc />
 0143        public string ProbePath => _ffprobePath;
 144
 145        /// <inheritdoc />
 0146        public Version EncoderVersion => _ffmpegVersion;
 147
 148        /// <inheritdoc />
 0149        public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
 150
 151        /// <inheritdoc />
 0152        public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
 153
 154        /// <inheritdoc />
 0155        public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
 156
 157        /// <inheritdoc />
 0158        public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
 159
 160        /// <inheritdoc />
 0161        public bool IsVaapiDeviceSupportVulkanDrmModifier => _isVaapiDeviceSupportVulkanDrmModifier;
 162
 163        /// <inheritdoc />
 0164        public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
 165
 0166        public bool IsVideoToolboxAv1DecodeAvailable => _isVideoToolboxAv1DecodeAvailable;
 167
 168        [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
 169        private static partial Regex FfprobePathRegex();
 170
 171        /// <summary>
 172        /// Run at startup to validate ffmpeg.
 173        /// Sets global variables FFmpegPath.
 174        /// Precedence is: CLI/Env var > Config > $PATH.
 175        /// </summary>
 176        /// <returns>bool indicates whether a valid ffmpeg is found.</returns>
 177        public bool SetFFmpegPath()
 178        {
 21179            var skipValidation = _config.GetFFmpegSkipValidation();
 21180            if (skipValidation)
 181            {
 21182                _logger.LogWarning("FFmpeg: Skipping FFmpeg Validation due to FFmpeg:novalidation set to true");
 21183                return true;
 184            }
 185
 186            // 1) Check if the --ffmpeg CLI switch has been given
 0187            var ffmpegPath = _startupOptionFFmpegPath;
 0188            string ffmpegPathSetMethodText = "command line or environment variable";
 0189            if (string.IsNullOrEmpty(ffmpegPath))
 190            {
 191                // 2) Custom path stored in config/encoding xml file under tag <EncoderAppPath> should be used as a fall
 0192                ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
 0193                ffmpegPathSetMethodText = "encoding.xml config file";
 0194                if (string.IsNullOrEmpty(ffmpegPath))
 195                {
 196                    // 3) Check "ffmpeg"
 0197                    ffmpegPath = "ffmpeg";
 0198                    ffmpegPathSetMethodText = "system $PATH";
 199                }
 200            }
 201
 0202            if (!ValidatePath(ffmpegPath))
 203            {
 0204                _ffmpegPath = null;
 0205                _logger.LogError("FFmpeg: Path set by {FfmpegPathSetMethodText} is invalid", ffmpegPathSetMethodText);
 0206                return false;
 207            }
 208
 209            // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
 0210            var options = _configurationManager.GetEncodingOptions();
 0211            options.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
 0212            _configurationManager.SaveConfiguration("encoding", options);
 213
 214            // Only if mpeg path is set, try and set path to probe
 0215            if (_ffmpegPath is not null)
 216            {
 217                // Determine a probe path from the mpeg path
 0218                _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, "ffprobe$1");
 219
 220                // Interrogate to understand what coders are supported
 0221                var validator = new EncoderValidator(_logger, _ffmpegPath);
 222
 0223                SetAvailableDecoders(validator.GetDecoders());
 0224                SetAvailableEncoders(validator.GetEncoders());
 0225                SetAvailableFilters(validator.GetFilters());
 0226                SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
 0227                SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption());
 0228                SetAvailableHwaccels(validator.GetHwaccels());
 0229                SetMediaEncoderVersion(validator);
 230
 0231                _threads = EncodingHelper.GetNumberOfThreads(null, options, null);
 232
 0233                _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p      pause transcoding", _ffmpegVersion);
 0234                _isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
 0235                _proberSupportsFirstVideoFrame = validator.CheckSupportedProberOption("only_first_vframe", _ffprobePath)
 236
 237                // Check the Vaapi device vendor
 0238                if (OperatingSystem.IsLinux()
 0239                    && SupportsHwaccel("vaapi")
 0240                    && !string.IsNullOrEmpty(options.VaapiDevice)
 0241                    && options.HardwareAccelerationType == HardwareAccelerationType.vaapi)
 242                {
 0243                    _isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevic
 0244                    _isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDev
 0245                    _isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiD
 0246                    _isVaapiDeviceSupportVulkanDrmModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.Vaapi
 0247                    _isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiD
 248
 0249                    if (_isVaapiDeviceAmd)
 250                    {
 0251                        _logger.LogInformation("VAAPI device {RenderNodePath} is AMD GPU", options.VaapiDevice);
 252                    }
 0253                    else if (_isVaapiDeviceInteliHD)
 254                    {
 0255                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (iHD)", options.VaapiDevice);
 256                    }
 0257                    else if (_isVaapiDeviceInteli965)
 258                    {
 0259                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice)
 260                    }
 261
 0262                    if (_isVaapiDeviceSupportVulkanDrmModifier)
 263                    {
 0264                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM modifier", options.Vaa
 265                    }
 266
 0267                    if (_isVaapiDeviceSupportVulkanDrmInterop)
 268                    {
 0269                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.Vaap
 270                    }
 271                }
 272
 273                // Check if VideoToolbox supports AV1 decode
 0274                if (OperatingSystem.IsMacOS() && SupportsHwaccel("videotoolbox"))
 275                {
 0276                    _isVideoToolboxAv1DecodeAvailable = validator.CheckIsVideoToolboxAv1DecodeAvailable();
 277                }
 278            }
 279
 0280            _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
 0281            return !string.IsNullOrWhiteSpace(ffmpegPath);
 282        }
 283
 284        /// <summary>
 285        /// Validates the supplied FQPN to ensure it is a ffmpeg utility.
 286        /// If checks pass, global variable FFmpegPath is updated.
 287        /// </summary>
 288        /// <param name="path">FQPN to test.</param>
 289        /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
 290        private bool ValidatePath(string path)
 291        {
 0292            if (string.IsNullOrEmpty(path))
 293            {
 0294                return false;
 295            }
 296
 0297            bool rc = new EncoderValidator(_logger, path).ValidateVersion();
 0298            if (!rc)
 299            {
 0300                _logger.LogError("FFmpeg: Failed version check: {Path}", path);
 0301                return false;
 302            }
 303
 0304            _ffmpegPath = path;
 0305            return true;
 306        }
 307
 308        private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false)
 309        {
 310            try
 311            {
 0312                var files = _fileSystem.GetFilePaths(path, recursive);
 313
 0314                return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringCom
 0315                                                    && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.Ordi
 316            }
 0317            catch (Exception)
 318            {
 319                // Trap all exceptions, like DirNotExists, and return null
 0320                return null;
 321            }
 0322        }
 323
 324        public void SetAvailableEncoders(IEnumerable<string> list)
 325        {
 0326            _encoders = list.ToList();
 0327        }
 328
 329        public void SetAvailableDecoders(IEnumerable<string> list)
 330        {
 0331            _decoders = list.ToList();
 0332        }
 333
 334        public void SetAvailableHwaccels(IEnumerable<string> list)
 335        {
 0336            _hwaccels = list.ToList();
 0337        }
 338
 339        public void SetAvailableFilters(IEnumerable<string> list)
 340        {
 0341            _filters = list.ToList();
 0342        }
 343
 344        public void SetAvailableFiltersWithOption(IDictionary<FilterOptionType, bool> dict)
 345        {
 0346            _filtersWithOption = dict;
 0347        }
 348
 349        public void SetAvailableBitStreamFiltersWithOption(IDictionary<BitStreamFilterOptionType, bool> dict)
 350        {
 0351            _bitStreamFiltersWithOption = dict;
 0352        }
 353
 354        public void SetMediaEncoderVersion(EncoderValidator validator)
 355        {
 0356            _ffmpegVersion = validator.GetFFmpegVersion();
 0357        }
 358
 359        /// <inheritdoc />
 360        public bool SupportsEncoder(string encoder)
 361        {
 0362            return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
 363        }
 364
 365        /// <inheritdoc />
 366        public bool SupportsDecoder(string decoder)
 367        {
 0368            return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase);
 369        }
 370
 371        /// <inheritdoc />
 372        public bool SupportsHwaccel(string hwaccel)
 373        {
 0374            return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
 375        }
 376
 377        /// <inheritdoc />
 378        public bool SupportsFilter(string filter)
 379        {
 0380            return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
 381        }
 382
 383        /// <inheritdoc />
 384        public bool SupportsFilterWithOption(FilterOptionType option)
 385        {
 0386            return _filtersWithOption.TryGetValue(option, out var val) && val;
 387        }
 388
 389        public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option)
 390        {
 0391            return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val;
 392        }
 393
 394        public bool CanEncodeToAudioCodec(string codec)
 395        {
 0396            if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
 397            {
 0398                codec = "libopus";
 399            }
 0400            else if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
 401            {
 0402                codec = "libmp3lame";
 403            }
 404
 0405            return SupportsEncoder(codec);
 406        }
 407
 408        public bool CanEncodeToSubtitleCodec(string codec)
 409        {
 410            // TODO
 0411            return true;
 412        }
 413
 414        /// <inheritdoc />
 415        public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
 416        {
 0417            var extractChapters = request.ExtractChapters;
 0418            var extraArgs = GetExtraArguments(request);
 419
 0420            return GetMediaInfoInternal(
 0421                GetInputArgument(request.MediaSource.Path, request.MediaSource),
 0422                request.MediaSource.Path,
 0423                request.MediaSource.Protocol,
 0424                extractChapters,
 0425                extraArgs,
 0426                request.MediaType == DlnaProfileType.Audio,
 0427                request.MediaSource.VideoType,
 0428                cancellationToken);
 429        }
 430
 431        internal string GetExtraArguments(MediaInfoRequest request)
 432        {
 1433            var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
 1434            var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
 1435            var analyzeDuration = string.Empty;
 1436            var extraArgs = string.Empty;
 437
 1438            if (request.MediaSource.AnalyzeDurationMs > 0)
 439            {
 0440                analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000);
 441            }
 1442            else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
 443            {
 0444                analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
 445            }
 446
 1447            if (!string.IsNullOrEmpty(analyzeDuration))
 448            {
 0449                extraArgs = analyzeDuration;
 450            }
 451
 1452            if (!string.IsNullOrEmpty(ffmpegProbeSize))
 453            {
 0454                extraArgs += " -probesize " + ffmpegProbeSize;
 455            }
 456
 1457            if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
 458            {
 1459                extraArgs += $" -user_agent \"{userAgent}\"";
 460            }
 461
 1462            if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
 463            {
 0464                extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
 465            }
 466
 1467            return extraArgs;
 468        }
 469
 470        /// <inheritdoc />
 471        public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource)
 472        {
 0473            return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol);
 474        }
 475
 476        /// <inheritdoc />
 477        public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
 478        {
 0479            var prefix = "file";
 0480            if (mediaSource.IsoType == IsoType.BluRay)
 481            {
 0482                prefix = "bluray";
 483            }
 484
 0485            return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol);
 486        }
 487
 488        /// <inheritdoc />
 489        public string GetExternalSubtitleInputArgument(string inputFile)
 490        {
 491            const string Prefix = "file";
 492
 0493            return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File);
 494        }
 495
 496        /// <summary>
 497        /// Gets the media info internal.
 498        /// </summary>
 499        /// <returns>Task{MediaInfoResult}.</returns>
 500        private async Task<MediaInfo> GetMediaInfoInternal(
 501            string inputPath,
 502            string primaryPath,
 503            MediaProtocol protocol,
 504            bool extractChapters,
 505            string probeSizeArgument,
 506            bool isAudio,
 507            VideoType? videoType,
 508            CancellationToken cancellationToken)
 509        {
 0510            var args = extractChapters
 0511                ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
 0512                : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
 513
 0514            if (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame)
 515            {
 0516                args += " -show_frames -only_first_vframe";
 517            }
 518
 0519            args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
 520
 0521            var process = new Process
 0522            {
 0523                StartInfo = new ProcessStartInfo
 0524                {
 0525                    CreateNoWindow = true,
 0526                    UseShellExecute = false,
 0527
 0528                    // Must consume both or ffmpeg may hang due to deadlocks.
 0529                    RedirectStandardOutput = true,
 0530
 0531                    FileName = _ffprobePath,
 0532                    Arguments = args,
 0533
 0534                    WindowStyle = ProcessWindowStyle.Hidden,
 0535                    ErrorDialog = false,
 0536                },
 0537                EnableRaisingEvents = true
 0538            };
 539
 0540            _logger.LogDebug("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
 541
 0542            var memoryStream = new MemoryStream();
 0543            await using (memoryStream.ConfigureAwait(false))
 0544            using (var processWrapper = new ProcessWrapper(process, this))
 545            {
 0546                StartProcess(processWrapper);
 0547                using var reader = process.StandardOutput;
 0548                await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
 0549                memoryStream.Seek(0, SeekOrigin.Begin);
 550                InternalMediaInfoResult result;
 551                try
 552                {
 0553                    result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
 0554                                        memoryStream,
 0555                                        _jsonSerializerOptions,
 0556                                        cancellationToken).ConfigureAwait(false);
 0557                }
 0558                catch
 559                {
 0560                    StopProcess(processWrapper, 100);
 561
 0562                    throw;
 563                }
 564
 0565                if (result is null || (result.Streams is null && result.Format is null))
 566                {
 0567                    throw new FfmpegException("ffprobe failed - streams and format are both null.");
 568                }
 569
 0570                if (result.Streams is not null)
 571                {
 572                    // Normalize aspect ratio if invalid
 0573                    foreach (var stream in result.Streams)
 574                    {
 0575                        if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 576                        {
 0577                            stream.DisplayAspectRatio = string.Empty;
 578                        }
 579
 0580                        if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 581                        {
 0582                            stream.SampleAspectRatio = string.Empty;
 583                        }
 584                    }
 585                }
 586
 0587                return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primar
 588            }
 0589        }
 590
 591        /// <inheritdoc />
 592        public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
 593        {
 0594            var mediaSource = new MediaSourceInfo
 0595            {
 0596                Protocol = MediaProtocol.File
 0597            };
 598
 0599            return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, canc
 600        }
 601
 602        /// <inheritdoc />
 603        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 604        {
 0605            return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, Image
 606        }
 607
 608        /// <inheritdoc />
 609        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 610        {
 0611            return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, tar
 612        }
 613
 614        private async Task<string> ExtractImage(
 615            string inputFile,
 616            string container,
 617            MediaStream videoStream,
 618            int? imageStreamIndex,
 619            MediaSourceInfo mediaSource,
 620            bool isAudio,
 621            Video3DFormat? threedFormat,
 622            TimeSpan? offset,
 623            ImageFormat? targetFormat,
 624            CancellationToken cancellationToken)
 625        {
 0626            var inputArgument = GetInputPathArgument(inputFile, mediaSource);
 627
 0628            if (!isAudio)
 629            {
 630                try
 631                {
 0632                    return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFor
 633                }
 0634                catch (ArgumentException)
 635                {
 0636                    throw;
 637                }
 0638                catch (Exception ex)
 639                {
 0640                    _logger.LogWarning(ex, "I-frame image extraction failed, will attempt standard way. Input: {Argument
 0641                }
 642            }
 643
 0644            return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, off
 0645        }
 646
 647        private string GetImageResolutionParameter()
 648        {
 0649            var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
 0650            {
 0651                ImageResolution.P144 => "256x144",
 0652                ImageResolution.P240 => "426x240",
 0653                ImageResolution.P360 => "640x360",
 0654                ImageResolution.P480 => "854x480",
 0655                ImageResolution.P720 => "1280x720",
 0656                ImageResolution.P1080 => "1920x1080",
 0657                ImageResolution.P1440 => "2560x1440",
 0658                ImageResolution.P2160 => "3840x2160",
 0659                _ => string.Empty
 0660            };
 661
 0662            if (!string.IsNullOrEmpty(imageResolutionParameter))
 663            {
 0664                imageResolutionParameter = " -s " + imageResolutionParameter;
 665            }
 666
 0667            return imageResolutionParameter;
 668        }
 669
 670        private async Task<string> ExtractImageInternal(
 671            string inputPath,
 672            string container,
 673            MediaStream videoStream,
 674            int? imageStreamIndex,
 675            Video3DFormat? threedFormat,
 676            TimeSpan? offset,
 677            bool useIFrame,
 678            ImageFormat? targetFormat,
 679            bool isAudio,
 680            CancellationToken cancellationToken)
 681        {
 0682            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 683
 0684            var useTradeoff = _config.GetFFmpegImgExtractPerfTradeoff();
 685
 0686            var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
 687
 0688            var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ou
 0689            Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
 690
 691            // deint -> scale -> thumbnail -> tonemap.
 692            // put the SW tonemap right after the thumbnail to do it only once to reduce cpu usage.
 0693            var filters = new List<string>();
 694
 695            // deinterlace using bwdif algorithm for video stream.
 0696            if (videoStream is not null && videoStream.IsInterlaced)
 697            {
 0698                filters.Add("bwdif=0:-1:0");
 699            }
 700
 701            // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the cor
 702            // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex
 0703            var scaler = threedFormat switch
 0704            {
 0705                // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may 
 0706                Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min
 0707                // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
 0708                Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw
 0709                // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may
 0710                Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):
 0711                // ftab crop height in half, set the display aspect,crop out any black bars we may have made
 0712                Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(
 0713                _ => "scale=round(iw*sar/2)*2:round(ih/2)*2"
 0714            };
 715
 0716            filters.Add(scaler);
 717
 718            // Use ffmpeg to sample N frames and pick the best thumbnail. Have a fall back just in case.
 0719            var enableThumbnail = !useTradeoff && useIFrame && !string.Equals("wtv", container, StringComparison.Ordinal
 0720            if (enableThumbnail)
 721            {
 0722                filters.Add("thumbnail=n=24");
 723            }
 724
 725            // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
 726            // Only enable Dolby Vision tonemap when tonemapx is available
 0727            var enableHdrExtraction = false;
 728
 0729            if (videoStream?.VideoRange == VideoRange.HDR)
 730            {
 0731                if (SupportsFilter("tonemapx"))
 732                {
 0733                    var peak = videoStream.VideoRangeType == VideoRangeType.DOVI ? "400" : "100";
 0734                    enableHdrExtraction = true;
 0735                    filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p:ran
 736                }
 0737                else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
 738                {
 0739                    enableHdrExtraction = true;
 0740                    filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:p
 741                }
 742            }
 743
 0744            var vf = string.Join(',', filters);
 0745            var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.Invariant
 0746            var args = string.Format(
 0747                CultureInfo.InvariantCulture,
 0748                "-i {0}{1} -threads {2} -v quiet -vframes 1 -vf {3}{4}{5} -f image2 \"{6}\"",
 0749                inputPath,
 0750                mapArg,
 0751                _threads,
 0752                vf,
 0753                isAudio ? string.Empty : GetImageResolutionParameter(),
 0754                EncodingHelper.GetVideoSyncOption("-1", EncoderVersion), // auto decide fps mode
 0755                tempExtractPath);
 756
 0757            if (offset.HasValue)
 758            {
 0759                args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
 760            }
 761
 762            // The mpegts demuxer cannot seek to keyframes, so we have to let the
 763            // decoder discard non-keyframes, which may contain corrupted images.
 0764            var seekMpegTs = offset.HasValue && string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
 0765            if (useIFrame && (useTradeoff || seekMpegTs))
 766            {
 0767                args = "-skip_frame nokey " + args;
 768            }
 769
 0770            if (!string.IsNullOrWhiteSpace(container))
 771            {
 0772                var inputFormat = EncodingHelper.GetInputFormat(container);
 0773                if (!string.IsNullOrWhiteSpace(inputFormat))
 774                {
 0775                    args = "-f " + inputFormat + " " + args;
 776                }
 777            }
 778
 0779            var process = new Process
 0780            {
 0781                StartInfo = new ProcessStartInfo
 0782                {
 0783                    CreateNoWindow = true,
 0784                    UseShellExecute = false,
 0785                    FileName = _ffmpegPath,
 0786                    Arguments = args,
 0787                    WindowStyle = ProcessWindowStyle.Hidden,
 0788                    ErrorDialog = false,
 0789                },
 0790                EnableRaisingEvents = true
 0791            };
 792
 0793            _logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Argum
 794
 0795            using (var processWrapper = new ProcessWrapper(process, this))
 796            {
 0797                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 798                {
 0799                    StartProcess(processWrapper);
 800
 0801                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 0802                    if (timeoutMs <= 0)
 803                    {
 0804                        timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTi
 805                    }
 806
 807                    try
 808                    {
 0809                        await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 0810                    }
 0811                    catch (OperationCanceledException ex)
 812                    {
 0813                        process.Kill(true);
 0814                        throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction t
 815                    }
 0816                }
 817
 0818                var file = _fileSystem.GetFileInfo(tempExtractPath);
 819
 0820                if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
 821                {
 0822                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 823                }
 824
 0825                return tempExtractPath;
 826            }
 0827        }
 828
 829        /// <inheritdoc />
 830        public async Task<string> ExtractVideoImagesOnIntervalAccelerated(
 831            string inputFile,
 832            string container,
 833            MediaSourceInfo mediaSource,
 834            MediaStream imageStream,
 835            int maxWidth,
 836            TimeSpan interval,
 837            bool allowHwAccel,
 838            bool enableHwEncoding,
 839            int? threads,
 840            int? qualityScale,
 841            ProcessPriorityClass? priority,
 842            bool enableKeyFrameOnlyExtraction,
 843            EncodingHelper encodingHelper,
 844            CancellationToken cancellationToken)
 845        {
 0846            var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
 0847            threads ??= _threads;
 848
 0849            if (allowHwAccel && enableKeyFrameOnlyExtraction)
 850            {
 0851                var hardwareAccelerationType = options.HardwareAccelerationType;
 0852                var supportsKeyFrameOnly = (hardwareAccelerationType == HardwareAccelerationType.nvenc && options.Enable
 0853                                           || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSyst
 0854                                           || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.Prefe
 0855                                           || hardwareAccelerationType == HardwareAccelerationType.vaapi
 0856                                           || hardwareAccelerationType == HardwareAccelerationType.videotoolbox
 0857                                           || hardwareAccelerationType == HardwareAccelerationType.rkmpp;
 0858                if (!supportsKeyFrameOnly)
 859                {
 860                    // Disable hardware acceleration when the hardware decoder does not support keyframe only mode.
 0861                    allowHwAccel = false;
 0862                    options = new EncodingOptions();
 863                }
 864            }
 865
 866            // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
 867            // Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
 0868            if (!allowHwAccel)
 869            {
 0870                options.EnableHardwareEncoding = false;
 0871                options.HardwareAccelerationType = HardwareAccelerationType.none;
 0872                options.EnableTonemapping = false;
 873            }
 874
 0875            if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.Asp
 876            {
 877                // For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimen
 0878                var darParts = imageStream.AspectRatio.Split(':');
 0879                var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], Cultu
 880                // When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
 881                // Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, 
 0882                var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05
 0883                if (shouldResetHeight)
 884                {
 885                    // SAR = DAR * Height / Width
 886                    // RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
 0887                    imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
 888                }
 889            }
 890
 0891            var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.To
 0892            var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
 0893            {
 0894                IsVideoRequest = true,  // must be true for InputVideoHwaccelArgs to return non-empty value
 0895                MediaSource = mediaSource,
 0896                VideoStream = imageStream,
 0897                BaseRequest = baseRequest,  // GetVideoProcessingFilterParam errors if null
 0898                MediaPath = inputFile,
 0899                OutputVideoCodec = "mjpeg"
 0900            };
 0901            var vidEncoder = enableHwEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideo
 902
 903            // Get input and filter arguments
 0904            var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
 0905            if (string.IsNullOrWhiteSpace(inputArg))
 906            {
 0907                throw new InvalidOperationException("EncodingHelper returned empty input arguments.");
 908            }
 909
 0910            if (!allowHwAccel)
 911            {
 0912                inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, 
 913            }
 914
 0915            if (options.HardwareAccelerationType == HardwareAccelerationType.videotoolbox && _isLowPriorityHwDecodeSuppo
 916            {
 917                // VideoToolbox supports low priority decoding, which is useful for trickplay
 0918                inputArg = "-hwaccel_flags +low_priority " + inputArg;
 919            }
 920
 0921            var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
 0922            if (string.IsNullOrWhiteSpace(filterParam))
 923            {
 0924                throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
 925            }
 926
 927            try
 928            {
 0929                return await ExtractVideoImagesOnIntervalInternal(
 0930                    (enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg,
 0931                    filterParam,
 0932                    vidEncoder,
 0933                    threads,
 0934                    qualityScale,
 0935                    priority,
 0936                    cancellationToken).ConfigureAwait(false);
 937            }
 0938            catch (FfmpegException ex)
 939            {
 0940                if (!enableKeyFrameOnlyExtraction)
 941                {
 0942                    throw;
 943                }
 944
 0945                _logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFil
 0946            }
 947
 0948            return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, 
 0949        }
 950
 951        private async Task<string> ExtractVideoImagesOnIntervalInternal(
 952            string inputArg,
 953            string filterParam,
 954            string vidEncoder,
 955            int? outputThreads,
 956            int? qualityScale,
 957            ProcessPriorityClass? priority,
 958            CancellationToken cancellationToken)
 959        {
 0960            if (string.IsNullOrWhiteSpace(inputArg))
 961            {
 0962                throw new InvalidOperationException("Empty or invalid input argument.");
 963            }
 964
 965            // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
 966            // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
 0967            var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
 0968            var encoderQualityOption = "-qscale:v ";
 969
 0970            if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
 0971                || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
 972            {
 973                // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
 0974                encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
 0975                encoderQualityOption = "-global_quality:v ";
 976            }
 977
 0978            if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
 979            {
 980                // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qs
 0981                encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
 982            }
 983
 0984            if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
 985            {
 986                // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qsca
 0987                encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
 0988                encoderQualityOption = "-qp_init:v ";
 989            }
 990
 991            // Output arguments
 0992            var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToSt
 0993            Directory.CreateDirectory(targetDirectory);
 0994            var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
 995
 996            // Final command arguments
 0997            var args = string.Format(
 0998                CultureInfo.InvariantCulture,
 0999                "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"",
 01000                inputArg,
 01001                filterParam,
 01002                outputThreads.GetValueOrDefault(_threads),
 01003                vidEncoder,
 01004                encoderQualityOption + encoderQuality + " ",
 01005                vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : stri
 01006                EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim() + " ", // passthrough timestamp
 01007                "image2",
 01008                outputPath);
 1009
 1010            // Start ffmpeg process
 01011            var process = new Process
 01012            {
 01013                StartInfo = new ProcessStartInfo
 01014                {
 01015                    CreateNoWindow = true,
 01016                    UseShellExecute = false,
 01017                    FileName = _ffmpegPath,
 01018                    Arguments = args,
 01019                    WindowStyle = ProcessWindowStyle.Hidden,
 01020                    ErrorDialog = false,
 01021                },
 01022                EnableRaisingEvents = true
 01023            };
 1024
 01025            var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, 
 01026            _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
 1027
 01028            using (var processWrapper = new ProcessWrapper(process, this))
 1029            {
 01030                bool ranToCompletion = false;
 1031
 01032                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 1033                {
 01034                    StartProcess(processWrapper);
 1035
 1036                    // Set process priority
 01037                    if (priority.HasValue)
 1038                    {
 1039                        try
 1040                        {
 01041                            processWrapper.Process.PriorityClass = priority.Value;
 01042                        }
 01043                        catch (Exception ex)
 1044                        {
 01045                            _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", prior
 01046                        }
 1047                    }
 1048
 1049                    // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
 1050                    // but we still need to detect if the process hangs.
 1051                    // Making the assumption that as long as new jpegs are showing up, everything is good.
 1052
 01053                    bool isResponsive = true;
 01054                    int lastCount = 0;
 01055                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 01056                    timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
 1057
 01058                    while (isResponsive && !cancellationToken.IsCancellationRequested)
 1059                    {
 1060                        try
 1061                        {
 01062                            await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 1063
 01064                            ranToCompletion = true;
 01065                            break;
 1066                        }
 01067                        catch (OperationCanceledException)
 1068                        {
 1069                            // We don't actually expect the process to be finished in one timeout span, just that one im
 01070                        }
 1071
 01072                        var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
 1073
 01074                        isResponsive = jpegCount > lastCount;
 01075                        lastCount = jpegCount;
 1076                    }
 1077
 01078                    if (!ranToCompletion)
 1079                    {
 01080                        if (!isResponsive)
 1081                        {
 01082                            _logger.LogInformation("Trickplay process unresponsive.");
 1083                        }
 1084
 01085                        _logger.LogInformation("Stopping trickplay extraction.");
 01086                        StopProcess(processWrapper, 1000);
 1087                    }
 01088                }
 1089
 01090                if (!ranToCompletion || processWrapper.ExitCode != 0)
 1091                {
 1092                    // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed 
 1093                    // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed
 1094                    try
 1095                    {
 01096                        Directory.Delete(targetDirectory, true);
 01097                    }
 01098                    catch (Exception e)
 1099                    {
 01100                        _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory)
 01101                    }
 1102
 01103                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 1104                }
 1105
 01106                return targetDirectory;
 1107            }
 01108        }
 1109
 1110        public string GetTimeParameter(long ticks)
 1111        {
 01112            var time = TimeSpan.FromTicks(ticks);
 1113
 01114            return GetTimeParameter(time);
 1115        }
 1116
 1117        public string GetTimeParameter(TimeSpan time)
 1118        {
 01119            return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
 1120        }
 1121
 1122        private void StartProcess(ProcessWrapper process)
 1123        {
 01124            process.Process.Start();
 1125
 1126            try
 1127            {
 01128                process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
 01129            }
 01130            catch (Exception ex)
 1131            {
 01132                _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Pr
 01133            }
 1134
 1135            lock (_runningProcessesLock)
 1136            {
 01137                _runningProcesses.Add(process);
 01138            }
 01139        }
 1140
 1141        private void StopProcess(ProcessWrapper process, int waitTimeMs)
 1142        {
 1143            try
 1144            {
 01145                if (process.Process.WaitForExit(waitTimeMs))
 1146                {
 01147                    return;
 1148                }
 1149
 01150                _logger.LogInformation("Killing ffmpeg process");
 1151
 01152                process.Process.Kill();
 01153            }
 01154            catch (InvalidOperationException)
 1155            {
 1156                // The process has already exited or
 1157                // there is no process associated with this Process object.
 01158            }
 01159            catch (Exception ex)
 1160            {
 01161                _logger.LogError(ex, "Error killing process");
 01162            }
 01163        }
 1164
 1165        private void StopProcesses()
 211166        {
 1167            List<ProcessWrapper> processes;
 1168            lock (_runningProcessesLock)
 1169            {
 211170                processes = _runningProcesses.ToList();
 211171                _runningProcesses.Clear();
 211172            }
 1173
 421174            foreach (var process in processes)
 1175            {
 01176                if (!process.HasExited)
 1177                {
 01178                    StopProcess(process, 500);
 1179                }
 1180            }
 211181        }
 1182
 1183        public string EscapeSubtitleFilterPath(string path)
 1184        {
 1185            // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
 1186            // We need to double escape
 1187
 01188            return path
 01189                .Replace('\\', '/')
 01190                .Replace(":", "\\:", StringComparison.Ordinal)
 01191                .Replace("'", @"'\\\''", StringComparison.Ordinal)
 01192                .Replace("\"", "\\\"", StringComparison.Ordinal);
 1193        }
 1194
 1195        /// <inheritdoc />
 1196        public void Dispose()
 1197        {
 211198            Dispose(true);
 211199            GC.SuppressFinalize(this);
 211200        }
 1201
 1202        /// <summary>
 1203        /// Releases unmanaged and - optionally - managed resources.
 1204        /// </summary>
 1205        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 1206        protected virtual void Dispose(bool dispose)
 1207        {
 211208            if (dispose)
 1209            {
 211210                StopProcesses();
 211211                _thumbnailResourcePool.Dispose();
 1212            }
 211213        }
 1214
 1215        /// <inheritdoc />
 1216        public Task ConvertImage(string inputPath, string outputPath)
 1217        {
 01218            throw new NotImplementedException();
 1219        }
 1220
 1221        /// <inheritdoc />
 1222        public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
 1223        {
 1224            // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VO
 01225            var allVobs = _fileSystem.GetFiles(path, true)
 01226                .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
 01227                .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
 01228                .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
 01229                .OrderBy(i => i.FullName)
 01230                .ToList();
 1231
 01232            if (titleNumber.HasValue)
 1233            {
 01234                var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
 01235                var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
 1236
 01237                if (vobs.Count > 0)
 1238                {
 01239                    return vobs.Select(i => i.FullName).ToList();
 1240                }
 1241
 01242                _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
 1243            }
 1244
 1245            // Check for multiple big titles (> 900 MB)
 01246            var titles = allVobs
 01247                .Where(vob => vob.Length >= 900 * 1024 * 1024)
 01248                .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
 01249                .Distinct()
 01250                .ToList();
 1251
 1252            // Fall back to first title if no big title is found
 01253            if (titles.Count == 0)
 1254            {
 01255                titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
 1256            }
 1257
 1258            // Aggregate all .vob files of the titles
 01259            return allVobs
 01260                .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToStr
 01261                .Select(i => i.FullName)
 01262                .Order()
 01263                .ToList();
 1264        }
 1265
 1266        /// <inheritdoc />
 1267        public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
 01268            => _blurayExaminer.GetDiscInfo(path).Files;
 1269
 1270        /// <inheritdoc />
 1271        public string GetInputPathArgument(EncodingJobInfo state)
 01272            => GetInputPathArgument(state.MediaPath, state.MediaSource);
 1273
 1274        /// <inheritdoc />
 1275        public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
 1276        {
 01277            return mediaSource.VideoType switch
 01278            {
 01279                VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource),
 01280                VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource),
 01281                _ => GetInputArgument(path, mediaSource)
 01282            };
 1283        }
 1284
 1285        /// <inheritdoc />
 1286        public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
 1287        {
 1288            // Get all playable files
 1289            IReadOnlyList<string> files;
 01290            var videoType = source.VideoType;
 01291            if (videoType == VideoType.Dvd)
 1292            {
 01293                files = GetPrimaryPlaylistVobFiles(source.Path, null);
 1294            }
 01295            else if (videoType == VideoType.BluRay)
 1296            {
 01297                files = GetPrimaryPlaylistM2tsFiles(source.Path);
 1298            }
 1299            else
 1300            {
 01301                return;
 1302            }
 1303
 1304            // Generate concat configuration entries for each file and write to file
 01305            Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
 01306            using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
 01307            foreach (var path in files)
 1308            {
 01309                var mediaInfoResult = GetMediaInfo(
 01310                    new MediaInfoRequest
 01311                    {
 01312                        MediaType = DlnaProfileType.Video,
 01313                        MediaSource = new MediaSourceInfo
 01314                        {
 01315                            Path = path,
 01316                            Protocol = MediaProtocol.File,
 01317                            VideoType = videoType
 01318                        }
 01319                    },
 01320                    CancellationToken.None).GetAwaiter().GetResult();
 1321
 01322                var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
 1323
 1324                // Add file path stanza to concat configuration
 01325                sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
 1326
 1327                // Add duration stanza to concat configuration
 01328                sw.WriteLine("duration {0}", duration);
 1329            }
 01330        }
 1331
 1332        public bool CanExtractSubtitles(string codec)
 1333        {
 01334            return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction;
 1335        }
 1336
 1337        private sealed class ProcessWrapper : IDisposable
 1338        {
 1339            private readonly MediaEncoder _mediaEncoder;
 1340
 1341            private bool _disposed = false;
 1342
 1343            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
 1344            {
 1345                Process = process;
 01346                _mediaEncoder = mediaEncoder;
 01347                Process.Exited += OnProcessExited;
 01348            }
 1349
 1350            public Process Process { get; }
 1351
 1352            public bool HasExited { get; private set; }
 1353
 1354            public int? ExitCode { get; private set; }
 1355
 1356            private void OnProcessExited(object sender, EventArgs e)
 1357            {
 01358                var process = (Process)sender;
 1359
 01360                HasExited = true;
 1361
 1362                try
 1363                {
 01364                    ExitCode = process.ExitCode;
 01365                }
 01366                catch
 1367                {
 01368                }
 1369
 01370                DisposeProcess(process);
 01371            }
 1372
 1373            private void DisposeProcess(Process process)
 01374            {
 1375                lock (_mediaEncoder._runningProcessesLock)
 1376                {
 01377                    _mediaEncoder._runningProcesses.Remove(this);
 01378                }
 1379
 01380                process.Dispose();
 01381            }
 1382
 1383            public void Dispose()
 1384            {
 01385                if (!_disposed)
 1386                {
 01387                    if (Process is not null)
 1388                    {
 01389                        Process.Exited -= OnProcessExited;
 01390                        DisposeProcess(Process);
 1391                    }
 1392                }
 1393
 01394                _disposed = true;
 01395            }
 1396        }
 1397    }
 1398}

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