diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding')
18 files changed, 1143 insertions, 623 deletions
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs new file mode 100644 index 000000000..c371e8b94 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -0,0 +1,281 @@ +using System; +using System.Diagnostics; +using System.Collections.Concurrent; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.MediaEncoding.Attachments +{ + public class AttachmentExtractor : IAttachmentExtractor, IDisposable + { + private readonly ILogger _logger; + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + private readonly IMediaEncoder _mediaEncoder; + private readonly IMediaSourceManager _mediaSourceManager; + + private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = + new ConcurrentDictionary<string, SemaphoreSlim>(); + + private bool _disposed = false; + + public AttachmentExtractor( + ILogger<AttachmentExtractor> logger, + IApplicationPaths appPaths, + IFileSystem fileSystem, + IMediaEncoder mediaEncoder, + IMediaSourceManager mediaSourceManager) + { + _logger = logger; + _appPaths = appPaths; + _fileSystem = fileSystem; + _mediaEncoder = mediaEncoder; + _mediaSourceManager = mediaSourceManager; + } + + /// <inheritdoc /> + public async Task<(MediaAttachment attachment, Stream stream)> GetAttachment(BaseItem item, string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + if (string.IsNullOrWhiteSpace(mediaSourceId)) + { + throw new ArgumentNullException(nameof(mediaSourceId)); + } + + var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false); + var mediaSource = mediaSources + .FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); + if (mediaSource == null) + { + throw new ResourceNotFoundException($"MediaSource {mediaSourceId} not found"); + } + + var mediaAttachment = mediaSource.MediaAttachments + .FirstOrDefault(i => i.Index == attachmentStreamIndex); + if (mediaAttachment == null) + { + throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}"); + } + + var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken) + .ConfigureAwait(false); + + return (mediaAttachment, attachmentStream); + } + + private async Task<Stream> GetAttachmentStream( + MediaSourceInfo mediaSource, + MediaAttachment mediaAttachment, + CancellationToken cancellationToken) + { + var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource.Protocol, mediaAttachment, cancellationToken).ConfigureAwait(false); + return File.OpenRead(attachmentPath); + } + + private async Task<string> GetReadableFile( + string mediaPath, + string inputFile, + MediaProtocol protocol, + MediaAttachment mediaAttachment, + CancellationToken cancellationToken) + { + var outputPath = GetAttachmentCachePath(mediaPath, protocol, mediaAttachment.Index); + await ExtractAttachment(inputFile, protocol, mediaAttachment.Index, outputPath, cancellationToken) + .ConfigureAwait(false); + + return outputPath; + } + + private async Task ExtractAttachment( + string inputFile, + MediaProtocol protocol, + int attachmentStreamIndex, + string outputPath, + CancellationToken cancellationToken) + { + var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1)); + + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + if (!File.Exists(outputPath)) + { + await ExtractAttachmentInternal( + _mediaEncoder.GetInputArgument(new[] { inputFile }, protocol), + attachmentStreamIndex, + outputPath, + cancellationToken).ConfigureAwait(false); + } + } + finally + { + semaphore.Release(); + } + } + + private async Task ExtractAttachmentInternal( + string inputPath, + int attachmentStreamIndex, + string outputPath, + CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(inputPath)) + { + throw new ArgumentNullException(nameof(inputPath)); + } + + if (string.IsNullOrEmpty(outputPath)) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + + var processArgs = string.Format( + CultureInfo.InvariantCulture, + "-dump_attachment:{1} {2} -i {0} -t 0 -f null null", + inputPath, + attachmentStreamIndex, + outputPath); + var startInfo = new ProcessStartInfo + { + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }; + var process = new Process + { + StartInfo = startInfo + }; + + _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); + + process.Start(); + + var processTcs = new TaskCompletionSource<bool>(); + process.EnableRaisingEvents = true; + process.Exited += (sender, args) => processTcs.TrySetResult(true); + var unregister = cancellationToken.Register(() => processTcs.TrySetResult(process.HasExited)); + var ranToCompletion = await processTcs.Task.ConfigureAwait(false); + unregister.Dispose(); + + if (!ranToCompletion) + { + try + { + _logger.LogWarning("Killing ffmpeg attachment extraction process"); + process.Kill(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error killing attachment extraction process"); + } + } + + var exitCode = ranToCompletion ? process.ExitCode : -1; + + process.Dispose(); + + var failed = false; + + if (exitCode != 0) + { + failed = true; + + _logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode); + try + { + if (File.Exists(outputPath)) + { + _fileSystem.DeleteFile(outputPath); + } + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath); + } + } + else if (!File.Exists(outputPath)) + { + failed = true; + } + + if (failed) + { + var msg = $"ffmpeg attachment extraction failed for {inputPath} to {outputPath}"; + + _logger.LogError(msg); + + throw new InvalidOperationException(msg); + } + else + { + _logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath); + } + } + + private string GetAttachmentCachePath(string mediaPath, MediaProtocol protocol, int attachmentStreamIndex) + { + string filename; + if (protocol == MediaProtocol.File) + { + var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); + filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D"); + } + else + { + filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D"); + } + + var prefix = filename.Substring(0, 1); + return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename); + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + + } + + _disposed = true; + } + } +} diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs new file mode 100644 index 000000000..91c8b2792 --- /dev/null +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BDInfo.IO; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.MediaEncoding.BdInfo +{ + class BdInfoDirectoryInfo : BDInfo.IO.IDirectoryInfo + { + IFileSystem _fileSystem = null; + + FileSystemMetadata _impl = null; + + public string Name => _impl.Name; + + public string FullName => _impl.FullName; + + public IDirectoryInfo Parent + { + get + { + var parentFolder = System.IO.Path.GetDirectoryName(_impl.FullName); + if (parentFolder != null) + { + return new BdInfoDirectoryInfo(_fileSystem, parentFolder); + } + return null; + } + } + + public BdInfoDirectoryInfo(IFileSystem fileSystem, string path) + { + _fileSystem = fileSystem; + _impl = _fileSystem.GetDirectoryInfo(path); + } + + private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl) + { + _fileSystem = fileSystem; + _impl = impl; + } + + public IDirectoryInfo[] GetDirectories() + { + return Array.ConvertAll(_fileSystem.GetDirectories(_impl.FullName).ToArray(), + x => new BdInfoDirectoryInfo(_fileSystem, x)); + } + + public IFileInfo[] GetFiles() + { + return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName).ToArray(), + x => new BdInfoFileInfo(_fileSystem, x)); + } + + public IFileInfo[] GetFiles(string searchPattern) + { + return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(), + x => new BdInfoFileInfo(_fileSystem, x)); + } + + public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption) + { + return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, + searchOption.HasFlag(System.IO.SearchOption.AllDirectories)).ToArray(), + x => new BdInfoFileInfo(_fileSystem, x)); + } + + public static IDirectoryInfo FromFileSystemPath(Model.IO.IFileSystem fs, string path) + { + return new BdInfoDirectoryInfo(fs, path); + } + } +} diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs index 3b6b91684..3260f3051 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using BDInfo; @@ -32,7 +32,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo throw new ArgumentNullException(nameof(path)); } - var bdrom = new BDROM(path, _fileSystem); + var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path)); bdrom.Scan(); diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs new file mode 100644 index 000000000..de9d7cb78 --- /dev/null +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs @@ -0,0 +1,40 @@ +using MediaBrowser.Model.IO; + +namespace MediaBrowser.MediaEncoding.BdInfo +{ + class BdInfoFileInfo : BDInfo.IO.IFileInfo + { + IFileSystem _fileSystem = null; + + FileSystemMetadata _impl = null; + + public string Name => _impl.Name; + + public string FullName => _impl.FullName; + + public string Extension => _impl.Extension; + + public long Length => _impl.Length; + + public bool IsDir => _impl.IsDirectory; + + public BdInfoFileInfo(IFileSystem fileSystem, FileSystemMetadata impl) + { + _fileSystem = fileSystem; + _impl = impl; + } + + public System.IO.Stream OpenRead() + { + return _fileSystem.GetFileStream(FullName, + FileOpenMode.Open, + FileAccessMode.Read, + FileShareMode.Read); + } + + public System.IO.StreamReader OpenText() + { + return new System.IO.StreamReader(OpenRead()); + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 04ff66991..e0f7b992c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; @@ -19,9 +19,9 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; namespace MediaBrowser.MediaEncoding.Encoder { @@ -31,55 +31,60 @@ namespace MediaBrowser.MediaEncoding.Encoder public class MediaEncoder : IMediaEncoder, IDisposable { /// <summary> - /// Gets the encoder path. + /// The default image extraction timeout in milliseconds. /// </summary> - /// <value>The encoder path.</value> - public string EncoderPath => FFmpegPath; - - /// <summary> - /// The location of the discovered FFmpeg tool. - /// </summary> - public FFmpegLocation EncoderLocation { get; private set; } + internal const int DefaultImageExtractionTimeout = 5000; private readonly ILogger _logger; - private readonly IJsonSerializer _jsonSerializer; - private string FFmpegPath; - private string FFprobePath; - protected readonly IServerConfigurationManager ConfigurationManager; - protected readonly IFileSystem FileSystem; - protected readonly Func<ISubtitleEncoder> SubtitleEncoder; - protected readonly Func<IMediaSourceManager> MediaSourceManager; + private readonly IServerConfigurationManager _configurationManager; + private readonly IFileSystem _fileSystem; private readonly IProcessFactory _processFactory; - private readonly int DefaultImageExtractionTimeoutMs; - private readonly string StartupOptionFFmpegPath; + private readonly ILocalizationManager _localization; + private readonly Func<ISubtitleEncoder> _subtitleEncoder; + private readonly IConfiguration _configuration; + private readonly string _startupOptionFFmpegPath; private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); + + private readonly object _runningProcessesLock = new object(); private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>(); - private readonly ILocalizationManager _localization; + + private EncodingHelper _encodingHelper; + + private string _ffmpegPath; + private string _ffprobePath; public MediaEncoder( - ILoggerFactory loggerFactory, - IJsonSerializer jsonSerializer, - string startupOptionsFFmpegPath, + ILogger<MediaEncoder> logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, - Func<ISubtitleEncoder> subtitleEncoder, - Func<IMediaSourceManager> mediaSourceManager, IProcessFactory processFactory, - int defaultImageExtractionTimeoutMs, - ILocalizationManager localization) - { - _logger = loggerFactory.CreateLogger(nameof(MediaEncoder)); - _jsonSerializer = jsonSerializer; - StartupOptionFFmpegPath = startupOptionsFFmpegPath; - ConfigurationManager = configurationManager; - FileSystem = fileSystem; - SubtitleEncoder = subtitleEncoder; + ILocalizationManager localization, + Func<ISubtitleEncoder> subtitleEncoder, + IConfiguration configuration, + string startupOptionsFFmpegPath) + { + _logger = logger; + _configurationManager = configurationManager; + _fileSystem = fileSystem; _processFactory = processFactory; - DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs; _localization = localization; + _startupOptionFFmpegPath = startupOptionsFFmpegPath; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; } + private EncodingHelper EncodingHelper + => LazyInitializer.EnsureInitialized( + ref _encodingHelper, + () => new EncodingHelper(this, _fileSystem, _subtitleEncoder(), _configuration)); + + /// <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. @@ -88,39 +93,39 @@ 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)) + if (!ValidatePath(_configurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom)) { // 2) Check if the --ffmpeg CLI switch has been given - if (!ValidatePath(StartupOptionFFmpegPath, FFmpegLocation.SetByArgument)) + if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument)) { // 3) Search system $PATH environment variable for valid FFmpeg if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System)) { EncoderLocation = FFmpegLocation.NotFound; - FFmpegPath = null; + _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"); - config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty; - ConfigurationManager.SaveConfiguration("encoding", config); + var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding"); + config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty; + _configurationManager.SaveConfiguration("encoding", config); // Only if mpeg path is set, try and set path to probe - if (FFmpegPath != null) + if (_ffmpegPath != null) { // Determine a probe path from the mpeg path - FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); + _ffprobePath = Regex.Replace(_ffmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); // Interrogate to understand what coders are supported - var validator = new EncoderValidator(_logger, FFmpegPath); + var validator = new EncoderValidator(_logger, _ffmpegPath); SetAvailableDecoders(validator.GetDecoders()); SetAvailableEncoders(validator.GetEncoders()); } - _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, FFmpegPath ?? string.Empty); + _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, _ffmpegPath ?? string.Empty); } /// <summary> @@ -160,9 +165,9 @@ namespace MediaBrowser.MediaEncoding.Encoder // Write the new ffmpeg path to the xml as <EncoderAppPath> // This ensures its not lost on next startup - var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding"); + var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding"); config.EncoderAppPath = newPath; - ConfigurationManager.SaveConfiguration("encoding", config); + _configurationManager.SaveConfiguration("encoding", config); // Trigger SetFFmpegPath so we validate the new path and setup probe path SetFFmpegPath(); @@ -193,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // ToDo - Enable the ffmpeg validator. At the moment any version can be used. rc = true; - FFmpegPath = path; + _ffmpegPath = path; EncoderLocation = location; } else @@ -209,7 +214,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { try { - var files = FileSystem.GetFilePaths(path); + var files = _fileSystem.GetFilePaths(path); var excludeExtensions = new[] { ".c" }; @@ -304,7 +309,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; - var inputFiles = MediaEncoderHelpers.GetInputArgument(FileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames); + var inputFiles = MediaEncoderHelpers.GetInputArgument(_fileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames); var probeSize = EncodingHelper.GetProbeSizeArgument(inputFiles.Length); string analyzeDuration; @@ -365,7 +370,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Must consume both or ffmpeg may hang due to deadlocks. See comments below. RedirectStandardOutput = true, - FileName = FFprobePath, + FileName = _ffprobePath, Arguments = args, @@ -383,7 +388,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); } - using (var processWrapper = new ProcessWrapper(process, this, _logger)) + using (var processWrapper = new ProcessWrapper(process, this)) { _logger.LogDebug("Starting ffprobe with args {Args}", args); StartProcess(processWrapper); @@ -391,8 +396,9 @@ namespace MediaBrowser.MediaEncoding.Encoder InternalMediaInfoResult result; try { - result = await _jsonSerializer.DeserializeFromStreamAsync<InternalMediaInfoResult>( - process.StandardOutput.BaseStream).ConfigureAwait(false); + result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>( + process.StandardOutput.BaseStream, + cancellationToken: cancellationToken).ConfigureAwait(false); } catch { @@ -401,29 +407,29 @@ namespace MediaBrowser.MediaEncoding.Encoder throw; } - if (result == null || (result.streams == null && result.format == null)) + if (result == null || (result.Streams == null && result.Format == null)) { throw new Exception("ffprobe failed - streams and format are both null."); } - if (result.streams != null) + if (result.Streams != null) { // Normalize aspect ratio if invalid - foreach (var stream in result.streams) + foreach (var stream in result.Streams) { - if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase)) { - stream.display_aspect_ratio = string.Empty; + stream.DisplayAspectRatio = string.Empty; } - if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase)) { - stream.sample_aspect_ratio = string.Empty; + stream.SampleAspectRatio = string.Empty; } } } - return new ProbeResultNormalizer(_logger, FileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); + return new ProbeResultNormalizer(_logger, _fileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); } } @@ -486,7 +492,7 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new ArgumentNullException(nameof(inputPath)); } - var tempExtractPath = Path.Combine(ConfigurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); + var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); 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 then scale to width 600. @@ -545,7 +551,6 @@ namespace MediaBrowser.MediaEncoding.Encoder args = string.Format("-ss {0} ", GetTimeParameter(offset.Value)) + args; } - var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder()); if (videoStream != null) { /* fix @@ -559,7 +564,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrWhiteSpace(container)) { - var inputFormat = encodinghelper.GetInputFormat(container); + var inputFormat = EncodingHelper.GetInputFormat(container); if (!string.IsNullOrWhiteSpace(inputFormat)) { args = "-f " + inputFormat + " " + args; @@ -570,7 +575,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFmpegPath, + FileName = _ffmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, @@ -579,7 +584,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - using (var processWrapper = new ProcessWrapper(process, this, _logger)) + using (var processWrapper = new ProcessWrapper(process, this)) { bool ranToCompletion; @@ -588,10 +593,10 @@ namespace MediaBrowser.MediaEncoding.Encoder { StartProcess(processWrapper); - var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs; + var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs; if (timeoutMs <= 0) { - timeoutMs = DefaultImageExtractionTimeoutMs; + timeoutMs = DefaultImageExtractionTimeout; } ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false); @@ -607,7 +612,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; - var file = FileSystem.GetFileInfo(tempExtractPath); + var file = _fileSystem.GetFileInfo(tempExtractPath); if (exitCode == -1 || !file.Exists || file.Length == 0) { @@ -675,7 +680,6 @@ namespace MediaBrowser.MediaEncoding.Encoder args = analyzeDurationArgument + " " + args; } - var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder()); if (videoStream != null) { /* fix @@ -689,7 +693,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrWhiteSpace(container)) { - var inputFormat = encodinghelper.GetInputFormat(container); + var inputFormat = EncodingHelper.GetInputFormat(container); if (!string.IsNullOrWhiteSpace(inputFormat)) { args = "-f " + inputFormat + " " + args; @@ -700,7 +704,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFmpegPath, + FileName = _ffmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, @@ -713,7 +717,7 @@ namespace MediaBrowser.MediaEncoding.Encoder bool ranToCompletion = false; - using (var processWrapper = new ProcessWrapper(process, this, _logger)) + using (var processWrapper = new ProcessWrapper(process, this)) { try { @@ -736,10 +740,10 @@ namespace MediaBrowser.MediaEncoding.Encoder cancellationToken.ThrowIfCancellationRequested(); - var jpegCount = FileSystem.GetFilePaths(targetDirectory) + var jpegCount = _fileSystem.GetFilePaths(targetDirectory) .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase)); - isResponsive = (jpegCount > lastCount); + isResponsive = jpegCount > lastCount; lastCount = jpegCount; } @@ -770,11 +774,12 @@ namespace MediaBrowser.MediaEncoding.Encoder { process.Process.Start(); - lock (_runningProcesses) + lock (_runningProcessesLock) { _runningProcesses.Add(process); } } + private void StopProcess(ProcessWrapper process, int waitTimeMs) { try @@ -783,18 +788,16 @@ namespace MediaBrowser.MediaEncoding.Encoder { return; } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in WaitForExit"); - } - try - { _logger.LogInformation("Killing ffmpeg process"); process.Process.Kill(); } + catch (InvalidOperationException) + { + // The process has already exited or + // there is no process associated with this Process object. + } catch (Exception ex) { _logger.LogError(ex, "Error killing process"); @@ -804,7 +807,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private void StopProcesses() { List<ProcessWrapper> proceses; - lock (_runningProcesses) + lock (_runningProcessesLock) { proceses = _runningProcesses.ToList(); _runningProcesses.Clear(); @@ -827,12 +830,11 @@ namespace MediaBrowser.MediaEncoding.Encoder return path.Replace('\\', '/').Replace(":", "\\:").Replace("'", "'\\\\\\''"); } - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> + /// <inheritdoc /> public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// <summary> @@ -852,11 +854,6 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new NotImplementedException(); } - public string[] GetPlayableStreamFileNames(string path, VideoType videoType) - { - throw new NotImplementedException(); - } - public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber) { throw new NotImplementedException(); @@ -870,21 +867,24 @@ namespace MediaBrowser.MediaEncoding.Encoder private class ProcessWrapper : IDisposable { - public readonly IProcess Process; - public bool HasExited; - public int? ExitCode; private readonly MediaEncoder _mediaEncoder; - private readonly ILogger _logger; - public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder, ILogger logger) + private bool _disposed = false; + + public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder) { Process = process; _mediaEncoder = mediaEncoder; - _logger = logger; - Process.Exited += Process_Exited; + Process.Exited += OnProcessExited; } - void Process_Exited(object sender, EventArgs e) + public IProcess Process { get; } + + public bool HasExited { get; private set; } + + public int? ExitCode { get; private set; } + + void OnProcessExited(object sender, EventArgs e) { var process = (IProcess)sender; @@ -903,7 +903,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private void DisposeProcess(IProcess process) { - lock (_mediaEncoder._runningProcesses) + lock (_mediaEncoder._runningProcessesLock) { _mediaEncoder._runningProcesses.Remove(this); } @@ -917,23 +917,18 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - private bool _disposed; - private readonly object _syncLock = new object(); public void Dispose() { - lock (_syncLock) + if (!_disposed) { - if (!_disposed) + if (Process != null) { - if (Process != null) - { - Process.Exited -= Process_Exited; - DisposeProcess(Process); - } + Process.Exited -= OnProcessExited; + DisposeProcess(Process); } - - _disposed = true; } + + _disposed = true; } } } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index e977bd8fe..783457bda 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -11,13 +11,13 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="..\BDInfo\BDInfo.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> </ItemGroup> <ItemGroup> + <PackageReference Include="BDInfo" Version="0.7.6.1" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="4.6.0" /> <PackageReference Include="UTF.Unknown" Version="2.2.0" /> </ItemGroup> diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index e4eabaf38..78dc7b607 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -16,24 +16,19 @@ namespace MediaBrowser.MediaEncoding.Probing throw new ArgumentNullException(nameof(result)); } - if (result.format != null && result.format.tags != null) + if (result.Format != null && result.Format.Tags != null) { - result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags); + result.Format.Tags = ConvertDictionaryToCaseInsensitive(result.Format.Tags); } - if (result.streams != null) + if (result.Streams != null) { // Convert all dictionaries to case insensitive - foreach (var stream in result.streams) + foreach (var stream in result.Streams) { - if (stream.tags != null) + if (stream.Tags != null) { - stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags); - } - - if (stream.disposition != null) - { - stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition); + stream.Tags = ConvertDictionaryToCaseInsensitive(stream.Tags); } } } @@ -45,7 +40,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// <param name="tags">The tags.</param> /// <param name="key">The key.</param> /// <returns>System.String.</returns> - public static string GetDictionaryValue(Dictionary<string, string> tags, string key) + public static string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key) { if (tags == null) { @@ -103,7 +98,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <param name="dict">The dict.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private static Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict) + private static Dictionary<string, string> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string> dict) { return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase); } diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs index cc9d27608..0e319c1a8 100644 --- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs +++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; namespace MediaBrowser.MediaEncoding.Probing { /// <summary> - /// Class MediaInfoResult + /// Class MediaInfoResult. /// </summary> public class InternalMediaInfoResult { @@ -11,331 +12,21 @@ namespace MediaBrowser.MediaEncoding.Probing /// Gets or sets the streams. /// </summary> /// <value>The streams.</value> - public MediaStreamInfo[] streams { get; set; } + [JsonPropertyName("streams")] + public IReadOnlyList<MediaStreamInfo> Streams { get; set; } /// <summary> /// Gets or sets the format. /// </summary> /// <value>The format.</value> - public MediaFormatInfo format { get; set; } + [JsonPropertyName("format")] + public MediaFormatInfo Format { get; set; } /// <summary> /// Gets or sets the chapters. /// </summary> /// <value>The chapters.</value> - public MediaChapter[] Chapters { get; set; } - } - - public class MediaChapter - { - public int id { get; set; } - public string time_base { get; set; } - public long start { get; set; } - public string start_time { get; set; } - public long end { get; set; } - public string end_time { get; set; } - public Dictionary<string, string> tags { get; set; } - } - - /// <summary> - /// Represents a stream within the output - /// </summary> - public class MediaStreamInfo - { - /// <summary> - /// Gets or sets the index. - /// </summary> - /// <value>The index.</value> - public int index { get; set; } - - /// <summary> - /// Gets or sets the profile. - /// </summary> - /// <value>The profile.</value> - public string profile { get; set; } - - /// <summary> - /// Gets or sets the codec_name. - /// </summary> - /// <value>The codec_name.</value> - public string codec_name { get; set; } - - /// <summary> - /// Gets or sets the codec_long_name. - /// </summary> - /// <value>The codec_long_name.</value> - public string codec_long_name { get; set; } - - /// <summary> - /// Gets or sets the codec_type. - /// </summary> - /// <value>The codec_type.</value> - public string codec_type { get; set; } - - /// <summary> - /// Gets or sets the sample_rate. - /// </summary> - /// <value>The sample_rate.</value> - public string sample_rate { get; set; } - - /// <summary> - /// Gets or sets the channels. - /// </summary> - /// <value>The channels.</value> - public int channels { get; set; } - - /// <summary> - /// Gets or sets the channel_layout. - /// </summary> - /// <value>The channel_layout.</value> - public string channel_layout { get; set; } - - /// <summary> - /// Gets or sets the avg_frame_rate. - /// </summary> - /// <value>The avg_frame_rate.</value> - public string avg_frame_rate { get; set; } - - /// <summary> - /// Gets or sets the duration. - /// </summary> - /// <value>The duration.</value> - public string duration { get; set; } - - /// <summary> - /// Gets or sets the bit_rate. - /// </summary> - /// <value>The bit_rate.</value> - public string bit_rate { get; set; } - - /// <summary> - /// Gets or sets the width. - /// </summary> - /// <value>The width.</value> - public int width { get; set; } - - /// <summary> - /// Gets or sets the refs. - /// </summary> - /// <value>The refs.</value> - public int refs { get; set; } - - /// <summary> - /// Gets or sets the height. - /// </summary> - /// <value>The height.</value> - public int height { get; set; } - - /// <summary> - /// Gets or sets the display_aspect_ratio. - /// </summary> - /// <value>The display_aspect_ratio.</value> - public string display_aspect_ratio { get; set; } - - /// <summary> - /// Gets or sets the tags. - /// </summary> - /// <value>The tags.</value> - public Dictionary<string, string> tags { get; set; } - - /// <summary> - /// Gets or sets the bits_per_sample. - /// </summary> - /// <value>The bits_per_sample.</value> - public int bits_per_sample { get; set; } - - /// <summary> - /// Gets or sets the bits_per_raw_sample. - /// </summary> - /// <value>The bits_per_raw_sample.</value> - public int bits_per_raw_sample { get; set; } - - /// <summary> - /// Gets or sets the r_frame_rate. - /// </summary> - /// <value>The r_frame_rate.</value> - public string r_frame_rate { get; set; } - - /// <summary> - /// Gets or sets the has_b_frames. - /// </summary> - /// <value>The has_b_frames.</value> - public int has_b_frames { get; set; } - - /// <summary> - /// Gets or sets the sample_aspect_ratio. - /// </summary> - /// <value>The sample_aspect_ratio.</value> - public string sample_aspect_ratio { get; set; } - - /// <summary> - /// Gets or sets the pix_fmt. - /// </summary> - /// <value>The pix_fmt.</value> - public string pix_fmt { get; set; } - - /// <summary> - /// Gets or sets the level. - /// </summary> - /// <value>The level.</value> - public int level { get; set; } - - /// <summary> - /// Gets or sets the time_base. - /// </summary> - /// <value>The time_base.</value> - public string time_base { get; set; } - - /// <summary> - /// Gets or sets the start_time. - /// </summary> - /// <value>The start_time.</value> - public string start_time { get; set; } - - /// <summary> - /// Gets or sets the codec_time_base. - /// </summary> - /// <value>The codec_time_base.</value> - public string codec_time_base { get; set; } - - /// <summary> - /// Gets or sets the codec_tag. - /// </summary> - /// <value>The codec_tag.</value> - public string codec_tag { get; set; } - - /// <summary> - /// Gets or sets the codec_tag_string. - /// </summary> - /// <value>The codec_tag_string.</value> - public string codec_tag_string { get; set; } - - /// <summary> - /// Gets or sets the sample_fmt. - /// </summary> - /// <value>The sample_fmt.</value> - public string sample_fmt { get; set; } - - /// <summary> - /// Gets or sets the dmix_mode. - /// </summary> - /// <value>The dmix_mode.</value> - public string dmix_mode { get; set; } - - /// <summary> - /// Gets or sets the start_pts. - /// </summary> - /// <value>The start_pts.</value> - public string start_pts { get; set; } - - /// <summary> - /// Gets or sets the is_avc. - /// </summary> - /// <value>The is_avc.</value> - public string is_avc { get; set; } - - /// <summary> - /// Gets or sets the nal_length_size. - /// </summary> - /// <value>The nal_length_size.</value> - public string nal_length_size { get; set; } - - /// <summary> - /// Gets or sets the ltrt_cmixlev. - /// </summary> - /// <value>The ltrt_cmixlev.</value> - public string ltrt_cmixlev { get; set; } - - /// <summary> - /// Gets or sets the ltrt_surmixlev. - /// </summary> - /// <value>The ltrt_surmixlev.</value> - public string ltrt_surmixlev { get; set; } - - /// <summary> - /// Gets or sets the loro_cmixlev. - /// </summary> - /// <value>The loro_cmixlev.</value> - public string loro_cmixlev { get; set; } - - /// <summary> - /// Gets or sets the loro_surmixlev. - /// </summary> - /// <value>The loro_surmixlev.</value> - public string loro_surmixlev { get; set; } - - public string field_order { get; set; } - - /// <summary> - /// Gets or sets the disposition. - /// </summary> - /// <value>The disposition.</value> - public Dictionary<string, string> disposition { get; set; } - } - - /// <summary> - /// Class MediaFormat - /// </summary> - public class MediaFormatInfo - { - /// <summary> - /// Gets or sets the filename. - /// </summary> - /// <value>The filename.</value> - public string filename { get; set; } - - /// <summary> - /// Gets or sets the nb_streams. - /// </summary> - /// <value>The nb_streams.</value> - public int nb_streams { get; set; } - - /// <summary> - /// Gets or sets the format_name. - /// </summary> - /// <value>The format_name.</value> - public string format_name { get; set; } - - /// <summary> - /// Gets or sets the format_long_name. - /// </summary> - /// <value>The format_long_name.</value> - public string format_long_name { get; set; } - - /// <summary> - /// Gets or sets the start_time. - /// </summary> - /// <value>The start_time.</value> - public string start_time { get; set; } - - /// <summary> - /// Gets or sets the duration. - /// </summary> - /// <value>The duration.</value> - public string duration { get; set; } - - /// <summary> - /// Gets or sets the size. - /// </summary> - /// <value>The size.</value> - public string size { get; set; } - - /// <summary> - /// Gets or sets the bit_rate. - /// </summary> - /// <value>The bit_rate.</value> - public string bit_rate { get; set; } - - /// <summary> - /// Gets or sets the probe_score. - /// </summary> - /// <value>The probe_score.</value> - public int probe_score { get; set; } - - /// <summary> - /// Gets or sets the tags. - /// </summary> - /// <value>The tags.</value> - public Dictionary<string, string> tags { get; set; } + [JsonPropertyName("chapters")] + public IReadOnlyList<MediaChapter> Chapters { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs new file mode 100644 index 000000000..6a45ccf49 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace MediaBrowser.MediaEncoding.Probing +{ + /// <summary> + /// Class MediaChapter. + /// </summary> + public class MediaChapter + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("time_base")] + public string TimeBase { get; set; } + + [JsonPropertyName("start")] + public long Start { get; set; } + + [JsonPropertyName("start_time")] + public string StartTime { get; set; } + + [JsonPropertyName("end")] + public long End { get; set; } + + [JsonPropertyName("end_time")] + public string EndTime { get; set; } + + [JsonPropertyName("tags")] + public IReadOnlyDictionary<string, string> Tags { get; set; } + } +} diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs new file mode 100644 index 000000000..8af122ef9 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace MediaBrowser.MediaEncoding.Probing +{ + /// <summary> + /// Class MediaFormat. + /// </summary> + public class MediaFormatInfo + { + /// <summary> + /// Gets or sets the filename. + /// </summary> + /// <value>The filename.</value> + [JsonPropertyName("filename")] + public string FileName { get; set; } + + /// <summary> + /// Gets or sets the nb_streams. + /// </summary> + /// <value>The nb_streams.</value> + [JsonPropertyName("nb_streams")] + public int NbStreams { get; set; } + + /// <summary> + /// Gets or sets the format_name. + /// </summary> + /// <value>The format_name.</value> + [JsonPropertyName("format_name")] + public string FormatName { get; set; } + + /// <summary> + /// Gets or sets the format_long_name. + /// </summary> + /// <value>The format_long_name.</value> + [JsonPropertyName("format_long_name")] + public string FormatLongName { get; set; } + + /// <summary> + /// Gets or sets the start_time. + /// </summary> + /// <value>The start_time.</value> + [JsonPropertyName("start_time")] + public string StartTime { get; set; } + + /// <summary> + /// Gets or sets the duration. + /// </summary> + /// <value>The duration.</value> + [JsonPropertyName("duration")] + public string Duration { get; set; } + + /// <summary> + /// Gets or sets the size. + /// </summary> + /// <value>The size.</value> + [JsonPropertyName("size")] + public string Size { get; set; } + + /// <summary> + /// Gets or sets the bit_rate. + /// </summary> + /// <value>The bit_rate.</value> + [JsonPropertyName("bit_rate")] + public string BitRate { get; set; } + + /// <summary> + /// Gets or sets the probe_score. + /// </summary> + /// <value>The probe_score.</value> + [JsonPropertyName("probe_score")] + public int ProbeScore { get; set; } + + /// <summary> + /// Gets or sets the tags. + /// </summary> + /// <value>The tags.</value> + [JsonPropertyName("tags")] + public IReadOnlyDictionary<string, string> Tags { get; set; } + } +} diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs new file mode 100644 index 000000000..7fa7afa5b --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -0,0 +1,282 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using MediaBrowser.Common.Json.Converters; + +namespace MediaBrowser.MediaEncoding.Probing +{ + /// <summary> + /// Represents a stream within the output. + /// </summary> + public class MediaStreamInfo + { + /// <summary> + /// Gets or sets the index. + /// </summary> + /// <value>The index.</value> + [JsonPropertyName("index")] + public int Index { get; set; } + + /// <summary> + /// Gets or sets the profile. + /// </summary> + /// <value>The profile.</value> + [JsonPropertyName("profile")] + public string Profile { get; set; } + + /// <summary> + /// Gets or sets the codec_name. + /// </summary> + /// <value>The codec_name.</value> + [JsonPropertyName("codec_name")] + public string CodecName { get; set; } + + /// <summary> + /// Gets or sets the codec_long_name. + /// </summary> + /// <value>The codec_long_name.</value> + [JsonPropertyName("codec_long_name")] + public string CodecLongName { get; set; } + + /// <summary> + /// Gets or sets the codec_type. + /// </summary> + /// <value>The codec_type.</value> + [JsonPropertyName("codec_type")] + public string CodecType { get; set; } + + /// <summary> + /// Gets or sets the sample_rate. + /// </summary> + /// <value>The sample_rate.</value> + [JsonPropertyName("sample_rate")] + public string SampleRate { get; set; } + + /// <summary> + /// Gets or sets the channels. + /// </summary> + /// <value>The channels.</value> + [JsonPropertyName("channels")] + public int Channels { get; set; } + + /// <summary> + /// Gets or sets the channel_layout. + /// </summary> + /// <value>The channel_layout.</value> + [JsonPropertyName("channel_layout")] + public string ChannelLayout { get; set; } + + /// <summary> + /// Gets or sets the avg_frame_rate. + /// </summary> + /// <value>The avg_frame_rate.</value> + [JsonPropertyName("avg_frame_rate")] + public string AverageFrameRate { get; set; } + + /// <summary> + /// Gets or sets the duration. + /// </summary> + /// <value>The duration.</value> + [JsonPropertyName("duration")] + public string Duration { get; set; } + + /// <summary> + /// Gets or sets the bit_rate. + /// </summary> + /// <value>The bit_rate.</value> + [JsonPropertyName("bit_rate")] + public string BitRate { get; set; } + + /// <summary> + /// Gets or sets the width. + /// </summary> + /// <value>The width.</value> + [JsonPropertyName("width")] + public int Width { get; set; } + + /// <summary> + /// Gets or sets the refs. + /// </summary> + /// <value>The refs.</value> + [JsonPropertyName("refs")] + public int Refs { get; set; } + + /// <summary> + /// Gets or sets the height. + /// </summary> + /// <value>The height.</value> + [JsonPropertyName("height")] + public int Height { get; set; } + + /// <summary> + /// Gets or sets the display_aspect_ratio. + /// </summary> + /// <value>The display_aspect_ratio.</value> + [JsonPropertyName("display_aspect_ratio")] + public string DisplayAspectRatio { get; set; } + + /// <summary> + /// Gets or sets the tags. + /// </summary> + /// <value>The tags.</value> + [JsonPropertyName("tags")] + public IReadOnlyDictionary<string, string> Tags { get; set; } + + /// <summary> + /// Gets or sets the bits_per_sample. + /// </summary> + /// <value>The bits_per_sample.</value> + [JsonPropertyName("bits_per_sample")] + public int BitsPerSample { get; set; } + + /// <summary> + /// Gets or sets the bits_per_raw_sample. + /// </summary> + /// <value>The bits_per_raw_sample.</value> + [JsonPropertyName("bits_per_raw_sample")] + [JsonConverter(typeof(JsonInt32Converter))] + public int BitsPerRawSample { get; set; } + + /// <summary> + /// Gets or sets the r_frame_rate. + /// </summary> + /// <value>The r_frame_rate.</value> + [JsonPropertyName("r_frame_rate")] + public string RFrameRate { get; set; } + + /// <summary> + /// Gets or sets the has_b_frames. + /// </summary> + /// <value>The has_b_frames.</value> + [JsonPropertyName("has_b_frames")] + public int HasBFrames { get; set; } + + /// <summary> + /// Gets or sets the sample_aspect_ratio. + /// </summary> + /// <value>The sample_aspect_ratio.</value> + [JsonPropertyName("sample_aspect_ratio")] + public string SampleAspectRatio { get; set; } + + /// <summary> + /// Gets or sets the pix_fmt. + /// </summary> + /// <value>The pix_fmt.</value> + [JsonPropertyName("pix_fmt")] + public string PixelFormat { get; set; } + + /// <summary> + /// Gets or sets the level. + /// </summary> + /// <value>The level.</value> + [JsonPropertyName("level")] + public int Level { get; set; } + + /// <summary> + /// Gets or sets the time_base. + /// </summary> + /// <value>The time_base.</value> + [JsonPropertyName("time_base")] + public string TimeBase { get; set; } + + /// <summary> + /// Gets or sets the start_time. + /// </summary> + /// <value>The start_time.</value> + [JsonPropertyName("start_time")] + public string StartTime { get; set; } + + /// <summary> + /// Gets or sets the codec_time_base. + /// </summary> + /// <value>The codec_time_base.</value> + [JsonPropertyName("codec_time_base")] + public string CodecTimeBase { get; set; } + + /// <summary> + /// Gets or sets the codec_tag. + /// </summary> + /// <value>The codec_tag.</value> + [JsonPropertyName("codec_tag")] + public string CodecTag { get; set; } + + /// <summary> + /// Gets or sets the codec_tag_string. + /// </summary> + /// <value>The codec_tag_string.</value> + [JsonPropertyName("codec_tag_string")] + public string CodecTagString { get; set; } + + /// <summary> + /// Gets or sets the sample_fmt. + /// </summary> + /// <value>The sample_fmt.</value> + [JsonPropertyName("sample_fmt")] + public string SampleFmt { get; set; } + + /// <summary> + /// Gets or sets the dmix_mode. + /// </summary> + /// <value>The dmix_mode.</value> + [JsonPropertyName("dmix_mode")] + public string DmixMode { get; set; } + + /// <summary> + /// Gets or sets the start_pts. + /// </summary> + /// <value>The start_pts.</value> + [JsonPropertyName("start_pts")] + public int StartPts { get; set; } + + /// <summary> + /// Gets or sets the is_avc. + /// </summary> + /// <value>The is_avc.</value> + [JsonPropertyName("is_avc")] + public string IsAvc { get; set; } + + /// <summary> + /// Gets or sets the nal_length_size. + /// </summary> + /// <value>The nal_length_size.</value> + [JsonPropertyName("nal_length_size")] + public string NalLengthSize { get; set; } + + /// <summary> + /// Gets or sets the ltrt_cmixlev. + /// </summary> + /// <value>The ltrt_cmixlev.</value> + [JsonPropertyName("ltrt_cmixlev")] + public string LtrtCmixlev { get; set; } + + /// <summary> + /// Gets or sets the ltrt_surmixlev. + /// </summary> + /// <value>The ltrt_surmixlev.</value> + [JsonPropertyName("ltrt_surmixlev")] + public string LtrtSurmixlev { get; set; } + + /// <summary> + /// Gets or sets the loro_cmixlev. + /// </summary> + /// <value>The loro_cmixlev.</value> + [JsonPropertyName("loro_cmixlev")] + public string LoroCmixlev { get; set; } + + /// <summary> + /// Gets or sets the loro_surmixlev. + /// </summary> + /// <value>The loro_surmixlev.</value> + [JsonPropertyName("loro_surmixlev")] + public string LoroSurmixlev { get; set; } + + [JsonPropertyName("field_order")] + public string FieldOrder { get; set; } + + /// <summary> + /// Gets or sets the disposition. + /// </summary> + /// <value>The disposition.</value> + [JsonPropertyName("disposition")] + public IReadOnlyDictionary<string, int> Disposition { get; set; } + } +} diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 54d02fc9f..bd89c6cae 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -8,7 +8,6 @@ using System.Xml; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -41,21 +40,25 @@ namespace MediaBrowser.MediaEncoding.Probing FFProbeHelpers.NormalizeFFProbeResult(data); SetSize(data, info); - var internalStreams = data.streams ?? new MediaStreamInfo[] { }; + var internalStreams = data.Streams ?? new MediaStreamInfo[] { }; - info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.format)) + info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format)) .Where(i => i != null) // Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them .Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec)) .ToList(); - if (data.format != null) + info.MediaAttachments = internalStreams.Select(s => GetMediaAttachment(s)) + .Where(i => i != null) + .ToList(); + + if (data.Format != null) { - info.Container = NormalizeFormat(data.format.format_name); + info.Container = NormalizeFormat(data.Format.FormatName); - if (!string.IsNullOrEmpty(data.format.bit_rate)) + if (!string.IsNullOrEmpty(data.Format.BitRate)) { - if (int.TryParse(data.format.bit_rate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(data.Format.BitRate, NumberStyles.Any, _usCulture, out var value)) { info.Bitrate = value; } @@ -65,22 +68,22 @@ namespace MediaBrowser.MediaEncoding.Probing var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var tagStreamType = isAudio ? "audio" : "video"; - if (data.streams != null) + if (data.Streams != null) { - var tagStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, tagStreamType, StringComparison.OrdinalIgnoreCase)); + var tagStream = data.Streams.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase)); - if (tagStream != null && tagStream.tags != null) + if (tagStream != null && tagStream.Tags != null) { - foreach (var pair in tagStream.tags) + foreach (var pair in tagStream.Tags) { tags[pair.Key] = pair.Value; } } } - if (data.format != null && data.format.tags != null) + if (data.Format != null && data.Format.Tags != null) { - foreach (var pair in data.format.tags) + foreach (var pair in data.Format.Tags) { tags[pair.Key] = pair.Value; } @@ -153,9 +156,9 @@ namespace MediaBrowser.MediaEncoding.Probing FetchFromItunesInfo(itunesXml, info); } - if (data.format != null && !string.IsNullOrEmpty(data.format.duration)) + 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, _usCulture)).Ticks; } FetchWtvInfo(info, data); @@ -514,6 +517,39 @@ namespace MediaBrowser.MediaEncoding.Probing } /// <summary> + /// Converts ffprobe stream info to our MediaAttachment class + /// </summary> + /// <param name="streamInfo">The stream info.</param> + /// <returns>MediaAttachments.</returns> + private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo) + { + if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var attachment = new MediaAttachment + { + Codec = streamInfo.CodecName, + Index = streamInfo.Index + }; + + if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString)) + { + attachment.CodecTag = streamInfo.CodecTagString; + } + + if (streamInfo.Tags != null) + { + attachment.FileName = GetDictionaryValue(streamInfo.Tags, "filename"); + attachment.MimeType = GetDictionaryValue(streamInfo.Tags, "mimetype"); + attachment.Comment = GetDictionaryValue(streamInfo.Tags, "comment"); + } + + return attachment; + } + + /// <summary> /// Converts ffprobe stream info to our MediaStream class /// </summary> /// <param name="isAudio">if set to <c>true</c> [is info].</param> @@ -523,7 +559,7 @@ namespace MediaBrowser.MediaEncoding.Probing private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo) { // These are mp4 chapters - if (string.Equals(streamInfo.codec_name, "mov_text", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase)) { // Edit: but these are also sometimes subtitles? //return null; @@ -531,71 +567,71 @@ namespace MediaBrowser.MediaEncoding.Probing var stream = new MediaStream { - Codec = streamInfo.codec_name, - Profile = streamInfo.profile, - Level = streamInfo.level, - Index = streamInfo.index, - PixelFormat = streamInfo.pix_fmt, - NalLengthSize = streamInfo.nal_length_size, - TimeBase = streamInfo.time_base, - CodecTimeBase = streamInfo.codec_time_base + Codec = streamInfo.CodecName, + Profile = streamInfo.Profile, + Level = streamInfo.Level, + Index = streamInfo.Index, + PixelFormat = streamInfo.PixelFormat, + NalLengthSize = streamInfo.NalLengthSize, + TimeBase = streamInfo.TimeBase, + CodecTimeBase = streamInfo.CodecTimeBase }; - if (string.Equals(streamInfo.is_avc, "true", StringComparison.OrdinalIgnoreCase) || - string.Equals(streamInfo.is_avc, "1", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(streamInfo.IsAvc, "true", StringComparison.OrdinalIgnoreCase) || + string.Equals(streamInfo.IsAvc, "1", StringComparison.OrdinalIgnoreCase)) { stream.IsAVC = true; } - else if (string.Equals(streamInfo.is_avc, "false", StringComparison.OrdinalIgnoreCase) || - string.Equals(streamInfo.is_avc, "0", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(streamInfo.IsAvc, "false", StringComparison.OrdinalIgnoreCase) || + string.Equals(streamInfo.IsAvc, "0", StringComparison.OrdinalIgnoreCase)) { stream.IsAVC = false; } - if (!string.IsNullOrWhiteSpace(streamInfo.field_order) && !string.Equals(streamInfo.field_order, "progressive", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(streamInfo.FieldOrder) && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase)) { stream.IsInterlaced = true; } // Filter out junk - if (!string.IsNullOrWhiteSpace(streamInfo.codec_tag_string) && streamInfo.codec_tag_string.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1) + if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && streamInfo.CodecTagString.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1) { - stream.CodecTag = streamInfo.codec_tag_string; + stream.CodecTag = streamInfo.CodecTagString; } - if (streamInfo.tags != null) + if (streamInfo.Tags != null) { - stream.Language = GetDictionaryValue(streamInfo.tags, "language"); - stream.Comment = GetDictionaryValue(streamInfo.tags, "comment"); - stream.Title = GetDictionaryValue(streamInfo.tags, "title"); + stream.Language = GetDictionaryValue(streamInfo.Tags, "language"); + stream.Comment = GetDictionaryValue(streamInfo.Tags, "comment"); + stream.Title = GetDictionaryValue(streamInfo.Tags, "title"); } - if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Audio; - stream.Channels = streamInfo.channels; + stream.Channels = streamInfo.Channels; - if (!string.IsNullOrEmpty(streamInfo.sample_rate)) + if (!string.IsNullOrEmpty(streamInfo.SampleRate)) { - if (int.TryParse(streamInfo.sample_rate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, _usCulture, out var value)) { stream.SampleRate = value; } } - stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout); + stream.ChannelLayout = ParseChannelLayout(streamInfo.ChannelLayout); - if (streamInfo.bits_per_sample > 0) + if (streamInfo.BitsPerSample > 0) { - stream.BitDepth = streamInfo.bits_per_sample; + stream.BitDepth = streamInfo.BitsPerSample; } - else if (streamInfo.bits_per_raw_sample > 0) + else if (streamInfo.BitsPerRawSample > 0) { - stream.BitDepth = streamInfo.bits_per_raw_sample; + stream.BitDepth = streamInfo.BitsPerRawSample; } } - else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Subtitle; stream.Codec = NormalizeSubtitleCodec(stream.Codec); @@ -603,14 +639,14 @@ namespace MediaBrowser.MediaEncoding.Probing stream.localizedDefault = _localization.GetLocalizedString("Default"); stream.localizedForced = _localization.GetLocalizedString("Forced"); } - else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase)) + 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.avg_frame_rate); - stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate); + 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)) @@ -635,17 +671,17 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Video; } - stream.Width = streamInfo.width; - stream.Height = streamInfo.height; + stream.Width = streamInfo.Width; + stream.Height = streamInfo.Height; stream.AspectRatio = GetAspectRatio(streamInfo); - if (streamInfo.bits_per_sample > 0) + if (streamInfo.BitsPerSample > 0) { - stream.BitDepth = streamInfo.bits_per_sample; + stream.BitDepth = streamInfo.BitsPerSample; } - else if (streamInfo.bits_per_raw_sample > 0) + else if (streamInfo.BitsPerRawSample > 0) { - stream.BitDepth = streamInfo.bits_per_raw_sample; + stream.BitDepth = streamInfo.BitsPerRawSample; } //stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) || @@ -653,11 +689,11 @@ namespace MediaBrowser.MediaEncoding.Probing // string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase); // http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe - stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase); + stream.IsAnamorphic = string.Equals(streamInfo.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase); - if (streamInfo.refs > 0) + if (streamInfo.Refs > 0) { - stream.RefFrames = streamInfo.refs; + stream.RefFrames = streamInfo.Refs; } } else @@ -668,18 +704,18 @@ namespace MediaBrowser.MediaEncoding.Probing // Get stream bitrate var bitrate = 0; - if (!string.IsNullOrEmpty(streamInfo.bit_rate)) + if (!string.IsNullOrEmpty(streamInfo.BitRate)) { - if (int.TryParse(streamInfo.bit_rate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, _usCulture, out var value)) { bitrate = value; } } - if (bitrate == 0 && formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video) + if (bitrate == 0 && formatInfo != null && !string.IsNullOrEmpty(formatInfo.BitRate) && stream.Type == MediaStreamType.Video) { // If the stream info doesn't have a bitrate get the value from the media format info - if (int.TryParse(formatInfo.bit_rate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value)) { bitrate = value; } @@ -690,14 +726,18 @@ namespace MediaBrowser.MediaEncoding.Probing stream.BitRate = bitrate; } - if (streamInfo.disposition != null) + var disposition = streamInfo.Disposition; + if (disposition != null) { - var isDefault = GetDictionaryValue(streamInfo.disposition, "default"); - var isForced = GetDictionaryValue(streamInfo.disposition, "forced"); - - stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase); + if (disposition.GetValueOrDefault("default") == 1) + { + stream.IsDefault = true; + } - stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase); + if (disposition.GetValueOrDefault("forced") == 1) + { + stream.IsForced = true; + } } NormalizeStreamTitle(stream); @@ -724,7 +764,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// <param name="tags">The tags.</param> /// <param name="key">The key.</param> /// <returns>System.String.</returns> - private string GetDictionaryValue(Dictionary<string, string> tags, string key) + private string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key) { if (tags == null) { @@ -747,7 +787,7 @@ namespace MediaBrowser.MediaEncoding.Probing private string GetAspectRatio(MediaStreamInfo info) { - var original = info.display_aspect_ratio; + var original = info.DisplayAspectRatio; var parts = (original ?? string.Empty).Split(':'); if (!(parts.Length == 2 && @@ -756,8 +796,8 @@ namespace MediaBrowser.MediaEncoding.Probing width > 0 && height > 0)) { - width = info.width; - height = info.height; + width = info.Width; + height = info.Height; } if (width > 0 && height > 0) @@ -850,20 +890,20 @@ namespace MediaBrowser.MediaEncoding.Probing private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data) { - if (result.streams != null) + if (result.Streams != null) { // Get the first info stream - var stream = result.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase)); + var stream = result.Streams.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase)); if (stream != null) { // Get duration from stream properties - var duration = stream.duration; + var duration = stream.Duration; // If it's not there go into format properties if (string.IsNullOrEmpty(duration)) { - duration = result.format.duration; + duration = result.Format.Duration; } // If we got something, parse it @@ -877,11 +917,11 @@ 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)) + if (!string.IsNullOrEmpty(data.Format.Size)) { - info.Size = long.Parse(data.format.size, _usCulture); + info.Size = long.Parse(data.Format.Size, _usCulture); } else { @@ -1194,16 +1234,16 @@ namespace MediaBrowser.MediaEncoding.Probing { var info = new ChapterInfo(); - if (chapter.tags != null) + if (chapter.Tags != null) { - if (chapter.tags.TryGetValue("title", out string name)) + if (chapter.Tags.TryGetValue("title", out string name)) { info.Name = name; } } // Limit accuracy to milliseconds to match xml saving - var secondsString = chapter.start_time; + var secondsString = chapter.StartTime; if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) { @@ -1218,12 +1258,12 @@ namespace MediaBrowser.MediaEncoding.Probing private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data) { - if (data.format == null || data.format.tags == null) + if (data.Format == null || data.Format.Tags == null) { return; } - var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre"); + var genres = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/Genre"); if (!string.IsNullOrWhiteSpace(genres)) { @@ -1239,14 +1279,14 @@ namespace MediaBrowser.MediaEncoding.Probing } } - var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating"); + var officialRating = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/ParentalRating"); if (!string.IsNullOrWhiteSpace(officialRating)) { video.OfficialRating = officialRating; } - var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits"); + var people = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaCredits"); if (!string.IsNullOrEmpty(people)) { @@ -1256,7 +1296,7 @@ namespace MediaBrowser.MediaEncoding.Probing .ToArray(); } - var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime"); + var year = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/OriginalReleaseTime"); if (!string.IsNullOrWhiteSpace(year)) { if (int.TryParse(year, NumberStyles.Integer, _usCulture, out var val)) @@ -1265,7 +1305,7 @@ namespace MediaBrowser.MediaEncoding.Probing } } - var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime"); + var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaOriginalBroadcastDateTime"); if (!string.IsNullOrWhiteSpace(premiereDateString)) { // Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ @@ -1276,9 +1316,9 @@ namespace MediaBrowser.MediaEncoding.Probing } } - var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription"); + var description = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitleDescription"); - var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle"); + var subTitle = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitle"); // For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ @@ -1334,24 +1374,25 @@ namespace MediaBrowser.MediaEncoding.Probing { video.Timestamp = GetMpegTimestamp(video.Path); - _logger.LogDebug("Video has {timestamp} timestamp", video.Timestamp); + _logger.LogDebug("Video has {Timestamp} timestamp", video.Timestamp); } catch (Exception ex) { - _logger.LogError(ex, "Error extracting timestamp info from {path}", video.Path); + _logger.LogError(ex, "Error extracting timestamp info from {Path}", video.Path); video.Timestamp = null; } } } } + // REVIEW: find out why the byte array needs to be 197 bytes long and comment the reason private TransportStreamTimestamp GetMpegTimestamp(string path) { - var packetBuffer = new byte['Å']; + var packetBuffer = new byte[197]; - using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { - fs.Read(packetBuffer, 0, packetBuffer.Length); + fs.Read(packetBuffer); } if (packetBuffer[0] == 71) @@ -1359,7 +1400,7 @@ namespace MediaBrowser.MediaEncoding.Probing return TransportStreamTimestamp.None; } - if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71)) + if ((packetBuffer[4] == 71) && (packetBuffer[196] == 71)) { if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0)) { diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs index 3401c2d67..dec714121 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles { /// <summary> - /// Interface ISubtitleWriter + /// Interface ISubtitleWriter. /// </summary> public interface ISubtitleWriter { diff --git a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs index 8995fcfe1..1b452b0ce 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs @@ -1,27 +1,43 @@ using System.IO; -using System.Text; +using System.Text.Json; using System.Threading; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.MediaEncoding.Subtitles { + /// <summary> + /// JSON subtitle writer. + /// </summary> public class JsonWriter : ISubtitleWriter { - private readonly IJsonSerializer _json; - - public JsonWriter(IJsonSerializer json) - { - _json = json; - } - + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { - using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + using (var writer = new Utf8JsonWriter(stream)) { - var json = _json.SerializeToString(info); + var trackevents = info.TrackEvents; + writer.WriteStartObject(); + writer.WriteStartArray("TrackEvents"); + + for (int i = 0; i < trackevents.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var current = trackevents[i]; + writer.WriteStartObject(); + + writer.WriteString("Id", current.Id); + writer.WriteString("Text", current.Text); + writer.WriteNumber("StartPositionTicks", current.StartPositionTicks); + writer.WriteNumber("EndPositionTicks", current.EndPositionTicks); + + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); - writer.Write(json); + writer.Flush(); } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs index 6f96a641e..45b317b2e 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs @@ -14,14 +14,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { - var index = 1; + var trackEvents = info.TrackEvents; - foreach (var trackEvent in info.TrackEvents) + for (int i = 0; i < trackEvents.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); - writer.WriteLine(index.ToString(CultureInfo.InvariantCulture)); - writer.WriteLine(@"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}", TimeSpan.FromTicks(trackEvent.StartPositionTicks), TimeSpan.FromTicks(trackEvent.EndPositionTicks)); + var trackEvent = trackEvents[i]; + + writer.WriteLine((i + 1).ToString(CultureInfo.InvariantCulture)); + writer.WriteLine( + @"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}", + TimeSpan.FromTicks(trackEvent.StartPositionTicks), + TimeSpan.FromTicks(trackEvent.EndPositionTicks)); var text = trackEvent.Text; @@ -29,9 +34,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); writer.WriteLine(text); - writer.WriteLine(string.Empty); - - index++; + writer.WriteLine(); } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index d5fa76c3a..183d7566d 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -17,7 +17,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; using UtfUnknown; @@ -30,28 +29,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; - private readonly IJsonSerializer _json; private readonly IHttpClient _httpClient; private readonly IMediaSourceManager _mediaSourceManager; private readonly IProcessFactory _processFactory; public SubtitleEncoder( ILibraryManager libraryManager, - ILoggerFactory loggerFactory, + ILogger<SubtitleEncoder> logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, - IJsonSerializer json, IHttpClient httpClient, IMediaSourceManager mediaSourceManager, IProcessFactory processFactory) { _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(nameof(SubtitleEncoder)); + _logger = logger; _appPaths = appPaths; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; - _json = json; _httpClient = httpClient; _mediaSourceManager = mediaSourceManager; _processFactory = processFactory; @@ -59,7 +55,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); - private Stream ConvertSubtitles(Stream stream, + private Stream ConvertSubtitles( + Stream stream, string inputFormat, string outputFormat, long startTimeTicks, @@ -170,7 +167,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles && (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd)) { var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSource.Id)); - inputFiles = mediaSourceItem.GetPlayableStreamFileNames(_mediaEncoder); + inputFiles = mediaSourceItem.GetPlayableStreamFileNames(); } else { @@ -179,32 +176,27 @@ namespace MediaBrowser.MediaEncoding.Subtitles var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false); - var stream = await GetSubtitleStream(fileInfo.Path, subtitleStream.Language, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); + var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); return (stream, fileInfo.Format); } - private async Task<Stream> GetSubtitleStream(string path, string language, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken) + private async Task<Stream> GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken) { if (requiresCharset) { - var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false); - - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; - _logger.LogDebug("charset {CharSet} detected for {Path}", charset ?? "null", path); - - if (!string.IsNullOrEmpty(charset)) + using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) { - // Make sure we have all the code pages we can get - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - using (var inputStream = new MemoryStream(bytes)) - using (var reader = new StreamReader(inputStream, Encoding.GetEncoding(charset))) + var result = CharsetDetector.DetectFromStream(stream).Detected; + + if (result != null) { - var text = await reader.ReadToEndAsync().ConfigureAwait(false); + _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, path); - bytes = Encoding.UTF8.GetBytes(text); + using var reader = new StreamReader(stream, result.Encoding); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); - return new MemoryStream(bytes); + return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } } @@ -323,7 +315,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) { - return new JsonWriter(_json); + return new JsonWriter(); } if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) { @@ -544,7 +536,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!File.Exists(outputPath)) { - await ExtractTextSubtitleInternal(_mediaEncoder.GetInputArgument(inputFiles, protocol), subtitleStreamIndex, outputCodec, outputPath, cancellationToken).ConfigureAwait(false); + await ExtractTextSubtitleInternal( + _mediaEncoder.GetInputArgument(inputFiles, protocol), + subtitleStreamIndex, + outputCodec, + outputPath, + cancellationToken).ConfigureAwait(false); } } finally @@ -572,8 +569,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath, - subtitleStreamIndex, outputCodec, outputPath); + var processArgs = string.Format( + CultureInfo.InvariantCulture, + "-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", + inputPath, + subtitleStreamIndex, + outputCodec, + outputPath); var process = _processFactory.Create(new ProcessOptions { @@ -721,41 +723,38 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } + /// <inheritdoc /> public async Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken) { - var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false); - - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; + using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) + { + var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName; - _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path); + _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path); - return charset; + return charset; + } } - private async Task<byte[]> GetBytes(string path, MediaProtocol protocol, CancellationToken cancellationToken) + private Task<Stream> GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) { - if (protocol == MediaProtocol.Http) + switch (protocol) { - var opts = new HttpRequestOptions() - { - Url = path, - CancellationToken = cancellationToken - }; - using (var file = await _httpClient.Get(opts).ConfigureAwait(false)) - using (var memoryStream = new MemoryStream()) - { - await file.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; + case MediaProtocol.Http: + var opts = new HttpRequestOptions() + { + Url = path, + CancellationToken = cancellationToken, + BufferContent = true + }; - return memoryStream.ToArray(); - } - } - if (protocol == MediaProtocol.File) - { - return File.ReadAllBytes(path); - } + return _httpClient.Get(opts); - throw new ArgumentOutOfRangeException(nameof(protocol)); + case MediaProtocol.File: + return Task.FromResult<Stream>(File.OpenRead(path)); + default: + throw new ArgumentOutOfRangeException(nameof(protocol)); + } } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs index cdaf94964..4f15bac49 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs @@ -49,12 +49,5 @@ namespace MediaBrowser.MediaEncoding.Subtitles writer.WriteLine("</tt>"); } } - - private string FormatTime(long ticks) - { - var time = TimeSpan.FromTicks(ticks); - - return string.Format(@"{0:hh\:mm\:ss\,fff}", time); - } } } diff --git a/MediaBrowser.MediaEncoding/packages.config b/MediaBrowser.MediaEncoding/packages.config deleted file mode 100644 index bbeaf5f00..000000000 --- a/MediaBrowser.MediaEncoding/packages.config +++ /dev/null @@ -1,3 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<packages> -</packages>
\ No newline at end of file |
