diff options
| author | Bond-009 <bond.009@outlook.com> | 2023-03-10 15:48:35 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-10 15:48:35 +0100 |
| commit | 6351d1022bffb1d0f152175965774eb709e143cb (patch) | |
| tree | c37c8e4c0311f31201f2a86702d74570c55979ae | |
| parent | 2146ddd20c5b8fe7a5bbe09d9ab0dbc18b1706f5 (diff) | |
| parent | 0da5255f1291ba510f829d36a3ca1a9eb65590dc (diff) | |
Merge pull request #9254 from Shadowghost/dvdbdfix
18 files changed, 852 insertions, 58 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index d104058cc..080c44829 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -80,11 +80,13 @@ using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; +using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; @@ -529,6 +531,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); + serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); + serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 9b133bef4..4fac91bf1 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -70,11 +70,23 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (IsDvdDirectory(child.FullName, filename, DirectoryService)) { - videoType = VideoType.Dvd; + var videoTmp = new TVideoType + { + Path = args.Path, + VideoType = VideoType.Dvd + }; + Set3DFormat(videoTmp); + return videoTmp; } else if (IsBluRayDirectory(filename)) { - videoType = VideoType.BluRay; + var videoTmp = new TVideoType + { + Path = args.Path, + VideoType = VideoType.BluRay + }; + Set3DFormat(videoTmp); + return videoTmp; } } else if (IsDvdFile(filename)) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index cd8ac4982..f25a71869 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -323,6 +323,15 @@ public class TranscodingJobHelper : IDisposable if (delete(job.Path!)) { await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false); + if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay) + { + var concatFilePath = Path.Join(_serverConfigurationManager.GetTranscodePath(), job.MediaSource.Id + ".concat"); + if (File.Exists(concatFilePath)) + { + _logger.LogInformation("Deleting ffmpeg concat configuration at {Path}", concatFilePath); + File.Delete(concatFilePath); + } + } } if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId)) @@ -524,7 +533,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 e6ff98843..3e338e871 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -561,9 +561,12 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetInputPathArgument(EncodingJobInfo state) { - var mediaPath = state.MediaPath ?? string.Empty; - - return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource); + return state.MediaSource.VideoType switch + { + VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource), + VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource), + _ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource) + }; } /// <summary> @@ -991,8 +994,18 @@ namespace MediaBrowser.Controller.MediaEncoding arg.Append(canvasArgs); } - arg.Append(" -i ") - .Append(GetInputPathArgument(state)); + if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) + { + var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat"); + _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); + arg.Append(" -f concat -safe 0 -i ") + .Append(tmpConcatPath); + } + else + { + arg.Append(" -i ") + .Append(GetInputPathArgument(state)); + } // sub2video for external graphical subtitles if (state.SubtitleStream is not null diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index bc6207ac5..f830b9f29 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -154,6 +154,14 @@ namespace MediaBrowser.Controller.MediaEncoding string GetInputArgument(string inputFile, MediaSourceInfo mediaSource); /// <summary> + /// Gets the input argument. + /// </summary> + /// <param name="inputFiles">The input files.</param> + /// <param name="mediaSource">The mediaSource.</param> + /// <returns>System.String.</returns> + string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource); + + /// <summary> /// Gets the input argument for an external subtitle file. /// </summary> /// <param name="inputFile">The input file.</param> @@ -187,5 +195,27 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="path">The path.</param> /// <param name="pathType">The type of path.</param> void UpdateEncoderPath(string path, string pathType); + + /// <summary> + /// Gets the primary playlist of .vob files. + /// </summary> + /// <param name="path">The to the .vob files.</param> + /// <param name="titleNumber">The title number to start with.</param> + /// <returns>A playlist.</returns> + IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber); + + /// <summary> + /// Gets the primary playlist of .m2ts files. + /// </summary> + /// <param name="path">The to the .m2ts files.</param> + /// <returns>A playlist.</returns> + IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path); + + /// <summary> + /// Generates a FFmpeg concat config for the source. + /// </summary> + /// <param name="source">The <see cref="MediaSourceInfo"/>.</param> + /// <param name="concatFilePath">The path the config should be written to.</param> + void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath); } } diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs new file mode 100644 index 000000000..ea520b1d6 --- /dev/null +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using System.Linq; +using BDInfo.IO; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.MediaEncoding.BdInfo; + +/// <summary> +/// Class BdInfoDirectoryInfo. +/// </summary> +public class BdInfoDirectoryInfo : IDirectoryInfo +{ + private readonly IFileSystem _fileSystem; + + private readonly FileSystemMetadata _impl; + + /// <summary> + /// Initializes a new instance of the <see cref="BdInfoDirectoryInfo" /> class. + /// </summary> + /// <param name="fileSystem">The filesystem.</param> + /// <param name="path">The path.</param> + public BdInfoDirectoryInfo(IFileSystem fileSystem, string path) + { + _fileSystem = fileSystem; + _impl = _fileSystem.GetDirectoryInfo(path); + } + + private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl) + { + _fileSystem = fileSystem; + _impl = impl; + } + + /// <summary> + /// Gets the name. + /// </summary> + public string Name => _impl.Name; + + /// <summary> + /// Gets the full name. + /// </summary> + public string FullName => _impl.FullName; + + /// <summary> + /// Gets the parent directory information. + /// </summary> + public IDirectoryInfo? Parent + { + get + { + var parentFolder = Path.GetDirectoryName(_impl.FullName); + if (parentFolder is not null) + { + return new BdInfoDirectoryInfo(_fileSystem, parentFolder); + } + + return null; + } + } + + /// <summary> + /// Gets the directories. + /// </summary> + /// <returns>An array with all directories.</returns> + public IDirectoryInfo[] GetDirectories() + { + return _fileSystem.GetDirectories(_impl.FullName) + .Select(x => new BdInfoDirectoryInfo(_fileSystem, x)) + .ToArray(); + } + + /// <summary> + /// Gets the files. + /// </summary> + /// <returns>All files of the directory.</returns> + public IFileInfo[] GetFiles() + { + return _fileSystem.GetFiles(_impl.FullName) + .Select(x => new BdInfoFileInfo(x)) + .ToArray(); + } + + /// <summary> + /// Gets the files matching a pattern. + /// </summary> + /// <param name="searchPattern">The search pattern.</param> + /// <returns>All files of the directory matchign the search pattern.</returns> + public IFileInfo[] GetFiles(string searchPattern) + { + return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false) + .Select(x => new BdInfoFileInfo(x)) + .ToArray(); + } + + /// <summary> + /// Gets the files matching a pattern and search options. + /// </summary> + /// <param name="searchPattern">The search pattern.</param> + /// <param name="searchOption">The search optin.</param> + /// <returns>All files of the directory matchign the search pattern and options.</returns> + public IFileInfo[] GetFiles(string searchPattern, SearchOption searchOption) + { + return _fileSystem.GetFiles( + _impl.FullName, + new[] { searchPattern }, + false, + (searchOption & SearchOption.AllDirectories) == SearchOption.AllDirectories) + .Select(x => new BdInfoFileInfo(x)) + .ToArray(); + } + + /// <summary> + /// Gets the bdinfo of a file system path. + /// </summary> + /// <param name="fs">The file system.</param> + /// <param name="path">The path.</param> + /// <returns>The BD directory information of the path on the file system.</returns> + public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path) + { + return new BdInfoDirectoryInfo(fs, path); + } +} diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs new file mode 100644 index 000000000..8ebb59c59 --- /dev/null +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BDInfo; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; + +namespace MediaBrowser.MediaEncoding.BdInfo; + +/// <summary> +/// Class BdInfoExaminer. +/// </summary> +public class BdInfoExaminer : IBlurayExaminer +{ + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class. + /// </summary> + /// <param name="fileSystem">The filesystem.</param> + public BdInfoExaminer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + /// <summary> + /// Gets the disc info. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>BlurayDiscInfo.</returns> + public BlurayDiscInfo GetDiscInfo(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path)); + + bdrom.Scan(); + + // Get the longest playlist + var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid); + + var outputStream = new BlurayDiscInfo + { + MediaStreams = Array.Empty<MediaStream>() + }; + + if (playlist is null) + { + return outputStream; + } + + outputStream.Chapters = playlist.Chapters.ToArray(); + + outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks; + + var sortedStreams = playlist.SortedStreams; + var mediaStreams = new List<MediaStream>(sortedStreams.Count); + + foreach (var stream in sortedStreams) + { + switch (stream) + { + case TSVideoStream videoStream: + AddVideoStream(mediaStreams, videoStream); + break; + case TSAudioStream audioStream: + AddAudioStream(mediaStreams, audioStream); + break; + case TSTextStream textStream: + AddSubtitleStream(mediaStreams, textStream); + break; + case TSGraphicsStream graphicStream: + AddSubtitleStream(mediaStreams, graphicStream); + break; + } + } + + outputStream.MediaStreams = mediaStreams.ToArray(); + + outputStream.PlaylistName = playlist.Name; + + if (playlist.StreamClips is not null && playlist.StreamClips.Count > 0) + { + // Get the files in the playlist + outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray(); + } + + return outputStream; + } + + /// <summary> + /// Adds the video stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="videoStream">The video stream.</param> + private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream) + { + var mediaStream = new MediaStream + { + BitRate = Convert.ToInt32(videoStream.BitRate), + Width = videoStream.Width, + Height = videoStream.Height, + Codec = videoStream.CodecShortName, + IsInterlaced = videoStream.IsInterlaced, + Type = MediaStreamType.Video, + Index = streams.Count + }; + + if (videoStream.FrameRateDenominator > 0) + { + float frameRateEnumerator = videoStream.FrameRateEnumerator; + float frameRateDenominator = videoStream.FrameRateDenominator; + + mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator; + } + + streams.Add(mediaStream); + } + + /// <summary> + /// Adds the audio stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="audioStream">The audio stream.</param> + private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream) + { + var stream = new MediaStream + { + Codec = audioStream.CodecShortName, + Language = audioStream.LanguageCode, + Channels = audioStream.ChannelCount, + SampleRate = audioStream.SampleRate, + Type = MediaStreamType.Audio, + Index = streams.Count + }; + + var bitrate = Convert.ToInt32(audioStream.BitRate); + + if (bitrate > 0) + { + stream.BitRate = bitrate; + } + + if (audioStream.LFE > 0) + { + stream.Channels = audioStream.ChannelCount + 1; + } + + streams.Add(stream); + } + + /// <summary> + /// Adds the subtitle stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="textStream">The text stream.</param> + private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream) + { + streams.Add(new MediaStream + { + Language = textStream.LanguageCode, + Codec = textStream.CodecShortName, + Type = MediaStreamType.Subtitle, + Index = streams.Count + }); + } + + /// <summary> + /// Adds the subtitle stream. + /// </summary> + /// <param name="streams">The streams.</param> + /// <param name="textStream">The text stream.</param> + private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream) + { + streams.Add(new MediaStream + { + Language = textStream.LanguageCode, + Codec = textStream.CodecShortName, + Type = MediaStreamType.Subtitle, + Index = streams.Count + }); + } +} diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs new file mode 100644 index 000000000..9e7a1d50a --- /dev/null +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs @@ -0,0 +1,68 @@ +using System.IO; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.MediaEncoding.BdInfo; + +/// <summary> +/// Class BdInfoFileInfo. +/// </summary> +public class BdInfoFileInfo : BDInfo.IO.IFileInfo +{ + private FileSystemMetadata _impl; + + /// <summary> + /// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class. + /// </summary> + /// <param name="impl">The <see cref="FileSystemMetadata" />.</param> + public BdInfoFileInfo(FileSystemMetadata impl) + { + _impl = impl; + } + + /// <summary> + /// Gets the name. + /// </summary> + public string Name => _impl.Name; + + /// <summary> + /// Gets the full name. + /// </summary> + public string FullName => _impl.FullName; + + /// <summary> + /// Gets the extension. + /// </summary> + public string Extension => _impl.Extension; + + /// <summary> + /// Gets the length. + /// </summary> + public long Length => _impl.Length; + + /// <summary> + /// Gets a value indicating whether this is a directory. + /// </summary> + public bool IsDir => _impl.IsDirectory; + + /// <summary> + /// Gets a file as file stream. + /// </summary> + /// <returns>A <see cref="FileStream" /> for the file.</returns> + public Stream OpenRead() + { + return new FileStream( + FullName, + FileMode.Open, + FileAccess.Read, + FileShare.Read); + } + + /// <summary> + /// Gets a files's content with a stream reader. + /// </summary> + /// <returns>A <see cref="StreamReader" /> for the file's content.</returns> + public StreamReader OpenText() + { + return new StreamReader(OpenRead()); + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index 95a93974a..04128c911 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<string> inputFiles, MediaProtocol protocol) + { + if (protocol != MediaProtocol.File) + { + return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFiles[0]); + } + + return GetConcatInputArgument(inputFiles, inputPrefix); } /// <summary> /// Gets the concat input argument. /// </summary> - /// <param name="inputFile">The input file.</param> + /// <param name="inputFiles">The input files.</param> /// <param name="inputPrefix">The input prefix.</param> /// <returns>System.String.</returns> - private static string GetConcatInputArgument(string inputFile, string inputPrefix) + private static string GetConcatInputArgument(IReadOnlyList<string> 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); } /// <summary> diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index cef02d5f8..d2112e5dc 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -11,6 +11,7 @@ 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; @@ -51,6 +52,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 +97,7 @@ namespace MediaBrowser.MediaEncoding.Encoder ILogger<MediaEncoder> logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, + IBlurayExaminer blurayExaminer, ILocalizationManager localization, IConfiguration config, IServerConfigurationManager serverConfig) @@ -102,6 +105,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; + _blurayExaminer = blurayExaminer; _localization = localization; _config = config; _serverConfig = serverConfig; @@ -117,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> @@ -344,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)) @@ -394,24 +409,16 @@ 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)) { @@ -419,7 +426,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 +436,30 @@ namespace MediaBrowser.MediaEncoding.Encoder 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> @@ -549,6 +550,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// <inheritdoc /> public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken) { var mediaSource = new MediaSourceInfo @@ -559,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); @@ -767,6 +771,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + /// <inheritdoc /> public string GetTimeParameter(long ticks) { var time = TimeSpan.FromTicks(ticks); @@ -865,6 +870,114 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new NotImplementedException(); } + /// <inheritdoc /> + public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber) + { + // 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.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase)) + .Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase)) + .OrderBy(i => i.FullName) + .ToList(); + + if (titleNumber.HasValue) + { + 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) + { + return vobs.Select(i => i.FullName).ToList(); + } + + _logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path); + } + + // 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(); + + // Fall back to first title if no big title is found + if (titles.Count == 0) + { + titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString()); + } + + // Aggregate all .vob files of the titles + return allVobs + .Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())) + .Select(i => i.FullName) + .ToList(); + } + + /// <inheritdoc /> + public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path) + { + // Get all playable .m2ts files + var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; + + // Get all files from the BDMV/STREAMING directory + var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")); + + // Only return playable local .m2ts files + return directoryFiles + .Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase)) + .Select(f => f.FullName) + .ToList(); + } + + /// <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; + } + + // 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) { // TODO is there ever a case when a subtitle can't be extracted?? diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index f4438fe19..a0624fe76 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -22,6 +22,7 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="BDInfo" /> <PackageReference Include="libse" /> <PackageReference Include="Microsoft.Extensions.Http" /> <PackageReference Include="System.Text.Encoding.CodePages" /> diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 8b8279588..cb482301f 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -248,12 +248,23 @@ namespace MediaBrowser.MediaEncoding.Probing return null; } + // Handle MPEG-1 container if (string.Equals(format, "mpegvideo", StringComparison.OrdinalIgnoreCase)) { return "mpeg"; } - format = format.Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase); + // Handle MPEG-2 container + if (string.Equals(format, "mpeg", StringComparison.OrdinalIgnoreCase)) + { + return "ts"; + } + + // Handle matroska container + if (string.Equals(format, "matroska", StringComparison.OrdinalIgnoreCase)) + { + return "mkv"; + } return format; } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index af71e0d3f..6f99bbc13 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -623,6 +623,12 @@ namespace MediaBrowser.Model.Dlna var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded); TranscodeReason transcodeReasons = 0; + // Force transcode or remux for BD/DVD folders + if (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay) + { + isEligibleForDirectPlay = false; + } + if (bitrateLimitExceeded) { transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit; diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 93ace43df..886b64a24 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -107,9 +107,8 @@ namespace MediaBrowser.Model.Dlna public string MediaSourceId => MediaSource?.Id; - public bool IsDirectStream => - PlayMethod == PlayMethod.DirectStream || - PlayMethod == PlayMethod.DirectPlay; + public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) + && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; /// <summary> /// Gets the audio stream that will be used. diff --git a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs new file mode 100644 index 000000000..d546ffccd --- /dev/null +++ b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs @@ -0,0 +1,41 @@ +#nullable disable + +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Model.MediaInfo; + +/// <summary> +/// Represents the result of BDInfo output. +/// </summary> +public class BlurayDiscInfo +{ + /// <summary> + /// Gets or sets the media streams. + /// </summary> + /// <value>The media streams.</value> + public MediaStream[] MediaStreams { get; set; } + + /// <summary> + /// Gets or sets the run time ticks. + /// </summary> + /// <value>The run time ticks.</value> + public long? RunTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the files. + /// </summary> + /// <value>The files.</value> + public string[] Files { get; set; } + + /// <summary> + /// Gets or sets the playlist name. + /// </summary> + /// <value>The playlist name.</value> + public string PlaylistName { get; set; } + + /// <summary> + /// Gets or sets the chapters. + /// </summary> + /// <value>The chapters.</value> + public double[] Chapters { get; set; } +} diff --git a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs new file mode 100644 index 000000000..d39725301 --- /dev/null +++ b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs @@ -0,0 +1,14 @@ +namespace MediaBrowser.Model.MediaInfo; + +/// <summary> +/// Interface IBlurayExaminer. +/// </summary> +public interface IBlurayExaminer +{ + /// <summary> + /// Gets the disc info. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>BlurayDiscInfo.</returns> + BlurayDiscInfo GetDiscInfo(string path); +} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 0f35c6a5e..e199db7f2 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -36,6 +36,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILogger<FFProbeVideoInfo> _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; + private readonly IBlurayExaminer _blurayExaminer; private readonly ILocalizationManager _localization; private readonly IEncodingManager _encodingManager; private readonly IServerConfigurationManager _config; @@ -51,6 +52,7 @@ namespace MediaBrowser.Providers.MediaInfo IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, + IBlurayExaminer blurayExaminer, ILocalizationManager localization, IEncodingManager encodingManager, IServerConfigurationManager config, @@ -64,6 +66,7 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; + _blurayExaminer = blurayExaminer; _localization = localization; _encodingManager = encodingManager; _config = config; @@ -80,16 +83,77 @@ namespace MediaBrowser.Providers.MediaInfo CancellationToken cancellationToken) where T : Video { + BlurayDiscInfo blurayDiscInfo = null; + Model.MediaInfo.MediaInfo mediaInfoResult = null; if (!item.IsShortcut || options.EnableRemoteContentProbe) { - mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); + if (item.VideoType == VideoType.Dvd) + { + // Get list of playable .vob files + var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null); + + // Return if no playable .vob files are found + if (vobs.Count == 0) + { + _logger.LogError("No playable .vob files found in DVD structure, skipping FFprobe."); + return ItemUpdateType.MetadataImport; + } + + // Fetch metadata of first .vob file + mediaInfoResult = await GetMediaInfo( + new Video + { + Path = vobs[0] + }, + cancellationToken).ConfigureAwait(false); + + // Sum up the runtime of all .vob files skipping the first .vob + for (var i = 1; i < vobs.Count; i++) + { + var tmpMediaInfo = await GetMediaInfo( + new Video + { + Path = vobs[i] + }, + cancellationToken).ConfigureAwait(false); + + mediaInfoResult.RunTimeTicks += tmpMediaInfo.RunTimeTicks; + } + } + else if (item.VideoType == VideoType.BluRay) + { + // Get BD disc information + blurayDiscInfo = GetBDInfo(item.Path); + + // Get playable .m2ts files + var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path); + + // Return if no playable .m2ts files are found + if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) + { + _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe."); + return ItemUpdateType.MetadataImport; + } + + // Fetch metadata of first .m2ts file + mediaInfoResult = await GetMediaInfo( + new Video + { + Path = m2ts[0] + }, + cancellationToken).ConfigureAwait(false); + } + else + { + mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); + } cancellationToken.ThrowIfCancellationRequested(); } - await Fetch(item, cancellationToken, mediaInfoResult, options).ConfigureAwait(false); + await Fetch(item, cancellationToken, mediaInfoResult, blurayDiscInfo, options).ConfigureAwait(false); return ItemUpdateType.MetadataImport; } @@ -129,6 +193,7 @@ namespace MediaBrowser.Providers.MediaInfo Video video, CancellationToken cancellationToken, Model.MediaInfo.MediaInfo mediaInfo, + BlurayDiscInfo blurayInfo, MetadataRefreshOptions options) { List<MediaStream> mediaStreams; @@ -153,19 +218,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) @@ -182,6 +236,10 @@ namespace MediaBrowser.Providers.MediaInfo video.Container = mediaInfo.Container; chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>(); + if (blurayInfo is not null) + { + FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo); + } } else { @@ -277,6 +335,86 @@ namespace MediaBrowser.Providers.MediaInfo } } + private void FetchBdInfo(Video video, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo) + { + if (blurayInfo.Files.Length <= 1) + { + return; + } + + // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output + int? currentHeight = null; + int? currentWidth = null; + int? currentBitRate = null; + + var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + + // Grab the values that ffprobe recorded + if (videoStream is not null) + { + currentBitRate = videoStream.BitRate; + currentWidth = videoStream.Width; + currentHeight = videoStream.Height; + } + + // Fill video properties from the BDInfo result + mediaStreams.Clear(); + mediaStreams.AddRange(blurayInfo.MediaStreams); + + if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0) + { + video.RunTimeTicks = blurayInfo.RunTimeTicks; + } + + if (blurayInfo.Chapters is not null) + { + double[] brChapter = blurayInfo.Chapters; + chapters = new ChapterInfo[brChapter.Length]; + for (int i = 0; i < brChapter.Length; i++) + { + chapters[i] = new ChapterInfo + { + StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks + }; + } + } + + videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); + + // Use the ffprobe values if these are empty + if (videoStream is not null) + { + videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate; + videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width; + videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height; + } + } + + private bool IsEmpty(int? num) + { + return !num.HasValue || num.Value == 0; + } + + /// <summary> + /// Gets information about the longest playlist on a bdrom. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>VideoStream.</returns> + private BlurayDiscInfo GetBDInfo(string path) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + try + { + return _blurayExaminer.GetDiscInfo(path); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting BDInfo"); + return null; + } + } + private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions) { var replaceData = refreshOptions.ReplaceAllMetadata; diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 31fa3da1c..280021955 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -53,6 +53,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param> + /// <param name="blurayExaminer">Instance of the <see cref="IBlurayExaminer"/> interface.</param> /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> /// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param> /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> @@ -66,6 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, + IBlurayExaminer blurayExaminer, ILocalizationManager localization, IEncodingManager encodingManager, IServerConfigurationManager config, @@ -85,6 +87,7 @@ namespace MediaBrowser.Providers.MediaInfo mediaSourceManager, mediaEncoder, itemRepo, + blurayExaminer, localization, encodingManager, config, |
