< 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: 310
Coverable lines: 363
Total lines: 1377
Line coverage: 14.6%
Branch coverage
13%
Covered branches: 21
Total branches: 161
Branch coverage: 13%
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(...)100%210%
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<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.MediaType == DlnaProfileType.Video && 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        {
 510            var args = extractChapters
 511                ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
 512                : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
 513
 514            if (_proberSupportsFirstVideoFrame)
 515            {
 516                args += " -show_frames -only_first_vframe";
 517            }
 518
 519            args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
 520
 521            var process = new Process
 522            {
 523                StartInfo = new ProcessStartInfo
 524                {
 525                    CreateNoWindow = true,
 526                    UseShellExecute = false,
 527
 528                    // Must consume both or ffmpeg may hang due to deadlocks.
 529                    RedirectStandardOutput = true,
 530
 531                    FileName = _ffprobePath,
 532                    Arguments = args,
 533
 534                    WindowStyle = ProcessWindowStyle.Hidden,
 535                    ErrorDialog = false,
 536                },
 537                EnableRaisingEvents = true
 538            };
 539
 540            _logger.LogDebug("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
 541
 542            var memoryStream = new MemoryStream();
 543            await using (memoryStream.ConfigureAwait(false))
 544            using (var processWrapper = new ProcessWrapper(process, this))
 545            {
 546                StartProcess(processWrapper);
 547                using var reader = process.StandardOutput;
 548                await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
 549                memoryStream.Seek(0, SeekOrigin.Begin);
 550                InternalMediaInfoResult result;
 551                try
 552                {
 553                    result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
 554                                        memoryStream,
 555                                        _jsonSerializerOptions,
 556                                        cancellationToken).ConfigureAwait(false);
 557                }
 558                catch
 559                {
 560                    StopProcess(processWrapper, 100);
 561
 562                    throw;
 563                }
 564
 565                if (result is null || (result.Streams is null && result.Format is null))
 566                {
 567                    throw new FfmpegException("ffprobe failed - streams and format are both null.");
 568                }
 569
 570                if (result.Streams is not null)
 571                {
 572                    // Normalize aspect ratio if invalid
 573                    foreach (var stream in result.Streams)
 574                    {
 575                        if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 576                        {
 577                            stream.DisplayAspectRatio = string.Empty;
 578                        }
 579
 580                        if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 581                        {
 582                            stream.SampleAspectRatio = string.Empty;
 583                        }
 584                    }
 585                }
 586
 587                return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primar
 588            }
 589        }
 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        {
 626            var inputArgument = GetInputPathArgument(inputFile, mediaSource);
 627
 628            if (!isAudio)
 629            {
 630                try
 631                {
 632                    return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFor
 633                }
 634                catch (ArgumentException)
 635                {
 636                    throw;
 637                }
 638                catch (Exception ex)
 639                {
 640                    _logger.LogWarning(ex, "I-frame image extraction failed, will attempt standard way. Input: {Argument
 641                }
 642            }
 643
 644            return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, off
 645        }
 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        {
 682            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 683
 684            var useTradeoff = _config.GetFFmpegImgExtractPerfTradeoff();
 685
 686            var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
 687
 688            var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ou
 689            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.
 693            var filters = new List<string>();
 694
 695            // deinterlace using bwdif algorithm for video stream.
 696            if (videoStream is not null && videoStream.IsInterlaced)
 697            {
 698                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
 703            var scaler = threedFormat switch
 704            {
 705                // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may 
 706                Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min
 707                // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
 708                Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw
 709                // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may
 710                Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):
 711                // ftab crop height in half, set the display aspect,crop out any black bars we may have made
 712                Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(
 713                _ => "scale=round(iw*sar/2)*2:round(ih/2)*2"
 714            };
 715
 716            filters.Add(scaler);
 717
 718            // Use ffmpeg to sample N frames and pick the best thumbnail. Have a fall back just in case.
 719            var enableThumbnail = !useTradeoff && useIFrame && !string.Equals("wtv", container, StringComparison.Ordinal
 720            if (enableThumbnail)
 721            {
 722                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
 727            var enableHdrExtraction = false;
 728
 729            if (videoStream?.VideoRange == VideoRange.HDR)
 730            {
 731                if (SupportsFilter("tonemapx"))
 732                {
 733                    var peak = videoStream.VideoRangeType == VideoRangeType.DOVI ? "400" : "100";
 734                    enableHdrExtraction = true;
 735                    filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p:ran
 736                }
 737                else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
 738                {
 739                    enableHdrExtraction = true;
 740                    filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:p
 741                }
 742            }
 743
 744            var vf = string.Join(',', filters);
 745            var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.Invariant
 746            var args = string.Format(
 747                CultureInfo.InvariantCulture,
 748                "-i {0}{1} -threads {2} -v quiet -vframes 1 -vf {3}{4}{5} -f image2 \"{6}\"",
 749                inputPath,
 750                mapArg,
 751                _threads,
 752                vf,
 753                isAudio ? string.Empty : GetImageResolutionParameter(),
 754                EncodingHelper.GetVideoSyncOption("-1", EncoderVersion), // auto decide fps mode
 755                tempExtractPath);
 756
 757            if (offset.HasValue)
 758            {
 759                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.
 764            var seekMpegTs = offset.HasValue && string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
 765            if (useIFrame && (useTradeoff || seekMpegTs))
 766            {
 767                args = "-skip_frame nokey " + args;
 768            }
 769
 770            if (!string.IsNullOrWhiteSpace(container))
 771            {
 772                var inputFormat = EncodingHelper.GetInputFormat(container);
 773                if (!string.IsNullOrWhiteSpace(inputFormat))
 774                {
 775                    args = "-f " + inputFormat + " " + args;
 776                }
 777            }
 778
 779            var process = new Process
 780            {
 781                StartInfo = new ProcessStartInfo
 782                {
 783                    CreateNoWindow = true,
 784                    UseShellExecute = false,
 785                    FileName = _ffmpegPath,
 786                    Arguments = args,
 787                    WindowStyle = ProcessWindowStyle.Hidden,
 788                    ErrorDialog = false,
 789                },
 790                EnableRaisingEvents = true
 791            };
 792
 793            _logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Argum
 794
 795            using (var processWrapper = new ProcessWrapper(process, this))
 796            {
 797                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 798                {
 799                    StartProcess(processWrapper);
 800
 801                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 802                    if (timeoutMs <= 0)
 803                    {
 804                        timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTi
 805                    }
 806
 807                    try
 808                    {
 809                        await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 810                    }
 811                    catch (OperationCanceledException ex)
 812                    {
 813                        process.Kill(true);
 814                        throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction t
 815                    }
 816                }
 817
 818                var file = _fileSystem.GetFileInfo(tempExtractPath);
 819
 820                if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
 821                {
 822                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 823                }
 824
 825                return tempExtractPath;
 826            }
 827        }
 828
 829        /// <inheritdoc />
 830        public 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            if (enableKeyFrameOnlyExtraction)
 922            {
 0923                inputArg = "-skip_frame nokey " + inputArg;
 924            }
 925
 0926            var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
 0927            if (string.IsNullOrWhiteSpace(filterParam))
 928            {
 0929                throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
 930            }
 931
 0932            return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priori
 933        }
 934
 935        private async Task<string> ExtractVideoImagesOnIntervalInternal(
 936            string inputArg,
 937            string filterParam,
 938            string vidEncoder,
 939            int? outputThreads,
 940            int? qualityScale,
 941            ProcessPriorityClass? priority,
 942            CancellationToken cancellationToken)
 943        {
 944            if (string.IsNullOrWhiteSpace(inputArg))
 945            {
 946                throw new InvalidOperationException("Empty or invalid input argument.");
 947            }
 948
 949            // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
 950            // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
 951            var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
 952            var encoderQualityOption = "-qscale:v ";
 953
 954            if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
 955                || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
 956            {
 957                // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
 958                encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
 959                encoderQualityOption = "-global_quality:v ";
 960            }
 961
 962            if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
 963            {
 964                // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qs
 965                encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
 966            }
 967
 968            if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
 969            {
 970                // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qsca
 971                encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
 972                encoderQualityOption = "-qp_init:v ";
 973            }
 974
 975            // Output arguments
 976            var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToSt
 977            Directory.CreateDirectory(targetDirectory);
 978            var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
 979
 980            // Final command arguments
 981            var args = string.Format(
 982                CultureInfo.InvariantCulture,
 983                "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"",
 984                inputArg,
 985                filterParam,
 986                outputThreads.GetValueOrDefault(_threads),
 987                vidEncoder,
 988                encoderQualityOption + encoderQuality + " ",
 989                vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : stri
 990                EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim() + " ", // passthrough timestamp
 991                "image2",
 992                outputPath);
 993
 994            // Start ffmpeg process
 995            var process = new Process
 996            {
 997                StartInfo = new ProcessStartInfo
 998                {
 999                    CreateNoWindow = true,
 1000                    UseShellExecute = false,
 1001                    FileName = _ffmpegPath,
 1002                    Arguments = args,
 1003                    WindowStyle = ProcessWindowStyle.Hidden,
 1004                    ErrorDialog = false,
 1005                },
 1006                EnableRaisingEvents = true
 1007            };
 1008
 1009            var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, 
 1010            _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
 1011
 1012            using (var processWrapper = new ProcessWrapper(process, this))
 1013            {
 1014                bool ranToCompletion = false;
 1015
 1016                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 1017                {
 1018                    StartProcess(processWrapper);
 1019
 1020                    // Set process priority
 1021                    if (priority.HasValue)
 1022                    {
 1023                        try
 1024                        {
 1025                            processWrapper.Process.PriorityClass = priority.Value;
 1026                        }
 1027                        catch (Exception ex)
 1028                        {
 1029                            _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", prior
 1030                        }
 1031                    }
 1032
 1033                    // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
 1034                    // but we still need to detect if the process hangs.
 1035                    // Making the assumption that as long as new jpegs are showing up, everything is good.
 1036
 1037                    bool isResponsive = true;
 1038                    int lastCount = 0;
 1039                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 1040                    timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
 1041
 1042                    while (isResponsive && !cancellationToken.IsCancellationRequested)
 1043                    {
 1044                        try
 1045                        {
 1046                            await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 1047
 1048                            ranToCompletion = true;
 1049                            break;
 1050                        }
 1051                        catch (OperationCanceledException)
 1052                        {
 1053                            // We don't actually expect the process to be finished in one timeout span, just that one im
 1054                        }
 1055
 1056                        var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
 1057
 1058                        isResponsive = jpegCount > lastCount;
 1059                        lastCount = jpegCount;
 1060                    }
 1061
 1062                    if (!ranToCompletion)
 1063                    {
 1064                        if (!isResponsive)
 1065                        {
 1066                            _logger.LogInformation("Trickplay process unresponsive.");
 1067                        }
 1068
 1069                        _logger.LogInformation("Stopping trickplay extraction.");
 1070                        StopProcess(processWrapper, 1000);
 1071                    }
 1072                }
 1073
 1074                var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
 1075
 1076                if (exitCode == -1)
 1077                {
 1078                    _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
 1079                    // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed 
 1080                    // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed
 1081                    try
 1082                    {
 1083                        Directory.Delete(targetDirectory, true);
 1084                    }
 1085                    catch (Exception e)
 1086                    {
 1087                        _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory)
 1088                    }
 1089
 1090                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 1091                }
 1092
 1093                return targetDirectory;
 1094            }
 1095        }
 1096
 1097        public string GetTimeParameter(long ticks)
 1098        {
 01099            var time = TimeSpan.FromTicks(ticks);
 1100
 01101            return GetTimeParameter(time);
 1102        }
 1103
 1104        public string GetTimeParameter(TimeSpan time)
 1105        {
 01106            return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
 1107        }
 1108
 1109        private void StartProcess(ProcessWrapper process)
 1110        {
 01111            process.Process.Start();
 1112
 1113            lock (_runningProcessesLock)
 1114            {
 01115                _runningProcesses.Add(process);
 01116            }
 01117        }
 1118
 1119        private void StopProcess(ProcessWrapper process, int waitTimeMs)
 1120        {
 1121            try
 1122            {
 01123                if (process.Process.WaitForExit(waitTimeMs))
 1124                {
 01125                    return;
 1126                }
 1127
 01128                _logger.LogInformation("Killing ffmpeg process");
 1129
 01130                process.Process.Kill();
 01131            }
 01132            catch (InvalidOperationException)
 1133            {
 1134                // The process has already exited or
 1135                // there is no process associated with this Process object.
 01136            }
 01137            catch (Exception ex)
 1138            {
 01139                _logger.LogError(ex, "Error killing process");
 01140            }
 01141        }
 1142
 1143        private void StopProcesses()
 211144        {
 1145            List<ProcessWrapper> processes;
 1146            lock (_runningProcessesLock)
 1147            {
 211148                processes = _runningProcesses.ToList();
 211149                _runningProcesses.Clear();
 211150            }
 1151
 421152            foreach (var process in processes)
 1153            {
 01154                if (!process.HasExited)
 1155                {
 01156                    StopProcess(process, 500);
 1157                }
 1158            }
 211159        }
 1160
 1161        public string EscapeSubtitleFilterPath(string path)
 1162        {
 1163            // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
 1164            // We need to double escape
 1165
 01166            return path
 01167                .Replace('\\', '/')
 01168                .Replace(":", "\\:", StringComparison.Ordinal)
 01169                .Replace("'", @"'\\\''", StringComparison.Ordinal)
 01170                .Replace("\"", "\\\"", StringComparison.Ordinal);
 1171        }
 1172
 1173        /// <inheritdoc />
 1174        public void Dispose()
 1175        {
 211176            Dispose(true);
 211177            GC.SuppressFinalize(this);
 211178        }
 1179
 1180        /// <summary>
 1181        /// Releases unmanaged and - optionally - managed resources.
 1182        /// </summary>
 1183        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 1184        protected virtual void Dispose(bool dispose)
 1185        {
 211186            if (dispose)
 1187            {
 211188                StopProcesses();
 211189                _thumbnailResourcePool.Dispose();
 1190            }
 211191        }
 1192
 1193        /// <inheritdoc />
 1194        public Task ConvertImage(string inputPath, string outputPath)
 1195        {
 01196            throw new NotImplementedException();
 1197        }
 1198
 1199        /// <inheritdoc />
 1200        public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
 1201        {
 1202            // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VO
 01203            var allVobs = _fileSystem.GetFiles(path, true)
 01204                .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
 01205                .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
 01206                .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
 01207                .OrderBy(i => i.FullName)
 01208                .ToList();
 1209
 01210            if (titleNumber.HasValue)
 1211            {
 01212                var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
 01213                var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
 1214
 01215                if (vobs.Count > 0)
 1216                {
 01217                    return vobs.Select(i => i.FullName).ToList();
 1218                }
 1219
 01220                _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
 1221            }
 1222
 1223            // Check for multiple big titles (> 900 MB)
 01224            var titles = allVobs
 01225                .Where(vob => vob.Length >= 900 * 1024 * 1024)
 01226                .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
 01227                .Distinct()
 01228                .ToList();
 1229
 1230            // Fall back to first title if no big title is found
 01231            if (titles.Count == 0)
 1232            {
 01233                titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
 1234            }
 1235
 1236            // Aggregate all .vob files of the titles
 01237            return allVobs
 01238                .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToStr
 01239                .Select(i => i.FullName)
 01240                .Order()
 01241                .ToList();
 1242        }
 1243
 1244        /// <inheritdoc />
 1245        public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
 01246            => _blurayExaminer.GetDiscInfo(path).Files;
 1247
 1248        /// <inheritdoc />
 1249        public string GetInputPathArgument(EncodingJobInfo state)
 01250            => GetInputPathArgument(state.MediaPath, state.MediaSource);
 1251
 1252        /// <inheritdoc />
 1253        public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
 1254        {
 01255            return mediaSource.VideoType switch
 01256            {
 01257                VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource),
 01258                VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource),
 01259                _ => GetInputArgument(path, mediaSource)
 01260            };
 1261        }
 1262
 1263        /// <inheritdoc />
 1264        public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
 1265        {
 1266            // Get all playable files
 1267            IReadOnlyList<string> files;
 01268            var videoType = source.VideoType;
 01269            if (videoType == VideoType.Dvd)
 1270            {
 01271                files = GetPrimaryPlaylistVobFiles(source.Path, null);
 1272            }
 01273            else if (videoType == VideoType.BluRay)
 1274            {
 01275                files = GetPrimaryPlaylistM2tsFiles(source.Path);
 1276            }
 1277            else
 1278            {
 01279                return;
 1280            }
 1281
 1282            // Generate concat configuration entries for each file and write to file
 01283            Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
 01284            using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
 01285            foreach (var path in files)
 1286            {
 01287                var mediaInfoResult = GetMediaInfo(
 01288                    new MediaInfoRequest
 01289                    {
 01290                        MediaType = DlnaProfileType.Video,
 01291                        MediaSource = new MediaSourceInfo
 01292                        {
 01293                            Path = path,
 01294                            Protocol = MediaProtocol.File,
 01295                            VideoType = videoType
 01296                        }
 01297                    },
 01298                    CancellationToken.None).GetAwaiter().GetResult();
 1299
 01300                var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
 1301
 1302                // Add file path stanza to concat configuration
 01303                sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
 1304
 1305                // Add duration stanza to concat configuration
 01306                sw.WriteLine("duration {0}", duration);
 1307            }
 01308        }
 1309
 1310        public bool CanExtractSubtitles(string codec)
 1311        {
 1312            // TODO is there ever a case when a subtitle can't be extracted??
 01313            return true;
 1314        }
 1315
 1316        private sealed class ProcessWrapper : IDisposable
 1317        {
 1318            private readonly MediaEncoder _mediaEncoder;
 1319
 1320            private bool _disposed = false;
 1321
 1322            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
 1323            {
 1324                Process = process;
 01325                _mediaEncoder = mediaEncoder;
 01326                Process.Exited += OnProcessExited;
 01327            }
 1328
 1329            public Process Process { get; }
 1330
 1331            public bool HasExited { get; private set; }
 1332
 1333            public int? ExitCode { get; private set; }
 1334
 1335            private void OnProcessExited(object sender, EventArgs e)
 1336            {
 01337                var process = (Process)sender;
 1338
 01339                HasExited = true;
 1340
 1341                try
 1342                {
 01343                    ExitCode = process.ExitCode;
 01344                }
 01345                catch
 1346                {
 01347                }
 1348
 01349                DisposeProcess(process);
 01350            }
 1351
 1352            private void DisposeProcess(Process process)
 01353            {
 1354                lock (_mediaEncoder._runningProcessesLock)
 1355                {
 01356                    _mediaEncoder._runningProcesses.Remove(this);
 01357                }
 1358
 01359                process.Dispose();
 01360            }
 1361
 1362            public void Dispose()
 1363            {
 01364                if (!_disposed)
 1365                {
 01366                    if (Process is not null)
 1367                    {
 01368                        Process.Exited -= OnProcessExited;
 01369                        DisposeProcess(Process);
 1370                    }
 1371                }
 1372
 01373                _disposed = true;
 01374            }
 1375        }
 1376    }
 1377}

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