diff options
| author | Cody Robibero <cody@robibe.ro> | 2021-12-23 19:38:10 -0700 |
|---|---|---|
| committer | Cody Robibero <cody@robibe.ro> | 2021-12-23 19:38:10 -0700 |
| commit | a04ab6b87637fe378759aaf2b7fa71726150b2b1 (patch) | |
| tree | 62f4e5bdb272e9312bab469cbcda1e13591e7834 /MediaBrowser.MediaEncoding | |
| parent | c52a2f2f7b130d73a96cdac00f1e63531a04139b (diff) | |
| parent | 8c7dd0a691d150ac4fa5719853554ff569abf1bb (diff) | |
Merge branch 'master' into studios-images-plugin
# Conflicts:
# MediaBrowser.Providers/MediaBrowser.Providers.csproj
Diffstat (limited to 'MediaBrowser.MediaEncoding')
21 files changed, 886 insertions, 1545 deletions
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index e8aeabf9d..9ebc0d0cf 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; @@ -88,7 +89,7 @@ namespace MediaBrowser.MediaEncoding.Attachments CancellationToken cancellationToken) { var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false); - return File.OpenRead(attachmentPath); + return AsyncFile.OpenRead(attachmentPath); } private async Task<string> GetReadableFile( @@ -222,11 +223,10 @@ namespace MediaBrowser.MediaEncoding.Attachments if (failed) { - var msg = $"ffmpeg attachment extraction failed for {inputPath} to {outputPath}"; + _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); - _logger.LogError(msg); - - throw new InvalidOperationException(msg); + throw new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath)); } else { diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index 4a54b677d..409379c35 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -9,9 +9,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo { public class BdInfoDirectoryInfo : IDirectoryInfo { - private readonly IFileSystem _fileSystem = null; + private readonly IFileSystem _fileSystem; - private readonly FileSystemMetadata _impl = null; + private readonly FileSystemMetadata _impl; public BdInfoDirectoryInfo(IFileSystem fileSystem, string path) { @@ -29,7 +29,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo public string FullName => _impl.FullName; - public IDirectoryInfo Parent + public IDirectoryInfo? Parent { get { @@ -71,11 +71,11 @@ namespace MediaBrowser.MediaEncoding.BdInfo _impl.FullName, new[] { searchPattern }, false, - searchOption.HasFlag(System.IO.SearchOption.AllDirectories)).ToArray(), + (searchOption & System.IO.SearchOption.AllDirectories) == System.IO.SearchOption.AllDirectories).ToArray(), x => new BdInfoFileInfo(x)); } - public static IDirectoryInfo FromFileSystemPath(Model.IO.IFileSystem fs, string path) + 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 index 9108d9649..6ebaa4fff 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -61,33 +61,25 @@ namespace MediaBrowser.MediaEncoding.BdInfo foreach (var stream in playlist.SortedStreams) { - var videoStream = stream as TSVideoStream; - - if (videoStream != null) + if (stream is TSVideoStream videoStream) { AddVideoStream(mediaStreams, videoStream); continue; } - var audioStream = stream as TSAudioStream; - - if (audioStream != null) + if (stream is TSAudioStream audioStream) { AddAudioStream(mediaStreams, audioStream); continue; } - var textStream = stream as TSTextStream; - - if (textStream != null) + if (stream is TSTextStream textStream) { AddSubtitleStream(mediaStreams, textStream); continue; } - var graphicsStream = stream as TSGraphicsStream; - - if (graphicsStream != null) + if (stream is TSGraphicsStream graphicsStream) { AddSubtitleStream(mediaStreams, graphicsStream); } diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs index 0a8af8e9c..d55688e3d 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo { public class BdInfoFileInfo : BDInfo.IO.IFileInfo { - private FileSystemMetadata _impl = null; + private FileSystemMetadata _impl; public BdInfoFileInfo(FileSystemMetadata impl) { @@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo public bool IsDir => _impl.IsDirectory; - public System.IO.Stream OpenRead() + public Stream OpenRead() { return new FileStream( FullName, @@ -33,9 +33,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo FileShare.Read); } - public System.IO.StreamReader OpenText() + public StreamReader OpenText() { - return new System.IO.StreamReader(OpenRead()); + return new StreamReader(OpenRead()); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 92f16ab95..60a2d39e5 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -12,8 +12,6 @@ namespace MediaBrowser.MediaEncoding.Encoder { public class EncoderValidator { - private const string DefaultEncoderPath = "ffmpeg"; - private static readonly string[] _requiredDecoders = new[] { "h264", @@ -89,6 +87,24 @@ namespace MediaBrowser.MediaEncoding.Encoder "hevc_videotoolbox" }; + private static readonly string[] _requiredFilters = new[] + { + "scale_cuda", + "yadif_cuda", + "hwupload_cuda", + "overlay_cuda", + "tonemap_cuda", + "tonemap_opencl", + "tonemap_vaapi", + }; + + private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]> + { + { 0, new string[] { "scale_cuda", "Output format (default \"same\")" } }, + { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } }, + { 2, new string[] { "tonemap_opencl", "bt2390" } } + }; + // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below private static readonly IReadOnlyDictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version> { @@ -106,7 +122,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly string _encoderPath; - public EncoderValidator(ILogger logger, string encoderPath = DefaultEncoderPath) + public EncoderValidator(ILogger logger, string encoderPath) { _logger = logger; _encoderPath = encoderPath; @@ -121,11 +137,11 @@ namespace MediaBrowser.MediaEncoding.Encoder // When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions public static Version MinVersion { get; } = new Version(4, 0); - public static Version MaxVersion { get; } = null; + public static Version? MaxVersion { get; } = null; public bool ValidateVersion() { - string output = null; + string output; try { output = GetProcessOutput(_encoderPath, "-version"); @@ -133,6 +149,7 @@ namespace MediaBrowser.MediaEncoding.Encoder catch (Exception ex) { _logger.LogError(ex, "Error validating encoder"); + return false; } if (string.IsNullOrWhiteSpace(output)) @@ -155,7 +172,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } // Work out what the version under test is - var version = GetFFmpegVersion(versionOutput); + var version = GetFFmpegVersionInternal(versionOutput); _logger.LogInformation("Found ffmpeg version {Version}", version != null ? version.ToString() : "unknown"); @@ -199,6 +216,34 @@ namespace MediaBrowser.MediaEncoding.Encoder public IEnumerable<string> GetHwaccels() => GetHwaccelTypes(); + public IEnumerable<string> GetFilters() => GetFFmpegFilters(); + + public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption(); + + public Version? GetFFmpegVersion() + { + string output; + try + { + output = GetProcessOutput(_encoderPath, "-version"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error validating encoder"); + return null; + } + + if (string.IsNullOrWhiteSpace(output)) + { + _logger.LogError("FFmpeg validation: The process returned no result"); + return null; + } + + _logger.LogDebug("ffmpeg output: {Output}", output); + + return GetFFmpegVersionInternal(output); + } + /// <summary> /// Using the output from "ffmpeg -version" work out the FFmpeg version. /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy @@ -207,7 +252,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="output">The output from "ffmpeg -version".</param> /// <returns>The FFmpeg version.</returns> - internal Version GetFFmpegVersion(string output) + internal Version? GetFFmpegVersionInternal(string output) { // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)"); @@ -275,7 +320,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private IEnumerable<string> GetHwaccelTypes() { - string output = null; + string? output = null; try { output = GetProcessOutput(_encoderPath, "-hwaccels"); @@ -296,10 +341,38 @@ namespace MediaBrowser.MediaEncoding.Encoder return found; } + public bool CheckFilterWithOption(string filter, string option) + { + if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option)) + { + return false; + } + + string output; + try + { + output = GetProcessOutput(_encoderPath, "-h filter=" + filter); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting the given filter"); + return false; + } + + if (output.Contains("Filter " + filter, StringComparison.Ordinal)) + { + return output.Contains(option, StringComparison.Ordinal); + } + + _logger.LogWarning("Filter: {Name} with option {Option} is not available", filter, option); + + return false; + } + private IEnumerable<string> GetCodecs(Codec codec) { string codecstr = codec == Codec.Encoder ? "encoders" : "decoders"; - string output = null; + string output; try { output = GetProcessOutput(_encoderPath, "-" + codecstr); @@ -307,6 +380,7 @@ namespace MediaBrowser.MediaEncoding.Encoder catch (Exception ex) { _logger.LogError(ex, "Error detecting available {Codec}", codecstr); + return Enumerable.Empty<string>(); } if (string.IsNullOrWhiteSpace(output)) @@ -327,6 +401,49 @@ namespace MediaBrowser.MediaEncoding.Encoder return found; } + private IEnumerable<string> GetFFmpegFilters() + { + string output; + try + { + output = GetProcessOutput(_encoderPath, "-filters"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting available filters"); + return Enumerable.Empty<string>(); + } + + if (string.IsNullOrWhiteSpace(output)) + { + return Enumerable.Empty<string>(); + } + + var found = Regex + .Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline) + .Cast<Match>() + .Select(x => x.Groups["filter"].Value) + .Where(x => _requiredFilters.Contains(x)); + + _logger.LogInformation("Available filters: {Filters}", found); + + return found; + } + + private IDictionary<int, bool> GetFFmpegFiltersWithOption() + { + IDictionary<int, bool> dict = new Dictionary<int, bool>(); + for (int i = 0; i < _filterOptionsDict.Count; i++) + { + if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2) + { + dict.Add(i, CheckFilterWithOption(val[0], val[1])); + } + } + + return dict; + } + private string GetProcessOutput(string path, string arguments) { using (var process = new Process() diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index fbd08a97c..1c97a1982 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; @@ -10,20 +11,20 @@ using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; +using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -44,16 +45,10 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> internal const int DefaultHdrImageExtractionTimeout = 20000; - /// <summary> - /// The us culture. - /// </summary> - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly ILogger<MediaEncoder> _logger; private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; - private readonly Lazy<EncodingHelper> _encodingHelperFactory; private readonly string _startupOptionFFmpegPath; private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); @@ -67,34 +62,32 @@ namespace MediaBrowser.MediaEncoding.Encoder private List<string> _encoders = new List<string>(); private List<string> _decoders = new List<string>(); private List<string> _hwaccels = new List<string>(); + private List<string> _filters = new List<string>(); + private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>(); + private Version _ffmpegVersion = null; private string _ffmpegPath = string.Empty; private string _ffprobePath; - private int threads; + private int _threads; public MediaEncoder( ILogger<MediaEncoder> logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, - Lazy<EncodingHelper> encodingHelperFactory, IConfiguration config) { _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; _localization = localization; - _encodingHelperFactory = encodingHelperFactory; _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty; - _jsonSerializerOptions = JsonDefaults.GetOptions(); + _jsonSerializerOptions = JsonDefaults.Options; } /// <inheritdoc /> public string EncoderPath => _ffmpegPath; - /// <inheritdoc /> - public FFmpegLocation EncoderLocation { get; private set; } - /// <summary> /// Run at startup or if the user removes a Custom path from transcode page. /// Sets global variables FFmpegPath. @@ -103,22 +96,25 @@ namespace MediaBrowser.MediaEncoding.Encoder public void SetFFmpegPath() { // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence - if (!ValidatePath(_configurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom)) + var ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath; + if (string.IsNullOrEmpty(ffmpegPath)) { // 2) Check if the --ffmpeg CLI switch has been given - if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument)) + ffmpegPath = _startupOptionFFmpegPath; + if (string.IsNullOrEmpty(ffmpegPath)) { - // 3) Search system $PATH environment variable for valid FFmpeg - if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System)) - { - EncoderLocation = FFmpegLocation.NotFound; - _ffmpegPath = null; - } + // 3) Check "ffmpeg" + ffmpegPath = "ffmpeg"; } } + if (!ValidatePath(ffmpegPath)) + { + _ffmpegPath = null; + } + // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI - var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding"); + var config = _configurationManager.GetEncodingOptions(); config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty; _configurationManager.SaveConfiguration("encoding", config); @@ -133,11 +129,15 @@ namespace MediaBrowser.MediaEncoding.Encoder SetAvailableDecoders(validator.GetDecoders()); SetAvailableEncoders(validator.GetEncoders()); + SetAvailableFilters(validator.GetFilters()); + SetAvailableFiltersWithOption(validator.GetFiltersWithOption()); SetAvailableHwaccels(validator.GetHwaccels()); - threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null); + SetMediaEncoderVersion(validator); + + _threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null); } - _logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty); + _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty); } /// <summary> @@ -148,6 +148,16 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <param name="pathType">The path type.</param> public void UpdateEncoderPath(string path, string pathType) { + var config = _configurationManager.GetEncodingOptions(); + + // Filesystem may not be case insensitive, but EncoderAppPathDisplay should always point to a valid file? + if (string.IsNullOrEmpty(config.EncoderAppPath) + && string.Equals(config.EncoderAppPathDisplay, path, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Existing ffmpeg path is empty and the new path is the same as {EncoderAppPathDisplay}. Skipping", nameof(config.EncoderAppPathDisplay)); + return; + } + string newPath; _logger.LogInformation("Attempting to update encoder path to {Path}. pathType: {PathType}", path ?? string.Empty, pathType ?? string.Empty); @@ -156,28 +166,32 @@ namespace MediaBrowser.MediaEncoding.Encoder { throw new ArgumentException("Unexpected pathType value"); } - else if (string.IsNullOrWhiteSpace(path)) + + if (string.IsNullOrWhiteSpace(path)) { // User had cleared the custom path in UI newPath = string.Empty; } - else if (File.Exists(path)) - { - newPath = path; - } - else if (Directory.Exists(path)) - { - // Given path is directory, so resolve down to filename - newPath = GetEncoderPathFromDirectory(path, "ffmpeg"); - } else { - throw new ResourceNotFoundException(); + if (Directory.Exists(path)) + { + // Given path is directory, so resolve down to filename + newPath = GetEncoderPathFromDirectory(path, "ffmpeg"); + } + else + { + newPath = path; + } + + if (!new EncoderValidator(_logger, newPath).ValidateVersion()) + { + throw new ResourceNotFoundException(); + } } // Write the new ffmpeg path to the xml as <EncoderAppPath> // This ensures its not lost on next startup - var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding"); config.EncoderAppPath = newPath; _configurationManager.SaveConfiguration("encoding", config); @@ -187,36 +201,26 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <summary> /// Validates the supplied FQPN to ensure it is a ffmpeg utility. - /// If checks pass, global variable FFmpegPath and EncoderLocation are updated. + /// If checks pass, global variable FFmpegPath is updated. /// </summary> /// <param name="path">FQPN to test.</param> - /// <param name="location">Location (External, Custom, System) of tool.</param> /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns> - private bool ValidatePath(string path, FFmpegLocation location) + private bool ValidatePath(string path) { - bool rc = false; - - if (!string.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) { - if (File.Exists(path)) - { - rc = new EncoderValidator(_logger, path).ValidateVersion(); - - if (!rc) - { - _logger.LogWarning("FFmpeg: {Location}: Failed version check: {Path}", location, path); - } + return false; + } - _ffmpegPath = path; - EncoderLocation = location; - } - else - { - _logger.LogWarning("FFmpeg: {Location}: File not found: {Path}", location, path); - } + bool rc = new EncoderValidator(_logger, path).ValidateVersion(); + if (!rc) + { + _logger.LogWarning("FFmpeg: Failed version check: {Path}", path); + return false; } - return rc; + _ffmpegPath = path; + return true; } private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false) @@ -237,34 +241,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - /// <summary> - /// Search the system $PATH environment variable looking for given filename. - /// </summary> - /// <param name="fileName">The filename.</param> - /// <returns>The full path to the file.</returns> - private string ExistsOnSystemPath(string fileName) - { - var inJellyfinPath = GetEncoderPathFromDirectory(AppContext.BaseDirectory, fileName, recursive: true); - if (!string.IsNullOrEmpty(inJellyfinPath)) - { - return inJellyfinPath; - } - - var values = Environment.GetEnvironmentVariable("PATH"); - - foreach (var path in values.Split(Path.PathSeparator)) - { - var candidatePath = GetEncoderPathFromDirectory(path, fileName); - - if (!string.IsNullOrEmpty(candidatePath)) - { - return candidatePath; - } - } - - return null; - } - public void SetAvailableEncoders(IEnumerable<string> list) { _encoders = list.ToList(); @@ -280,6 +256,21 @@ namespace MediaBrowser.MediaEncoding.Encoder _hwaccels = list.ToList(); } + public void SetAvailableFilters(IEnumerable<string> list) + { + _filters = list.ToList(); + } + + public void SetAvailableFiltersWithOption(IDictionary<int, bool> dict) + { + _filtersWithOption = dict; + } + + public void SetMediaEncoderVersion(EncoderValidator validator) + { + _ffmpegVersion = validator.GetFFmpegVersion(); + } + public bool SupportsEncoder(string encoder) { return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase); @@ -295,6 +286,26 @@ namespace MediaBrowser.MediaEncoding.Encoder return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase); } + public bool SupportsFilter(string filter) + { + return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase); + } + + public bool SupportsFilterWithOption(FilterOptionType option) + { + if (_filtersWithOption.TryGetValue((int)option, out var val)) + { + return val; + } + + return false; + } + + public Version GetMediaEncoderVersion() + { + return _ffmpegVersion; + } + public bool CanEncodeToAudioCodec(string codec) { if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase)) @@ -358,7 +369,8 @@ namespace MediaBrowser.MediaEncoding.Encoder public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) { var prefix = "file"; - if (mediaSource.VideoType == VideoType.BluRay || mediaSource.VideoType == VideoType.Iso) + if (mediaSource.VideoType == VideoType.BluRay + || mediaSource.IsoType == IsoType.BluRay) { prefix = "bluray"; } @@ -384,7 +396,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = extractChapters ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format" : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format"; - args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, threads).Trim(); + args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim(); var process = new Process { @@ -436,7 +448,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (result == null || (result.Streams == null && result.Format == null)) { - throw new Exception("ffprobe failed - streams and format are both null."); + throw new FfmpegException("ffprobe failed - streams and format are both null."); } if (result.Streams != null) @@ -467,17 +479,17 @@ namespace MediaBrowser.MediaEncoding.Encoder Protocol = MediaProtocol.File }; - return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken); + return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken); } 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, cancellationToken); + return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ImageFormat.Jpg, cancellationToken); } - public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken) + 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, cancellationToken); + return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken); } private async Task<string> ExtractImage( @@ -489,24 +501,17 @@ namespace MediaBrowser.MediaEncoding.Encoder bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, + ImageFormat? targetFormat, CancellationToken cancellationToken) { var inputArgument = GetInputArgument(inputFile, mediaSource); - if (isAudio) - { - if (imageStreamIndex.HasValue && imageStreamIndex.Value > 0) - { - // It seems for audio files we need to subtract 1 (for the audio stream??) - imageStreamIndex = imageStreamIndex.Value - 1; - } - } - else + if (!isAudio) { // The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter. try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, targetFormat, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -519,7 +524,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, targetFormat, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -532,7 +537,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, targetFormat, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -544,55 +549,51 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, targetFormat, cancellationToken).ConfigureAwait(false); } - private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, CancellationToken cancellationToken) + private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, ImageFormat? targetFormat, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) { throw new ArgumentNullException(nameof(inputPath)); } - var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); + var outputExtension = targetFormat switch + { + ImageFormat.Bmp => ".bmp", + ImageFormat.Gif => ".gif", + ImageFormat.Jpg => ".jpg", + ImageFormat.Png => ".png", + ImageFormat.Webp => ".webp", + _ => ".jpg" + }; + + var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar. // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar - var vf = string.Empty; - - if (threedFormat.HasValue) - { - switch (threedFormat.Value) - { - case Video3DFormat.HalfSideBySide: - vf = "-vf crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1"; - // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not. - break; - case Video3DFormat.FullSideBySide: - vf = "-vf crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1"; - // fsbs crop width in half,set the display aspect,crop out any black bars we may have made - break; - case Video3DFormat.HalfTopAndBottom: - vf = "-vf crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1"; - // htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made - break; - case Video3DFormat.FullTopAndBottom: - vf = "-vf crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1"; - // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made - break; - default: - break; - } - } + var vf = threedFormat switch + { + // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not. + Video3DFormat.HalfSideBySide => "-vf crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + // fsbs crop width in half,set the display aspect,crop out any black bars we may have made + Video3DFormat.FullSideBySide => "-vf crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + // htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made + Video3DFormat.HalfTopAndBottom => "-vf crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made + Video3DFormat.FullTopAndBottom => "-vf crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + _ => string.Empty + }; - var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; + var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; var enableHdrExtraction = allowTonemap && string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase); if (enableHdrExtraction) { string tonemapFilters = "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p"; - if (string.IsNullOrEmpty(vf)) + if (vf.Length == 0) { vf = "-vf " + tonemapFilters; } @@ -619,37 +620,13 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads); - - var probeSizeArgument = string.Empty; - var analyzeDurationArgument = string.Empty; - - if (!string.IsNullOrWhiteSpace(probeSizeArgument)) - { - args = probeSizeArgument + " " + args; - } - - if (!string.IsNullOrWhiteSpace(analyzeDurationArgument)) - { - args = analyzeDurationArgument + " " + args; - } + var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads); if (offset.HasValue) { args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args; } - if (videoStream != null) - { - /* fix - var decoder = encodinghelper.GetHardwareAcceleratedVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions()); - if (!string.IsNullOrWhiteSpace(decoder)) - { - args = decoder + " " + args; - } - */ - } - if (!string.IsNullOrWhiteSpace(container)) { var inputFormat = EncodingHelper.GetInputFormat(container); @@ -707,11 +684,9 @@ namespace MediaBrowser.MediaEncoding.Encoder if (exitCode == -1 || !file.Exists || file.Length == 0) { - var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath); - - _logger.LogError(msg); + _logger.LogError("ffmpeg image extraction failed for {Path}", inputPath); - throw new Exception(msg); + throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath)); } return tempExtractPath; @@ -727,142 +702,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public string GetTimeParameter(TimeSpan time) { - return time.ToString(@"hh\:mm\:ss\.fff", _usCulture); - } - - public async Task ExtractVideoImagesOnInterval( - string inputFile, - string container, - MediaStream videoStream, - MediaSourceInfo mediaSource, - Video3DFormat? threedFormat, - TimeSpan interval, - string targetDirectory, - string filenamePrefix, - int? maxWidth, - CancellationToken cancellationToken) - { - var inputArgument = GetInputArgument(inputFile, mediaSource); - - var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture); - - if (maxWidth.HasValue) - { - var maxWidthParam = maxWidth.Value.ToString(_usCulture); - - vf += string.Format(CultureInfo.InvariantCulture, ",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam); - } - - Directory.CreateDirectory(targetDirectory); - var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg"); - - var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads); - - var probeSizeArgument = string.Empty; - var analyzeDurationArgument = string.Empty; - - if (!string.IsNullOrWhiteSpace(probeSizeArgument)) - { - args = probeSizeArgument + " " + args; - } - - if (!string.IsNullOrWhiteSpace(analyzeDurationArgument)) - { - args = analyzeDurationArgument + " " + args; - } - - if (videoStream != null) - { - /* fix - var decoder = encodinghelper.GetHardwareAcceleratedVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions()); - if (!string.IsNullOrWhiteSpace(decoder)) - { - args = decoder + " " + args; - } - */ - } - - if (!string.IsNullOrWhiteSpace(container)) - { - var inputFormat = EncodingHelper.GetInputFormat(container); - if (!string.IsNullOrWhiteSpace(inputFormat)) - { - args = "-f " + inputFormat + " " + args; - } - } - - var processStartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - FileName = _ffmpegPath, - Arguments = args, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }; - - _logger.LogInformation(processStartInfo.FileName + " " + processStartInfo.Arguments); - - await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - bool ranToCompletion = false; - - var process = new Process - { - StartInfo = processStartInfo, - EnableRaisingEvents = true - }; - using (var processWrapper = new ProcessWrapper(process, this)) - { - try - { - StartProcess(processWrapper); - - // Need to give ffmpeg enough time to make all the thumbnails, which could be a while, - // but we still need to detect if the process hangs. - // Making the assumption that as long as new jpegs are showing up, everything is good. - - bool isResponsive = true; - int lastCount = 0; - - while (isResponsive) - { - if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false)) - { - ranToCompletion = true; - break; - } - - cancellationToken.ThrowIfCancellationRequested(); - - var jpegCount = _fileSystem.GetFilePaths(targetDirectory) - .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase)); - - isResponsive = jpegCount > lastCount; - lastCount = jpegCount; - } - - if (!ranToCompletion) - { - StopProcess(processWrapper, 1000); - } - } - finally - { - _thumbnailResourcePool.Release(); - } - - var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; - - if (exitCode == -1) - { - var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputArgument); - - _logger.LogError(msg); - - throw new Exception(msg); - } - } + return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture); } private void StartProcess(ProcessWrapper process) diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index f8af499e4..9f6d8e7fe 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -6,10 +6,9 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net5.0</TargetFramework> + <TargetFramework>net6.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> @@ -23,19 +22,15 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="BDInfo" Version="0.7.6.1" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> - <PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" /> - <PackageReference Include="UTF.Unknown" Version="2.3.0" /> + <PackageReference Include="BDInfo" Version="0.7.6.2" /> + <PackageReference Include="libse" Version="3.6.4" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> + <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> + <PackageReference Include="UTF.Unknown" Version="2.5.0" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.nuget.targets b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.nuget.targets deleted file mode 100644 index f793e09bc..000000000 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.nuget.targets +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="no"?> -<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <Target Name="EmitMSBuildWarning" BeforeTargets="Build"> - <Warning Text="Packages containing MSBuild targets and props files cannot be fully installed in projects targeting multiple frameworks. The MSBuild targets and props files have been ignored." /> - </Target> -</Project>
\ No newline at end of file diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index b2d4db894..a9e753726 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace MediaBrowser.MediaEncoding.Probing { @@ -19,7 +20,7 @@ namespace MediaBrowser.MediaEncoding.Probing throw new ArgumentNullException(nameof(result)); } - if (result.Format != null && result.Format.Tags != null) + if (result.Format?.Tags != null) { result.Format.Tags = ConvertDictionaryToCaseInsensitive(result.Format.Tags); } @@ -38,38 +39,16 @@ namespace MediaBrowser.MediaEncoding.Probing } /// <summary> - /// Gets a string from an FFProbeResult tags dictionary. - /// </summary> - /// <param name="tags">The tags.</param> - /// <param name="key">The key.</param> - /// <returns>System.String.</returns> - public static string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key) - { - if (tags == null) - { - return null; - } - - tags.TryGetValue(key, out var val); - return val; - } - - /// <summary> /// Gets an int from an FFProbeResult tags dictionary. /// </summary> /// <param name="tags">The tags.</param> /// <param name="key">The key.</param> /// <returns>System.Nullable{System.Int32}.</returns> - public static int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key) + public static int? GetDictionaryNumericValue(IReadOnlyDictionary<string, string> tags, string key) { - var val = GetDictionaryValue(tags, key); - - if (!string.IsNullOrEmpty(val)) + if (tags.TryGetValue(key, out var val) && int.TryParse(val, out var i)) { - if (int.TryParse(val, out var i)) - { - return i; - } + return i; } return null; @@ -81,16 +60,13 @@ namespace MediaBrowser.MediaEncoding.Probing /// <param name="tags">The tags.</param> /// <param name="key">The key.</param> /// <returns>System.Nullable{DateTime}.</returns> - public static DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key) + public static DateTime? GetDictionaryDateTime(IReadOnlyDictionary<string, string> tags, string key) { - var val = GetDictionaryValue(tags, key); - - if (!string.IsNullOrEmpty(val)) + if (tags.TryGetValue(key, out var val) + && (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTime) + || DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out dateTime))) { - if (DateTime.TryParse(val, out var i)) - { - return i.ToUniversalTime(); - } + return dateTime; } return null; diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs index 0e319c1a8..d4d153b08 100644 --- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs +++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs index de062d06b..a1cef7a9f 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs index 8af122ef9..d50da37b8 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index 8a7c032c5..c9c8c34c2 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -1,6 +1,7 @@ +#nullable disable + using System.Collections.Generic; using System.Text.Json.Serialization; -using MediaBrowser.Common.Json.Converters; namespace MediaBrowser.MediaEncoding.Probing { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index bd026bce1..9057a101a 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; @@ -6,7 +7,9 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Xml; +using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -25,11 +28,12 @@ namespace MediaBrowser.MediaEncoding.Probing private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' }; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private static readonly Regex _performerPattern = new (@"(?<name>.*) \((?<instrument>.*)\)"); + private readonly ILogger _logger; private readonly ILocalizationManager _localization; - private List<string> _splitWhiteList = null; + private string[] _splitWhiteList; public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization) { @@ -37,6 +41,19 @@ namespace MediaBrowser.MediaEncoding.Probing _localization = localization; } + private IReadOnlyList<string> SplitWhitelist => _splitWhiteList ??= new string[] + { + "AC/DC", + "Au/Ra", + "Bremer/McCoy", + "이달의 소녀 1/3", + "LOONA 1/3", + "LOONA / yyxy", + "LOONA / ODD EYE CIRCLE", + "K/DA", + "22/7" + }; + public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol) { var info = new MediaInfo @@ -57,7 +74,7 @@ namespace MediaBrowser.MediaEncoding.Probing .Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec)) .ToList(); - info.MediaAttachments = internalStreams.Select(s => GetMediaAttachment(s)) + info.MediaAttachments = internalStreams.Select(GetMediaAttachment) .Where(i => i != null) .ToList(); @@ -67,7 +84,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (!string.IsNullOrEmpty(data.Format.BitRate)) { - if (int.TryParse(data.Format.BitRate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(data.Format.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { info.Bitrate = value; } @@ -77,62 +94,66 @@ namespace MediaBrowser.MediaEncoding.Probing var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var tagStreamType = isAudio ? "audio" : "video"; - if (data.Streams != null) - { - var tagStream = data.Streams.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase)); + var tagStream = data.Streams?.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase)); - if (tagStream != null && tagStream.Tags != null) + if (tagStream?.Tags != null) + { + foreach (var (key, value) in tagStream.Tags) { - foreach (var pair in tagStream.Tags) - { - tags[pair.Key] = pair.Value; - } + tags[key] = value; } } - if (data.Format != null && data.Format.Tags != null) + if (data.Format?.Tags != null) { - foreach (var pair in data.Format.Tags) + foreach (var (key, value) in data.Format.Tags) { - tags[pair.Key] = pair.Value; + tags[key] = value; } } FetchGenres(info, tags); - var overview = FFProbeHelpers.GetDictionaryValue(tags, "synopsis"); - if (string.IsNullOrWhiteSpace(overview)) - { - overview = FFProbeHelpers.GetDictionaryValue(tags, "description"); - } - - if (string.IsNullOrWhiteSpace(overview)) - { - overview = FFProbeHelpers.GetDictionaryValue(tags, "desc"); - } - - if (!string.IsNullOrWhiteSpace(overview)) - { - info.Overview = overview; - } - - var title = FFProbeHelpers.GetDictionaryValue(tags, "title"); - if (!string.IsNullOrWhiteSpace(title)) - { - info.Name = title; - } + info.Name = tags.GetFirstNotNullNorWhiteSpaceValue("title", "title-eng"); + info.ForcedSortName = tags.GetFirstNotNullNorWhiteSpaceValue("sort_name", "title-sort", "titlesort"); + info.Overview = tags.GetFirstNotNullNorWhiteSpaceValue("synopsis", "description", "desc"); info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort"); info.ParentIndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "season_number"); - info.ShowName = FFProbeHelpers.GetDictionaryValue(tags, "show_name"); + info.ShowName = tags.GetValueOrDefault("show_name"); info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date"); - // Several different forms of retaildate - info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? + // Several different forms of retail/premiere date + info.PremiereDate = + FFProbeHelpers.GetDictionaryDateTime(tags, "originaldate") ?? + FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ?? + FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "date"); + // Set common metadata for music (audio) and music videos (video) + info.Album = tags.GetValueOrDefault("album"); + + if (tags.TryGetValue("artists", out var artists) && !string.IsNullOrWhiteSpace(artists)) + { + info.Artists = SplitDistinctArtists(artists, new[] { '/', ';' }, false).ToArray(); + } + else + { + var artist = tags.GetFirstNotNullNorWhiteSpaceValue("artist"); + info.Artists = artist == null + ? Array.Empty<string>() + : SplitDistinctArtists(artist, _nameDelimiters, true).ToArray(); + } + + // Guess ProductionYear from PremiereDate if missing + if (!info.ProductionYear.HasValue && info.PremiereDate.HasValue) + { + info.ProductionYear = info.PremiereDate.Value.Year; + } + + // Set mediaType-specific metadata if (isAudio) { SetAudioRuntimeTicks(data, info); @@ -146,10 +167,10 @@ namespace MediaBrowser.MediaEncoding.Probing { FetchStudios(info, tags, "copyright"); - var iTunEXTC = FFProbeHelpers.GetDictionaryValue(tags, "iTunEXTC"); - if (!string.IsNullOrWhiteSpace(iTunEXTC)) + var iTunExtc = tags.GetFirstNotNullNorWhiteSpaceValue("iTunEXTC"); + if (iTunExtc != null) { - var parts = iTunEXTC.Split('|', StringSplitOptions.RemoveEmptyEntries); + var parts = iTunExtc.Split('|', StringSplitOptions.RemoveEmptyEntries); // Example // mpaa|G|100|For crude humor if (parts.Length > 1) @@ -163,15 +184,15 @@ namespace MediaBrowser.MediaEncoding.Probing } } - var itunesXml = FFProbeHelpers.GetDictionaryValue(tags, "iTunMOVI"); - if (!string.IsNullOrWhiteSpace(itunesXml)) + var iTunXml = tags.GetFirstNotNullNorWhiteSpaceValue("iTunMOVI"); + if (iTunXml != null) { - FetchFromItunesInfo(itunesXml, info); + FetchFromItunesInfo(iTunXml, info); } if (data.Format != null && !string.IsNullOrEmpty(data.Format.Duration)) { - info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, _usCulture)).Ticks; + info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, CultureInfo.InvariantCulture)).Ticks; } FetchWtvInfo(info, data); @@ -183,8 +204,7 @@ namespace MediaBrowser.MediaEncoding.Probing ExtractTimestamp(info); - var stereoMode = GetDictionaryValue(tags, "stereo_mode"); - if (string.Equals(stereoMode, "left_right", StringComparison.OrdinalIgnoreCase)) + if (tags.TryGetValue("stereo_mode", out var stereoMode) && string.Equals(stereoMode, "left_right", StringComparison.OrdinalIgnoreCase)) { info.Video3DFormat = Video3DFormat.FullSideBySide; } @@ -237,42 +257,36 @@ namespace MediaBrowser.MediaEncoding.Probing if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) { - if (channelsValue <= 2) - { - return 192000; - } - - if (channelsValue >= 5) + switch (channelsValue) { - return 320000; + case <= 2: + return 192000; + case >= 5: + return 320000; } } if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase)) { - if (channelsValue <= 2) - { - return 192000; - } - - if (channelsValue >= 5) + switch (channelsValue) { - return 640000; + case <= 2: + return 192000; + case >= 5: + return 640000; } } if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase)) { - if (channelsValue <= 2) - { - return 960000; - } - - if (channelsValue >= 5) + switch (channelsValue) { - return 2880000; + case <= 2: + return 960000; + case >= 5: + return 2880000; } } @@ -569,7 +583,8 @@ namespace MediaBrowser.MediaEncoding.Probing /// <returns>MediaAttachments.</returns> private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo) { - if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase) + && streamInfo.Disposition?.GetValueOrDefault("attached_pic") != 1) { return null; } @@ -634,13 +649,8 @@ namespace MediaBrowser.MediaEncoding.Probing stream.IsAVC = false; } - if (!string.IsNullOrWhiteSpace(streamInfo.FieldOrder) && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase)) - { - stream.IsInterlaced = true; - } - // Filter out junk - if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && streamInfo.CodecTagString.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1) + if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && !streamInfo.CodecTagString.Contains("[0]", StringComparison.OrdinalIgnoreCase)) { stream.CodecTag = streamInfo.CodecTagString; } @@ -660,7 +670,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (!string.IsNullOrEmpty(streamInfo.SampleRate)) { - if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { stream.SampleRate = value; } @@ -676,26 +686,61 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.BitDepth = streamInfo.BitsPerRawSample; } + + if (string.IsNullOrEmpty(stream.Title)) + { + // mp4 missing track title workaround: fall back to handler_name if populated + string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name"); + if (!string.IsNullOrEmpty(handlerName)) + { + stream.Title = handlerName; + } + } } else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Subtitle; stream.Codec = NormalizeSubtitleCodec(stream.Codec); - stream.localizedUndefined = _localization.GetLocalizedString("Undefined"); - stream.localizedDefault = _localization.GetLocalizedString("Default"); - stream.localizedForced = _localization.GetLocalizedString("Forced"); + stream.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); + stream.LocalizedDefault = _localization.GetLocalizedString("Default"); + stream.LocalizedForced = _localization.GetLocalizedString("Forced"); + + if (string.IsNullOrEmpty(stream.Title)) + { + // mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler" + string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name"); + if (!string.IsNullOrEmpty(handlerName) && !string.Equals(handlerName, "SubtitleHandler", StringComparison.OrdinalIgnoreCase)) + { + stream.Title = handlerName; + } + } } else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)) { - stream.Type = isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase) - ? MediaStreamType.EmbeddedImage - : MediaStreamType.Video; - stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate); stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate); - if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || - string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)) + // Some interlaced H.264 files in mp4 containers using MBAFF coding aren't flagged as being interlaced by FFprobe, + // so for H.264 files we also calculate the frame rate from the codec time base and check if it is double the reported + // frame rate (both rounded to the nearest integer) to determine if the file is interlaced + int roundedTimeBaseFPS = Convert.ToInt32(1 / GetFrameRate(stream.CodecTimeBase) ?? 0); + int roundedDoubleFrameRate = Convert.ToInt32(stream.AverageFrameRate * 2 ?? 0); + + bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder) + && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase); + bool h264MbaffCoded = string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase) + && string.IsNullOrWhiteSpace(streamInfo.FieldOrder) + && roundedTimeBaseFPS == roundedDoubleFrameRate; + + if (videoInterlaced || h264MbaffCoded) + { + stream.IsInterlaced = true; + } + + if (isAudio + || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.EmbeddedImage; } @@ -730,6 +775,23 @@ namespace MediaBrowser.MediaEncoding.Probing stream.BitDepth = streamInfo.BitsPerRawSample; } + if (!stream.BitDepth.HasValue) + { + if (!string.IsNullOrEmpty(streamInfo.PixelFormat) + && streamInfo.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase)) + { + stream.BitDepth = 10; + } + + if (!string.IsNullOrEmpty(streamInfo.Profile) + && (streamInfo.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) + || streamInfo.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase) + || streamInfo.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase))) + { + stream.BitDepth = 10; + } + } + // stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) || // string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) || // string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase); @@ -772,7 +834,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (!string.IsNullOrEmpty(streamInfo.BitRate)) { - if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { bitrate = value; } @@ -785,7 +847,7 @@ namespace MediaBrowser.MediaEncoding.Probing && (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio))) { // If the stream info doesn't have a bitrate get the value from the media format info - if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { bitrate = value; } @@ -802,7 +864,7 @@ namespace MediaBrowser.MediaEncoding.Probing || string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))) { var bps = GetBPSFromTags(streamInfo); - if (bps != null && bps > 0) + if (bps > 0) { stream.BitRate = bps; } @@ -871,6 +933,7 @@ namespace MediaBrowser.MediaEncoding.Probing } tags.TryGetValue(key, out var val); + return val; } @@ -878,7 +941,7 @@ namespace MediaBrowser.MediaEncoding.Probing { if (string.IsNullOrEmpty(input)) { - return input; + return null; } return input.Split('(').FirstOrDefault(); @@ -890,8 +953,8 @@ namespace MediaBrowser.MediaEncoding.Probing var parts = (original ?? string.Empty).Split(':'); if (!(parts.Length == 2 && - int.TryParse(parts[0], NumberStyles.Any, _usCulture, out var width) && - int.TryParse(parts[1], NumberStyles.Any, _usCulture, out var height) && + int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width) && + int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height) && width > 0 && height > 0)) { @@ -964,66 +1027,71 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <param name="value">The value.</param> /// <returns>System.Nullable{System.Single}.</returns> - private float? GetFrameRate(string value) + internal static float? GetFrameRate(ReadOnlySpan<char> value) { - if (!string.IsNullOrEmpty(value)) + if (value.IsEmpty) { - var parts = value.Split('/'); - - float result; + return null; + } - if (parts.Length == 2) - { - result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture); - } - else + int index = value.IndexOf('/'); + if (index == -1) + { + // REVIEW: is this branch actually required? (i.e. does ffprobe ever output something other than a fraction?) + if (float.TryParse(value, NumberStyles.AllowThousands | NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { - result = float.Parse(parts[0], _usCulture); + return result; } - return float.IsNaN(result) ? (float?)null : result; + return null; } - return null; + if (!float.TryParse(value[..index], NumberStyles.Integer, CultureInfo.InvariantCulture, out var dividend) + || !float.TryParse(value[(index + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var divisor)) + { + return null; + } + + return divisor == 0f ? null : dividend / divisor; } private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data) { - if (result.Streams != null) + // Get the first info stream + var stream = result.Streams?.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase)); + if (stream == null) { - // Get the first info stream - var stream = result.Streams.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase)); + return; + } - if (stream != null) - { - // Get duration from stream properties - var duration = stream.Duration; + // Get duration from stream properties + var duration = stream.Duration; - // If it's not there go into format properties - if (string.IsNullOrEmpty(duration)) - { - duration = result.Format.Duration; - } + // If it's not there go into format properties + if (string.IsNullOrEmpty(duration)) + { + duration = result.Format.Duration; + } - // If we got something, parse it - if (!string.IsNullOrEmpty(duration)) - { - data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks; - } - } + // If we got something, parse it + if (!string.IsNullOrEmpty(duration)) + { + data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, CultureInfo.InvariantCulture)).Ticks; } } private int? GetBPSFromTags(MediaStreamInfo streamInfo) { - if (streamInfo != null && streamInfo.Tags != null) + if (streamInfo?.Tags == null) { - var bps = GetDictionaryValue(streamInfo.Tags, "BPS-eng") ?? GetDictionaryValue(streamInfo.Tags, "BPS"); - if (!string.IsNullOrEmpty(bps) - && int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps)) - { - return parsedBps; - } + return null; + } + + var bps = GetDictionaryValue(streamInfo.Tags, "BPS-eng") ?? GetDictionaryValue(streamInfo.Tags, "BPS"); + if (!string.IsNullOrEmpty(bps) + && int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps)) + { + return parsedBps; } return null; @@ -1031,13 +1099,15 @@ namespace MediaBrowser.MediaEncoding.Probing private double? GetRuntimeSecondsFromTags(MediaStreamInfo streamInfo) { - if (streamInfo != null && streamInfo.Tags != null) + if (streamInfo?.Tags == null) { - var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION"); - if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration)) - { - return parsedDuration.TotalSeconds; - } + return null; + } + + var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION"); + if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration)) + { + return parsedDuration.TotalSeconds; } return null; @@ -1045,14 +1115,17 @@ namespace MediaBrowser.MediaEncoding.Probing private long? GetNumberOfBytesFromTags(MediaStreamInfo streamInfo) { - if (streamInfo != null && streamInfo.Tags != null) + if (streamInfo?.Tags == null) { - var numberOfBytes = GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES-eng") ?? GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES"); - if (!string.IsNullOrEmpty(numberOfBytes) - && long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes)) - { - return parsedBytes; - } + return null; + } + + var numberOfBytes = GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES-eng") + ?? GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES"); + if (!string.IsNullOrEmpty(numberOfBytes) + && long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes)) + { + return parsedBytes; } return null; @@ -1060,124 +1133,120 @@ namespace MediaBrowser.MediaEncoding.Probing private void SetSize(InternalMediaInfoResult data, MediaInfo info) { - if (data.Format != null) + if (data.Format == null) { - if (!string.IsNullOrEmpty(data.Format.Size)) - { - info.Size = long.Parse(data.Format.Size, _usCulture); - } - else - { - info.Size = null; - } + return; } + + info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, CultureInfo.InvariantCulture); } - private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags) + private void SetAudioInfoFromTags(MediaInfo audio, IReadOnlyDictionary<string, string> tags) { - var peoples = new List<BaseItemPerson>(); - var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer"); - if (!string.IsNullOrWhiteSpace(composer)) + var people = new List<BaseItemPerson>(); + if (tags.TryGetValue("composer", out var composer) && !string.IsNullOrWhiteSpace(composer)) { foreach (var person in Split(composer, false)) { - peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer }); + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer }); } } - var conductor = FFProbeHelpers.GetDictionaryValue(tags, "conductor"); - if (!string.IsNullOrWhiteSpace(conductor)) + if (tags.TryGetValue("conductor", out var conductor) && !string.IsNullOrWhiteSpace(conductor)) { foreach (var person in Split(conductor, false)) { - peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor }); + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor }); } } - var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist"); - if (!string.IsNullOrWhiteSpace(lyricist)) + if (tags.TryGetValue("lyricist", out var lyricist) && !string.IsNullOrWhiteSpace(lyricist)) { foreach (var person in Split(lyricist, false)) { - peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist }); + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist }); } } - // Check for writer some music is tagged that way as alternative to composer/lyricist - var writer = FFProbeHelpers.GetDictionaryValue(tags, "writer"); - - if (!string.IsNullOrWhiteSpace(writer)) + if (tags.TryGetValue("performer", out var performer) && !string.IsNullOrWhiteSpace(performer)) { - foreach (var person in Split(writer, false)) + foreach (var person in Split(performer, false)) { - peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer }); + Match match = _performerPattern.Match(person); + + // If the performer doesn't have any instrument/role associated, it won't match. In that case, chances are it's simply a band name, so we skip it. + if (match.Success) + { + people.Add(new BaseItemPerson + { + Name = match.Groups["name"].Value, + Type = PersonType.Actor, + Role = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value) + }); + } } } - audio.People = peoples.ToArray(); - audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album"); - - var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists"); - - if (!string.IsNullOrWhiteSpace(artists)) + // In cases where there isn't sufficient information as to which role a writer performed on a recording, tagging software uses the "writer" tag. + if (tags.TryGetValue("writer", out var writer) && !string.IsNullOrWhiteSpace(writer)) { - audio.Artists = SplitArtists(artists, new[] { '/', ';' }, false) - .DistinctNames() - .ToArray(); - } - else - { - var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist"); - if (string.IsNullOrWhiteSpace(artist)) - { - audio.Artists = Array.Empty<string>(); - } - else + foreach (var person in Split(writer, false)) { - audio.Artists = SplitArtists(artist, _nameDelimiters, true) - .DistinctNames() - .ToArray(); + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer }); } } - var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist"); - if (string.IsNullOrWhiteSpace(albumArtist)) + if (tags.TryGetValue("arranger", out var arranger) && !string.IsNullOrWhiteSpace(arranger)) { - albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album artist"); + foreach (var person in Split(arranger, false)) + { + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Arranger }); + } } - if (string.IsNullOrWhiteSpace(albumArtist)) + if (tags.TryGetValue("engineer", out var engineer) && !string.IsNullOrWhiteSpace(engineer)) { - albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album_artist"); + foreach (var person in Split(engineer, false)) + { + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Engineer }); + } } - if (string.IsNullOrWhiteSpace(albumArtist)) + if (tags.TryGetValue("mixer", out var mixer) && !string.IsNullOrWhiteSpace(mixer)) { - audio.AlbumArtists = Array.Empty<string>(); + foreach (var person in Split(mixer, false)) + { + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Mixer }); + } } - else + + if (tags.TryGetValue("remixer", out var remixer) && !string.IsNullOrWhiteSpace(remixer)) { - audio.AlbumArtists = SplitArtists(albumArtist, _nameDelimiters, true) - .DistinctNames() - .ToArray(); + foreach (var person in Split(remixer, false)) + { + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Remixer }); + } } + audio.People = people.ToArray(); + + // Set album artist + var albumArtist = tags.GetFirstNotNullNorWhiteSpaceValue("albumartist", "album artist", "album_artist"); + audio.AlbumArtists = albumArtist != null + ? SplitDistinctArtists(albumArtist, _nameDelimiters, true).ToArray() + : Array.Empty<string>(); + + // Set album artist to artist if empty if (audio.AlbumArtists.Length == 0) { audio.AlbumArtists = audio.Artists; } // Track number - audio.IndexNumber = GetDictionaryDiscValue(tags, "track"); + audio.IndexNumber = GetDictionaryTrackOrDiscNumber(tags, "track"); // Disc number - audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc"); - - // If we don't have a ProductionYear try and get it from PremiereDate - if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue) - { - audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year; - } + audio.ParentIndexNumber = GetDictionaryTrackOrDiscNumber(tags, "disc"); // There's several values in tags may or may not be present FetchStudios(audio, tags, "organization"); @@ -1185,45 +1254,25 @@ namespace MediaBrowser.MediaEncoding.Probing FetchStudios(audio, tags, "publisher"); FetchStudios(audio, tags, "label"); - // These support mulitple values, but for now we only store the first. - var mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMARTISTID")); - } - + // These support multiple values, but for now we only store the first. + var mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Artist Id")) + ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMARTISTID")); audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb); - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ARTISTID")); - } - + mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Artist Id")) + ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ARTISTID")); audio.SetProviderId(MetadataProvider.MusicBrainzArtist, mb); - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMID")); - } - + mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Id")) + ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMID")); audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, mb); - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASEGROUPID")); - } - + mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Group Id")) + ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASEGROUPID")); audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb); - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASETRACKID")); - } - + mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Track Id")) + ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASETRACKID")); audio.SetProviderId(MetadataProvider.MusicBrainzTrack, mb); } @@ -1247,18 +1296,18 @@ namespace MediaBrowser.MediaEncoding.Probing /// <returns>System.String[][].</returns> private IEnumerable<string> Split(string val, bool allowCommaDelimiter) { - // Only use the comma as a delimeter if there are no slashes or pipes. + // Only use the comma as a delimiter if there are no slashes or pipes. // We want to be careful not to split names that have commas in them - var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i, StringComparison.Ordinal) != -1) ? + var delimiter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i, StringComparison.Ordinal) != -1) ? _nameDelimiters : new[] { ',' }; - return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries) + return val.Split(delimiter, StringSplitOptions.RemoveEmptyEntries) .Where(i => !string.IsNullOrWhiteSpace(i)) .Select(i => i.Trim()); } - private IEnumerable<string> SplitArtists(string val, char[] delimiters, bool splitFeaturing) + private IEnumerable<string> SplitDistinctArtists(string val, char[] delimiters, bool splitFeaturing) { if (splitFeaturing) { @@ -1268,7 +1317,7 @@ namespace MediaBrowser.MediaEncoding.Probing var artistsFound = new List<string>(); - foreach (var whitelistArtist in GetSplitWhitelist()) + foreach (var whitelistArtist in SplitWhitelist) { var originalVal = val; val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase); @@ -1284,20 +1333,7 @@ namespace MediaBrowser.MediaEncoding.Probing .Select(i => i.Trim()); artistsFound.AddRange(artists); - return artistsFound; - } - - private IEnumerable<string> GetSplitWhitelist() - { - if (_splitWhiteList == null) - { - _splitWhiteList = new List<string> - { - "AC/DC" - }; - } - - return _splitWhiteList; + return artistsFound.DistinctNames(); } /// <summary> @@ -1306,36 +1342,38 @@ namespace MediaBrowser.MediaEncoding.Probing /// <param name="info">The info.</param> /// <param name="tags">The tags.</param> /// <param name="tagName">Name of the tag.</param> - private void FetchStudios(MediaInfo info, Dictionary<string, string> tags, string tagName) + private void FetchStudios(MediaInfo info, IReadOnlyDictionary<string, string> tags, string tagName) { - var val = FFProbeHelpers.GetDictionaryValue(tags, tagName); + var val = tags.GetValueOrDefault(tagName); - if (!string.IsNullOrEmpty(val)) + if (string.IsNullOrEmpty(val)) { - var studios = Split(val, true); - var studioList = new List<string>(); + return; + } - foreach (var studio in studios) - { - // Sometimes the artist name is listed here, account for that - if (info.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase)) - { - continue; - } + var studios = Split(val, true); + var studioList = new List<string>(); - if (info.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase)) - { - continue; - } + foreach (var studio in studios) + { + if (string.IsNullOrWhiteSpace(studio)) + { + continue; + } - studioList.Add(studio); + // Don't add artist/album artist name to studios, even if it's listed there + if (info.Artists.Contains(studio, StringComparison.OrdinalIgnoreCase) + || info.AlbumArtists.Contains(studio, StringComparison.OrdinalIgnoreCase)) + { + continue; } - info.Studios = studioList - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); + studioList.Add(studio); } + + info.Studios = studioList + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); } /// <summary> @@ -1343,58 +1381,55 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <param name="info">The information.</param> /// <param name="tags">The tags.</param> - private void FetchGenres(MediaInfo info, Dictionary<string, string> tags) + private void FetchGenres(MediaInfo info, IReadOnlyDictionary<string, string> tags) { - var val = FFProbeHelpers.GetDictionaryValue(tags, "genre"); + var genreVal = tags.GetValueOrDefault("genre"); + if (string.IsNullOrEmpty(genreVal)) + { + return; + } - if (!string.IsNullOrEmpty(val)) + var genres = new List<string>(info.Genres); + foreach (var genre in Split(genreVal, true)) { - var genres = new List<string>(info.Genres); - foreach (var genre in Split(val, true)) + if (string.IsNullOrWhiteSpace(genre)) { - genres.Add(genre); + continue; } - info.Genres = genres - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); + genres.Add(genre); } + + info.Genres = genres + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); } /// <summary> - /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'. + /// Gets the track or disc number, which can be in the form of '1', or '1/3'. /// </summary> /// <param name="tags">The tags.</param> /// <param name="tagName">Name of the tag.</param> - /// <returns>System.Nullable{System.Int32}.</returns> - private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName) + /// <returns>The track or disc number, or null, if missing or not parseable.</returns> + private static int? GetDictionaryTrackOrDiscNumber(IReadOnlyDictionary<string, string> tags, string tagName) { - var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName); + var disc = tags.GetValueOrDefault(tagName); - if (!string.IsNullOrEmpty(disc)) + if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.AsSpan().LeftPart('/'), out var discNum)) { - disc = disc.Split('/')[0]; - - if (int.TryParse(disc, out var num)) - { - return num; - } + return discNum; } return null; } - private ChapterInfo GetChapterInfo(MediaChapter chapter) + private static ChapterInfo GetChapterInfo(MediaChapter chapter) { var info = new ChapterInfo(); - if (chapter.Tags != null) + if (chapter.Tags != null && chapter.Tags.TryGetValue("title", out string name)) { - if (chapter.Tags.TryGetValue("title", out string name)) - { - info.Name = name; - } + info.Name = name; } // Limit accuracy to milliseconds to match xml saving @@ -1411,14 +1446,14 @@ namespace MediaBrowser.MediaEncoding.Probing private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data) { - if (data.Format == null || data.Format.Tags == null) + var tags = data.Format?.Tags; + + if (tags == null) { return; } - var genres = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/Genre"); - - if (!string.IsNullOrWhiteSpace(genres)) + if (tags.TryGetValue("WM/Genre", out var genres) && !string.IsNullOrWhiteSpace(genres)) { var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries) .Where(i => !string.IsNullOrWhiteSpace(i)) @@ -1432,16 +1467,12 @@ namespace MediaBrowser.MediaEncoding.Probing } } - var officialRating = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/ParentalRating"); - - if (!string.IsNullOrWhiteSpace(officialRating)) + if (tags.TryGetValue("WM/ParentalRating", out var officialRating) && !string.IsNullOrWhiteSpace(officialRating)) { video.OfficialRating = officialRating; } - var people = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaCredits"); - - if (!string.IsNullOrEmpty(people)) + if (tags.TryGetValue("WM/MediaCredits", out var people) && !string.IsNullOrEmpty(people)) { video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries) .Where(i => !string.IsNullOrWhiteSpace(i)) @@ -1449,29 +1480,21 @@ namespace MediaBrowser.MediaEncoding.Probing .ToArray(); } - var year = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/OriginalReleaseTime"); - if (!string.IsNullOrWhiteSpace(year)) + if (tags.TryGetValue("WM/OriginalReleaseTime", out var year) && int.TryParse(year, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear)) { - if (int.TryParse(year, NumberStyles.Integer, _usCulture, out var val)) - { - video.ProductionYear = val; - } + video.ProductionYear = parsedYear; } - var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaOriginalBroadcastDateTime"); - if (!string.IsNullOrWhiteSpace(premiereDateString)) + // Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ + // DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None) + if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, null, DateTimeStyles.AdjustToUniversal, out var parsedDate)) { - // Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ - // DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None) - if (DateTime.TryParse(year, null, DateTimeStyles.None, out var val)) - { - video.PremiereDate = val.ToUniversalTime(); - } + video.PremiereDate = parsedDate; } - var description = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitleDescription"); + var description = tags.GetValueOrDefault("WM/SubTitleDescription"); - var subTitle = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitle"); + var subTitle = tags.GetValueOrDefault("WM/SubTitle"); // For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ @@ -1482,37 +1505,48 @@ namespace MediaBrowser.MediaEncoding.Probing // e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S] if (string.IsNullOrWhiteSpace(subTitle) && !string.IsNullOrWhiteSpace(description) - && description.AsSpan().Slice(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename + && description.AsSpan()[..Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)].Contains(':')) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename { - string[] parts = description.Split(':'); - if (parts.Length > 0) + string[] descriptionParts = description.Split(':'); + if (descriptionParts.Length > 0) { - string subtitle = parts[0]; + string subtitle = descriptionParts[0]; try { - if (subtitle.Contains('/', StringComparison.Ordinal)) // It contains a episode number and season number + // Check if it contains a episode number and season number + if (subtitle.Contains('/', StringComparison.Ordinal)) { - string[] numbers = subtitle.Split(' '); - video.IndexNumber = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[0], CultureInfo.InvariantCulture); - int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[1], CultureInfo.InvariantCulture); + string[] subtitleParts = subtitle.Split(' '); + string[] numbers = subtitleParts[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/'); + video.IndexNumber = int.Parse(numbers[0], CultureInfo.InvariantCulture); + // int totalEpisodesInSeason = int.Parse(numbers[1], CultureInfo.InvariantCulture); - description = string.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it + // Skip the numbers, concatenate the rest, trim and set as new description + description = string.Join(' ', subtitleParts, 1, subtitleParts.Length - 1).Trim(); + } + else if (subtitle.Contains('.', StringComparison.Ordinal)) + { + var subtitleParts = subtitle.Split('.'); + description = string.Join('.', subtitleParts, 1, subtitleParts.Length - 1).Trim(); } else { - throw new Exception(); // Switch to default parsing + description = subtitle.Trim(); } } - catch // Default parsing + catch (Exception ex) { + _logger.LogError(ex, "Error while parsing subtitle field"); + + // Fallback to default parsing if (subtitle.Contains('.', StringComparison.Ordinal)) { - // skip the comment, keep the subtitle - description = string.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first + var subtitleParts = subtitle.Split('.'); + description = string.Join('.', subtitleParts, 1, subtitleParts.Length - 1).Trim(); } else { - description = subtitle.Trim(); // Clean up whitespaces and save it + description = subtitle.Trim(); } } } @@ -1526,24 +1560,27 @@ namespace MediaBrowser.MediaEncoding.Probing private void ExtractTimestamp(MediaInfo video) { - if (video.VideoType == VideoType.VideoFile) + if (video.VideoType != VideoType.VideoFile) { - if (string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) || - string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) || - string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase)) - { - try - { - video.Timestamp = GetMpegTimestamp(video.Path); + return; + } - _logger.LogDebug("Video has {Timestamp} timestamp", video.Timestamp); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error extracting timestamp info from {Path}", video.Path); - video.Timestamp = null; - } - } + if (!string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) + && !string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) + && !string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + try + { + video.Timestamp = GetMpegTimestamp(video.Path); + _logger.LogDebug("Video has {Timestamp} timestamp", video.Timestamp); + } + catch (Exception ex) + { + video.Timestamp = null; + _logger.LogError(ex, "Error extracting timestamp info from {Path}", video.Path); } } @@ -1552,7 +1589,7 @@ namespace MediaBrowser.MediaEncoding.Probing { var packetBuffer = new byte[197]; - using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1)) { fs.Read(packetBuffer); } @@ -1562,17 +1599,17 @@ namespace MediaBrowser.MediaEncoding.Probing return TransportStreamTimestamp.None; } - if ((packetBuffer[4] == 71) && (packetBuffer[196] == 71)) + if ((packetBuffer[4] != 71) || (packetBuffer[196] != 71)) { - if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0)) - { - return TransportStreamTimestamp.Zero; - } + return TransportStreamTimestamp.None; + } - return TransportStreamTimestamp.Valid; + if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0)) + { + return TransportStreamTimestamp.Zero; } - return TransportStreamTimestamp.None; + return TransportStreamTimestamp.Valid; } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs index e0b7914fb..08ee5c72e 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs @@ -1,130 +1,19 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; +using Nikse.SubtitleEdit.Core.SubtitleFormats; namespace MediaBrowser.MediaEncoding.Subtitles { - public class AssParser : ISubtitleParser + /// <summary> + /// Advanced SubStation Alpha subtitle parser. + /// </summary> + public class AssParser : SubtitleEditParser<AdvancedSubStationAlpha> { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - /// <inheritdoc /> - public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) - { - var trackInfo = new SubtitleTrackInfo(); - var trackEvents = new List<SubtitleTrackEvent>(); - var eventIndex = 1; - using (var reader = new StreamReader(stream)) - { - string line; - while (!string.Equals(reader.ReadLine(), "[Events]", StringComparison.Ordinal)) - { - } - - var headers = ParseFieldHeaders(reader.ReadLine()); - - while ((line = reader.ReadLine()) != null) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - - if (line[0] == '[') - { - break; - } - - var subEvent = new SubtitleTrackEvent { Id = eventIndex.ToString(_usCulture) }; - eventIndex++; - const string Dialogue = "Dialogue: "; - var sections = line.Substring(Dialogue.Length).Split(','); - - subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]); - subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]); - - subEvent.Text = string.Join(',', sections[headers["Text"]..]); - RemoteNativeFormatting(subEvent); - - subEvent.Text = subEvent.Text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase); - - subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase); - - trackEvents.Add(subEvent); - } - } - - trackInfo.TrackEvents = trackEvents; - return trackInfo; - } - - private long GetTicks(ReadOnlySpan<char> time) + /// <summary> + /// Initializes a new instance of the <see cref="AssParser"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + public AssParser(ILogger logger) : base(logger) { - return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out var span) - ? span.Ticks : 0; - } - - internal static Dictionary<string, int> ParseFieldHeaders(string line) - { - const string Format = "Format: "; - var fields = line.Substring(Format.Length).Split(',').Select(x => x.Trim()).ToList(); - - return new Dictionary<string, int> - { - { "Start", fields.IndexOf("Start") }, - { "End", fields.IndexOf("End") }, - { "Text", fields.IndexOf("Text") } - }; - } - - private void RemoteNativeFormatting(SubtitleTrackEvent p) - { - int indexOfBegin = p.Text.IndexOf('{', StringComparison.Ordinal); - string pre = string.Empty; - while (indexOfBegin >= 0 && p.Text.IndexOf('}', StringComparison.Ordinal) > indexOfBegin) - { - string s = p.Text.Substring(indexOfBegin); - if (s.StartsWith("{\\an1}", StringComparison.Ordinal) || - s.StartsWith("{\\an2}", StringComparison.Ordinal) || - s.StartsWith("{\\an3}", StringComparison.Ordinal) || - s.StartsWith("{\\an4}", StringComparison.Ordinal) || - s.StartsWith("{\\an5}", StringComparison.Ordinal) || - s.StartsWith("{\\an6}", StringComparison.Ordinal) || - s.StartsWith("{\\an7}", StringComparison.Ordinal) || - s.StartsWith("{\\an8}", StringComparison.Ordinal) || - s.StartsWith("{\\an9}", StringComparison.Ordinal)) - { - pre = s.Substring(0, 6); - } - else if (s.StartsWith("{\\an1\\", StringComparison.Ordinal) || - s.StartsWith("{\\an2\\", StringComparison.Ordinal) || - s.StartsWith("{\\an3\\", StringComparison.Ordinal) || - s.StartsWith("{\\an4\\", StringComparison.Ordinal) || - s.StartsWith("{\\an5\\", StringComparison.Ordinal) || - s.StartsWith("{\\an6\\", StringComparison.Ordinal) || - s.StartsWith("{\\an7\\", StringComparison.Ordinal) || - s.StartsWith("{\\an8\\", StringComparison.Ordinal) || - s.StartsWith("{\\an9\\", StringComparison.Ordinal)) - { - pre = s.Substring(0, 5) + "}"; - } - - int indexOfEnd = p.Text.IndexOf('}', StringComparison.Ordinal); - p.Text = p.Text.Remove(indexOfBegin, (indexOfEnd - indexOfBegin) + 1); - - indexOfBegin = p.Text.IndexOf('{', StringComparison.Ordinal); - } - - p.Text = pre + p.Text; } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs deleted file mode 100644 index dca5c1e8a..000000000 --- a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable enable -#pragma warning disable CS1591 - -namespace MediaBrowser.MediaEncoding.Subtitles -{ - public static class ParserValues - { - public const string NewLine = "\r\n"; - } -} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs index 4a87f87dc..78d54ca51 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs @@ -1,102 +1,19 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading; -using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; +using Nikse.SubtitleEdit.Core.SubtitleFormats; namespace MediaBrowser.MediaEncoding.Subtitles { - public class SrtParser : ISubtitleParser + /// <summary> + /// SubRip subtitle parser. + /// </summary> + public class SrtParser : SubtitleEditParser<SubRip> { - private readonly ILogger _logger; - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public SrtParser(ILogger logger) - { - _logger = logger; - } - - /// <inheritdoc /> - public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) - { - var trackInfo = new SubtitleTrackInfo(); - var trackEvents = new List<SubtitleTrackEvent>(); - using (var reader = new StreamReader(stream)) - { - string line; - while ((line = reader.ReadLine()) != null) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - - var subEvent = new SubtitleTrackEvent { Id = line }; - line = reader.ReadLine(); - - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - - var time = Regex.Split(line, @"[\t ]*-->[\t ]*"); - - if (time.Length < 2) - { - // This occurs when subtitle text has an empty line as part of the text. - // Need to adjust the break statement below to resolve this. - _logger.LogWarning("Unrecognized line in srt: {0}", line); - continue; - } - - subEvent.StartPositionTicks = GetTicks(time[0]); - var endTime = time[1].AsSpan(); - var idx = endTime.IndexOf(' '); - if (idx > 0) - { - endTime = endTime.Slice(0, idx); - } - - subEvent.EndPositionTicks = GetTicks(endTime); - var multiline = new List<string>(); - while ((line = reader.ReadLine()) != null) - { - if (line.Length == 0) - { - break; - } - - multiline.Add(line); - } - - subEvent.Text = string.Join(ParserValues.NewLine, multiline); - subEvent.Text = subEvent.Text.Replace(@"\N", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase); - subEvent.Text = Regex.Replace(subEvent.Text, @"\{(?:\\\d?[\w.-]+(?:\([^\)]*\)|&H?[0-9A-Fa-f]+&|))+\}", string.Empty, RegexOptions.IgnoreCase); - subEvent.Text = Regex.Replace(subEvent.Text, "<", "<", RegexOptions.IgnoreCase); - subEvent.Text = Regex.Replace(subEvent.Text, ">", ">", RegexOptions.IgnoreCase); - subEvent.Text = Regex.Replace(subEvent.Text, "<(\\/?(font|b|u|i|s))((\\s+(\\w|\\w[\\w\\-]*\\w)(\\s*=\\s*(?:\\\".*?\\\"|'.*?'|[^'\\\">\\s]+))?)+\\s*|\\s*)(\\/?)>", "<$1$3$7>", RegexOptions.IgnoreCase); - trackEvents.Add(subEvent); - } - } - - trackInfo.TrackEvents = trackEvents; - return trackInfo; - } - - private long GetTicks(ReadOnlySpan<char> time) + /// <summary> + /// Initializes a new instance of the <see cref="SrtParser"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + public SrtParser(ILogger logger) : base(logger) { - return TimeSpan.TryParseExact(time, @"hh\:mm\:ss\.fff", _usCulture, out var span) - ? span.Ticks - : (TimeSpan.TryParseExact(time, @"hh\:mm\:ss\,fff", _usCulture, out span) - ? span.Ticks : 0); } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs index bc84c5074..17c2ae40e 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -1,477 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; -using System.Threading; -using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; +using Nikse.SubtitleEdit.Core.SubtitleFormats; namespace MediaBrowser.MediaEncoding.Subtitles { /// <summary> - /// <see href="https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs">Credit</see>. + /// SubStation Alpha subtitle parser. /// </summary> - public class SsaParser : ISubtitleParser + public class SsaParser : SubtitleEditParser<SubStationAlpha> { - /// <inheritdoc /> - public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) + /// <summary> + /// Initializes a new instance of the <see cref="SsaParser"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + public SsaParser(ILogger logger) : base(logger) { - var trackInfo = new SubtitleTrackInfo(); - var trackEvents = new List<SubtitleTrackEvent>(); - - using (var reader = new StreamReader(stream)) - { - bool eventsStarted = false; - - string[] format = "Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text".Split(','); - int indexLayer = 0; - int indexStart = 1; - int indexEnd = 2; - int indexStyle = 3; - int indexName = 4; - int indexEffect = 8; - int indexText = 9; - int lineNumber = 0; - - var header = new StringBuilder(); - - string line; - - while ((line = reader.ReadLine()) != null) - { - cancellationToken.ThrowIfCancellationRequested(); - - lineNumber++; - if (!eventsStarted) - { - header.AppendLine(line); - } - - if (string.Equals(line.Trim(), "[events]", StringComparison.OrdinalIgnoreCase)) - { - eventsStarted = true; - } - else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(';')) - { - // skip comment lines - } - else if (eventsStarted && line.Trim().Length > 0) - { - string s = line.Trim().ToLowerInvariant(); - if (s.StartsWith("format:", StringComparison.Ordinal)) - { - if (line.Length > 10) - { - format = line.ToLowerInvariant().Substring(8).Split(','); - for (int i = 0; i < format.Length; i++) - { - if (string.Equals(format[i].Trim(), "layer", StringComparison.OrdinalIgnoreCase)) - { - indexLayer = i; - } - else if (string.Equals(format[i].Trim(), "start", StringComparison.OrdinalIgnoreCase)) - { - indexStart = i; - } - else if (string.Equals(format[i].Trim(), "end", StringComparison.OrdinalIgnoreCase)) - { - indexEnd = i; - } - else if (string.Equals(format[i].Trim(), "text", StringComparison.OrdinalIgnoreCase)) - { - indexText = i; - } - else if (string.Equals(format[i].Trim(), "effect", StringComparison.OrdinalIgnoreCase)) - { - indexEffect = i; - } - else if (string.Equals(format[i].Trim(), "style", StringComparison.OrdinalIgnoreCase)) - { - indexStyle = i; - } - } - } - } - else if (!string.IsNullOrEmpty(s)) - { - string text = string.Empty; - string start = string.Empty; - string end = string.Empty; - string style = string.Empty; - string layer = string.Empty; - string effect = string.Empty; - string name = string.Empty; - - string[] splittedLine; - - if (s.StartsWith("dialogue:", StringComparison.Ordinal)) - { - splittedLine = line.Substring(10).Split(','); - } - else - { - splittedLine = line.Split(','); - } - - for (int i = 0; i < splittedLine.Length; i++) - { - if (i == indexStart) - { - start = splittedLine[i].Trim(); - } - else if (i == indexEnd) - { - end = splittedLine[i].Trim(); - } - else if (i == indexLayer) - { - layer = splittedLine[i]; - } - else if (i == indexEffect) - { - effect = splittedLine[i]; - } - else if (i == indexText) - { - text = splittedLine[i]; - } - else if (i == indexStyle) - { - style = splittedLine[i]; - } - else if (i == indexName) - { - name = splittedLine[i]; - } - else if (i > indexText) - { - text += "," + splittedLine[i]; - } - } - - try - { - trackEvents.Add( - new SubtitleTrackEvent - { - StartPositionTicks = GetTimeCodeFromString(start), - EndPositionTicks = GetTimeCodeFromString(end), - Text = GetFormattedText(text) - }); - } - catch - { - } - } - } - } - - // if (header.Length > 0) - // subtitle.Header = header.ToString(); - - // subtitle.Renumber(1); - } - - trackInfo.TrackEvents = trackEvents.ToArray(); - return trackInfo; - } - - private static long GetTimeCodeFromString(string time) - { - // h:mm:ss.cc - string[] timeCode = time.Split(':', '.'); - return new TimeSpan( - 0, - int.Parse(timeCode[0], CultureInfo.InvariantCulture), - int.Parse(timeCode[1], CultureInfo.InvariantCulture), - int.Parse(timeCode[2], CultureInfo.InvariantCulture), - int.Parse(timeCode[3], CultureInfo.InvariantCulture) * 10).Ticks; - } - - private static string GetFormattedText(string text) - { - text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase); - - for (int i = 0; i < 10; i++) // just look ten times... - { - if (text.Contains(@"{\fn", StringComparison.Ordinal)) - { - int start = text.IndexOf(@"{\fn", StringComparison.Ordinal); - int end = text.IndexOf('}', start); - if (end > 0 && !text.Substring(start).StartsWith("{\\fn}", StringComparison.Ordinal)) - { - string fontName = text.Substring(start + 4, end - (start + 4)); - string extraTags = string.Empty; - CheckAndAddSubTags(ref fontName, ref extraTags, out bool italic); - text = text.Remove(start, end - start + 1); - if (italic) - { - text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + "><i>"); - } - else - { - text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + ">"); - } - - int indexOfEndTag = text.IndexOf("{\\fn}", start, StringComparison.Ordinal); - if (indexOfEndTag > 0) - { - text = text.Remove(indexOfEndTag, "{\\fn}".Length).Insert(indexOfEndTag, "</font>"); - } - else - { - text += "</font>"; - } - } - } - - if (text.Contains(@"{\fs", StringComparison.Ordinal)) - { - int start = text.IndexOf(@"{\fs", StringComparison.Ordinal); - int end = text.IndexOf('}', start); - if (end > 0 && !text.Substring(start).StartsWith("{\\fs}", StringComparison.Ordinal)) - { - string fontSize = text.Substring(start + 4, end - (start + 4)); - string extraTags = string.Empty; - CheckAndAddSubTags(ref fontSize, ref extraTags, out bool italic); - if (IsInteger(fontSize)) - { - text = text.Remove(start, end - start + 1); - if (italic) - { - text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + "><i>"); - } - else - { - text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + ">"); - } - - int indexOfEndTag = text.IndexOf("{\\fs}", start, StringComparison.Ordinal); - if (indexOfEndTag > 0) - { - text = text.Remove(indexOfEndTag, "{\\fs}".Length).Insert(indexOfEndTag, "</font>"); - } - else - { - text += "</font>"; - } - } - } - } - - if (text.Contains(@"{\c", StringComparison.Ordinal)) - { - int start = text.IndexOf(@"{\c", StringComparison.Ordinal); - int end = text.IndexOf('}', start); - if (end > 0 && !text.Substring(start).StartsWith("{\\c}", StringComparison.Ordinal)) - { - string color = text.Substring(start + 4, end - (start + 4)); - string extraTags = string.Empty; - CheckAndAddSubTags(ref color, ref extraTags, out bool italic); - - color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H'); - color = color.PadLeft(6, '0'); - - // switch to rrggbb from bbggrr - color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2); - color = color.ToLowerInvariant(); - - text = text.Remove(start, end - start + 1); - if (italic) - { - text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>"); - } - else - { - text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">"); - } - - int indexOfEndTag = text.IndexOf("{\\c}", start, StringComparison.Ordinal); - if (indexOfEndTag > 0) - { - text = text.Remove(indexOfEndTag, "{\\c}".Length).Insert(indexOfEndTag, "</font>"); - } - else - { - text += "</font>"; - } - } - } - - if (text.Contains(@"{\1c", StringComparison.Ordinal)) // "1" specifices primary color - { - int start = text.IndexOf(@"{\1c", StringComparison.Ordinal); - int end = text.IndexOf('}', start); - if (end > 0 && !text.Substring(start).StartsWith("{\\1c}", StringComparison.Ordinal)) - { - string color = text.Substring(start + 5, end - (start + 5)); - string extraTags = string.Empty; - CheckAndAddSubTags(ref color, ref extraTags, out bool italic); - - color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H'); - color = color.PadLeft(6, '0'); - - // switch to rrggbb from bbggrr - color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2); - color = color.ToLowerInvariant(); - - text = text.Remove(start, end - start + 1); - if (italic) - { - text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>"); - } - else - { - text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">"); - } - - int indexOfEndTag = text.IndexOf("{\\1c}", start, StringComparison.Ordinal); - if (indexOfEndTag > 0) - { - text = text.Remove(indexOfEndTag, "{\\1c}".Length).Insert(indexOfEndTag, "</font>"); - } - else - { - text += "</font>"; - } - } - } - } - - text = text.Replace(@"{\i1}", "<i>", StringComparison.Ordinal); - text = text.Replace(@"{\i0}", "</i>", StringComparison.Ordinal); - text = text.Replace(@"{\i}", "</i>", StringComparison.Ordinal); - if (CountTagInText(text, "<i>") > CountTagInText(text, "</i>")) - { - text += "</i>"; - } - - text = text.Replace(@"{\u1}", "<u>", StringComparison.Ordinal); - text = text.Replace(@"{\u0}", "</u>", StringComparison.Ordinal); - text = text.Replace(@"{\u}", "</u>", StringComparison.Ordinal); - if (CountTagInText(text, "<u>") > CountTagInText(text, "</u>")) - { - text += "</u>"; - } - - text = text.Replace(@"{\b1}", "<b>", StringComparison.Ordinal); - text = text.Replace(@"{\b0}", "</b>", StringComparison.Ordinal); - text = text.Replace(@"{\b}", "</b>", StringComparison.Ordinal); - if (CountTagInText(text, "<b>") > CountTagInText(text, "</b>")) - { - text += "</b>"; - } - - return text; - } - - private static bool IsInteger(string s) - => int.TryParse(s, out _); - - private static int CountTagInText(string text, string tag) - { - int count = 0; - int index = text.IndexOf(tag, StringComparison.Ordinal); - while (index >= 0) - { - count++; - if (index == text.Length) - { - return count; - } - - index = text.IndexOf(tag, index + 1, StringComparison.Ordinal); - } - - return count; - } - - private static void CheckAndAddSubTags(ref string tagName, ref string extraTags, out bool italic) - { - italic = false; - int indexOfSPlit = tagName.IndexOf('\\', StringComparison.Ordinal); - if (indexOfSPlit > 0) - { - string rest = tagName.Substring(indexOfSPlit).TrimStart('\\'); - tagName = tagName.Remove(indexOfSPlit); - - for (int i = 0; i < 10; i++) - { - if (rest.StartsWith("fs", StringComparison.Ordinal) && rest.Length > 2) - { - indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); - string fontSize = rest; - if (indexOfSPlit > 0) - { - fontSize = rest.Substring(0, indexOfSPlit); - rest = rest.Substring(indexOfSPlit).TrimStart('\\'); - } - else - { - rest = string.Empty; - } - - extraTags += " size=\"" + fontSize.Substring(2) + "\""; - } - else if (rest.StartsWith("fn", StringComparison.Ordinal) && rest.Length > 2) - { - indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); - string fontName = rest; - if (indexOfSPlit > 0) - { - fontName = rest.Substring(0, indexOfSPlit); - rest = rest.Substring(indexOfSPlit).TrimStart('\\'); - } - else - { - rest = string.Empty; - } - - extraTags += " face=\"" + fontName.Substring(2) + "\""; - } - else if (rest.StartsWith("c", StringComparison.Ordinal) && rest.Length > 2) - { - indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); - string fontColor = rest; - if (indexOfSPlit > 0) - { - fontColor = rest.Substring(0, indexOfSPlit); - rest = rest.Substring(indexOfSPlit).TrimStart('\\'); - } - else - { - rest = string.Empty; - } - - string color = fontColor.Substring(2); - color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H'); - color = color.PadLeft(6, '0'); - // switch to rrggbb from bbggrr - color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2); - color = color.ToLowerInvariant(); - - extraTags += " color=\"" + color + "\""; - } - else if (rest.StartsWith("i1", StringComparison.Ordinal) && rest.Length > 1) - { - indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); - italic = true; - if (indexOfSPlit > 0) - { - rest = rest.Substring(indexOfSPlit).TrimStart('\\'); - } - else - { - rest = string.Empty; - } - } - else if (rest.Length > 0 && rest.Contains('\\', StringComparison.Ordinal)) - { - indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); - rest = rest.Substring(indexOfSPlit).TrimStart('\\'); - } - } - } } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs new file mode 100644 index 000000000..52c1b6467 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs @@ -0,0 +1,61 @@ +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using Jellyfin.Extensions; +using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; +using Nikse.SubtitleEdit.Core.Common; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + /// <summary> + /// SubStation Alpha subtitle parser. + /// </summary> + /// <typeparam name="T">The <see cref="SubtitleFormat" />.</typeparam> + public abstract class SubtitleEditParser<T> : ISubtitleParser + where T : SubtitleFormat, new() + { + private readonly ILogger _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="SubtitleEditParser{T}"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + protected SubtitleEditParser(ILogger logger) + { + _logger = logger; + } + + /// <inheritdoc /> + public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) + { + var subtitle = new Subtitle(); + var subRip = new T(); + var lines = stream.ReadAllLines().ToList(); + subRip.LoadSubtitle(subtitle, lines, "untitled"); + if (subRip.ErrorCount > 0) + { + _logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount); + } + + var trackInfo = new SubtitleTrackInfo(); + int len = subtitle.Paragraphs.Count; + var trackEvents = new SubtitleTrackEvent[len]; + for (int i = 0; i < len; i++) + { + var p = subtitle.Paragraphs[i]; + trackEvents[i] = new SubtitleTrackEvent(p.Number.ToString(CultureInfo.InvariantCulture), p.Text) + { + StartPositionTicks = p.StartTime.TimeSpan.Ticks, + EndPositionTicks = p.EndTime.TimeSpan.Ticks + }; + } + + trackInfo.TrackEvents = trackEvents; + return trackInfo; + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index b92c4ee06..89365a516 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -10,6 +11,7 @@ using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -25,9 +27,8 @@ using UtfUnknown; namespace MediaBrowser.MediaEncoding.Subtitles { - public class SubtitleEncoder : ISubtitleEncoder + public sealed class SubtitleEncoder : ISubtitleEncoder { - private readonly ILibraryManager _libraryManager; private readonly ILogger<SubtitleEncoder> _logger; private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; @@ -42,7 +43,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles new ConcurrentDictionary<string, SemaphoreSlim>(); public SubtitleEncoder( - ILibraryManager libraryManager, ILogger<SubtitleEncoder> logger, IApplicationPaths appPaths, IFileSystem fileSystem, @@ -50,7 +50,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles IHttpClientFactory httpClientFactory, IMediaSourceManager mediaSourceManager) { - _libraryManager = libraryManager; _logger = logger; _appPaths = appPaths; _fileSystem = fileSystem; @@ -74,8 +73,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - var reader = GetReader(inputFormat, true); - + var reader = GetReader(inputFormat); var trackInfo = reader.Parse(stream, cancellationToken); FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); @@ -142,10 +140,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles .ConfigureAwait(false); var inputFormat = subtitle.format; - var writer = TryGetWriter(outputFormat); // Return the original if we don't have any way of converting it - if (writer == null) + if (!TryGetWriter(outputFormat, out var writer)) { return subtitle.stream; } @@ -168,33 +165,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles MediaStream subtitleStream, CancellationToken cancellationToken) { - var inputFile = mediaSource.Path; - - var protocol = mediaSource.Protocol; - if (subtitleStream.IsExternal) - { - protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path); - } - - var fileInfo = await GetReadableFile(mediaSource.Path, inputFile, mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false); + var fileInfo = await GetReadableFile(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false); - var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); + var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false); return (stream, fileInfo.Format); } - private async Task<Stream> GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken) + private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken) { - if (requiresCharset) + if (fileInfo.IsExternal) { - using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) + using (var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false)) { var result = CharsetDetector.DetectFromStream(stream).Detected; stream.Position = 0; if (result != null) { - _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, path); + _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, fileInfo.Path); using var reader = new StreamReader(stream, result.Encoding); var text = await reader.ReadToEndAsync().ConfigureAwait(false); @@ -204,12 +193,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - return File.OpenRead(path); + return AsyncFile.OpenRead(fileInfo.Path); } - private async Task<SubtitleInfo> GetReadableFile( - string mediaPath, - string inputFile, + internal async Task<SubtitleInfo> GetReadableFile( MediaSourceInfo mediaSource, MediaStream subtitleStream, CancellationToken cancellationToken) @@ -219,9 +206,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles string outputFormat; string outputCodec; - if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || - string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) || - string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) + || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) + || string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase)) { // Extract outputCodec = "copy"; @@ -241,9 +228,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles } // Extract - var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, "." + outputFormat); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat); - await ExtractTextSubtitle(inputFile, mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken) + await ExtractTextSubtitle(mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken) .ConfigureAwait(false); return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false); @@ -252,50 +239,55 @@ namespace MediaBrowser.MediaEncoding.Subtitles var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) .TrimStart('.'); - if (GetReader(currentFormat, false) == null) + if (!TryGetReader(currentFormat, out _)) { // Convert - var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, ".srt"); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt"); await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true); } - return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true); + // It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs) + return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true); } - private ISubtitleParser GetReader(string format, bool throwIfMissing) + private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value) { - if (string.IsNullOrEmpty(format)) - { - throw new ArgumentNullException(nameof(format)); - } - if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) { - return new SrtParser(_logger); + value = new SrtParser(_logger); + return true; } if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)) { - return new SsaParser(); + value = new SsaParser(_logger); + return true; } if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) { - return new AssParser(); + value = new AssParser(_logger); + return true; } - if (throwIfMissing) + value = null; + return false; + } + + private ISubtitleParser GetReader(string format) + { + if (TryGetReader(format, out var reader)) { - throw new ArgumentException("Unsupported format: " + format); + return reader; } - return null; + throw new ArgumentException("Unsupported format: " + format); } - private ISubtitleWriter TryGetWriter(string format) + private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value) { if (string.IsNullOrEmpty(format)) { @@ -304,32 +296,35 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) { - return new JsonWriter(); + value = new JsonWriter(); + return true; } if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) { - return new SrtWriter(); + value = new SrtWriter(); + return true; } if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase)) { - return new VttWriter(); + value = new VttWriter(); + return true; } if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase)) { - return new TtmlWriter(); + value = new TtmlWriter(); + return true; } - return null; + value = null; + return false; } private ISubtitleWriter GetWriter(string format) { - var writer = TryGetWriter(format); - - if (writer != null) + if (TryGetWriter(format, out var writer)) { return writer; } @@ -399,7 +394,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw new ArgumentNullException(nameof(outputPath)); } - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath))); var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); @@ -492,7 +487,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath); - throw new Exception( + throw new FfmpegException( string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath)); } @@ -504,7 +499,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <summary> /// Extracts the text subtitle. /// </summary> - /// <param name="inputFile">The input file.</param> /// <param name="mediaSource">The mediaSource.</param> /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param> /// <param name="outputCodec">The output codec.</param> @@ -513,7 +507,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <returns>Task.</returns> /// <exception cref="ArgumentException">Must use inputPath list overload.</exception> private async Task ExtractTextSubtitle( - string inputFile, MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputCodec, @@ -529,7 +522,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!File.Exists(outputPath)) { await ExtractTextSubtitleInternal( - _mediaEncoder.GetInputArgument(inputFile, mediaSource), + _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource), subtitleStreamIndex, outputCodec, outputPath, @@ -559,7 +552,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw new ArgumentNullException(nameof(outputPath)); } - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath))); var processArgs = string.Format( CultureInfo.InvariantCulture, @@ -643,17 +636,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (failed) { - var msg = $"ffmpeg subtitle extraction failed for {inputPath} to {outputPath}"; + _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); - _logger.LogError(msg); - - throw new Exception(msg); + throw new FfmpegException( + string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath)); } else { - var msg = $"ffmpeg subtitle extraction completed for {inputPath} to {outputPath}"; - - _logger.LogInformation(msg); + _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); } if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase)) @@ -675,7 +665,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles string text; Encoding encoding; - using (var fileStream = File.OpenRead(file)) + using (var fileStream = AsyncFile.OpenRead(file)) using (var reader = new StreamReader(fileStream, true)) { encoding = reader.CurrentEncoding; @@ -687,7 +677,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!string.Equals(text, newText, StringComparison.Ordinal)) { - using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) + using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) using (var writer = new StreamWriter(fileStream, encoding)) { await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false); @@ -695,15 +685,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - private string GetSubtitleCachePath(string mediaPath, MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) + private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) { if (mediaSource.Protocol == MediaProtocol.File) { var ticksParam = string.Empty; - var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); + var date = _fileSystem.GetLastWriteTimeUtc(mediaSource.Path); - ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension; + ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension; var prefix = filename.Slice(0, 1); @@ -711,7 +701,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } else { - ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension; + ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension; var prefix = filename.Slice(0, 1); @@ -724,7 +714,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) { - var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName; + var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty; // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal)) @@ -734,7 +724,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles charset = string.Empty; } - _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path); + _logger.LogDebug("charset {0} detected for {Path}", charset, path); return charset; } @@ -753,13 +743,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles } case MediaProtocol.File: - return File.OpenRead(path); + return AsyncFile.OpenRead(path); default: throw new ArgumentOutOfRangeException(nameof(protocol)); } } - private struct SubtitleInfo + internal readonly struct SubtitleInfo { public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal) { @@ -769,13 +759,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles IsExternal = isExternal; } - public string Path { get; set; } + public string Path { get; } - public MediaProtocol Protocol { get; set; } + public MediaProtocol Protocol { get; } - public string Format { get; set; } + public string Format { get; } - public bool IsExternal { get; set; } + public bool IsExternal { get; } } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs index ad32cb794..38ef57dee 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs @@ -18,14 +18,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { writer.WriteLine("WEBVTT"); - writer.WriteLine(string.Empty); - writer.WriteLine("REGION"); - writer.WriteLine("id:subtitle"); - writer.WriteLine("width:80%"); - writer.WriteLine("lines:3"); - writer.WriteLine("regionanchor:50%,100%"); - writer.WriteLine("viewportanchor:50%,90%"); - writer.WriteLine(string.Empty); + writer.WriteLine(); + writer.WriteLine("Region: id:subtitle width:80% lines:3 regionanchor:50%,100% viewportanchor:50%,90%"); + writer.WriteLine(); foreach (var trackEvent in info.TrackEvents) { cancellationToken.ThrowIfCancellationRequested(); @@ -39,7 +34,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles endTime = startTime.Add(TimeSpan.FromMilliseconds(1)); } - writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle", startTime, endTime); + writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle line:90%", startTime, endTime); var text = trackEvent.Text; |
