diff options
| author | Luke <luke.pulverenti@gmail.com> | 2016-12-18 00:44:33 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2016-12-18 00:44:33 -0500 |
| commit | e7cebb91a73354dc3e0d0b6340c9fbd6511f4406 (patch) | |
| tree | 6f1c368c766c17b7514fe749c0e92e69cd89194a /MediaBrowser.MediaEncoding | |
| parent | 025905a3e4d50b9a2e07fbf4ff0a203af6604ced (diff) | |
| parent | aaa027f3229073e9a40756c3157d41af2a442922 (diff) | |
Merge pull request #2350 from MediaBrowser/beta
Beta
Diffstat (limited to 'MediaBrowser.MediaEncoding')
19 files changed, 856 insertions, 426 deletions
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs index e435c00f2..bf7343f3d 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -4,6 +4,8 @@ using MediaBrowser.Model.MediaInfo; using System; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Text; namespace MediaBrowser.MediaEncoding.BdInfo { @@ -12,6 +14,15 @@ namespace MediaBrowser.MediaEncoding.BdInfo /// </summary> public class BdInfoExaminer : IBlurayExaminer { + private readonly IFileSystem _fileSystem; + private readonly ITextEncoding _textEncoding; + + public BdInfoExaminer(IFileSystem fileSystem, ITextEncoding textEncoding) + { + _fileSystem = fileSystem; + _textEncoding = textEncoding; + } + /// <summary> /// Gets the disc info. /// </summary> @@ -19,7 +30,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo /// <returns>BlurayDiscInfo.</returns> public BlurayDiscInfo GetDiscInfo(string path) { - var bdrom = new BDROM(path); + var bdrom = new BDROM(path, _fileSystem, _textEncoding); bdrom.Scan(); diff --git a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs index 42a28d313..5beab746d 100644 --- a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs +++ b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs @@ -2,7 +2,9 @@ using MediaBrowser.Model.Configuration; using System.Collections.Generic; using System.IO; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; namespace MediaBrowser.MediaEncoding.Configuration { @@ -48,7 +50,7 @@ namespace MediaBrowser.MediaEncoding.Configuration // Validate if (!_fileSystem.DirectoryExists(newPath)) { - throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath)); + throw new FileNotFoundException(string.Format("{0} does not exist.", newPath)); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs index f42582270..5a554d26f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs @@ -7,13 +7,13 @@ using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.Diagnostics; namespace MediaBrowser.MediaEncoding.Encoder { public class AudioEncoder : BaseEncoder { - public AudioEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IIsoManager isoManager, ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager) : base(mediaEncoder, logger, configurationManager, fileSystem, isoManager, libraryManager, sessionManager, subtitleEncoder, mediaSourceManager) + public AudioEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IIsoManager isoManager, ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IProcessFactory processFactory) : base(mediaEncoder, logger, configurationManager, fileSystem, isoManager, libraryManager, sessionManager, subtitleEncoder, mediaSourceManager, processFactory) { } @@ -114,5 +114,6 @@ namespace MediaBrowser.MediaEncoding.Encoder { get { return false; } } + } } diff --git a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs index c7b78aae3..b8087fded 100644 --- a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs @@ -11,13 +11,12 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dlna; namespace MediaBrowser.MediaEncoding.Encoder @@ -33,6 +32,7 @@ namespace MediaBrowser.MediaEncoding.Encoder protected readonly ISessionManager SessionManager; protected readonly ISubtitleEncoder SubtitleEncoder; protected readonly IMediaSourceManager MediaSourceManager; + protected IProcessFactory ProcessFactory; protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); @@ -44,7 +44,7 @@ namespace MediaBrowser.MediaEncoding.Encoder ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, - IMediaSourceManager mediaSourceManager) + IMediaSourceManager mediaSourceManager, IProcessFactory processFactory) { MediaEncoder = mediaEncoder; Logger = logger; @@ -55,6 +55,7 @@ namespace MediaBrowser.MediaEncoding.Encoder SessionManager = sessionManager; SubtitleEncoder = subtitleEncoder; MediaSourceManager = mediaSourceManager; + ProcessFactory = processFactory; } public async Task<EncodingJob> Start(EncodingJobOptions options, @@ -73,27 +74,23 @@ namespace MediaBrowser.MediaEncoding.Encoder var commandLineArgs = await GetCommandLineArguments(encodingJob).ConfigureAwait(false); - var process = new Process + var process = ProcessFactory.Create(new ProcessOptions { - StartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - - // Must consume both stdout and stderr or deadlocks may occur - //RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, + CreateNoWindow = true, + UseShellExecute = false, - FileName = MediaEncoder.EncoderPath, - Arguments = commandLineArgs, + // Must consume both stdout and stderr or deadlocks may occur + //RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }, + FileName = MediaEncoder.EncoderPath, + Arguments = commandLineArgs, + IsHidden = true, + ErrorDialog = false, EnableRaisingEvents = true - }; + }); var workingDirectory = GetWorkingDirectory(options); if (!string.IsNullOrWhiteSpace(workingDirectory)) @@ -110,7 +107,7 @@ namespace MediaBrowser.MediaEncoding.Encoder FileSystem.CreateDirectory(Path.GetDirectoryName(logFilePath)); // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - encodingJob.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); + encodingJob.LogFileStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine); await encodingJob.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationToken).ConfigureAwait(false); @@ -147,7 +144,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return encodingJob; } - private void Cancel(Process process, EncodingJob job) + private void Cancel(IProcess process, EncodingJob job) { Logger.Info("Killing ffmpeg process for {0}", job.OutputFilePath); @@ -162,7 +159,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="process">The process.</param> /// <param name="job">The job.</param> - private void OnFfMpegProcessExited(Process process, EncodingJob job) + private void OnFfMpegProcessExited(IProcess process, EncodingJob job) { job.HasExited = true; @@ -215,7 +212,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } try { - job.TaskCompletionSource.TrySetException(new ApplicationException("Encoding failed")); + job.TaskCompletionSource.TrySetException(new Exception("Encoding failed")); } catch { @@ -773,7 +770,6 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { param = "-pix_fmt yuv420p " + param; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 7acff8fc8..b37e783b8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; +using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Logging; namespace MediaBrowser.MediaEncoding.Encoder @@ -8,10 +10,12 @@ namespace MediaBrowser.MediaEncoding.Encoder public class EncoderValidator { private readonly ILogger _logger; + private readonly IProcessFactory _processFactory; - public EncoderValidator(ILogger logger) + public EncoderValidator(ILogger logger, IProcessFactory processFactory) { _logger = logger; + _processFactory = processFactory; } public Tuple<List<string>, List<string>> Validate(string encoderPath) @@ -26,15 +30,19 @@ namespace MediaBrowser.MediaEncoding.Encoder return new Tuple<List<string>, List<string>>(decoders, encoders); } - public bool ValidateVersion(string encoderAppPath) + public bool ValidateVersion(string encoderAppPath, bool logOutput) { string output = string.Empty; try { output = GetProcessOutput(encoderAppPath, "-version"); } - catch + catch (Exception ex) { + if (logOutput) + { + _logger.ErrorException("Error validating encoder", ex); + } } if (string.IsNullOrWhiteSpace(output)) @@ -42,11 +50,27 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } + if (logOutput) + { + _logger.Info("ffmpeg info: {0}", output); + } + if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) { return false; } + output = " " + output + " "; + + for (var i = 2013; i <= 2015; i++) + { + var yearString = i.ToString(CultureInfo.InvariantCulture); + if (output.IndexOf(" " + yearString + " ", StringComparison.OrdinalIgnoreCase) != -1) + { + return false; + } + } + return true; } @@ -57,8 +81,9 @@ namespace MediaBrowser.MediaEncoding.Encoder { output = GetProcessOutput(encoderAppPath, "-decoders"); } - catch + catch (Exception ) { + //_logger.ErrorException("Error detecting available decoders", ex); } var found = new List<string>(); @@ -140,19 +165,16 @@ namespace MediaBrowser.MediaEncoding.Encoder private string GetProcessOutput(string path, string arguments) { - var process = new Process + var process = _processFactory.Create(new ProcessOptions { - StartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - FileName = path, - Arguments = arguments, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false, - RedirectStandardOutput = true - } - }; + CreateNoWindow = true, + UseShellExecute = false, + FileName = path, + Arguments = arguments, + IsHidden = true, + ErrorDialog = false, + RedirectStandardOutput = true + }); _logger.Info("Running {0} {1}", path, arguments); diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs index 0c7ff1b76..b5ff5cbb6 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs @@ -11,6 +11,7 @@ using MediaBrowser.Model.Net; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs index f811a8d48..e197bdb6f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs @@ -606,9 +606,12 @@ namespace MediaBrowser.MediaEncoding.Encoder { // vaapi will throw an error with this input // [vaapi @ 0x7faed8000960] No VAAPI support for codec mpeg4 profile -99. - if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase) && videoStream.Level == -99) + if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase)) { - return false; + if (videoStream.Level == -99 || videoStream.Level == 15) + { + return false; + } } } return true; diff --git a/MediaBrowser.MediaEncoding/Encoder/FontConfigLoader.cs b/MediaBrowser.MediaEncoding/Encoder/FontConfigLoader.cs index 71306e0ec..42048ab9e 100644 --- a/MediaBrowser.MediaEncoding/Encoder/FontConfigLoader.cs +++ b/MediaBrowser.MediaEncoding/Encoder/FontConfigLoader.cs @@ -3,9 +3,11 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.IO; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; @@ -86,9 +88,9 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <returns>Task.</returns> private async Task DownloadFontFile(string fontsDirectory, string fontFilename, IProgress<double> progress) { - var existingFile = Directory - .EnumerateFiles(_appPaths.ProgramDataPath, fontFilename, SearchOption.AllDirectories) - .FirstOrDefault(); + var existingFile = _fileSystem + .GetFilePaths(_appPaths.ProgramDataPath, true) + .FirstOrDefault(i => string.Equals(fontFilename, Path.GetFileName(i), StringComparison.OrdinalIgnoreCase)); if (existingFile != null) { @@ -168,8 +170,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var bytes = Encoding.UTF8.GetBytes(contents); - using (var fileStream = _fileSystem.GetFileStream(fontConfigFile, FileMode.Create, FileAccess.Write, - FileShare.Read, true)) + using (var fileStream = _fileSystem.GetFileStream(fontConfigFile, FileOpenMode.Create, FileAccessMode.Write, + FileShareMode.Read, true)) { await fileStream.WriteAsync(bytes, 0, bytes.Length); } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 340990373..23e63dad0 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -14,18 +14,17 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; using MediaBrowser.Model.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; +using MediaBrowser.Model.Diagnostics; +using MediaBrowser.Model.System; namespace MediaBrowser.MediaEncoding.Encoder { @@ -80,14 +79,21 @@ namespace MediaBrowser.MediaEncoding.Encoder protected readonly Func<IMediaSourceManager> MediaSourceManager; private readonly IHttpClient _httpClient; private readonly IZipClient _zipClient; - private readonly IMemoryStreamProvider _memoryStreamProvider; + private readonly IProcessFactory _processFactory; + private readonly IMemoryStreamFactory _memoryStreamProvider; private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>(); private readonly bool _hasExternalEncoder; - private string _originalFFMpegPath; - private string _originalFFProbePath; + private readonly string _originalFFMpegPath; + private readonly string _originalFFProbePath; + private readonly int DefaultImageExtractionTimeoutMs; + private readonly bool EnableEncoderFontFile; - public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, bool hasExternalEncoder, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager, IHttpClient httpClient, IZipClient zipClient, IMemoryStreamProvider memoryStreamProvider) + private readonly IEnvironmentInfo _environmentInfo; + + public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, bool hasExternalEncoder, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager, IHttpClient httpClient, IZipClient zipClient, IMemoryStreamFactory memoryStreamProvider, IProcessFactory processFactory, + int defaultImageExtractionTimeoutMs, + bool enableEncoderFontFile, IEnvironmentInfo environmentInfo) { _logger = logger; _jsonSerializer = jsonSerializer; @@ -103,18 +109,80 @@ namespace MediaBrowser.MediaEncoding.Encoder _httpClient = httpClient; _zipClient = zipClient; _memoryStreamProvider = memoryStreamProvider; + _processFactory = processFactory; + DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs; + EnableEncoderFontFile = enableEncoderFontFile; + _environmentInfo = environmentInfo; FFProbePath = ffProbePath; FFMpegPath = ffMpegPath; _originalFFProbePath = ffProbePath; _originalFFMpegPath = ffMpegPath; _hasExternalEncoder = hasExternalEncoder; + + SetEnvironmentVariable(); + } + + private readonly object _logLock = new object(); + public void SetLogFilename(string name) + { + lock (_logLock) + { + try + { + _environmentInfo.SetProcessEnvironmentVariable("FFREPORT", "file=" + name + ":level=32"); + } + catch (Exception ex) + { + _logger.ErrorException("Error setting FFREPORT environment variable", ex); + } + } + } + + public void ClearLogFilename() + { + lock (_logLock) + { + try + { + _environmentInfo.SetProcessEnvironmentVariable("FFREPORT", null); + } + catch (Exception ex) + { + //_logger.ErrorException("Error setting FFREPORT environment variable", ex); + } + } + } + + private void SetEnvironmentVariable() + { + try + { + //_environmentInfo.SetProcessEnvironmentVariable("FFREPORT", "file=program-YYYYMMDD-HHMMSS.txt:level=32"); + } + catch (Exception ex) + { + _logger.ErrorException("Error setting FFREPORT environment variable", ex); + } + try + { + //_environmentInfo.SetUserEnvironmentVariable("FFREPORT", "file=program-YYYYMMDD-HHMMSS.txt:level=32"); + } + catch (Exception ex) + { + _logger.ErrorException("Error setting FFREPORT environment variable", ex); + } } public string EncoderLocationType { get { + if (_hasExternalEncoder) + { + return "External"; + } + if (string.IsNullOrWhiteSpace(FFMpegPath)) { return null; @@ -157,12 +225,12 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrWhiteSpace(FFMpegPath)) { - var result = new EncoderValidator(_logger).Validate(FFMpegPath); + var result = new EncoderValidator(_logger, _processFactory).Validate(FFMpegPath); SetAvailableDecoders(result.Item1); SetAvailableEncoders(result.Item2); - if (Environment.OSVersion.Platform == PlatformID.Win32NT) + if (EnableEncoderFontFile) { var directory = Path.GetDirectoryName(FFMpegPath); @@ -179,6 +247,12 @@ namespace MediaBrowser.MediaEncoding.Encoder { ConfigureEncoderPaths(); + if (_hasExternalEncoder) + { + LogPaths(); + return; + } + // If the path was passed in, save it into config now. var encodingOptions = GetEncodingOptions(); var appPath = encodingOptions.EncoderAppPath; @@ -188,7 +262,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrWhiteSpace(valueToSave)) { // if using system variable, don't save this. - if (IsSystemInstalledPath(valueToSave)) + if (IsSystemInstalledPath(valueToSave) || _hasExternalEncoder) { valueToSave = null; } @@ -203,6 +277,11 @@ namespace MediaBrowser.MediaEncoding.Encoder public async Task UpdateEncoderPath(string path, string pathType) { + if (_hasExternalEncoder) + { + return; + } + Tuple<string, string> newPaths; if (string.Equals(pathType, "system", StringComparison.OrdinalIgnoreCase)) @@ -218,7 +297,7 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new ArgumentNullException("path"); } - if (!File.Exists(path) && !Directory.Exists(path)) + if (!FileSystem.FileExists(path) && !FileSystem.DirectoryExists(path)) { throw new ResourceNotFoundException(); } @@ -240,7 +319,7 @@ namespace MediaBrowser.MediaEncoding.Encoder path = newPaths.Item1; - if (!ValidateVersion(path)) + if (!ValidateVersion(path, true)) { throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required."); } @@ -252,13 +331,18 @@ namespace MediaBrowser.MediaEncoding.Encoder Init(); } - private bool ValidateVersion(string path) + private bool ValidateVersion(string path, bool logOutput) { - return new EncoderValidator(_logger).ValidateVersion(path); + return new EncoderValidator(_logger, _processFactory).ValidateVersion(path, logOutput); } private void ConfigureEncoderPaths() { + if (_hasExternalEncoder) + { + return; + } + var appPath = GetEncodingOptions().EncoderAppPath; if (string.IsNullOrWhiteSpace(appPath)) @@ -287,12 +371,12 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrWhiteSpace(appPath)) { - if (Directory.Exists(appPath)) + if (FileSystem.DirectoryExists(appPath)) { return GetPathsFromDirectory(appPath); } - if (File.Exists(appPath)) + if (FileSystem.FileExists(appPath)) { return new Tuple<string, string>(appPath, GetProbePathFromEncoderPath(appPath)); } @@ -306,7 +390,7 @@ namespace MediaBrowser.MediaEncoding.Encoder string encoderPath = null; string probePath = null; - if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath)) + if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath, true)) { encoderPath = _originalFFMpegPath; probePath = _originalFFProbePath; @@ -314,7 +398,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (string.IsNullOrWhiteSpace(encoderPath)) { - if (ValidateVersion("ffmpeg") && ValidateVersion("ffprobe")) + if (ValidateVersion("ffmpeg", true) && ValidateVersion("ffprobe", false)) { encoderPath = "ffmpeg"; probePath = "ffprobe"; @@ -328,16 +412,16 @@ namespace MediaBrowser.MediaEncoding.Encoder { // Since we can't predict the file extension, first try directly within the folder // If that doesn't pan out, then do a recursive search - var files = Directory.GetFiles(path); + var files = FileSystem.GetFilePaths(path); var excludeExtensions = new[] { ".c" }; var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); - if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath)) + if (string.IsNullOrWhiteSpace(ffmpegPath) || !FileSystem.FileExists(ffmpegPath)) { - files = Directory.GetFiles(path, "*", SearchOption.AllDirectories); + files = FileSystem.GetFilePaths(path, true); ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); @@ -352,7 +436,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private string GetProbePathFromEncoderPath(string appPath) { - return Directory.GetFiles(Path.GetDirectoryName(appPath)) + return FileSystem.GetFilePaths(Path.GetDirectoryName(appPath)) .FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase)); } @@ -432,7 +516,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (request.AnalyzeDurationSections > 0) { analyzeDuration = "-analyzeduration " + - (request.AnalyzeDurationSections*1000000).ToString(CultureInfo.InvariantCulture); + (request.AnalyzeDurationSections * 1000000).ToString(CultureInfo.InvariantCulture); } else { @@ -495,7 +579,6 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <param name="videoType">Type of the video.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{MediaInfoResult}.</returns> - /// <exception cref="System.ApplicationException">ffprobe failed - streams and format are both null.</exception> private async Task<MediaInfo> GetMediaInfoInternal(string inputPath, string primaryPath, MediaProtocol protocol, @@ -509,31 +592,24 @@ namespace MediaBrowser.MediaEncoding.Encoder ? "{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_chapters -show_format" : "{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_format"; - var process = new Process + var process = _processFactory.Create(new ProcessOptions { - StartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - - // Must consume both or ffmpeg may hang due to deadlocks. See comments below. - RedirectStandardOutput = true, - //RedirectStandardError = true, - RedirectStandardInput = false, - FileName = FFProbePath, - Arguments = string.Format(args, - probeSizeArgument, inputPath).Trim(), + CreateNoWindow = true, + UseShellExecute = false, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }, + // Must consume both or ffmpeg may hang due to deadlocks. See comments below. + RedirectStandardOutput = true, + FileName = FFProbePath, + Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(), + IsHidden = true, + ErrorDialog = false, EnableRaisingEvents = true - }; + }); _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - using (var processWrapper = new ProcessWrapper(process, this, _logger, false)) + using (var processWrapper = new ProcessWrapper(process, this, _logger)) { await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -558,7 +634,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (result.streams == null && result.format == null) { - throw new ApplicationException("ffprobe failed - streams and format are both null."); + throw new Exception("ffprobe failed - streams and format are both null."); } if (result.streams != null) @@ -595,7 +671,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } catch { - StopProcess(processWrapper, 100, true); + StopProcess(processWrapper, 100); throw; } @@ -644,31 +720,25 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = "{0} -i {1} -map 0:v:{2} -an -filter:v idet -frames:v 500 -an -f null /dev/null"; - var process = new Process + var process = _processFactory.Create(new ProcessOptions { - StartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - - // Must consume both or ffmpeg may hang due to deadlocks. See comments below. - //RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = false, - FileName = FFMpegPath, - Arguments = string.Format(args, probeSizeArgument, inputPath, videoStream.Index.ToString(CultureInfo.InvariantCulture)).Trim(), + CreateNoWindow = true, + UseShellExecute = false, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }, + // Must consume both or ffmpeg may hang due to deadlocks. See comments below. + RedirectStandardError = true, + FileName = FFMpegPath, + Arguments = string.Format(args, probeSizeArgument, inputPath, videoStream.Index.ToString(CultureInfo.InvariantCulture)).Trim(), + IsHidden = true, + ErrorDialog = false, EnableRaisingEvents = true - }; + }); _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); var idetFoundInterlaced = false; - using (var processWrapper = new ProcessWrapper(process, this, _logger, false)) + using (var processWrapper = new ProcessWrapper(process, this, _logger)) { try { @@ -711,7 +781,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } catch { - StopProcess(processWrapper, 100, true); + StopProcess(processWrapper, 100); throw; } @@ -864,7 +934,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } var tempExtractPath = Path.Combine(ConfigurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); - Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); + FileSystem.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. // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar @@ -898,7 +968,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; var enableThumbnail = !new List<string> { "wtv" }.Contains(container ?? string.Empty, StringComparer.OrdinalIgnoreCase); - var thumbnail = enableThumbnail ? ",thumbnail=30" : string.Empty; + var thumbnail = enableThumbnail ? ",thumbnail=24" : string.Empty; // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case. var args = useIFrame ? string.Format("-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}{4}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail) : @@ -916,22 +986,19 @@ namespace MediaBrowser.MediaEncoding.Encoder args = string.Format("-ss {0} ", GetTimeParameter(offset.Value)) + args; } - var process = new Process + var process = _processFactory.Create(new ProcessOptions { - StartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - FileName = FFMpegPath, - Arguments = args, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - } - }; + CreateNoWindow = true, + UseShellExecute = false, + FileName = FFMpegPath, + Arguments = args, + IsHidden = true, + ErrorDialog = false + }); _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - using (var processWrapper = new ProcessWrapper(process, this, _logger, false)) + using (var processWrapper = new ProcessWrapper(process, this, _logger)) { await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -944,14 +1011,14 @@ namespace MediaBrowser.MediaEncoding.Encoder var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs; if (timeoutMs <= 0) { - timeoutMs = Environment.Is64BitOperatingSystem ? (Environment.ProcessorCount > 2 ? 14000 : 20000) : 40000; + timeoutMs = DefaultImageExtractionTimeoutMs; } ranToCompletion = process.WaitForExit(timeoutMs); if (!ranToCompletion) { - StopProcess(processWrapper, 1000, false); + StopProcess(processWrapper, 1000); } } @@ -961,7 +1028,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; - var file = new FileInfo(tempExtractPath); + var file = FileSystem.GetFileInfo(tempExtractPath); if (exitCode == -1 || !file.Exists || file.Length == 0) { @@ -969,7 +1036,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.Error(msg); - throw new ApplicationException(msg); + throw new Exception(msg); } return tempExtractPath; @@ -1022,19 +1089,15 @@ namespace MediaBrowser.MediaEncoding.Encoder args = probeSize + " " + args; } - var process = new Process + var process = _processFactory.Create(new ProcessOptions { - StartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - FileName = FFMpegPath, - Arguments = args, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false, - RedirectStandardInput = true - } - }; + CreateNoWindow = true, + UseShellExecute = false, + FileName = FFMpegPath, + Arguments = args, + IsHidden = true, + ErrorDialog = false + }); _logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments); @@ -1042,7 +1105,7 @@ namespace MediaBrowser.MediaEncoding.Encoder bool ranToCompletion = false; - using (var processWrapper = new ProcessWrapper(process, this, _logger, true)) + using (var processWrapper = new ProcessWrapper(process, this, _logger)) { try { @@ -1065,7 +1128,7 @@ namespace MediaBrowser.MediaEncoding.Encoder cancellationToken.ThrowIfCancellationRequested(); - var jpegCount = Directory.GetFiles(targetDirectory) + var jpegCount = FileSystem.GetFilePaths(targetDirectory) .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase)); isResponsive = (jpegCount > lastCount); @@ -1074,7 +1137,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!ranToCompletion) { - StopProcess(processWrapper, 1000, false); + StopProcess(processWrapper, 1000); } } finally @@ -1090,7 +1153,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.Error(msg); - throw new ApplicationException(msg); + throw new Exception(msg); } } } @@ -1107,7 +1170,8 @@ namespace MediaBrowser.MediaEncoding.Encoder LibraryManager, SessionManager, SubtitleEncoder(), - MediaSourceManager()) + MediaSourceManager(), + _processFactory) .Start(options, progress, cancellationToken).ConfigureAwait(false); await job.TaskCompletionSource.Task.ConfigureAwait(false); @@ -1127,7 +1191,8 @@ namespace MediaBrowser.MediaEncoding.Encoder LibraryManager, SessionManager, SubtitleEncoder(), - MediaSourceManager()) + MediaSourceManager(), + _processFactory) .Start(options, progress, cancellationToken).ConfigureAwait(false); await job.TaskCompletionSource.Task.ConfigureAwait(false); @@ -1144,40 +1209,25 @@ namespace MediaBrowser.MediaEncoding.Encoder _runningProcesses.Add(process); } } - private void StopProcess(ProcessWrapper process, int waitTimeMs, bool enableForceKill) + private void StopProcess(ProcessWrapper process, int waitTimeMs) { try { - _logger.Info("Killing ffmpeg process"); - - if (process.IsRedirectingStdin) + if (process.Process.WaitForExit(waitTimeMs)) { - try - { - process.Process.StandardInput.WriteLine("q"); - } - catch (Exception) - { - _logger.Error("Error sending q command to process"); - } + return; } + } + catch (Exception ex) + { + _logger.Error("Error in WaitForExit", ex); + } - try - { - if (process.Process.WaitForExit(waitTimeMs)) - { - return; - } - } - catch (Exception ex) - { - _logger.Error("Error in WaitForExit", ex); - } + try + { + _logger.Info("Killing ffmpeg process"); - if (enableForceKill) - { - process.Process.Kill(); - } + process.Process.Kill(); } catch (Exception ex) { @@ -1198,14 +1248,17 @@ namespace MediaBrowser.MediaEncoding.Encoder { if (!process.HasExited) { - StopProcess(process, 500, true); + StopProcess(process, 500); } } } public string EscapeSubtitleFilterPath(string path) { - return path.Replace('\\', '/').Replace(":/", "\\:/").Replace("'", "'\\\\\\''"); + // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping + // We need to double escape + + return path.Replace('\\', '/').Replace(":", "\\:").Replace("'", "'\\\\\\''"); } /// <summary> @@ -1231,25 +1284,23 @@ namespace MediaBrowser.MediaEncoding.Encoder private class ProcessWrapper : IDisposable { - public readonly Process Process; + public readonly IProcess Process; public bool HasExited; public int? ExitCode; private readonly MediaEncoder _mediaEncoder; private readonly ILogger _logger; - public bool IsRedirectingStdin { get; private set; } - public ProcessWrapper(Process process, MediaEncoder mediaEncoder, ILogger logger, bool isRedirectingStdin) + public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder, ILogger logger) { Process = process; _mediaEncoder = mediaEncoder; _logger = logger; Process.Exited += Process_Exited; - IsRedirectingStdin = isRedirectingStdin; } void Process_Exited(object sender, EventArgs e) { - var process = (Process)sender; + var process = (IProcess)sender; HasExited = true; @@ -1269,7 +1320,7 @@ namespace MediaBrowser.MediaEncoding.Encoder DisposeProcess(process); } - private void DisposeProcess(Process process) + private void DisposeProcess(IProcess process) { try { diff --git a/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs index 457fbe2c2..cbbca479a 100644 --- a/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs @@ -8,13 +8,13 @@ using MediaBrowser.Model.Logging; using System; using System.IO; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.Diagnostics; namespace MediaBrowser.MediaEncoding.Encoder { public class VideoEncoder : BaseEncoder { - public VideoEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IIsoManager isoManager, ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager) : base(mediaEncoder, logger, configurationManager, fileSystem, isoManager, libraryManager, sessionManager, subtitleEncoder, mediaSourceManager) + public VideoEncoder(MediaEncoder mediaEncoder, ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IIsoManager isoManager, ILibraryManager libraryManager, ISessionManager sessionManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IProcessFactory processFactory) : base(mediaEncoder, logger, configurationManager, fileSystem, isoManager, libraryManager, sessionManager, subtitleEncoder, mediaSourceManager, processFactory) { } @@ -191,5 +191,6 @@ namespace MediaBrowser.MediaEncoding.Encoder { get { return true; } } + } }
\ No newline at end of file diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 1b5599577..63e789a59 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -9,9 +9,10 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>MediaBrowser.MediaEncoding</RootNamespace> <AssemblyName>MediaBrowser.MediaEncoding</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> - <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> + <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> + <TargetFrameworkProfile>Profile7</TargetFrameworkProfile> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -36,28 +37,9 @@ <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> - <Reference Include="BDInfo"> - <HintPath>..\packages\MediaBrowser.BdInfo.1.0.0.10\lib\net35\BDInfo.dll</HintPath> - </Reference> - <Reference Include="CommonIO, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\CommonIO.1.0.0.9\lib\net45\CommonIO.dll</HintPath> - </Reference> - <Reference Include="DvdLib"> - <HintPath>..\packages\MediaBrowser.BdInfo.1.0.0.10\lib\net35\DvdLib.dll</HintPath> - </Reference> - <Reference Include="Patterns.Logging"> - <HintPath>..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath> - </Reference> - <Reference Include="System" /> - <Reference Include="System.Core" /> - <Reference Include="System.Xml.Linq" /> - <Reference Include="System.Data.DataSetExtensions" /> - <Reference Include="Microsoft.CSharp" /> - <Reference Include="System.Data" /> - <Reference Include="System.Xml" /> - <Reference Include="UniversalDetector"> - <HintPath>..\ThirdParty\UniversalDetector\UniversalDetector.dll</HintPath> + <Reference Include="UniversalDetector, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\UniversalDetector.1.0.1\lib\portable-net45+sl4+wp71+win8+wpa81\UniversalDetector.dll</HintPath> + <Private>True</Private> </Reference> </ItemGroup> <ItemGroup> @@ -80,9 +62,11 @@ <Compile Include="Probing\InternalMediaInfoResult.cs" /> <Compile Include="Probing\ProbeResultNormalizer.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Subtitles\ConfigurationExtension.cs" /> <Compile Include="Subtitles\ISubtitleParser.cs" /> <Compile Include="Subtitles\ISubtitleWriter.cs" /> <Compile Include="Subtitles\JsonWriter.cs" /> + <Compile Include="Subtitles\OpenSubtitleDownloader.cs" /> <Compile Include="Subtitles\ParserValues.cs" /> <Compile Include="Subtitles\SrtParser.cs" /> <Compile Include="Subtitles\SrtWriter.cs" /> @@ -93,6 +77,10 @@ <Compile Include="Subtitles\VttWriter.cs" /> </ItemGroup> <ItemGroup> + <ProjectReference Include="..\BDInfo\BDInfo.csproj"> + <Project>{88ae38df-19d7-406f-a6a9-09527719a21e}</Project> + <Name>BDInfo</Name> + </ProjectReference> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> <Name>MediaBrowser.Common</Name> @@ -105,14 +93,15 @@ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> <Name>MediaBrowser.Model</Name> </ProjectReference> + <ProjectReference Include="..\OpenSubtitlesHandler\OpenSubtitlesHandler.csproj"> + <Project>{4a4402d4-e910-443b-b8fc-2c18286a2ca0}</Project> + <Name>OpenSubtitlesHandler</Name> + </ProjectReference> </ItemGroup> <ItemGroup> <None Include="packages.config" /> </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Probing\whitelist.txt" /> - </ItemGroup> - <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. <Target Name="BeforeBuild"> diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.nuget.targets b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.nuget.targets new file mode 100644 index 000000000..e69ce0e64 --- /dev/null +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.nuget.targets @@ -0,0 +1,6 @@ +<?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/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index b8c110e14..de055146a 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -8,10 +8,11 @@ using System.IO; using System.Linq; using System.Text; using System.Xml; -using CommonIO; +using MediaBrowser.Model.IO; using MediaBrowser.Common.IO; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; @@ -22,9 +23,9 @@ namespace MediaBrowser.MediaEncoding.Probing private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly ILogger _logger; private readonly IFileSystem _fileSystem; - private readonly IMemoryStreamProvider _memoryStreamProvider; + private readonly IMemoryStreamFactory _memoryStreamProvider; - public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem, IMemoryStreamProvider memoryStreamProvider) + public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem, IMemoryStreamFactory memoryStreamProvider) { _logger = logger; _fileSystem = fileSystem; @@ -187,7 +188,13 @@ namespace MediaBrowser.MediaEncoding.Probing private void FetchFromItunesInfo(string xml, MediaInfo info) { // Make things simpler and strip out the dtd - xml = xml.Substring(xml.IndexOf("<plist", StringComparison.OrdinalIgnoreCase)); + var plistIndex = xml.IndexOf("<plist", StringComparison.OrdinalIgnoreCase); + + if (plistIndex != -1) + { + xml = xml.Substring(plistIndex); + } + xml = "<?xml version=\"1.0\"?>" + xml; // <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>cast</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Blender Foundation</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Janus Bager Kristensen</string>\n\t\t</dict>\n\t</array>\n\t<key>directors</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Sacha Goedegebure</string>\n\t\t</dict>\n\t</array>\n\t<key>studio</key>\n\t<string>Blender Foundation</string>\n</dict>\n</plist>\n @@ -195,44 +202,63 @@ namespace MediaBrowser.MediaEncoding.Probing { using (var streamReader = new StreamReader(stream)) { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader)) + try { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader)) { - if (reader.NodeType == XmlNodeType.Element) + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - switch (reader.Name) + if (reader.NodeType == XmlNodeType.Element) { - case "dict": - using (var subtree = reader.ReadSubtree()) - { - ReadFromDictNode(subtree, info); - } - break; - default: - reader.Skip(); - break; + switch (reader.Name) + { + case "dict": + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + using (var subtree = reader.ReadSubtree()) + { + ReadFromDictNode(subtree, info); + } + break; + default: + reader.Skip(); + break; + } + } + else + { + reader.Read(); } } } } + catch (XmlException) + { + // I've seen probe examples where the iTunMOVI value is just "<" + // So we should not allow this to fail the entire probing operation + } } } } private void ReadFromDictNode(XmlReader reader, MediaInfo info) { - reader.MoveToContent(); - string currentKey = null; List<NameValuePair> pairs = new List<NameValuePair>(); + reader.MoveToContent(); + reader.Read(); + // Loop through each element - while (reader.Read()) + while (!reader.EOF && reader.ReadState == ReadState.Interactive) { if (reader.NodeType == XmlNodeType.Element) { @@ -258,9 +284,14 @@ namespace MediaBrowser.MediaEncoding.Probing } break; case "array": - if (!string.IsNullOrWhiteSpace(currentKey)) + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + using (var subtree = reader.ReadSubtree()) { - using (var subtree = reader.ReadSubtree()) + if (!string.IsNullOrWhiteSpace(currentKey)) { pairs.AddRange(ReadValueArray(subtree)); } @@ -271,23 +302,35 @@ namespace MediaBrowser.MediaEncoding.Probing break; } } + else + { + reader.Read(); + } } } private List<NameValuePair> ReadValueArray(XmlReader reader) { - reader.MoveToContent(); List<NameValuePair> pairs = new List<NameValuePair>(); + reader.MoveToContent(); + reader.Read(); + // Loop through each element - while (reader.Read()) + while (!reader.EOF && reader.ReadState == ReadState.Interactive) { if (reader.NodeType == XmlNodeType.Element) { switch (reader.Name) { case "dict": + + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } using (var subtree = reader.ReadSubtree()) { var dict = GetNameValuePair(subtree); @@ -302,6 +345,10 @@ namespace MediaBrowser.MediaEncoding.Probing break; } } + else + { + reader.Read(); + } } return pairs; @@ -359,13 +406,14 @@ namespace MediaBrowser.MediaEncoding.Probing private NameValuePair GetNameValuePair(XmlReader reader) { - reader.MoveToContent(); - string name = null; string value = null; + reader.MoveToContent(); + reader.Read(); + // Loop through each element - while (reader.Read()) + while (!reader.EOF && reader.ReadState == ReadState.Interactive) { if (reader.NodeType == XmlNodeType.Element) { @@ -382,6 +430,10 @@ namespace MediaBrowser.MediaEncoding.Probing break; } } + else + { + reader.Read(); + } } if (string.IsNullOrWhiteSpace(name) || @@ -780,24 +832,24 @@ namespace MediaBrowser.MediaEncoding.Probing } } - var conductor = FFProbeHelpers.GetDictionaryValue(tags, "conductor"); - if (!string.IsNullOrWhiteSpace(conductor)) - { - foreach (var person in Split(conductor, false)) - { - audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor }); - } - } - - var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist"); + //var conductor = FFProbeHelpers.GetDictionaryValue(tags, "conductor"); + //if (!string.IsNullOrWhiteSpace(conductor)) + //{ + // foreach (var person in Split(conductor, false)) + // { + // audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor }); + // } + //} + + //var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist"); + //if (!string.IsNullOrWhiteSpace(lyricist)) + //{ + // foreach (var person in Split(lyricist, false)) + // { + // audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist }); + // } + //} - if (!string.IsNullOrWhiteSpace(lyricist)) - { - foreach (var person in Split(lyricist, false)) - { - audio.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"); @@ -972,27 +1024,10 @@ namespace MediaBrowser.MediaEncoding.Probing { if (_splitWhiteList == null) { - var file = GetType().Namespace + ".whitelist.txt"; - - using (var stream = GetType().Assembly.GetManifestResourceStream(file)) - { - using (var reader = new StreamReader(stream)) - { - var list = new List<string>(); - - while (!reader.EndOfStream) + _splitWhiteList = new List<string> { - var val = reader.ReadLine(); - - if (!string.IsNullOrWhiteSpace(val)) - { - list.Add(val); - } - } - - _splitWhiteList = list; - } - } + "AC/DC" + }; } return _splitWhiteList; @@ -1247,7 +1282,7 @@ namespace MediaBrowser.MediaEncoding.Probing { var packetBuffer = new byte['Å']; - using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) { fs.Read(packetBuffer, 0, packetBuffer.Length); } diff --git a/MediaBrowser.MediaEncoding/Probing/whitelist.txt b/MediaBrowser.MediaEncoding/Probing/whitelist.txt deleted file mode 100644 index 1fd366551..000000000 --- a/MediaBrowser.MediaEncoding/Probing/whitelist.txt +++ /dev/null @@ -1 +0,0 @@ -AC/DC
\ No newline at end of file diff --git a/MediaBrowser.MediaEncoding/Subtitles/ConfigurationExtension.cs b/MediaBrowser.MediaEncoding/Subtitles/ConfigurationExtension.cs new file mode 100644 index 000000000..973c653a4 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/ConfigurationExtension.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + public static class ConfigurationExtension + { + public static SubtitleOptions GetSubtitleConfiguration(this IConfigurationManager manager) + { + return manager.GetConfiguration<SubtitleOptions>("subtitles"); + } + } + + public class SubtitleConfigurationFactory : IConfigurationFactory + { + public IEnumerable<ConfigurationStore> GetConfigurations() + { + return new List<ConfigurationStore> + { + new ConfigurationStore + { + Key = "subtitles", + ConfigurationType = typeof (SubtitleOptions) + } + }; + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs new file mode 100644 index 000000000..e26a6392c --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/OpenSubtitleDownloader.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +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.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using OpenSubtitlesHandler; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + public class OpenSubtitleDownloader : ISubtitleProvider, IDisposable + { + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private readonly IServerConfigurationManager _config; + private readonly IEncryptionManager _encryption; + + private readonly IJsonSerializer _json; + private readonly IFileSystem _fileSystem; + + public OpenSubtitleDownloader(ILogManager logManager, IHttpClient httpClient, IServerConfigurationManager config, IEncryptionManager encryption, IJsonSerializer json, IFileSystem fileSystem) + { + _logger = logManager.GetLogger(GetType().Name); + _httpClient = httpClient; + _config = config; + _encryption = encryption; + _json = json; + _fileSystem = fileSystem; + + _config.NamedConfigurationUpdating += _config_NamedConfigurationUpdating; + + Utilities.HttpClient = httpClient; + OpenSubtitles.SetUserAgent("mediabrowser.tv"); + } + + private const string PasswordHashPrefix = "h:"; + void _config_NamedConfigurationUpdating(object sender, ConfigurationUpdateEventArgs e) + { + if (!string.Equals(e.Key, "subtitles", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + var options = (SubtitleOptions)e.NewConfiguration; + + if (options != null && + !string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) && + !options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) + { + options.OpenSubtitlesPasswordHash = EncryptPassword(options.OpenSubtitlesPasswordHash); + } + } + + private string EncryptPassword(string password) + { + return PasswordHashPrefix + _encryption.EncryptString(password); + } + + private string DecryptPassword(string password) + { + if (password == null || + !password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) + { + return string.Empty; + } + + return _encryption.DecryptString(password.Substring(2)); + } + + public string Name + { + get { return "Open Subtitles"; } + } + + private SubtitleOptions GetOptions() + { + return _config.GetSubtitleConfiguration(); + } + + public IEnumerable<VideoContentType> SupportedMediaTypes + { + get + { + var options = GetOptions(); + + if (string.IsNullOrWhiteSpace(options.OpenSubtitlesUsername) || + string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash)) + { + return new VideoContentType[] { }; + } + + return new[] { VideoContentType.Episode, VideoContentType.Movie }; + } + } + + public Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken) + { + return GetSubtitlesInternal(id, GetOptions(), cancellationToken); + } + + private DateTime _lastRateLimitException; + private async Task<SubtitleResponse> GetSubtitlesInternal(string id, + SubtitleOptions options, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + + var idParts = id.Split(new[] { '-' }, 3); + + var format = idParts[0]; + var language = idParts[1]; + var ossId = idParts[2]; + + var downloadsList = new[] { int.Parse(ossId, _usCulture) }; + + await Login(cancellationToken).ConfigureAwait(false); + + if ((DateTime.UtcNow - _lastRateLimitException).TotalHours < 1) + { + throw new Exception("OpenSubtitles rate limit reached"); + } + + var resultDownLoad = await OpenSubtitles.DownloadSubtitlesAsync(downloadsList, cancellationToken).ConfigureAwait(false); + + if ((resultDownLoad.Status ?? string.Empty).IndexOf("407", StringComparison.OrdinalIgnoreCase) != -1) + { + _lastRateLimitException = DateTime.UtcNow; + throw new Exception("OpenSubtitles rate limit reached"); + } + + if (!(resultDownLoad is MethodResponseSubtitleDownload)) + { + throw new Exception("Invalid response type"); + } + + var results = ((MethodResponseSubtitleDownload)resultDownLoad).Results; + + _lastRateLimitException = DateTime.MinValue; + + if (results.Count == 0) + { + var msg = string.Format("Subtitle with Id {0} was not found. Name: {1}. Status: {2}. Message: {3}", + ossId, + resultDownLoad.Name ?? string.Empty, + resultDownLoad.Status ?? string.Empty, + resultDownLoad.Message ?? string.Empty); + + throw new ResourceNotFoundException(msg); + } + + var data = Convert.FromBase64String(results.First().Data); + + return new SubtitleResponse + { + Format = format, + Language = language, + + Stream = new MemoryStream(Utilities.Decompress(new MemoryStream(data))) + }; + } + + private DateTime _lastLogin; + private async Task Login(CancellationToken cancellationToken) + { + if ((DateTime.UtcNow - _lastLogin).TotalSeconds < 60) + { + return; + } + + var options = GetOptions(); + + var user = options.OpenSubtitlesUsername ?? string.Empty; + var password = DecryptPassword(options.OpenSubtitlesPasswordHash); + + var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false); + + if (!(loginResponse is MethodResponseLogIn)) + { + throw new Exception("Authentication to OpenSubtitles failed."); + } + + _lastLogin = DateTime.UtcNow; + } + + public async Task<IEnumerable<NameIdPair>> GetSupportedLanguages(CancellationToken cancellationToken) + { + await Login(cancellationToken).ConfigureAwait(false); + + var result = OpenSubtitles.GetSubLanguages("en"); + if (!(result is MethodResponseGetSubLanguages)) + { + _logger.Error("Invalid response type"); + return new List<NameIdPair>(); + } + + var results = ((MethodResponseGetSubLanguages)result).Languages; + + return results.Select(i => new NameIdPair + { + Name = i.LanguageName, + Id = i.SubLanguageID + }); + } + + private string NormalizeLanguage(string language) + { + // Problem with Greek subtitle download #1349 + if (string.Equals(language, "gre", StringComparison.OrdinalIgnoreCase)) + { + + return "ell"; + } + + return language; + } + + public async Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken) + { + var imdbIdText = request.GetProviderId(MetadataProviders.Imdb); + long imdbId = 0; + + switch (request.ContentType) + { + case VideoContentType.Episode: + if (!request.IndexNumber.HasValue || !request.ParentIndexNumber.HasValue || string.IsNullOrEmpty(request.SeriesName)) + { + _logger.Debug("Episode information missing"); + return new List<RemoteSubtitleInfo>(); + } + break; + case VideoContentType.Movie: + if (string.IsNullOrEmpty(request.Name)) + { + _logger.Debug("Movie name missing"); + return new List<RemoteSubtitleInfo>(); + } + if (string.IsNullOrWhiteSpace(imdbIdText) || !long.TryParse(imdbIdText.TrimStart('t'), NumberStyles.Any, _usCulture, out imdbId)) + { + _logger.Debug("Imdb id missing"); + return new List<RemoteSubtitleInfo>(); + } + break; + } + + if (string.IsNullOrEmpty(request.MediaPath)) + { + _logger.Debug("Path Missing"); + return new List<RemoteSubtitleInfo>(); + } + + await Login(cancellationToken).ConfigureAwait(false); + + var subLanguageId = NormalizeLanguage(request.Language); + string hash; + + using (var fileStream = _fileSystem.OpenRead(request.MediaPath)) + { + hash = Utilities.ComputeHash(fileStream); + } + var fileInfo = _fileSystem.GetFileInfo(request.MediaPath); + var movieByteSize = fileInfo.Length; + var searchImdbId = request.ContentType == VideoContentType.Movie ? imdbId.ToString(_usCulture) : ""; + var subtitleSearchParameters = request.ContentType == VideoContentType.Episode + ? new List<SubtitleSearchParameters> { + new SubtitleSearchParameters(subLanguageId, + query: request.SeriesName, + season: request.ParentIndexNumber.Value.ToString(_usCulture), + episode: request.IndexNumber.Value.ToString(_usCulture)) + } + : new List<SubtitleSearchParameters> { + new SubtitleSearchParameters(subLanguageId, imdbid: searchImdbId), + new SubtitleSearchParameters(subLanguageId, query: request.Name, imdbid: searchImdbId) + }; + var parms = new List<SubtitleSearchParameters> { + new SubtitleSearchParameters( subLanguageId, + movieHash: hash, + movieByteSize: movieByteSize, + imdbid: searchImdbId ), + }; + parms.AddRange(subtitleSearchParameters); + var result = await OpenSubtitles.SearchSubtitlesAsync(parms.ToArray(), cancellationToken).ConfigureAwait(false); + if (!(result is MethodResponseSubtitleSearch)) + { + _logger.Error("Invalid response type"); + return new List<RemoteSubtitleInfo>(); + } + + Predicate<SubtitleSearchResult> mediaFilter = + x => + request.ContentType == VideoContentType.Episode + ? !string.IsNullOrEmpty(x.SeriesSeason) && !string.IsNullOrEmpty(x.SeriesEpisode) && + int.Parse(x.SeriesSeason, _usCulture) == request.ParentIndexNumber && + int.Parse(x.SeriesEpisode, _usCulture) == request.IndexNumber + : !string.IsNullOrEmpty(x.IDMovieImdb) && long.Parse(x.IDMovieImdb, _usCulture) == imdbId; + + var results = ((MethodResponseSubtitleSearch)result).Results; + + // Avoid implicitly captured closure + var hasCopy = hash; + + return results.Where(x => x.SubBad == "0" && mediaFilter(x) && (!request.IsPerfectMatch || string.Equals(x.MovieHash, hash, StringComparison.OrdinalIgnoreCase))) + .OrderBy(x => (string.Equals(x.MovieHash, hash, StringComparison.OrdinalIgnoreCase) ? 0 : 1)) + .ThenBy(x => Math.Abs(long.Parse(x.MovieByteSize, _usCulture) - movieByteSize)) + .ThenByDescending(x => int.Parse(x.SubDownloadsCnt, _usCulture)) + .ThenByDescending(x => double.Parse(x.SubRating, _usCulture)) + .Select(i => new RemoteSubtitleInfo + { + Author = i.UserNickName, + Comment = i.SubAuthorComment, + CommunityRating = float.Parse(i.SubRating, _usCulture), + DownloadCount = int.Parse(i.SubDownloadsCnt, _usCulture), + Format = i.SubFormat, + ProviderName = Name, + ThreeLetterISOLanguageName = i.SubLanguageID, + + Id = i.SubFormat + "-" + i.SubLanguageID + "-" + i.IDSubtitleFile, + + Name = i.SubFileName, + DateCreated = DateTime.Parse(i.SubAddDate, _usCulture), + IsHashMatch = i.MovieHash == hasCopy + + }).Where(i => !string.Equals(i.Format, "sub", StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Format, "idx", StringComparison.OrdinalIgnoreCase)); + } + + public void Dispose() + { + _config.NamedConfigurationUpdating -= _config_NamedConfigurationUpdating; + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 6dda49a55..0bbc2d916 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -17,9 +17,9 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -using CommonIO; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Diagnostics; +using MediaBrowser.Model.Text; using UniversalDetector; namespace MediaBrowser.MediaEncoding.Subtitles @@ -34,9 +34,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IJsonSerializer _json; private readonly IHttpClient _httpClient; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IMemoryStreamProvider _memoryStreamProvider; + private readonly IMemoryStreamFactory _memoryStreamProvider; + private readonly IProcessFactory _processFactory; + private readonly ITextEncoding _textEncoding; - public SubtitleEncoder(ILibraryManager libraryManager, ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IJsonSerializer json, IHttpClient httpClient, IMediaSourceManager mediaSourceManager, IMemoryStreamProvider memoryStreamProvider) + public SubtitleEncoder(ILibraryManager libraryManager, ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IJsonSerializer json, IHttpClient httpClient, IMediaSourceManager mediaSourceManager, IMemoryStreamFactory memoryStreamProvider, IProcessFactory processFactory, ITextEncoding textEncoding) { _libraryManager = libraryManager; _logger = logger; @@ -47,6 +49,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles _httpClient = httpClient; _mediaSourceManager = mediaSourceManager; _memoryStreamProvider = memoryStreamProvider; + _processFactory = processFactory; + _textEncoding = textEncoding; } private string SubtitleCachePath @@ -137,8 +141,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles .ConfigureAwait(false); var inputFormat = subtitle.Item2; + var writer = TryGetWriter(outputFormat); - if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase) && TryGetWriter(outputFormat) == null) + if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase) && writer == null) + { + return subtitle.Item1; + } + + if (writer == null) { return subtitle.Item1; } @@ -417,7 +427,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// or /// outputPath /// </exception> - /// <exception cref="System.ApplicationException"></exception> private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) @@ -439,46 +448,29 @@ namespace MediaBrowser.MediaEncoding.Subtitles encodingParam = " -sub_charenc " + encodingParam; } - var process = new Process + var process = _processFactory.Create(new ProcessOptions { - StartInfo = new ProcessStartInfo - { - RedirectStandardOutput = false, - RedirectStandardError = true, - RedirectStandardInput = true, - - CreateNoWindow = true, - UseShellExecute = false, - FileName = _mediaEncoder.EncoderPath, - Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), + CreateNoWindow = true, + UseShellExecute = false, + FileName = _mediaEncoder.EncoderPath, + Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - } - }; + IsHidden = true, + ErrorDialog = false + }); _logger.Info("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-convert-" + Guid.NewGuid() + ".txt"); - _fileSystem.CreateDirectory(Path.GetDirectoryName(logFilePath)); - - var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, - true); - try { process.Start(); } catch (Exception ex) { - logFileStream.Dispose(); - _logger.ErrorException("Error starting ffmpeg", ex); throw; } - - var logTask = process.StandardError.BaseStream.CopyToAsync(logFileStream); var ranToCompletion = process.WaitForExit(60000); @@ -488,19 +480,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.Info("Killing ffmpeg subtitle conversion process"); - process.StandardInput.WriteLine("q"); - process.WaitForExit(1000); - - await logTask.ConfigureAwait(false); + process.Kill(); } catch (Exception ex) { _logger.ErrorException("Error killing subtitle conversion process", ex); } - finally - { - logFileStream.Dispose(); - } } var exitCode = ranToCompletion ? process.ExitCode : -1; @@ -533,13 +518,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (failed) { - var msg = string.Format("ffmpeg subtitle converted failed for {0}", inputPath); + var msg = string.Format("ffmpeg subtitle conversion failed for {0}", inputPath); _logger.Error(msg); - throw new ApplicationException(msg); + throw new Exception(msg); } await SetAssFont(outputPath).ConfigureAwait(false); + + _logger.Info("ffmpeg subtitle conversion succeeded for {0}", inputPath); } /// <summary> @@ -592,48 +579,30 @@ namespace MediaBrowser.MediaEncoding.Subtitles var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath, subtitleStreamIndex, outputCodec, outputPath); - var process = new Process + var process = _processFactory.Create(new ProcessOptions { - StartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - - RedirectStandardOutput = false, - RedirectStandardError = true, - RedirectStandardInput = true, + CreateNoWindow = true, + UseShellExecute = false, - FileName = _mediaEncoder.EncoderPath, - Arguments = processArgs, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - } - }; + FileName = _mediaEncoder.EncoderPath, + Arguments = processArgs, + IsHidden = true, + ErrorDialog = false + }); _logger.Info("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-extract-" + Guid.NewGuid() + ".txt"); - _fileSystem.CreateDirectory(Path.GetDirectoryName(logFilePath)); - - var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, - true); - try { process.Start(); } catch (Exception ex) { - logFileStream.Dispose(); - _logger.ErrorException("Error starting ffmpeg", ex); throw; } - // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - Task.Run(() => StartStreamingLog(process.StandardError.BaseStream, logFileStream)); - var ranToCompletion = process.WaitForExit(300000); if (!ranToCompletion) @@ -642,17 +611,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.Info("Killing ffmpeg subtitle extraction process"); - process.StandardInput.WriteLine("q"); - process.WaitForExit(1000); + process.Kill(); } catch (Exception ex) { _logger.ErrorException("Error killing subtitle extraction process", ex); } - finally - { - logFileStream.Dispose(); - } } var exitCode = ranToCompletion ? process.ExitCode : -1; @@ -674,10 +638,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles { } - catch (DirectoryNotFoundException) - { - - } catch (IOException ex) { _logger.ErrorException("Error deleting extracted subtitle {0}", ex, outputPath); @@ -694,7 +654,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger.Error(msg); - throw new ApplicationException(msg); + throw new Exception(msg); } else { @@ -709,33 +669,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - private async Task StartStreamingLog(Stream source, Stream target) - { - try - { - using (var reader = new StreamReader(source)) - { - while (!reader.EndOfStream) - { - var line = await reader.ReadLineAsync().ConfigureAwait(false); - - var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); - - await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); - await target.FlushAsync().ConfigureAwait(false); - } - } - } - catch (ObjectDisposedException) - { - // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux - } - catch (Exception ex) - { - _logger.ErrorException("Error reading ffmpeg log", ex); - } - } - /// <summary> /// Sets the ass font. /// </summary> @@ -748,20 +681,26 @@ namespace MediaBrowser.MediaEncoding.Subtitles string text; Encoding encoding; - using (var reader = new StreamReader(file, true)) + using (var fileStream = _fileSystem.OpenRead(file)) { - encoding = reader.CurrentEncoding; + using (var reader = new StreamReader(fileStream, true)) + { + encoding = reader.CurrentEncoding; - text = await reader.ReadToEndAsync().ConfigureAwait(false); + text = await reader.ReadToEndAsync().ConfigureAwait(false); + } } var newText = text.Replace(",Arial,", ",Arial Unicode MS,"); if (!string.Equals(text, newText)) { - using (var writer = new StreamWriter(file, false, encoding)) + using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) { - writer.Write(newText); + using (var writer = new StreamWriter(fileStream, encoding)) + { + writer.Write(newText); + } } } } @@ -794,7 +733,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (protocol == MediaProtocol.File) { - if (GetFileEncoding(path).Equals(Encoding.UTF8)) + var fileEncoding = _textEncoding.GetFileEncoding(path); + + if (fileEncoding != null && fileEncoding.Equals(Encoding.UTF8)) { return string.Empty; } @@ -901,29 +842,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles return null; } - private Encoding GetFileEncoding(string srcFile) - { - // *** Detect byte order mark if any - otherwise assume default - var buffer = new byte[5]; - - using (var file = _fileSystem.GetFileStream(srcFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - file.Read(buffer, 0, 5); - } - - if (buffer[0] == 0xef && buffer[1] == 0xbb && buffer[2] == 0xbf) - return Encoding.UTF8; - if (buffer[0] == 0xfe && buffer[1] == 0xff) - return Encoding.Unicode; - if (buffer[0] == 0 && buffer[1] == 0 && buffer[2] == 0xfe && buffer[3] == 0xff) - return Encoding.UTF32; - if (buffer[0] == 0x2b && buffer[1] == 0x2f && buffer[2] == 0x76) - return Encoding.UTF7; - - // It's ok - anything aside from utf is ok since that's what we're looking for - return Encoding.Default; - } - private async Task<Stream> GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) { if (protocol == MediaProtocol.Http) @@ -932,7 +850,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } if (protocol == MediaProtocol.File) { - return _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + return _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite); } throw new ArgumentOutOfRangeException("protocol"); diff --git a/MediaBrowser.MediaEncoding/packages.config b/MediaBrowser.MediaEncoding/packages.config index 32a5c213d..bac7840a5 100644 --- a/MediaBrowser.MediaEncoding/packages.config +++ b/MediaBrowser.MediaEncoding/packages.config @@ -1,6 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="CommonIO" version="1.0.0.9" targetFramework="net45" /> - <package id="MediaBrowser.BdInfo" version="1.0.0.10" targetFramework="net45" /> - <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" /> + <package id="UniversalDetector" version="1.0.1" targetFramework="net46" /> </packages>
\ No newline at end of file diff --git a/MediaBrowser.MediaEncoding/project.json b/MediaBrowser.MediaEncoding/project.json new file mode 100644 index 000000000..fbbe9eaf3 --- /dev/null +++ b/MediaBrowser.MediaEncoding/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +}
\ No newline at end of file |
