diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 12960f87a..29cb3d2f9 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -525,7 +525,10 @@ public class TranscodingJobHelper : IDisposable if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id); - await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + if (state.VideoType != VideoType.Dvd) + { + await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + } if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index a844e6443..b3a1b2a99 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -535,6 +535,16 @@ namespace MediaBrowser.Controller.MediaEncoding { var mediaPath = state.MediaPath ?? string.Empty; + if (state.MediaSource.VideoType == VideoType.Dvd) + { + return _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource); + } + + if (state.MediaSource.VideoType == VideoType.BluRay) + { + return _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2TsFiles(state.MediaPath, null).ToList(), state.MediaSource); + } + return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource); } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index fe8e9063e..c34ce5d29 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -153,6 +153,14 @@ namespace MediaBrowser.Controller.MediaEncoding /// System.String. string GetInputArgument(string inputFile, MediaSourceInfo mediaSource); + /// + /// Gets the input argument. + /// + /// The input files. + /// The mediaSource. + /// System.String. + string GetInputArgument(IReadOnlyList inputFiles, MediaSourceInfo mediaSource); + /// /// Gets the input argument for an external subtitle file. /// @@ -195,5 +203,13 @@ namespace MediaBrowser.Controller.MediaEncoding /// The title number to start with. /// A playlist. IEnumerable GetPrimaryPlaylistVobFiles(string path, uint? titleNumber); + + /// + /// Gets the primary playlist of .m2ts files. + /// + /// The to the .m2ts files. + /// The title number to start with. + /// A playlist. + IEnumerable GetPrimaryPlaylistM2TsFiles(string path, uint? titleNumber); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index d0ea0429b..0f202a90e 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -1,7 +1,9 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; using System.Globalization; +using System.Linq; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Encoder @@ -15,21 +17,38 @@ namespace MediaBrowser.MediaEncoding.Encoder return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFile); } - return GetConcatInputArgument(inputFile, inputPrefix); + return GetFileInputArgument(inputFile, inputPrefix); + } + + public static string GetInputArgument(string inputPrefix, IReadOnlyList inputFiles, MediaProtocol protocol) + { + if (protocol != MediaProtocol.File) + { + return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFiles[0]); + } + + return GetConcatInputArgument(inputFiles, inputPrefix); } /// /// Gets the concat input argument. /// - /// The input file. + /// The input files. /// The input prefix. /// System.String. - private static string GetConcatInputArgument(string inputFile, string inputPrefix) + private static string GetConcatInputArgument(IReadOnlyList inputFiles, string inputPrefix) { // Get all streams // If there's more than one we'll need to use the concat command + if (inputFiles.Count > 1) + { + var files = string.Join("|", inputFiles.Select(NormalizePath)); + + return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files); + } + // Determine the input path for video files - return GetFileInputArgument(inputFile, inputPrefix); + return GetFileInputArgument(inputFiles[0], inputPrefix); } /// diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4a795625d..df31ba83e 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -51,6 +51,7 @@ 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; @@ -95,6 +96,7 @@ namespace MediaBrowser.MediaEncoding.Encoder ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, + IBlurayExaminer blurayExaminer, ILocalizationManager localization, IConfiguration config, IServerConfigurationManager serverConfig) @@ -102,6 +104,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; + _blurayExaminer = blurayExaminer; _localization = localization; _config = config; _serverConfig = serverConfig; @@ -117,16 +120,22 @@ namespace MediaBrowser.MediaEncoding.Encoder /// public string ProbePath => _ffprobePath; + /// public Version EncoderVersion => _ffmpegVersion; + /// public bool IsPkeyPauseSupported => _isPkeyPauseSupported; + /// public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd; + /// public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD; + /// public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965; + /// public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier; /// @@ -344,26 +353,31 @@ namespace MediaBrowser.MediaEncoding.Encoder _ffmpegVersion = validator.GetFFmpegVersion(); } + /// public bool SupportsEncoder(string encoder) { return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase); } + /// public bool SupportsDecoder(string decoder) { return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase); } + /// public bool SupportsHwaccel(string hwaccel) { return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase); } + /// public bool SupportsFilter(string filter) { return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase); } + /// public bool SupportsFilterWithOption(FilterOptionType option) { if (_filtersWithOption.TryGetValue((int)option, out var val)) @@ -394,24 +408,16 @@ namespace MediaBrowser.MediaEncoding.Encoder return true; } - /// - /// Gets the media info. - /// - /// The request. - /// The cancellation token. - /// Task. + /// public Task 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)) { @@ -419,7 +425,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } return GetMediaInfoInternal( - GetInputArgument(inputFile, request.MediaSource), + GetInputArgument(request.MediaSource.Path, request.MediaSource), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters, @@ -429,36 +435,24 @@ namespace MediaBrowser.MediaEncoding.Encoder cancellationToken); } - /// - /// Gets the input argument. - /// - /// The input file. - /// The mediaSource. - /// System.String. - /// Unrecognized InputType. - public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) + /// + public string GetInputArgument(IReadOnlyList inputFiles, MediaSourceInfo mediaSource) { - var prefix = "file"; - if (mediaSource.VideoType == VideoType.BluRay - || mediaSource.IsoType == IsoType.BluRay) - { - prefix = "bluray"; - } - - return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol); + return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol); } - /// - /// Gets the input argument for an external subtitle file. - /// - /// The input file. - /// System.String. - /// Unrecognized InputType. + /// + public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) + { + return EncodingUtils.GetInputArgument("file", new List() { inputFile }, mediaSource.Protocol); + } + + /// public string GetExternalSubtitleInputArgument(string inputFile) { const string Prefix = "file"; - return EncodingUtils.GetInputArgument(Prefix, inputFile, MediaProtocol.File); + return EncodingUtils.GetInputArgument(Prefix, new List { inputFile }, MediaProtocol.File); } /// @@ -549,6 +543,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// public Task ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken) { var mediaSource = new MediaSourceInfo @@ -559,11 +554,13 @@ namespace MediaBrowser.MediaEncoding.Encoder return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken); } + /// public Task 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); } + /// public Task 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); @@ -767,6 +764,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// public string GetTimeParameter(long ticks) { var time = TimeSpan.FromTicks(ticks); @@ -868,80 +866,56 @@ namespace MediaBrowser.MediaEncoding.Encoder /// public IEnumerable 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 VOBs 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(); - } - 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); } - _logger.LogWarning("Could not determine vob file list for {Path} using DvdLib. Will scan using file sizes.", path); + _logger.LogWarning("Could not determine VOB file list 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).Split('_')[1]) + .GroupBy(x => x) + .Select(y => y.First()) .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.FirstOrDefault() == null) { - _logger.LogWarning("Vob size filter resulted in zero matches. Taking all vobs."); - files = allVobs; + titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).Split('_')[1]); } - // 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 VOBs of the titles + return allVobs + .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).Split('_')[1])) + .Select(i => i.FullName) + .ToList(); + } - if (parts.Length == 3) - { - var title = parts[1]; + public IEnumerable GetPrimaryPlaylistM2TsFiles(string path, uint? titleNumber) + { + var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; + var directoryFiles = _fileSystem.GetFiles(path + "/BDMV/STREAM/"); - files = files.TakeWhile(f => - { - var fileParts = _fileSystem.GetFileNameWithoutExtension(f).Split('_'); - - return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase); - }).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; - } - } - } - - return files.Select(i => i.FullName); + return directoryFiles + .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase)) + .Select(f => f.FullName); } public bool CanExtractSubtitles(string codec) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 81434b862..393e1f22a 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -90,37 +90,50 @@ namespace MediaBrowser.Providers.MediaInfo if (!item.IsShortcut || options.EnableRemoteContentProbe) { - string[] streamFileNames = null; - if (item.VideoType == VideoType.Dvd) { - streamFileNames = FetchFromDvdLib(item); + // Fetch metadata of first VOB + var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null).ToList(); + mediaInfoResult = await GetMediaInfo( + new Video + { + Path = vobs.First() + }, + cancellationToken).ConfigureAwait(false); - if (streamFileNames.Length == 0) + // Remove first VOB + vobs.RemoveAt(0); + + // Add runtime from all other VOBs + foreach (var vob in vobs) { - _logger.LogError("No playable vobs found in dvd structure, skipping ffprobe."); - return ItemUpdateType.MetadataImport; + var tmpMediaInfo = await GetMediaInfo( + new Video + { + Path = vob + }, + cancellationToken).ConfigureAwait(false); + + mediaInfoResult.RunTimeTicks += tmpMediaInfo.RunTimeTicks; } } - else if (item.VideoType == VideoType.BluRay) + else { - var inputPath = item.Path; - - blurayDiscInfo = GetBDInfo(inputPath); - - streamFileNames = blurayDiscInfo.Files; - - if (streamFileNames.Length == 0) + if (item.VideoType == VideoType.BluRay) { - _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe."); - return ItemUpdateType.MetadataImport; + blurayDiscInfo = GetBDInfo(item.Path); + + if (blurayDiscInfo.Files.Length == 0) + { + _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe."); + return ItemUpdateType.MetadataImport; + } } + + mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); } - streamFileNames ??= Array.Empty(); - - mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); } @@ -189,19 +202,8 @@ namespace MediaBrowser.Providers.MediaInfo } mediaAttachments = mediaInfo.MediaAttachments; - video.TotalBitrate = mediaInfo.Bitrate; - // video.FormatName = (mediaInfo.Container ?? string.Empty) - // .Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase); - - // For DVDs this may not always be accurate, so don't set the runtime if the item already has one - var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks is null || video.RunTimeTicks.Value == 0; - - if (needToSetRuntime) - { - video.RunTimeTicks = mediaInfo.RunTimeTicks; - } - + video.RunTimeTicks = mediaInfo.RunTimeTicks; video.Size = mediaInfo.Size; if (video.VideoType == VideoType.VideoFile) @@ -321,8 +323,6 @@ namespace MediaBrowser.Providers.MediaInfo { var video = (Video)item; - // video.PlayableStreamFileNames = blurayInfo.Files.ToList(); - // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output if (blurayInfo.Files.Length > 1) {