< Summary - Jellyfin

Information
Class: MediaBrowser.MediaEncoding.Encoder.MediaEncoder
Assembly: MediaBrowser.MediaEncoding
File(s): /srv/git/jellyfin/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
Line coverage
14%
Covered lines: 53
Uncovered lines: 312
Coverable lines: 365
Total lines: 1382
Line coverage: 14.5%
Branch coverage
12%
Covered branches: 21
Total branches: 163
Branch coverage: 12.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)83.33%66100%
.cctor()100%210%
get_EncoderPath()100%210%
get_ProbePath()100%210%
get_EncoderVersion()100%210%
get_IsPkeyPauseSupported()100%210%
get_IsVaapiDeviceAmd()100%210%
get_IsVaapiDeviceInteliHD()100%210%
get_IsVaapiDeviceInteli965()100%210%
get_IsVaapiDeviceSupportVulkanDrmModifier()100%210%
get_IsVaapiDeviceSupportVulkanDrmInterop()100%210%
get_IsVideoToolboxAv1DecodeAvailable()100%210%
SetFFmpegPath()2.77%1074367.14%
ValidatePath(...)0%2040%
GetEncoderPathFromDirectory(...)100%210%
SetAvailableEncoders(...)100%210%
SetAvailableDecoders(...)100%210%
SetAvailableHwaccels(...)100%210%
SetAvailableFilters(...)100%210%
SetAvailableFiltersWithOption(...)100%210%
SetAvailableBitStreamFiltersWithOption(...)100%210%
SetMediaEncoderVersion(...)100%210%
SupportsEncoder(...)100%210%
SupportsDecoder(...)100%210%
SupportsHwaccel(...)100%210%
SupportsFilter(...)100%210%
SupportsFilterWithOption(...)0%620%
SupportsBitStreamFilterWithOption(...)100%210%
CanEncodeToAudioCodec(...)0%2040%
CanEncodeToSubtitleCodec(...)100%210%
GetMediaInfo(...)0%620%
GetExtraArguments(...)61.11%261870.58%
GetInputArgument(...)100%210%
GetInputArgument(...)0%620%
GetExternalSubtitleInputArgument(...)100%210%
ExtractAudioImage(...)100%210%
ExtractVideoImage(...)100%210%
ExtractVideoImage(...)100%210%
GetImageResolutionParameter()0%132110%
ExtractVideoImagesOnIntervalAccelerated(...)0%2352480%
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<int, bool> _filtersWithOption = new Dictionary<int, 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<int, 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            if (_filtersWithOption.TryGetValue((int)option, out var val))
 387            {
 0388                return val;
 389            }
 390
 0391            return false;
 392        }
 393
 394        public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option)
 395        {
 0396            return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val;
 397        }
 398
 399        public bool CanEncodeToAudioCodec(string codec)
 400        {
 0401            if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
 402            {
 0403                codec = "libopus";
 404            }
 0405            else if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
 406            {
 0407                codec = "libmp3lame";
 408            }
 409
 0410            return SupportsEncoder(codec);
 411        }
 412
 413        public bool CanEncodeToSubtitleCodec(string codec)
 414        {
 415            // TODO
 0416            return true;
 417        }
 418
 419        /// <inheritdoc />
 420        public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
 421        {
 0422            var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
 0423            var extraArgs = GetExtraArguments(request);
 424
 0425            return GetMediaInfoInternal(
 0426                GetInputArgument(request.MediaSource.Path, request.MediaSource),
 0427                request.MediaSource.Path,
 0428                request.MediaSource.Protocol,
 0429                extractChapters,
 0430                extraArgs,
 0431                request.MediaType == DlnaProfileType.Audio,
 0432                request.MediaSource.VideoType,
 0433                cancellationToken);
 434        }
 435
 436        internal string GetExtraArguments(MediaInfoRequest request)
 437        {
 1438            var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
 1439            var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
 1440            var analyzeDuration = string.Empty;
 1441            var extraArgs = string.Empty;
 442
 1443            if (request.MediaSource.AnalyzeDurationMs > 0)
 444            {
 0445                analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000);
 446            }
 1447            else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
 448            {
 0449                analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
 450            }
 451
 1452            if (!string.IsNullOrEmpty(analyzeDuration))
 453            {
 0454                extraArgs = analyzeDuration;
 455            }
 456
 1457            if (!string.IsNullOrEmpty(ffmpegProbeSize))
 458            {
 0459                extraArgs += " -probesize " + ffmpegProbeSize;
 460            }
 461
 1462            if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
 463            {
 1464                extraArgs += $" -user_agent \"{userAgent}\"";
 465            }
 466
 1467            if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
 468            {
 0469                extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
 470            }
 471
 1472            return extraArgs;
 473        }
 474
 475        /// <inheritdoc />
 476        public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource)
 477        {
 0478            return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol);
 479        }
 480
 481        /// <inheritdoc />
 482        public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
 483        {
 0484            var prefix = "file";
 0485            if (mediaSource.IsoType == IsoType.BluRay)
 486            {
 0487                prefix = "bluray";
 488            }
 489
 0490            return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol);
 491        }
 492
 493        /// <inheritdoc />
 494        public string GetExternalSubtitleInputArgument(string inputFile)
 495        {
 496            const string Prefix = "file";
 497
 0498            return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File);
 499        }
 500
 501        /// <summary>
 502        /// Gets the media info internal.
 503        /// </summary>
 504        /// <returns>Task{MediaInfoResult}.</returns>
 505        private async Task<MediaInfo> GetMediaInfoInternal(
 506            string inputPath,
 507            string primaryPath,
 508            MediaProtocol protocol,
 509            bool extractChapters,
 510            string probeSizeArgument,
 511            bool isAudio,
 512            VideoType? videoType,
 513            CancellationToken cancellationToken)
 514        {
 515            var args = extractChapters
 516                ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
 517                : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
 518
 519            if (_proberSupportsFirstVideoFrame)
 520            {
 521                args += " -show_frames -only_first_vframe";
 522            }
 523
 524            args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
 525
 526            var process = new Process
 527            {
 528                StartInfo = new ProcessStartInfo
 529                {
 530                    CreateNoWindow = true,
 531                    UseShellExecute = false,
 532
 533                    // Must consume both or ffmpeg may hang due to deadlocks.
 534                    RedirectStandardOutput = true,
 535
 536                    FileName = _ffprobePath,
 537                    Arguments = args,
 538
 539                    WindowStyle = ProcessWindowStyle.Hidden,
 540                    ErrorDialog = false,
 541                },
 542                EnableRaisingEvents = true
 543            };
 544
 545            _logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
 546
 547            var memoryStream = new MemoryStream();
 548            await using (memoryStream.ConfigureAwait(false))
 549            using (var processWrapper = new ProcessWrapper(process, this))
 550            {
 551                StartProcess(processWrapper);
 552                using var reader = process.StandardOutput;
 553                await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
 554                memoryStream.Seek(0, SeekOrigin.Begin);
 555                InternalMediaInfoResult result;
 556                try
 557                {
 558                    result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
 559                                        memoryStream,
 560                                        _jsonSerializerOptions,
 561                                        cancellationToken).ConfigureAwait(false);
 562                }
 563                catch
 564                {
 565                    StopProcess(processWrapper, 100);
 566
 567                    throw;
 568                }
 569
 570                if (result is null || (result.Streams is null && result.Format is null))
 571                {
 572                    throw new FfmpegException("ffprobe failed - streams and format are both null.");
 573                }
 574
 575                if (result.Streams is not null)
 576                {
 577                    // Normalize aspect ratio if invalid
 578                    foreach (var stream in result.Streams)
 579                    {
 580                        if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 581                        {
 582                            stream.DisplayAspectRatio = string.Empty;
 583                        }
 584
 585                        if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 586                        {
 587                            stream.SampleAspectRatio = string.Empty;
 588                        }
 589                    }
 590                }
 591
 592                return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primar
 593            }
 594        }
 595
 596        /// <inheritdoc />
 597        public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
 598        {
 0599            var mediaSource = new MediaSourceInfo
 0600            {
 0601                Protocol = MediaProtocol.File
 0602            };
 603
 0604            return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, canc
 605        }
 606
 607        /// <inheritdoc />
 608        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 609        {
 0610            return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, Image
 611        }
 612
 613        /// <inheritdoc />
 614        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 615        {
 0616            return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, tar
 617        }
 618
 619        private async Task<string> ExtractImage(
 620            string inputFile,
 621            string container,
 622            MediaStream videoStream,
 623            int? imageStreamIndex,
 624            MediaSourceInfo mediaSource,
 625            bool isAudio,
 626            Video3DFormat? threedFormat,
 627            TimeSpan? offset,
 628            ImageFormat? targetFormat,
 629            CancellationToken cancellationToken)
 630        {
 631            var inputArgument = GetInputPathArgument(inputFile, mediaSource);
 632
 633            if (!isAudio)
 634            {
 635                try
 636                {
 637                    return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFor
 638                }
 639                catch (ArgumentException)
 640                {
 641                    throw;
 642                }
 643                catch (Exception ex)
 644                {
 645                    _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}
 646                }
 647            }
 648
 649            return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, off
 650        }
 651
 652        private string GetImageResolutionParameter()
 653        {
 0654            var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
 0655            {
 0656                ImageResolution.P144 => "256x144",
 0657                ImageResolution.P240 => "426x240",
 0658                ImageResolution.P360 => "640x360",
 0659                ImageResolution.P480 => "854x480",
 0660                ImageResolution.P720 => "1280x720",
 0661                ImageResolution.P1080 => "1920x1080",
 0662                ImageResolution.P1440 => "2560x1440",
 0663                ImageResolution.P2160 => "3840x2160",
 0664                _ => string.Empty
 0665            };
 666
 0667            if (!string.IsNullOrEmpty(imageResolutionParameter))
 668            {
 0669                imageResolutionParameter = " -s " + imageResolutionParameter;
 670            }
 671
 0672            return imageResolutionParameter;
 673        }
 674
 675        private async Task<string> ExtractImageInternal(
 676            string inputPath,
 677            string container,
 678            MediaStream videoStream,
 679            int? imageStreamIndex,
 680            Video3DFormat? threedFormat,
 681            TimeSpan? offset,
 682            bool useIFrame,
 683            ImageFormat? targetFormat,
 684            bool isAudio,
 685            CancellationToken cancellationToken)
 686        {
 687            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 688
 689            var useTradeoff = _config.GetFFmpegImgExtractPerfTradeoff();
 690
 691            var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
 692
 693            var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ou
 694            Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
 695
 696            // deint -> scale -> thumbnail -> tonemap.
 697            // put the SW tonemap right after the thumbnail to do it only once to reduce cpu usage.
 698            var filters = new List<string>();
 699
 700            // deinterlace using bwdif algorithm for video stream.
 701            if (videoStream is not null && videoStream.IsInterlaced)
 702            {
 703                filters.Add("bwdif=0:-1:0");
 704            }
 705
 706            // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the cor
 707            // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex
 708            var scaler = threedFormat switch
 709            {
 710                // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may 
 711                Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min
 712                // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
 713                Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw
 714                // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may
 715                Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):
 716                // ftab crop height in half, set the display aspect,crop out any black bars we may have made
 717                Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(
 718                _ => "scale=round(iw*sar/2)*2:round(ih/2)*2"
 719            };
 720
 721            filters.Add(scaler);
 722
 723            // Use ffmpeg to sample N frames and pick the best thumbnail. Have a fall back just in case.
 724            var enableThumbnail = !useTradeoff && useIFrame && !string.Equals("wtv", container, StringComparison.Ordinal
 725            if (enableThumbnail)
 726            {
 727                filters.Add("thumbnail=n=24");
 728            }
 729
 730            // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
 731            // Only enable Dolby Vision tonemap when tonemapx is available
 732            var enableHdrExtraction = false;
 733
 734            if (videoStream?.VideoRange == VideoRange.HDR)
 735            {
 736                if (SupportsFilter("tonemapx"))
 737                {
 738                    var peak = videoStream.VideoRangeType == VideoRangeType.DOVI ? "400" : "100";
 739                    enableHdrExtraction = true;
 740                    filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p");
 741                }
 742                else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
 743                {
 744                    enableHdrExtraction = true;
 745                    filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:p
 746                }
 747            }
 748
 749            var vf = string.Join(',', filters);
 750            var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.Invariant
 751            var args = string.Format(
 752                CultureInfo.InvariantCulture,
 753                "-i {0}{1} -threads {2} -v quiet -vframes 1 -vf {3}{4}{5} -f image2 \"{6}\"",
 754                inputPath,
 755                mapArg,
 756                _threads,
 757                vf,
 758                isAudio ? string.Empty : GetImageResolutionParameter(),
 759                EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim(), // passthrough timestamp
 760                tempExtractPath);
 761
 762            if (offset.HasValue)
 763            {
 764                args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
 765            }
 766
 767            // The mpegts demuxer cannot seek to keyframes, so we have to let the
 768            // decoder discard non-keyframes, which may contain corrupted images.
 769            var seekMpegTs = offset.HasValue && string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
 770            if ((useIFrame && useTradeoff) || seekMpegTs)
 771            {
 772                args = "-skip_frame nokey " + args;
 773            }
 774
 775            if (!string.IsNullOrWhiteSpace(container))
 776            {
 777                var inputFormat = EncodingHelper.GetInputFormat(container);
 778                if (!string.IsNullOrWhiteSpace(inputFormat))
 779                {
 780                    args = "-f " + inputFormat + " " + args;
 781                }
 782            }
 783
 784            var process = new Process
 785            {
 786                StartInfo = new ProcessStartInfo
 787                {
 788                    CreateNoWindow = true,
 789                    UseShellExecute = false,
 790                    FileName = _ffmpegPath,
 791                    Arguments = args,
 792                    WindowStyle = ProcessWindowStyle.Hidden,
 793                    ErrorDialog = false,
 794                },
 795                EnableRaisingEvents = true
 796            };
 797
 798            _logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Argum
 799
 800            using (var processWrapper = new ProcessWrapper(process, this))
 801            {
 802                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 803                {
 804                    StartProcess(processWrapper);
 805
 806                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 807                    if (timeoutMs <= 0)
 808                    {
 809                        timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTi
 810                    }
 811
 812                    try
 813                    {
 814                        await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 815                    }
 816                    catch (OperationCanceledException ex)
 817                    {
 818                        process.Kill(true);
 819                        throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction t
 820                    }
 821                }
 822
 823                var file = _fileSystem.GetFileInfo(tempExtractPath);
 824
 825                if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
 826                {
 827                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 828                }
 829
 830                return tempExtractPath;
 831            }
 832        }
 833
 834        /// <inheritdoc />
 835        public Task<string> ExtractVideoImagesOnIntervalAccelerated(
 836            string inputFile,
 837            string container,
 838            MediaSourceInfo mediaSource,
 839            MediaStream imageStream,
 840            int maxWidth,
 841            TimeSpan interval,
 842            bool allowHwAccel,
 843            bool enableHwEncoding,
 844            int? threads,
 845            int? qualityScale,
 846            ProcessPriorityClass? priority,
 847            bool enableKeyFrameOnlyExtraction,
 848            EncodingHelper encodingHelper,
 849            CancellationToken cancellationToken)
 850        {
 0851            var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
 0852            threads ??= _threads;
 853
 0854            if (allowHwAccel && enableKeyFrameOnlyExtraction)
 855            {
 0856                var hardwareAccelerationType = options.HardwareAccelerationType;
 0857                var supportsKeyFrameOnly = (hardwareAccelerationType == HardwareAccelerationType.nvenc && options.Enable
 0858                                           || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSyst
 0859                                           || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.Prefe
 0860                                           || hardwareAccelerationType == HardwareAccelerationType.vaapi
 0861                                           || hardwareAccelerationType == HardwareAccelerationType.videotoolbox
 0862                                           || hardwareAccelerationType == HardwareAccelerationType.rkmpp;
 0863                if (!supportsKeyFrameOnly)
 864                {
 865                    // Disable hardware acceleration when the hardware decoder does not support keyframe only mode.
 0866                    allowHwAccel = false;
 0867                    options = new EncodingOptions();
 868                }
 869            }
 870
 871            // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
 872            // Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
 0873            if (!allowHwAccel)
 874            {
 0875                options.EnableHardwareEncoding = false;
 0876                options.HardwareAccelerationType = HardwareAccelerationType.none;
 0877                options.EnableTonemapping = false;
 878            }
 879
 0880            if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.Asp
 881            {
 882                // For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimen
 0883                var darParts = imageStream.AspectRatio.Split(':');
 0884                var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], Cultu
 885                // When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
 886                // Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, 
 0887                var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05
 0888                if (shouldResetHeight)
 889                {
 890                    // SAR = DAR * Height / Width
 891                    // RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
 0892                    imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
 893                }
 894            }
 895
 0896            var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.To
 0897            var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
 0898            {
 0899                IsVideoRequest = true,  // must be true for InputVideoHwaccelArgs to return non-empty value
 0900                MediaSource = mediaSource,
 0901                VideoStream = imageStream,
 0902                BaseRequest = baseRequest,  // GetVideoProcessingFilterParam errors if null
 0903                MediaPath = inputFile,
 0904                OutputVideoCodec = "mjpeg"
 0905            };
 0906            var vidEncoder = enableHwEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideo
 907
 908            // Get input and filter arguments
 0909            var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
 0910            if (string.IsNullOrWhiteSpace(inputArg))
 911            {
 0912                throw new InvalidOperationException("EncodingHelper returned empty input arguments.");
 913            }
 914
 0915            if (!allowHwAccel)
 916            {
 0917                inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, 
 918            }
 919
 0920            if (options.HardwareAccelerationType == HardwareAccelerationType.videotoolbox && _isLowPriorityHwDecodeSuppo
 921            {
 922                // VideoToolbox supports low priority decoding, which is useful for trickplay
 0923                inputArg = "-hwaccel_flags +low_priority " + inputArg;
 924            }
 925
 0926            if (enableKeyFrameOnlyExtraction)
 927            {
 0928                inputArg = "-skip_frame nokey " + inputArg;
 929            }
 930
 0931            var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
 0932            if (string.IsNullOrWhiteSpace(filterParam))
 933            {
 0934                throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
 935            }
 936
 0937            return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priori
 938        }
 939
 940        private async Task<string> ExtractVideoImagesOnIntervalInternal(
 941            string inputArg,
 942            string filterParam,
 943            string vidEncoder,
 944            int? outputThreads,
 945            int? qualityScale,
 946            ProcessPriorityClass? priority,
 947            CancellationToken cancellationToken)
 948        {
 949            if (string.IsNullOrWhiteSpace(inputArg))
 950            {
 951                throw new InvalidOperationException("Empty or invalid input argument.");
 952            }
 953
 954            // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
 955            // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
 956            var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
 957            var encoderQualityOption = "-qscale:v ";
 958
 959            if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
 960                || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
 961            {
 962                // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
 963                encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
 964                encoderQualityOption = "-global_quality:v ";
 965            }
 966
 967            if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
 968            {
 969                // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qs
 970                encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
 971            }
 972
 973            if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
 974            {
 975                // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qsca
 976                encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
 977                encoderQualityOption = "-qp_init:v ";
 978            }
 979
 980            // Output arguments
 981            var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToSt
 982            Directory.CreateDirectory(targetDirectory);
 983            var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
 984
 985            // Final command arguments
 986            var args = string.Format(
 987                CultureInfo.InvariantCulture,
 988                "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"",
 989                inputArg,
 990                filterParam,
 991                outputThreads.GetValueOrDefault(_threads),
 992                vidEncoder,
 993                encoderQualityOption + encoderQuality + " ",
 994                vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : stri
 995                EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim() + " ", // passthrough timestamp
 996                "image2",
 997                outputPath);
 998
 999            // Start ffmpeg process
 1000            var process = new Process
 1001            {
 1002                StartInfo = new ProcessStartInfo
 1003                {
 1004                    CreateNoWindow = true,
 1005                    UseShellExecute = false,
 1006                    FileName = _ffmpegPath,
 1007                    Arguments = args,
 1008                    WindowStyle = ProcessWindowStyle.Hidden,
 1009                    ErrorDialog = false,
 1010                },
 1011                EnableRaisingEvents = true
 1012            };
 1013
 1014            var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, 
 1015            _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
 1016
 1017            using (var processWrapper = new ProcessWrapper(process, this))
 1018            {
 1019                bool ranToCompletion = false;
 1020
 1021                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 1022                {
 1023                    StartProcess(processWrapper);
 1024
 1025                    // Set process priority
 1026                    if (priority.HasValue)
 1027                    {
 1028                        try
 1029                        {
 1030                            processWrapper.Process.PriorityClass = priority.Value;
 1031                        }
 1032                        catch (Exception ex)
 1033                        {
 1034                            _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", prior
 1035                        }
 1036                    }
 1037
 1038                    // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
 1039                    // but we still need to detect if the process hangs.
 1040                    // Making the assumption that as long as new jpegs are showing up, everything is good.
 1041
 1042                    bool isResponsive = true;
 1043                    int lastCount = 0;
 1044                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 1045                    timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
 1046
 1047                    while (isResponsive && !cancellationToken.IsCancellationRequested)
 1048                    {
 1049                        try
 1050                        {
 1051                            await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 1052
 1053                            ranToCompletion = true;
 1054                            break;
 1055                        }
 1056                        catch (OperationCanceledException)
 1057                        {
 1058                            // We don't actually expect the process to be finished in one timeout span, just that one im
 1059                        }
 1060
 1061                        var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
 1062
 1063                        isResponsive = jpegCount > lastCount;
 1064                        lastCount = jpegCount;
 1065                    }
 1066
 1067                    if (!ranToCompletion)
 1068                    {
 1069                        if (!isResponsive)
 1070                        {
 1071                            _logger.LogInformation("Trickplay process unresponsive.");
 1072                        }
 1073
 1074                        _logger.LogInformation("Stopping trickplay extraction.");
 1075                        StopProcess(processWrapper, 1000);
 1076                    }
 1077                }
 1078
 1079                var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
 1080
 1081                if (exitCode == -1)
 1082                {
 1083                    _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
 1084                    // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed 
 1085                    // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed
 1086                    try
 1087                    {
 1088                        Directory.Delete(targetDirectory, true);
 1089                    }
 1090                    catch (Exception e)
 1091                    {
 1092                        _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory)
 1093                    }
 1094
 1095                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 1096                }
 1097
 1098                return targetDirectory;
 1099            }
 1100        }
 1101
 1102        public string GetTimeParameter(long ticks)
 1103        {
 01104            var time = TimeSpan.FromTicks(ticks);
 1105
 01106            return GetTimeParameter(time);
 1107        }
 1108
 1109        public string GetTimeParameter(TimeSpan time)
 1110        {
 01111            return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
 1112        }
 1113
 1114        private void StartProcess(ProcessWrapper process)
 1115        {
 01116            process.Process.Start();
 1117
 1118            lock (_runningProcessesLock)
 1119            {
 01120                _runningProcesses.Add(process);
 01121            }
 01122        }
 1123
 1124        private void StopProcess(ProcessWrapper process, int waitTimeMs)
 1125        {
 1126            try
 1127            {
 01128                if (process.Process.WaitForExit(waitTimeMs))
 1129                {
 01130                    return;
 1131                }
 1132
 01133                _logger.LogInformation("Killing ffmpeg process");
 1134
 01135                process.Process.Kill();
 01136            }
 01137            catch (InvalidOperationException)
 1138            {
 1139                // The process has already exited or
 1140                // there is no process associated with this Process object.
 01141            }
 01142            catch (Exception ex)
 1143            {
 01144                _logger.LogError(ex, "Error killing process");
 01145            }
 01146        }
 1147
 1148        private void StopProcesses()
 211149        {
 1150            List<ProcessWrapper> processes;
 1151            lock (_runningProcessesLock)
 1152            {
 211153                processes = _runningProcesses.ToList();
 211154                _runningProcesses.Clear();
 211155            }
 1156
 421157            foreach (var process in processes)
 1158            {
 01159                if (!process.HasExited)
 1160                {
 01161                    StopProcess(process, 500);
 1162                }
 1163            }
 211164        }
 1165
 1166        public string EscapeSubtitleFilterPath(string path)
 1167        {
 1168            // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
 1169            // We need to double escape
 1170
 01171            return path
 01172                .Replace('\\', '/')
 01173                .Replace(":", "\\:", StringComparison.Ordinal)
 01174                .Replace("'", @"'\\\''", StringComparison.Ordinal)
 01175                .Replace("\"", "\\\"", StringComparison.Ordinal);
 1176        }
 1177
 1178        /// <inheritdoc />
 1179        public void Dispose()
 1180        {
 211181            Dispose(true);
 211182            GC.SuppressFinalize(this);
 211183        }
 1184
 1185        /// <summary>
 1186        /// Releases unmanaged and - optionally - managed resources.
 1187        /// </summary>
 1188        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 1189        protected virtual void Dispose(bool dispose)
 1190        {
 211191            if (dispose)
 1192            {
 211193                StopProcesses();
 211194                _thumbnailResourcePool.Dispose();
 1195            }
 211196        }
 1197
 1198        /// <inheritdoc />
 1199        public Task ConvertImage(string inputPath, string outputPath)
 1200        {
 01201            throw new NotImplementedException();
 1202        }
 1203
 1204        /// <inheritdoc />
 1205        public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
 1206        {
 1207            // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VO
 01208            var allVobs = _fileSystem.GetFiles(path, true)
 01209                .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
 01210                .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
 01211                .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
 01212                .OrderBy(i => i.FullName)
 01213                .ToList();
 1214
 01215            if (titleNumber.HasValue)
 1216            {
 01217                var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
 01218                var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
 1219
 01220                if (vobs.Count > 0)
 1221                {
 01222                    return vobs.Select(i => i.FullName).ToList();
 1223                }
 1224
 01225                _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
 1226            }
 1227
 1228            // Check for multiple big titles (> 900 MB)
 01229            var titles = allVobs
 01230                .Where(vob => vob.Length >= 900 * 1024 * 1024)
 01231                .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
 01232                .Distinct()
 01233                .ToList();
 1234
 1235            // Fall back to first title if no big title is found
 01236            if (titles.Count == 0)
 1237            {
 01238                titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
 1239            }
 1240
 1241            // Aggregate all .vob files of the titles
 01242            return allVobs
 01243                .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToStr
 01244                .Select(i => i.FullName)
 01245                .Order()
 01246                .ToList();
 1247        }
 1248
 1249        /// <inheritdoc />
 1250        public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
 01251            => _blurayExaminer.GetDiscInfo(path).Files;
 1252
 1253        /// <inheritdoc />
 1254        public string GetInputPathArgument(EncodingJobInfo state)
 01255            => GetInputPathArgument(state.MediaPath, state.MediaSource);
 1256
 1257        /// <inheritdoc />
 1258        public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
 1259        {
 01260            return mediaSource.VideoType switch
 01261            {
 01262                VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource),
 01263                VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource),
 01264                _ => GetInputArgument(path, mediaSource)
 01265            };
 1266        }
 1267
 1268        /// <inheritdoc />
 1269        public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
 1270        {
 1271            // Get all playable files
 1272            IReadOnlyList<string> files;
 01273            var videoType = source.VideoType;
 01274            if (videoType == VideoType.Dvd)
 1275            {
 01276                files = GetPrimaryPlaylistVobFiles(source.Path, null);
 1277            }
 01278            else if (videoType == VideoType.BluRay)
 1279            {
 01280                files = GetPrimaryPlaylistM2tsFiles(source.Path);
 1281            }
 1282            else
 1283            {
 01284                return;
 1285            }
 1286
 1287            // Generate concat configuration entries for each file and write to file
 01288            Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
 01289            using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
 01290            foreach (var path in files)
 1291            {
 01292                var mediaInfoResult = GetMediaInfo(
 01293                    new MediaInfoRequest
 01294                    {
 01295                        MediaType = DlnaProfileType.Video,
 01296                        MediaSource = new MediaSourceInfo
 01297                        {
 01298                            Path = path,
 01299                            Protocol = MediaProtocol.File,
 01300                            VideoType = videoType
 01301                        }
 01302                    },
 01303                    CancellationToken.None).GetAwaiter().GetResult();
 1304
 01305                var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
 1306
 1307                // Add file path stanza to concat configuration
 01308                sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
 1309
 1310                // Add duration stanza to concat configuration
 01311                sw.WriteLine("duration {0}", duration);
 1312            }
 01313        }
 1314
 1315        public bool CanExtractSubtitles(string codec)
 1316        {
 1317            // TODO is there ever a case when a subtitle can't be extracted??
 01318            return true;
 1319        }
 1320
 1321        private sealed class ProcessWrapper : IDisposable
 1322        {
 1323            private readonly MediaEncoder _mediaEncoder;
 1324
 1325            private bool _disposed = false;
 1326
 1327            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
 1328            {
 1329                Process = process;
 01330                _mediaEncoder = mediaEncoder;
 01331                Process.Exited += OnProcessExited;
 01332            }
 1333
 1334            public Process Process { get; }
 1335
 1336            public bool HasExited { get; private set; }
 1337
 1338            public int? ExitCode { get; private set; }
 1339
 1340            private void OnProcessExited(object sender, EventArgs e)
 1341            {
 01342                var process = (Process)sender;
 1343
 01344                HasExited = true;
 1345
 1346                try
 1347                {
 01348                    ExitCode = process.ExitCode;
 01349                }
 01350                catch
 1351                {
 01352                }
 1353
 01354                DisposeProcess(process);
 01355            }
 1356
 1357            private void DisposeProcess(Process process)
 01358            {
 1359                lock (_mediaEncoder._runningProcessesLock)
 1360                {
 01361                    _mediaEncoder._runningProcesses.Remove(this);
 01362                }
 1363
 01364                process.Dispose();
 01365            }
 1366
 1367            public void Dispose()
 1368            {
 01369                if (!_disposed)
 1370                {
 01371                    if (Process is not null)
 1372                    {
 01373                        Process.Exited -= OnProcessExited;
 01374                        DisposeProcess(Process);
 1375                    }
 1376                }
 1377
 01378                _disposed = true;
 01379            }
 1380        }
 1381    }
 1382}

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<System.Int32,System.Boolean>)
SetAvailableBitStreamFiltersWithOption(System.Collections.Generic.IDictionary`2<MediaBrowser.Controller.MediaEncoding.BitStreamFilterOptionType,System.Boolean>)
SetMediaEncoderVersion(MediaBrowser.MediaEncoding.Encoder.EncoderValidator)
SupportsEncoder(System.String)
SupportsDecoder(System.String)
SupportsHwaccel(System.String)
SupportsFilter(System.String)
SupportsFilterWithOption(MediaBrowser.Controller.MediaEncoding.FilterOptionType)
SupportsBitStreamFilterWithOption(MediaBrowser.Controller.MediaEncoding.BitStreamFilterOptionType)
CanEncodeToAudioCodec(System.String)
CanEncodeToSubtitleCodec(System.String)
GetMediaInfo(MediaBrowser.Controller.MediaEncoding.MediaInfoRequest,System.Threading.CancellationToken)
GetExtraArguments(MediaBrowser.Controller.MediaEncoding.MediaInfoRequest)
GetInputArgument(System.Collections.Generic.IReadOnlyList`1<System.String>,MediaBrowser.Model.Dto.MediaSourceInfo)
GetInputArgument(System.String,MediaBrowser.Model.Dto.MediaSourceInfo)
GetExternalSubtitleInputArgument(System.String)
ExtractAudioImage(System.String,System.Nullable`1<System.Int32>,System.Threading.CancellationToken)
ExtractVideoImage(System.String,System.String,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,System.Nullable`1<MediaBrowser.Model.Entities.Video3DFormat>,System.Nullable`1<System.TimeSpan>,System.Threading.CancellationToken)
ExtractVideoImage(System.String,System.String,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,System.Nullable`1<System.Int32>,System.Nullable`1<MediaBrowser.Model.Drawing.ImageFormat>,System.Threading.CancellationToken)
GetImageResolutionParameter()
ExtractVideoImagesOnIntervalAccelerated(System.String,System.String,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,System.Int32,System.TimeSpan,System.Boolean,System.Boolean,System.Nullable`1<System.Int32>,System.Nullable`1<System.Int32>,System.Nullable`1<System.Diagnostics.ProcessPriorityClass>,System.Boolean,MediaBrowser.Controller.MediaEncoding.EncodingHelper,System.Threading.CancellationToken)
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()