Jelajahi Sumber

Merge pull request #9254 from Shadowghost/dvdbdfix

Bond-009 2 tahun lalu
induk
melakukan
6351d1022b

+ 4 - 0
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>();
 

+ 14 - 2
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))

+ 13 - 1
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))
             {

+ 18 - 5
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

+ 30 - 0
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -153,6 +153,14 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <returns>System.String.</returns>
         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>
@@ -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);
     }
 }

+ 123 - 0
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);
+    }
+}

+ 187 - 0
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
+        });
+    }
+}

+ 68 - 0
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());
+    }
+}

+ 23 - 4
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>

+ 141 - 28
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??

+ 1 - 0
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" />

+ 12 - 1
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;
         }

+ 6 - 0
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;

+ 2 - 3
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.

+ 41 - 0
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; }
+}

+ 14 - 0
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);
+}

+ 152 - 14
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;

+ 3 - 0
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,