Merge pull request #9254 from Shadowghost/dvdbdfix

pull/9468/head
Bond-009 1 year ago committed by GitHub
commit 6351d1022b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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>();

@ -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))

@ -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))
{

@ -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

@ -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);
}
}

@ -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);
}
}

@ -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
});
}
}

@ -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());
}
}

@ -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>

@ -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??

@ -22,6 +22,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BDInfo" />
<PackageReference Include="libse" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="System.Text.Encoding.CodePages" />

@ -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;
}

@ -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;

@ -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.

@ -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; }
}

@ -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);
}

@ -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;

@ -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,

Loading…
Cancel
Save