< 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: 50
Uncovered lines: 300
Coverable lines: 350
Total lines: 1312
Line coverage: 14.2%
Branch coverage
11%
Covered branches: 18
Total branches: 153
Branch coverage: 11.7%
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(...)100%22100%
.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%
SetFFmpegPath()3.12%837.46327.69%
ValidatePath(...)0%2040%
GetEncoderPathFromDirectory(...)100%210%
SetAvailableEncoders(...)100%210%
SetAvailableDecoders(...)100%210%
SetAvailableHwaccels(...)100%210%
SetAvailableFilters(...)100%210%
SetAvailableFiltersWithOption(...)100%210%
SetMediaEncoderVersion(...)100%210%
SupportsEncoder(...)100%210%
SupportsDecoder(...)100%210%
SupportsHwaccel(...)100%210%
SupportsFilter(...)100%210%
SupportsFilterWithOption(...)0%620%
CanEncodeToAudioCodec(...)0%2040%
CanEncodeToSubtitleCodec(...)100%210%
GetMediaInfo(...)0%620%
GetExtraArguments(...)61.11%26.251870.58%
GetInputArgument(...)100%210%
GetInputArgument(...)0%620%
GetExternalSubtitleInputArgument(...)100%210%
ExtractAudioImage(...)100%210%
ExtractVideoImage(...)100%210%
ExtractVideoImage(...)100%210%
GetImageResolutionParameter()0%132110%
ExtractVideoImagesOnIntervalAccelerated(...)0%2162460%
GetTimeParameter(...)100%210%
GetTimeParameter(...)100%210%
StartProcess(...)100%210%
StopProcess(...)0%620%
StopProcesses()50%4.25475%
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
 2365        private readonly object _runningProcessesLock = new object();
 2366        private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
 67
 68        // MediaEncoder is registered as a Singleton
 69        private readonly JsonSerializerOptions _jsonSerializerOptions;
 70
 2371        private List<string> _encoders = new List<string>();
 2372        private List<string> _decoders = new List<string>();
 2373        private List<string> _hwaccels = new List<string>();
 2374        private List<string> _filters = new List<string>();
 2375        private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
 76
 77        private bool _isPkeyPauseSupported = false;
 78        private bool _isLowPriorityHwDecodeSupported = false;
 79
 80        private bool _isVaapiDeviceAmd = false;
 81        private bool _isVaapiDeviceInteliHD = false;
 82        private bool _isVaapiDeviceInteli965 = false;
 83        private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
 84        private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
 85
 086        private static string[] _vulkanImageDrmFmtModifierExts =
 087        {
 088            "VK_EXT_image_drm_format_modifier",
 089        };
 90
 091        private static string[] _vulkanExternalMemoryDmaBufExts =
 092        {
 093            "VK_KHR_external_memory_fd",
 094            "VK_EXT_external_memory_dma_buf",
 095            "VK_KHR_external_semaphore_fd",
 096            "VK_EXT_external_memory_host"
 097        };
 98
 99        private Version _ffmpegVersion = null;
 23100        private string _ffmpegPath = string.Empty;
 101        private string _ffprobePath;
 102        private int _threads;
 103
 104        public MediaEncoder(
 105            ILogger<MediaEncoder> logger,
 106            IServerConfigurationManager configurationManager,
 107            IFileSystem fileSystem,
 108            IBlurayExaminer blurayExaminer,
 109            ILocalizationManager localization,
 110            IConfiguration config,
 111            IServerConfigurationManager serverConfig)
 112        {
 23113            _logger = logger;
 23114            _configurationManager = configurationManager;
 23115            _fileSystem = fileSystem;
 23116            _blurayExaminer = blurayExaminer;
 23117            _localization = localization;
 23118            _config = config;
 23119            _serverConfig = serverConfig;
 23120            _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathK
 121
 23122            _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
 23123            _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
 124
 23125            var semaphoreCount = 2 * Environment.ProcessorCount;
 23126            _thumbnailResourcePool = new(semaphoreCount);
 23127        }
 128
 129        /// <inheritdoc />
 0130        public string EncoderPath => _ffmpegPath;
 131
 132        /// <inheritdoc />
 0133        public string ProbePath => _ffprobePath;
 134
 135        /// <inheritdoc />
 0136        public Version EncoderVersion => _ffmpegVersion;
 137
 138        /// <inheritdoc />
 0139        public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
 140
 141        /// <inheritdoc />
 0142        public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
 143
 144        /// <inheritdoc />
 0145        public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
 146
 147        /// <inheritdoc />
 0148        public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
 149
 150        /// <inheritdoc />
 0151        public bool IsVaapiDeviceSupportVulkanDrmModifier => _isVaapiDeviceSupportVulkanDrmModifier;
 152
 153        /// <inheritdoc />
 0154        public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
 155
 156        [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
 157        private static partial Regex FfprobePathRegex();
 158
 159        /// <summary>
 160        /// Run at startup to validate ffmpeg.
 161        /// Sets global variables FFmpegPath.
 162        /// Precedence is: CLI/Env var > Config > $PATH.
 163        /// </summary>
 164        /// <returns>bool indicates whether a valid ffmpeg is found.</returns>
 165        public bool SetFFmpegPath()
 166        {
 22167            var skipValidation = _config.GetFFmpegSkipValidation();
 22168            if (skipValidation)
 169            {
 22170                _logger.LogWarning("FFmpeg: Skipping FFmpeg Validation due to FFmpeg:novalidation set to true");
 22171                return true;
 172            }
 173
 174            // 1) Check if the --ffmpeg CLI switch has been given
 0175            var ffmpegPath = _startupOptionFFmpegPath;
 0176            string ffmpegPathSetMethodText = "command line or environment variable";
 0177            if (string.IsNullOrEmpty(ffmpegPath))
 178            {
 179                // 2) Custom path stored in config/encoding xml file under tag <EncoderAppPath> should be used as a fall
 0180                ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
 0181                ffmpegPathSetMethodText = "encoding.xml config file";
 0182                if (string.IsNullOrEmpty(ffmpegPath))
 183                {
 184                    // 3) Check "ffmpeg"
 0185                    ffmpegPath = "ffmpeg";
 0186                    ffmpegPathSetMethodText = "system $PATH";
 187                }
 188            }
 189
 0190            if (!ValidatePath(ffmpegPath))
 191            {
 0192                _ffmpegPath = null;
 0193                _logger.LogError("FFmpeg: Path set by {FfmpegPathSetMethodText} is invalid", ffmpegPathSetMethodText);
 0194                return false;
 195            }
 196
 197            // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
 0198            var options = _configurationManager.GetEncodingOptions();
 0199            options.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
 0200            _configurationManager.SaveConfiguration("encoding", options);
 201
 202            // Only if mpeg path is set, try and set path to probe
 0203            if (_ffmpegPath is not null)
 204            {
 205                // Determine a probe path from the mpeg path
 0206                _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, "ffprobe$1");
 207
 208                // Interrogate to understand what coders are supported
 0209                var validator = new EncoderValidator(_logger, _ffmpegPath);
 210
 0211                SetAvailableDecoders(validator.GetDecoders());
 0212                SetAvailableEncoders(validator.GetEncoders());
 0213                SetAvailableFilters(validator.GetFilters());
 0214                SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
 0215                SetAvailableHwaccels(validator.GetHwaccels());
 0216                SetMediaEncoderVersion(validator);
 217
 0218                _threads = EncodingHelper.GetNumberOfThreads(null, options, null);
 219
 0220                _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p      pause transcoding", _ffmpegVersion);
 0221                _isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
 222
 223                // Check the Vaapi device vendor
 0224                if (OperatingSystem.IsLinux()
 0225                    && SupportsHwaccel("vaapi")
 0226                    && !string.IsNullOrEmpty(options.VaapiDevice)
 0227                    && options.HardwareAccelerationType == HardwareAccelerationType.vaapi)
 228                {
 0229                    _isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevic
 0230                    _isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDev
 0231                    _isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiD
 0232                    _isVaapiDeviceSupportVulkanDrmModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.Vaapi
 0233                    _isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiD
 234
 0235                    if (_isVaapiDeviceAmd)
 236                    {
 0237                        _logger.LogInformation("VAAPI device {RenderNodePath} is AMD GPU", options.VaapiDevice);
 238                    }
 0239                    else if (_isVaapiDeviceInteliHD)
 240                    {
 0241                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (iHD)", options.VaapiDevice);
 242                    }
 0243                    else if (_isVaapiDeviceInteli965)
 244                    {
 0245                        _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice)
 246                    }
 247
 0248                    if (_isVaapiDeviceSupportVulkanDrmModifier)
 249                    {
 0250                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM modifier", options.Vaa
 251                    }
 252
 0253                    if (_isVaapiDeviceSupportVulkanDrmInterop)
 254                    {
 0255                        _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.Vaap
 256                    }
 257                }
 258            }
 259
 0260            _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
 0261            return !string.IsNullOrWhiteSpace(ffmpegPath);
 262        }
 263
 264        /// <summary>
 265        /// Validates the supplied FQPN to ensure it is a ffmpeg utility.
 266        /// If checks pass, global variable FFmpegPath is updated.
 267        /// </summary>
 268        /// <param name="path">FQPN to test.</param>
 269        /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
 270        private bool ValidatePath(string path)
 271        {
 0272            if (string.IsNullOrEmpty(path))
 273            {
 0274                return false;
 275            }
 276
 0277            bool rc = new EncoderValidator(_logger, path).ValidateVersion();
 0278            if (!rc)
 279            {
 0280                _logger.LogError("FFmpeg: Failed version check: {Path}", path);
 0281                return false;
 282            }
 283
 0284            _ffmpegPath = path;
 0285            return true;
 286        }
 287
 288        private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false)
 289        {
 290            try
 291            {
 0292                var files = _fileSystem.GetFilePaths(path, recursive);
 293
 0294                return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringCom
 0295                                                    && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.Ordi
 296            }
 0297            catch (Exception)
 298            {
 299                // Trap all exceptions, like DirNotExists, and return null
 0300                return null;
 301            }
 0302        }
 303
 304        public void SetAvailableEncoders(IEnumerable<string> list)
 305        {
 0306            _encoders = list.ToList();
 0307        }
 308
 309        public void SetAvailableDecoders(IEnumerable<string> list)
 310        {
 0311            _decoders = list.ToList();
 0312        }
 313
 314        public void SetAvailableHwaccels(IEnumerable<string> list)
 315        {
 0316            _hwaccels = list.ToList();
 0317        }
 318
 319        public void SetAvailableFilters(IEnumerable<string> list)
 320        {
 0321            _filters = list.ToList();
 0322        }
 323
 324        public void SetAvailableFiltersWithOption(IDictionary<int, bool> dict)
 325        {
 0326            _filtersWithOption = dict;
 0327        }
 328
 329        public void SetMediaEncoderVersion(EncoderValidator validator)
 330        {
 0331            _ffmpegVersion = validator.GetFFmpegVersion();
 0332        }
 333
 334        /// <inheritdoc />
 335        public bool SupportsEncoder(string encoder)
 336        {
 0337            return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
 338        }
 339
 340        /// <inheritdoc />
 341        public bool SupportsDecoder(string decoder)
 342        {
 0343            return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase);
 344        }
 345
 346        /// <inheritdoc />
 347        public bool SupportsHwaccel(string hwaccel)
 348        {
 0349            return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
 350        }
 351
 352        /// <inheritdoc />
 353        public bool SupportsFilter(string filter)
 354        {
 0355            return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
 356        }
 357
 358        /// <inheritdoc />
 359        public bool SupportsFilterWithOption(FilterOptionType option)
 360        {
 0361            if (_filtersWithOption.TryGetValue((int)option, out var val))
 362            {
 0363                return val;
 364            }
 365
 0366            return false;
 367        }
 368
 369        public bool CanEncodeToAudioCodec(string codec)
 370        {
 0371            if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
 372            {
 0373                codec = "libopus";
 374            }
 0375            else if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
 376            {
 0377                codec = "libmp3lame";
 378            }
 379
 0380            return SupportsEncoder(codec);
 381        }
 382
 383        public bool CanEncodeToSubtitleCodec(string codec)
 384        {
 385            // TODO
 0386            return true;
 387        }
 388
 389        /// <inheritdoc />
 390        public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
 391        {
 0392            var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
 0393            var extraArgs = GetExtraArguments(request);
 394
 0395            return GetMediaInfoInternal(
 0396                GetInputArgument(request.MediaSource.Path, request.MediaSource),
 0397                request.MediaSource.Path,
 0398                request.MediaSource.Protocol,
 0399                extractChapters,
 0400                extraArgs,
 0401                request.MediaType == DlnaProfileType.Audio,
 0402                request.MediaSource.VideoType,
 0403                cancellationToken);
 404        }
 405
 406        internal string GetExtraArguments(MediaInfoRequest request)
 407        {
 1408            var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
 1409            var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
 1410            var analyzeDuration = string.Empty;
 1411            var extraArgs = string.Empty;
 412
 1413            if (request.MediaSource.AnalyzeDurationMs > 0)
 414            {
 0415                analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000);
 416            }
 1417            else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
 418            {
 0419                analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
 420            }
 421
 1422            if (!string.IsNullOrEmpty(analyzeDuration))
 423            {
 0424                extraArgs = analyzeDuration;
 425            }
 426
 1427            if (!string.IsNullOrEmpty(ffmpegProbeSize))
 428            {
 0429                extraArgs += " -probesize " + ffmpegProbeSize;
 430            }
 431
 1432            if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
 433            {
 1434                extraArgs += $" -user_agent \"{userAgent}\"";
 435            }
 436
 1437            if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
 438            {
 0439                extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
 440            }
 441
 1442            return extraArgs;
 443        }
 444
 445        /// <inheritdoc />
 446        public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource)
 447        {
 0448            return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol);
 449        }
 450
 451        /// <inheritdoc />
 452        public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
 453        {
 0454            var prefix = "file";
 0455            if (mediaSource.IsoType == IsoType.BluRay)
 456            {
 0457                prefix = "bluray";
 458            }
 459
 0460            return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol);
 461        }
 462
 463        /// <inheritdoc />
 464        public string GetExternalSubtitleInputArgument(string inputFile)
 465        {
 466            const string Prefix = "file";
 467
 0468            return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File);
 469        }
 470
 471        /// <summary>
 472        /// Gets the media info internal.
 473        /// </summary>
 474        /// <returns>Task{MediaInfoResult}.</returns>
 475        private async Task<MediaInfo> GetMediaInfoInternal(
 476            string inputPath,
 477            string primaryPath,
 478            MediaProtocol protocol,
 479            bool extractChapters,
 480            string probeSizeArgument,
 481            bool isAudio,
 482            VideoType? videoType,
 483            CancellationToken cancellationToken)
 484        {
 485            var args = extractChapters
 486                ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
 487                : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
 488            args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
 489
 490            var process = new Process
 491            {
 492                StartInfo = new ProcessStartInfo
 493                {
 494                    CreateNoWindow = true,
 495                    UseShellExecute = false,
 496
 497                    // Must consume both or ffmpeg may hang due to deadlocks.
 498                    RedirectStandardOutput = true,
 499
 500                    FileName = _ffprobePath,
 501                    Arguments = args,
 502
 503                    WindowStyle = ProcessWindowStyle.Hidden,
 504                    ErrorDialog = false,
 505                },
 506                EnableRaisingEvents = true
 507            };
 508
 509            _logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
 510
 511            var memoryStream = new MemoryStream();
 512            await using (memoryStream.ConfigureAwait(false))
 513            using (var processWrapper = new ProcessWrapper(process, this))
 514            {
 515                StartProcess(processWrapper);
 516                using var reader = process.StandardOutput;
 517                await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
 518                memoryStream.Seek(0, SeekOrigin.Begin);
 519                InternalMediaInfoResult result;
 520                try
 521                {
 522                    result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
 523                                        memoryStream,
 524                                        _jsonSerializerOptions,
 525                                        cancellationToken).ConfigureAwait(false);
 526                }
 527                catch
 528                {
 529                    StopProcess(processWrapper, 100);
 530
 531                    throw;
 532                }
 533
 534                if (result is null || (result.Streams is null && result.Format is null))
 535                {
 536                    throw new FfmpegException("ffprobe failed - streams and format are both null.");
 537                }
 538
 539                if (result.Streams is not null)
 540                {
 541                    // Normalize aspect ratio if invalid
 542                    foreach (var stream in result.Streams)
 543                    {
 544                        if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 545                        {
 546                            stream.DisplayAspectRatio = string.Empty;
 547                        }
 548
 549                        if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
 550                        {
 551                            stream.SampleAspectRatio = string.Empty;
 552                        }
 553                    }
 554                }
 555
 556                return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primar
 557            }
 558        }
 559
 560        /// <inheritdoc />
 561        public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
 562        {
 0563            var mediaSource = new MediaSourceInfo
 0564            {
 0565                Protocol = MediaProtocol.File
 0566            };
 567
 0568            return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, canc
 569        }
 570
 571        /// <inheritdoc />
 572        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 573        {
 0574            return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, Image
 575        }
 576
 577        /// <inheritdoc />
 578        public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStre
 579        {
 0580            return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, tar
 581        }
 582
 583        private async Task<string> ExtractImage(
 584            string inputFile,
 585            string container,
 586            MediaStream videoStream,
 587            int? imageStreamIndex,
 588            MediaSourceInfo mediaSource,
 589            bool isAudio,
 590            Video3DFormat? threedFormat,
 591            TimeSpan? offset,
 592            ImageFormat? targetFormat,
 593            CancellationToken cancellationToken)
 594        {
 595            var inputArgument = GetInputPathArgument(inputFile, mediaSource);
 596
 597            if (!isAudio)
 598            {
 599                try
 600                {
 601                    return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFor
 602                }
 603                catch (ArgumentException)
 604                {
 605                    throw;
 606                }
 607                catch (Exception ex)
 608                {
 609                    _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}
 610                }
 611            }
 612
 613            return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, off
 614        }
 615
 616        private string GetImageResolutionParameter()
 617        {
 0618            var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
 0619            {
 0620                ImageResolution.P144 => "256x144",
 0621                ImageResolution.P240 => "426x240",
 0622                ImageResolution.P360 => "640x360",
 0623                ImageResolution.P480 => "854x480",
 0624                ImageResolution.P720 => "1280x720",
 0625                ImageResolution.P1080 => "1920x1080",
 0626                ImageResolution.P1440 => "2560x1440",
 0627                ImageResolution.P2160 => "3840x2160",
 0628                _ => string.Empty
 0629            };
 630
 0631            if (!string.IsNullOrEmpty(imageResolutionParameter))
 632            {
 0633                imageResolutionParameter = " -s " + imageResolutionParameter;
 634            }
 635
 0636            return imageResolutionParameter;
 637        }
 638
 639        private async Task<string> ExtractImageInternal(
 640            string inputPath,
 641            string container,
 642            MediaStream videoStream,
 643            int? imageStreamIndex,
 644            Video3DFormat? threedFormat,
 645            TimeSpan? offset,
 646            bool useIFrame,
 647            ImageFormat? targetFormat,
 648            bool isAudio,
 649            CancellationToken cancellationToken)
 650        {
 651            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 652
 653            var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
 654
 655            var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ou
 656            Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
 657
 658            // deint -> scale -> thumbnail -> tonemap.
 659            // put the SW tonemap right after the thumbnail to do it only once to reduce cpu usage.
 660            var filters = new List<string>();
 661
 662            // deinterlace using bwdif algorithm for video stream.
 663            if (videoStream is not null && videoStream.IsInterlaced)
 664            {
 665                filters.Add("bwdif=0:-1:0");
 666            }
 667
 668            // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the cor
 669            // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex
 670            var scaler = threedFormat switch
 671            {
 672                // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may 
 673                Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min
 674                // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
 675                Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw
 676                // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may
 677                Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):
 678                // ftab crop height in half, set the display aspect,crop out any black bars we may have made
 679                Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(
 680                _ => "scale=round(iw*sar/2)*2:round(ih/2)*2"
 681            };
 682
 683            filters.Add(scaler);
 684
 685            // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick 
 686            // mpegts need larger batch size otherwise the corrupted thumbnail will be created. Larger batch size will l
 687            var enableThumbnail = useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase);
 688            if (enableThumbnail)
 689            {
 690                var useLargerBatchSize = string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
 691                filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24"));
 692            }
 693
 694            // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available.
 695            // Only enable Dolby Vision tonemap when tonemapx is available
 696            var enableHdrExtraction = false;
 697
 698            if (videoStream?.VideoRange == VideoRange.HDR)
 699            {
 700                if (SupportsFilter("tonemapx"))
 701                {
 702                    enableHdrExtraction = true;
 703                    filters.Add("tonemapx=tonemap=bt2390:desat=0:peak=100:t=bt709:m=bt709:p=bt709:format=yuv420p");
 704                }
 705                else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
 706                {
 707                    enableHdrExtraction = true;
 708                    filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:p
 709                }
 710            }
 711
 712            var vf = string.Join(',', filters);
 713            var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.Invariant
 714            var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5
 715
 716            if (offset.HasValue)
 717            {
 718                args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
 719            }
 720
 721            if (!string.IsNullOrWhiteSpace(container))
 722            {
 723                var inputFormat = EncodingHelper.GetInputFormat(container);
 724                if (!string.IsNullOrWhiteSpace(inputFormat))
 725                {
 726                    args = "-f " + inputFormat + " " + args;
 727                }
 728            }
 729
 730            var process = new Process
 731            {
 732                StartInfo = new ProcessStartInfo
 733                {
 734                    CreateNoWindow = true,
 735                    UseShellExecute = false,
 736                    FileName = _ffmpegPath,
 737                    Arguments = args,
 738                    WindowStyle = ProcessWindowStyle.Hidden,
 739                    ErrorDialog = false,
 740                },
 741                EnableRaisingEvents = true
 742            };
 743
 744            _logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Argum
 745
 746            using (var processWrapper = new ProcessWrapper(process, this))
 747            {
 748                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 749                {
 750                    StartProcess(processWrapper);
 751
 752                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 753                    if (timeoutMs <= 0)
 754                    {
 755                        timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTi
 756                    }
 757
 758                    try
 759                    {
 760                        await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 761                    }
 762                    catch (OperationCanceledException ex)
 763                    {
 764                        process.Kill(true);
 765                        throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction t
 766                    }
 767                }
 768
 769                var file = _fileSystem.GetFileInfo(tempExtractPath);
 770
 771                if (processWrapper.ExitCode > 0 || !file.Exists || file.Length == 0)
 772                {
 773                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 774                }
 775
 776                return tempExtractPath;
 777            }
 778        }
 779
 780        /// <inheritdoc />
 781        public Task<string> ExtractVideoImagesOnIntervalAccelerated(
 782            string inputFile,
 783            string container,
 784            MediaSourceInfo mediaSource,
 785            MediaStream imageStream,
 786            int maxWidth,
 787            TimeSpan interval,
 788            bool allowHwAccel,
 789            bool enableHwEncoding,
 790            int? threads,
 791            int? qualityScale,
 792            ProcessPriorityClass? priority,
 793            bool enableKeyFrameOnlyExtraction,
 794            EncodingHelper encodingHelper,
 795            CancellationToken cancellationToken)
 796        {
 0797            var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
 0798            threads ??= _threads;
 799
 0800            if (allowHwAccel && enableKeyFrameOnlyExtraction)
 801            {
 0802                var hardwareAccelerationType = options.HardwareAccelerationType;
 0803                var supportsKeyFrameOnly = (hardwareAccelerationType == HardwareAccelerationType.nvenc && options.Enable
 0804                                           || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSyst
 0805                                           || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.Prefe
 0806                                           || hardwareAccelerationType == HardwareAccelerationType.vaapi
 0807                                           || hardwareAccelerationType == HardwareAccelerationType.videotoolbox;
 0808                if (!supportsKeyFrameOnly)
 809                {
 810                    // Disable hardware acceleration when the hardware decoder does not support keyframe only mode.
 0811                    allowHwAccel = false;
 0812                    options = new EncodingOptions();
 813                }
 814            }
 815
 816            // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
 817            // Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
 0818            if (!allowHwAccel)
 819            {
 0820                options.EnableHardwareEncoding = false;
 0821                options.HardwareAccelerationType = HardwareAccelerationType.none;
 0822                options.EnableTonemapping = false;
 823            }
 824
 0825            if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.Asp
 826            {
 827                // For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimen
 0828                var darParts = imageStream.AspectRatio.Split(':');
 0829                var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], Cultu
 830                // When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
 831                // Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, 
 0832                var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05
 0833                if (shouldResetHeight)
 834                {
 835                    // SAR = DAR * Height / Width
 836                    // RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
 0837                    imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
 838                }
 839            }
 840
 0841            var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.To
 0842            var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
 0843            {
 0844                IsVideoRequest = true,  // must be true for InputVideoHwaccelArgs to return non-empty value
 0845                MediaSource = mediaSource,
 0846                VideoStream = imageStream,
 0847                BaseRequest = baseRequest,  // GetVideoProcessingFilterParam errors if null
 0848                MediaPath = inputFile,
 0849                OutputVideoCodec = "mjpeg"
 0850            };
 0851            var vidEncoder = enableHwEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideo
 852
 853            // Get input and filter arguments
 0854            var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
 0855            if (string.IsNullOrWhiteSpace(inputArg))
 856            {
 0857                throw new InvalidOperationException("EncodingHelper returned empty input arguments.");
 858            }
 859
 0860            if (!allowHwAccel)
 861            {
 0862                inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, 
 863            }
 864
 0865            if (options.HardwareAccelerationType == HardwareAccelerationType.videotoolbox && _isLowPriorityHwDecodeSuppo
 866            {
 867                // VideoToolbox supports low priority decoding, which is useful for trickplay
 0868                inputArg = "-hwaccel_flags +low_priority " + inputArg;
 869            }
 870
 0871            if (enableKeyFrameOnlyExtraction)
 872            {
 0873                inputArg = "-skip_frame nokey " + inputArg;
 874            }
 875
 0876            var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
 0877            if (string.IsNullOrWhiteSpace(filterParam))
 878            {
 0879                throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
 880            }
 881
 0882            return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priori
 883        }
 884
 885        private async Task<string> ExtractVideoImagesOnIntervalInternal(
 886            string inputArg,
 887            string filterParam,
 888            string vidEncoder,
 889            int? outputThreads,
 890            int? qualityScale,
 891            ProcessPriorityClass? priority,
 892            CancellationToken cancellationToken)
 893        {
 894            if (string.IsNullOrWhiteSpace(inputArg))
 895            {
 896                throw new InvalidOperationException("Empty or invalid input argument.");
 897            }
 898
 899            // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
 900            // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
 901            var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
 902            var encoderQualityOption = "-qscale:v ";
 903
 904            if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
 905                || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
 906            {
 907                // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
 908                encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
 909                encoderQualityOption = "-global_quality:v ";
 910            }
 911
 912            if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
 913            {
 914                // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qs
 915                encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
 916            }
 917
 918            if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
 919            {
 920                // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qsca
 921                encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
 922                encoderQualityOption = "-qp_init:v ";
 923            }
 924
 925            // Output arguments
 926            var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToSt
 927            Directory.CreateDirectory(targetDirectory);
 928            var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
 929
 930            // Final command arguments
 931            var args = string.Format(
 932                CultureInfo.InvariantCulture,
 933                "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}-f {6} \"{7}\"",
 934                inputArg,
 935                filterParam,
 936                outputThreads.GetValueOrDefault(_threads),
 937                vidEncoder,
 938                encoderQualityOption + encoderQuality + " ",
 939                vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : stri
 940                "image2",
 941                outputPath);
 942
 943            // Start ffmpeg process
 944            var process = new Process
 945            {
 946                StartInfo = new ProcessStartInfo
 947                {
 948                    CreateNoWindow = true,
 949                    UseShellExecute = false,
 950                    FileName = _ffmpegPath,
 951                    Arguments = args,
 952                    WindowStyle = ProcessWindowStyle.Hidden,
 953                    ErrorDialog = false,
 954                },
 955                EnableRaisingEvents = true
 956            };
 957
 958            var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, 
 959            _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
 960
 961            using (var processWrapper = new ProcessWrapper(process, this))
 962            {
 963                bool ranToCompletion = false;
 964
 965                using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
 966                {
 967                    StartProcess(processWrapper);
 968
 969                    // Set process priority
 970                    if (priority.HasValue)
 971                    {
 972                        try
 973                        {
 974                            processWrapper.Process.PriorityClass = priority.Value;
 975                        }
 976                        catch (Exception ex)
 977                        {
 978                            _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", prior
 979                        }
 980                    }
 981
 982                    // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
 983                    // but we still need to detect if the process hangs.
 984                    // Making the assumption that as long as new jpegs are showing up, everything is good.
 985
 986                    bool isResponsive = true;
 987                    int lastCount = 0;
 988                    var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
 989                    timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
 990
 991                    while (isResponsive && !cancellationToken.IsCancellationRequested)
 992                    {
 993                        try
 994                        {
 995                            await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
 996
 997                            ranToCompletion = true;
 998                            break;
 999                        }
 1000                        catch (OperationCanceledException)
 1001                        {
 1002                            // We don't actually expect the process to be finished in one timeout span, just that one im
 1003                        }
 1004
 1005                        var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
 1006
 1007                        isResponsive = jpegCount > lastCount;
 1008                        lastCount = jpegCount;
 1009                    }
 1010
 1011                    if (!ranToCompletion)
 1012                    {
 1013                        if (!isResponsive)
 1014                        {
 1015                            _logger.LogInformation("Trickplay process unresponsive.");
 1016                        }
 1017
 1018                        _logger.LogInformation("Stopping trickplay extraction.");
 1019                        StopProcess(processWrapper, 1000);
 1020                    }
 1021                }
 1022
 1023                var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
 1024
 1025                if (exitCode == -1)
 1026                {
 1027                    _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
 1028
 1029                    throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction faile
 1030                }
 1031
 1032                return targetDirectory;
 1033            }
 1034        }
 1035
 1036        public string GetTimeParameter(long ticks)
 1037        {
 01038            var time = TimeSpan.FromTicks(ticks);
 1039
 01040            return GetTimeParameter(time);
 1041        }
 1042
 1043        public string GetTimeParameter(TimeSpan time)
 1044        {
 01045            return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
 1046        }
 1047
 1048        private void StartProcess(ProcessWrapper process)
 1049        {
 01050            process.Process.Start();
 1051
 01052            lock (_runningProcessesLock)
 1053            {
 01054                _runningProcesses.Add(process);
 01055            }
 01056        }
 1057
 1058        private void StopProcess(ProcessWrapper process, int waitTimeMs)
 1059        {
 1060            try
 1061            {
 01062                if (process.Process.WaitForExit(waitTimeMs))
 1063                {
 01064                    return;
 1065                }
 1066
 01067                _logger.LogInformation("Killing ffmpeg process");
 1068
 01069                process.Process.Kill();
 01070            }
 01071            catch (InvalidOperationException)
 1072            {
 1073                // The process has already exited or
 1074                // there is no process associated with this Process object.
 01075            }
 01076            catch (Exception ex)
 1077            {
 01078                _logger.LogError(ex, "Error killing process");
 01079            }
 01080        }
 1081
 1082        private void StopProcesses()
 1083        {
 1084            List<ProcessWrapper> proceses;
 221085            lock (_runningProcessesLock)
 1086            {
 221087                proceses = _runningProcesses.ToList();
 221088                _runningProcesses.Clear();
 221089            }
 1090
 441091            foreach (var process in proceses)
 1092            {
 01093                if (!process.HasExited)
 1094                {
 01095                    StopProcess(process, 500);
 1096                }
 1097            }
 221098        }
 1099
 1100        public string EscapeSubtitleFilterPath(string path)
 1101        {
 1102            // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
 1103            // We need to double escape
 1104
 01105            return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", @"'\\\''", StringC
 1106        }
 1107
 1108        /// <inheritdoc />
 1109        public void Dispose()
 1110        {
 221111            Dispose(true);
 221112            GC.SuppressFinalize(this);
 221113        }
 1114
 1115        /// <summary>
 1116        /// Releases unmanaged and - optionally - managed resources.
 1117        /// </summary>
 1118        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 1119        protected virtual void Dispose(bool dispose)
 1120        {
 221121            if (dispose)
 1122            {
 221123                StopProcesses();
 221124                _thumbnailResourcePool.Dispose();
 1125            }
 221126        }
 1127
 1128        /// <inheritdoc />
 1129        public Task ConvertImage(string inputPath, string outputPath)
 1130        {
 01131            throw new NotImplementedException();
 1132        }
 1133
 1134        /// <inheritdoc />
 1135        public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
 1136        {
 1137            // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VO
 01138            var allVobs = _fileSystem.GetFiles(path, true)
 01139                .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
 01140                .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
 01141                .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
 01142                .OrderBy(i => i.FullName)
 01143                .ToList();
 1144
 01145            if (titleNumber.HasValue)
 1146            {
 01147                var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
 01148                var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
 1149
 01150                if (vobs.Count > 0)
 1151                {
 01152                    return vobs.Select(i => i.FullName).ToList();
 1153                }
 1154
 01155                _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
 1156            }
 1157
 1158            // Check for multiple big titles (> 900 MB)
 01159            var titles = allVobs
 01160                .Where(vob => vob.Length >= 900 * 1024 * 1024)
 01161                .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
 01162                .Distinct()
 01163                .ToList();
 1164
 1165            // Fall back to first title if no big title is found
 01166            if (titles.Count == 0)
 1167            {
 01168                titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
 1169            }
 1170
 1171            // Aggregate all .vob files of the titles
 01172            return allVobs
 01173                .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToStr
 01174                .Select(i => i.FullName)
 01175                .Order()
 01176                .ToList();
 1177        }
 1178
 1179        /// <inheritdoc />
 1180        public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
 01181            => _blurayExaminer.GetDiscInfo(path).Files;
 1182
 1183        /// <inheritdoc />
 1184        public string GetInputPathArgument(EncodingJobInfo state)
 01185            => GetInputPathArgument(state.MediaPath, state.MediaSource);
 1186
 1187        /// <inheritdoc />
 1188        public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
 1189        {
 01190            return mediaSource.VideoType switch
 01191            {
 01192                VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null), mediaSource),
 01193                VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path), mediaSource),
 01194                _ => GetInputArgument(path, mediaSource)
 01195            };
 1196        }
 1197
 1198        /// <inheritdoc />
 1199        public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
 1200        {
 1201            // Get all playable files
 1202            IReadOnlyList<string> files;
 01203            var videoType = source.VideoType;
 01204            if (videoType == VideoType.Dvd)
 1205            {
 01206                files = GetPrimaryPlaylistVobFiles(source.Path, null);
 1207            }
 01208            else if (videoType == VideoType.BluRay)
 1209            {
 01210                files = GetPrimaryPlaylistM2tsFiles(source.Path);
 1211            }
 1212            else
 1213            {
 01214                return;
 1215            }
 1216
 1217            // Generate concat configuration entries for each file and write to file
 01218            Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
 01219            using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
 01220            foreach (var path in files)
 1221            {
 01222                var mediaInfoResult = GetMediaInfo(
 01223                    new MediaInfoRequest
 01224                    {
 01225                        MediaType = DlnaProfileType.Video,
 01226                        MediaSource = new MediaSourceInfo
 01227                        {
 01228                            Path = path,
 01229                            Protocol = MediaProtocol.File,
 01230                            VideoType = videoType
 01231                        }
 01232                    },
 01233                    CancellationToken.None).GetAwaiter().GetResult();
 1234
 01235                var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
 1236
 1237                // Add file path stanza to concat configuration
 01238                sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
 1239
 1240                // Add duration stanza to concat configuration
 01241                sw.WriteLine("duration {0}", duration);
 1242            }
 01243        }
 1244
 1245        public bool CanExtractSubtitles(string codec)
 1246        {
 1247            // TODO is there ever a case when a subtitle can't be extracted??
 01248            return true;
 1249        }
 1250
 1251        private sealed class ProcessWrapper : IDisposable
 1252        {
 1253            private readonly MediaEncoder _mediaEncoder;
 1254
 1255            private bool _disposed = false;
 1256
 1257            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
 1258            {
 1259                Process = process;
 01260                _mediaEncoder = mediaEncoder;
 01261                Process.Exited += OnProcessExited;
 01262            }
 1263
 1264            public Process Process { get; }
 1265
 1266            public bool HasExited { get; private set; }
 1267
 1268            public int? ExitCode { get; private set; }
 1269
 1270            private void OnProcessExited(object sender, EventArgs e)
 1271            {
 01272                var process = (Process)sender;
 1273
 01274                HasExited = true;
 1275
 1276                try
 1277                {
 01278                    ExitCode = process.ExitCode;
 01279                }
 01280                catch
 1281                {
 01282                }
 1283
 01284                DisposeProcess(process);
 01285            }
 1286
 1287            private void DisposeProcess(Process process)
 1288            {
 01289                lock (_mediaEncoder._runningProcessesLock)
 1290                {
 01291                    _mediaEncoder._runningProcesses.Remove(this);
 01292                }
 1293
 01294                process.Dispose();
 01295            }
 1296
 1297            public void Dispose()
 1298            {
 01299                if (!_disposed)
 1300                {
 01301                    if (Process is not null)
 1302                    {
 01303                        Process.Exited -= OnProcessExited;
 01304                        DisposeProcess(Process);
 1305                    }
 1306                }
 1307
 01308                _disposed = true;
 01309            }
 1310        }
 1311    }
 1312}

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()
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>)
SetMediaEncoderVersion(MediaBrowser.MediaEncoding.Encoder.EncoderValidator)
SupportsEncoder(System.String)
SupportsDecoder(System.String)
SupportsHwaccel(System.String)
SupportsFilter(System.String)
SupportsFilterWithOption(MediaBrowser.Controller.MediaEncoding.FilterOptionType)
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()