diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs')
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 266 |
1 files changed, 159 insertions, 107 deletions
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index ec3412f90..d2112e5dc 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -11,7 +11,9 @@ using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -50,7 +52,9 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; + private readonly IBlurayExaminer _blurayExaminer; private readonly IConfiguration _config; + private readonly IServerConfigurationManager _serverConfig; private readonly string _startupOptionFFmpegPath; private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); @@ -74,7 +78,8 @@ namespace MediaBrowser.MediaEncoding.Encoder private bool _isVaapiDeviceInteli965 = false; private bool _isVaapiDeviceSupportVulkanFmtModifier = false; - private static string[] _vulkanFmtModifierExts = { + private static string[] _vulkanFmtModifierExts = + { "VK_KHR_sampler_ycbcr_conversion", "VK_EXT_image_drm_format_modifier", "VK_KHR_external_memory_fd", @@ -92,16 +97,22 @@ namespace MediaBrowser.MediaEncoding.Encoder ILogger<MediaEncoder> logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, + IBlurayExaminer blurayExaminer, ILocalizationManager localization, - IConfiguration config) + IConfiguration config, + IServerConfigurationManager serverConfig) { _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; + _blurayExaminer = blurayExaminer; _localization = localization; _config = config; + _serverConfig = serverConfig; _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty; - _jsonSerializerOptions = JsonDefaults.Options; + + _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options); + _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter()); } /// <inheritdoc /> @@ -110,16 +121,22 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <inheritdoc /> public string ProbePath => _ffprobePath; + /// <inheritdoc /> public Version EncoderVersion => _ffmpegVersion; + /// <inheritdoc /> public bool IsPkeyPauseSupported => _isPkeyPauseSupported; + /// <inheritdoc /> public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd; + /// <inheritdoc /> public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD; + /// <inheritdoc /> public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965; + /// <inheritdoc /> public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier; /// <summary> @@ -153,7 +170,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _configurationManager.SaveConfiguration("encoding", options); // Only if mpeg path is set, try and set path to probe - if (_ffmpegPath != null) + if (_ffmpegPath is not null) { // Determine a probe path from the mpeg path _ffprobePath = Regex.Replace(_ffmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); @@ -337,26 +354,31 @@ namespace MediaBrowser.MediaEncoding.Encoder _ffmpegVersion = validator.GetFFmpegVersion(); } + /// <inheritdoc /> public bool SupportsEncoder(string encoder) { return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase); } + /// <inheritdoc /> public bool SupportsDecoder(string decoder) { return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase); } + /// <inheritdoc /> public bool SupportsHwaccel(string hwaccel) { return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase); } + /// <inheritdoc /> public bool SupportsFilter(string filter) { return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase); } + /// <inheritdoc /> public bool SupportsFilterWithOption(FilterOptionType option) { if (_filtersWithOption.TryGetValue((int)option, out var val)) @@ -387,74 +409,57 @@ namespace MediaBrowser.MediaEncoding.Encoder return true; } - /// <summary> - /// Gets the media info. - /// </summary> - /// <param name="request">The request.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> + /// <inheritdoc /> public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken) { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; - var inputFile = request.MediaSource.Path; - string analyzeDuration = string.Empty; string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; if (request.MediaSource.AnalyzeDurationMs > 0) { - analyzeDuration = "-analyzeduration " + - (request.MediaSource.AnalyzeDurationMs * 1000).ToString(); + analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString(); } else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration)) { analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration; } - var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File; - return GetMediaInfoInternal( - GetInputArgument(inputFile, request.MediaSource), + GetInputArgument(request.MediaSource.Path, request.MediaSource), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters, analyzeDuration, request.MediaType == DlnaProfileType.Audio, request.MediaSource.VideoType, - forceEnableLogging, cancellationToken); } - /// <summary> - /// Gets the input argument. - /// </summary> - /// <param name="inputFile">The input file.</param> - /// <param name="mediaSource">The mediaSource.</param> - /// <returns>System.String.</returns> - /// <exception cref="ArgumentException">Unrecognized InputType.</exception> + /// <inheritdoc /> + public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource) + { + return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol); + } + + /// <inheritdoc /> public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) { var prefix = "file"; - if (mediaSource.VideoType == VideoType.BluRay - || mediaSource.IsoType == IsoType.BluRay) + if (mediaSource.IsoType == IsoType.BluRay) { prefix = "bluray"; } - return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol); + return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol); } - /// <summary> - /// Gets the input argument for an external subtitle file. - /// </summary> - /// <param name="inputFile">The input file.</param> - /// <returns>System.String.</returns> - /// <exception cref="ArgumentException">Unrecognized InputType.</exception> + /// <inheritdoc /> public string GetExternalSubtitleInputArgument(string inputFile) { const string Prefix = "file"; - return EncodingUtils.GetInputArgument(Prefix, inputFile, MediaProtocol.File); + return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File); } /// <summary> @@ -469,7 +474,6 @@ namespace MediaBrowser.MediaEncoding.Encoder string probeSizeArgument, bool isAudio, VideoType? videoType, - bool forceEnableLogging, CancellationToken cancellationToken) { var args = extractChapters @@ -484,7 +488,7 @@ namespace MediaBrowser.MediaEncoding.Encoder CreateNoWindow = true, UseShellExecute = false, - // Must consume both or ffmpeg may hang due to deadlocks. See comments below. + // Must consume both or ffmpeg may hang due to deadlocks. RedirectStandardOutput = true, FileName = _ffprobePath, @@ -496,21 +500,14 @@ namespace MediaBrowser.MediaEncoding.Encoder EnableRaisingEvents = true }; - if (forceEnableLogging) - { - _logger.LogInformation("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments); - } - else - { - _logger.LogDebug("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments); - } + _logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args); + var memoryStream = new MemoryStream(); + await using (memoryStream.ConfigureAwait(false)) using (var processWrapper = new ProcessWrapper(process, this)) { - await using var memoryStream = new MemoryStream(); - _logger.LogDebug("Starting ffprobe with args {Args}", args); StartProcess(processWrapper); - await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken: cancellationToken); + await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); memoryStream.Seek(0, SeekOrigin.Begin); InternalMediaInfoResult result; try @@ -518,7 +515,7 @@ namespace MediaBrowser.MediaEncoding.Encoder result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>( memoryStream, _jsonSerializerOptions, - cancellationToken: cancellationToken).ConfigureAwait(false); + cancellationToken).ConfigureAwait(false); } catch { @@ -527,12 +524,12 @@ namespace MediaBrowser.MediaEncoding.Encoder throw; } - if (result == null || (result.Streams == null && result.Format == null)) + if (result is null || (result.Streams is null && result.Format is null)) { throw new FfmpegException("ffprobe failed - streams and format are both null."); } - if (result.Streams != null) + if (result.Streams is not null) { // Normalize aspect ratio if invalid foreach (var stream in result.Streams) @@ -553,6 +550,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// <inheritdoc /> public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken) { var mediaSource = new MediaSourceInfo @@ -563,11 +561,13 @@ namespace MediaBrowser.MediaEncoding.Encoder return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken); } + /// <inheritdoc /> public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ImageFormat.Jpg, cancellationToken); } + /// <inheritdoc /> public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken) { return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken); @@ -606,13 +606,35 @@ namespace MediaBrowser.MediaEncoding.Encoder return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, cancellationToken).ConfigureAwait(false); } - private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken) + private string GetImageResolutionParameter() { - if (string.IsNullOrEmpty(inputPath)) + string imageResolutionParameter; + + imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch + { + ImageResolution.P144 => "256x144", + ImageResolution.P240 => "426x240", + ImageResolution.P360 => "640x360", + ImageResolution.P480 => "854x480", + ImageResolution.P720 => "1280x720", + ImageResolution.P1080 => "1920x1080", + ImageResolution.P1440 => "2560x1440", + ImageResolution.P2160 => "3840x2160", + _ => string.Empty + }; + + if (!string.IsNullOrEmpty(imageResolutionParameter)) { - throw new ArgumentNullException(nameof(inputPath)); + imageResolutionParameter = " -s " + imageResolutionParameter; } + return imageResolutionParameter; + } + + private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(inputPath); + var outputExtension = targetFormat switch { ImageFormat.Bmp => ".bmp", @@ -631,7 +653,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var filters = new List<string>(); // deinterlace using bwdif algorithm for video stream. - if (videoStream != null && videoStream.IsInterlaced) + if (videoStream is not null && videoStream.IsInterlaced) { filters.Add("bwdif=0:-1:0"); } @@ -676,7 +698,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var vf = string.Join(',', filters); var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; - var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads); + var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, GetImageResolutionParameter()); if (offset.HasValue) { @@ -749,6 +771,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// <inheritdoc /> public string GetTimeParameter(long ticks) { var time = TimeSpan.FromTicks(ticks); @@ -848,82 +871,111 @@ namespace MediaBrowser.MediaEncoding.Encoder } /// <inheritdoc /> - public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber) + public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber) { - // min size 300 mb - const long MinPlayableSize = 314572800; - - // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size - // Once we reach a file that is at least the minimum, return all subsequent ones + // Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VOB var allVobs = _fileSystem.GetFiles(path, true) - .Where(file => string.Equals(file.Extension, ".vob", StringComparison.OrdinalIgnoreCase)) + .Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase)) + .Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase)) + .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase)) .OrderBy(i => i.FullName) .ToList(); - // If we didn't find any satisfying the min length, just take them all - if (allVobs.Count == 0) - { - _logger.LogWarning("No vobs found in dvd structure."); - return Enumerable.Empty<string>(); - } - if (titleNumber.HasValue) { - var prefix = string.Format( - CultureInfo.InvariantCulture, - titleNumber.Value >= 10 ? "VTS_{0}_" : "VTS_0{0}_", - titleNumber.Value); + var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value); var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList(); if (vobs.Count > 0) { - var minSizeVobs = vobs - .SkipWhile(f => f.Length < MinPlayableSize) - .ToList(); - - return minSizeVobs.Count == 0 ? vobs.Select(i => i.FullName) : minSizeVobs.Select(i => i.FullName); + return vobs.Select(i => i.FullName).ToList(); } - _logger.LogWarning("Could not determine vob file list for {Path} using DvdLib. Will scan using file sizes.", path); + _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path); } - var files = allVobs - .SkipWhile(f => f.Length < MinPlayableSize) + // Check for multiple big titles (> 900 MB) + var titles = allVobs + .Where(vob => vob.Length >= 900 * 1024 * 1024) + .Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString()) + .Distinct() .ToList(); - // If we didn't find any satisfying the min length, just take them all - if (files.Count == 0) + // Fall back to first title if no big title is found + if (titles.Count == 0) { - _logger.LogWarning("Vob size filter resulted in zero matches. Taking all vobs."); - files = allVobs; + titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString()); } - // Assuming they're named "vts_05_01", take all files whose second part matches that of the first file - if (files.Count > 0) - { - var parts = _fileSystem.GetFileNameWithoutExtension(files[0]).Split('_'); + // Aggregate all .vob files of the titles + return allVobs + .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())) + .Select(i => i.FullName) + .ToList(); + } - if (parts.Length == 3) - { - var title = parts[1]; + /// <inheritdoc /> + public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path) + { + // Get all playable .m2ts files + var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; - files = files.TakeWhile(f => - { - var fileParts = _fileSystem.GetFileNameWithoutExtension(f).Split('_'); + // Get all files from the BDMV/STREAMING directory + var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")); - return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase); - }).ToList(); + // Only return playable local .m2ts files + return directoryFiles + .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase)) + .Select(f => f.FullName) + .ToList(); + } - // If this resulted in not getting any vobs, just take them all - if (files.Count == 0) - { - _logger.LogWarning("Vob filename filter resulted in zero matches. Taking all vobs."); - files = allVobs; - } - } + /// <inheritdoc /> + public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath) + { + // Get all playable files + IReadOnlyList<string> files; + var videoType = source.VideoType; + if (videoType == VideoType.Dvd) + { + files = GetPrimaryPlaylistVobFiles(source.Path, null); + } + else if (videoType == VideoType.BluRay) + { + files = GetPrimaryPlaylistM2tsFiles(source.Path); + } + else + { + return; } - return files.Select(i => i.FullName); + // Generate concat configuration entries for each file and write to file + using (StreamWriter sw = new StreamWriter(concatFilePath)) + { + foreach (var path in files) + { + var mediaInfoResult = GetMediaInfo( + new MediaInfoRequest + { + MediaType = DlnaProfileType.Video, + MediaSource = new MediaSourceInfo + { + Path = path, + Protocol = MediaProtocol.File, + VideoType = videoType + } + }, + CancellationToken.None).GetAwaiter().GetResult(); + + var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds; + + // Add file path stanza to concat configuration + sw.WriteLine("file '{0}'", path); + + // Add duration stanza to concat configuration + sw.WriteLine("duration {0}", duration); + } + } } public bool CanExtractSubtitles(string codec) @@ -988,7 +1040,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { if (!_disposed) { - if (Process != null) + if (Process is not null) { Process.Exited -= OnProcessExited; DisposeProcess(Process); |
