diff options
| author | Vasily <JustAMan@users.noreply.github.com> | 2019-03-07 17:23:06 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-03-07 17:23:06 +0300 |
| commit | a4b52b7264fd1788aeecacd520b33005547246f4 (patch) | |
| tree | f4b8c30e1f07c89705689cd2d2f81488378d20af /MediaBrowser.MediaEncoding | |
| parent | 5ae0ef0527eac21e934d77f5d6c2372f3ef3086e (diff) | |
| parent | 2617a49b78c99f72ba36e53a4c97c4e042116a53 (diff) | |
Merge pull request #844 from ploughpuff/ffmpeg
Reworked FFmpeg path discovery and always display to user
Diffstat (limited to 'MediaBrowser.MediaEncoding')
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 10 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 379 |
2 files changed, 136 insertions, 253 deletions
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f725d2c01..3eed891cb 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _processFactory = processFactory; } - public (IEnumerable<string> decoders, IEnumerable<string> encoders) Validate(string encoderPath) + public (IEnumerable<string> decoders, IEnumerable<string> encoders) GetAvailableCoders(string encoderPath) { _logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath); @@ -48,6 +48,10 @@ namespace MediaBrowser.MediaEncoding.Encoder if (string.IsNullOrWhiteSpace(output)) { + if (logOutput) + { + _logger.LogError("FFmpeg validation: The process returned no result"); + } return false; } @@ -55,6 +59,10 @@ namespace MediaBrowser.MediaEncoding.Encoder if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) { + if (logOutput) + { + _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported"); + } return false; } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 7f29c06b4..292457788 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -3,17 +3,14 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Session; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Diagnostics; @@ -22,6 +19,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder @@ -32,340 +30,223 @@ namespace MediaBrowser.MediaEncoding.Encoder public class MediaEncoder : IMediaEncoder, IDisposable { /// <summary> - /// The _logger - /// </summary> - private readonly ILogger _logger; - - /// <summary> - /// Gets the json serializer. + /// Gets the encoder path. /// </summary> - /// <value>The json serializer.</value> - private readonly IJsonSerializer _jsonSerializer; + /// <value>The encoder path.</value> + public string EncoderPath => FFmpegPath; /// <summary> - /// The _thumbnail resource pool + /// The location of the discovered FFmpeg tool. /// </summary> - private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); - - public string FFMpegPath { get; private set; } - - public string FFProbePath { get; private set; } + public FFmpegLocation EncoderLocation { get; private set; } + private readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; + private string FFmpegPath; + private string FFprobePath; protected readonly IServerConfigurationManager ConfigurationManager; protected readonly IFileSystem FileSystem; - protected readonly ILiveTvManager LiveTvManager; - protected readonly IIsoManager IsoManager; - protected readonly ILibraryManager LibraryManager; - protected readonly IChannelManager ChannelManager; - protected readonly ISessionManager SessionManager; protected readonly Func<ISubtitleEncoder> SubtitleEncoder; protected readonly Func<IMediaSourceManager> MediaSourceManager; - private readonly IHttpClient _httpClient; - private readonly IZipClient _zipClient; private readonly IProcessFactory _processFactory; + private readonly int DefaultImageExtractionTimeoutMs; + private readonly string StartupOptionFFmpegPath; + private readonly string StartupOptionFFprobePath; + private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>(); - private readonly bool _hasExternalEncoder; - private readonly string _originalFFMpegPath; - private readonly string _originalFFProbePath; - private readonly int DefaultImageExtractionTimeoutMs; public MediaEncoder( ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, - string ffMpegPath, - string ffProbePath, - bool hasExternalEncoder, + string startupOptionsFFmpegPath, + string startupOptionsFFprobePath, IServerConfigurationManager configurationManager, IFileSystem fileSystem, - ILiveTvManager liveTvManager, - IIsoManager isoManager, - ILibraryManager libraryManager, - IChannelManager channelManager, - ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager, - IHttpClient httpClient, - IZipClient zipClient, IProcessFactory processFactory, int defaultImageExtractionTimeoutMs) { _logger = loggerFactory.CreateLogger(nameof(MediaEncoder)); _jsonSerializer = jsonSerializer; + StartupOptionFFmpegPath = startupOptionsFFmpegPath; + StartupOptionFFprobePath = startupOptionsFFprobePath; ConfigurationManager = configurationManager; FileSystem = fileSystem; - LiveTvManager = liveTvManager; - IsoManager = isoManager; - LibraryManager = libraryManager; - ChannelManager = channelManager; - SessionManager = sessionManager; SubtitleEncoder = subtitleEncoder; - MediaSourceManager = mediaSourceManager; - _httpClient = httpClient; - _zipClient = zipClient; _processFactory = processFactory; DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs; - FFProbePath = ffProbePath; - FFMpegPath = ffMpegPath; - _originalFFProbePath = ffProbePath; - _originalFFMpegPath = ffMpegPath; - _hasExternalEncoder = hasExternalEncoder; } - public string EncoderLocationType + /// <summary> + /// Run at startup or if the user removes a Custom path from transcode page. + /// Sets global variables FFmpegPath. + /// Precedence is: Config > CLI > $PATH + /// </summary> + public void SetFFmpegPath() { - get + // ToDo - Finalise removal of the --ffprobe switch + if (!string.IsNullOrEmpty(StartupOptionFFprobePath)) { - if (_hasExternalEncoder) - { - return "External"; - } - - if (string.IsNullOrWhiteSpace(FFMpegPath)) - { - return null; - } - - if (IsSystemInstalledPath(FFMpegPath)) - { - return "System"; - } - - return "Custom"; + _logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release"); } - } - private bool IsSystemInstalledPath(string path) - { - if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1) + // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence + if (!ValidatePath(ConfigurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom)) { - return true; + // 2) Check if the --ffmpeg CLI switch has been given + 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; + } + } } - return false; - } - - public void Init() - { - InitPaths(); + // 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); - if (!string.IsNullOrWhiteSpace(FFMpegPath)) + // Only if mpeg path is set, try and set path to probe + if (FFmpegPath != null) { - var result = new EncoderValidator(_logger, _processFactory).Validate(FFMpegPath); + // Determine a probe path from the mpeg path + FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); + + // Interrogate to understand what coders are supported + var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath); SetAvailableDecoders(result.decoders); SetAvailableEncoders(result.encoders); } - } - - private void InitPaths() - { - ConfigureEncoderPaths(); - - if (_hasExternalEncoder) - { - LogPaths(); - return; - } - // If the path was passed in, save it into config now. - var encodingOptions = GetEncodingOptions(); - var appPath = encodingOptions.EncoderAppPath; - - var valueToSave = FFMpegPath; - - if (!string.IsNullOrWhiteSpace(valueToSave)) - { - // if using system variable, don't save this. - if (IsSystemInstalledPath(valueToSave) || _hasExternalEncoder) - { - valueToSave = null; - } - } - - if (!string.Equals(valueToSave, appPath, StringComparison.Ordinal)) - { - encodingOptions.EncoderAppPath = valueToSave; - ConfigurationManager.SaveConfiguration("encoding", encodingOptions); - } + _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty); } + /// <summary> + /// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use. + /// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here. + /// </summary> + /// <param name="path"></param> + /// <param name="pathType"></param> public void UpdateEncoderPath(string path, string pathType) { - if (_hasExternalEncoder) - { - return; - } + string newPath; _logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty); - Tuple<string, string> newPaths; - - if (string.Equals(pathType, "system", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) { - path = "ffmpeg"; - - newPaths = TestForInstalledVersions(); - } - else if (string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - if (!File.Exists(path) && !Directory.Exists(path)) - { - throw new ResourceNotFoundException(); - } - newPaths = GetEncoderPaths(path); + throw new ArgumentException("Unexpected pathType value"); } - else + else if (string.IsNullOrWhiteSpace(path)) { - throw new ArgumentException("Unexpected pathType value"); + // User had cleared the custom path in UI + newPath = string.Empty; } - - if (string.IsNullOrWhiteSpace(newPaths.Item1)) + else if (File.Exists(path)) { - throw new ResourceNotFoundException("ffmpeg not found"); + newPath = path; } - if (string.IsNullOrWhiteSpace(newPaths.Item2)) + else if (Directory.Exists(path)) { - throw new ResourceNotFoundException("ffprobe not found"); + // Given path is directory, so resolve down to filename + newPath = GetEncoderPathFromDirectory(path, "ffmpeg"); } - - path = newPaths.Item1; - - if (!ValidateVersion(path, true)) + else { - throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required."); + throw new ResourceNotFoundException(); } - var config = GetEncodingOptions(); - config.EncoderAppPath = path; + // Write the new ffmpeg path to the xml as <EncoderAppPath> + // This ensures its not lost on next startup + var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding"); + config.EncoderAppPath = newPath; ConfigurationManager.SaveConfiguration("encoding", config); - Init(); + // Trigger SetFFmpegPath so we validate the new path and setup probe path + SetFFmpegPath(); } - private bool ValidateVersion(string path, bool logOutput) + /// <summary> + /// Validates the supplied FQPN to ensure it is a ffmpeg utility. + /// If checks pass, global variable FFmpegPath and EncoderLocation are updated. + /// </summary> + /// <param name="path">FQPN to test</param> + /// <param name="location">Location (External, Custom, System) of tool</param> + /// <returns></returns> + private bool ValidatePath(string path, FFmpegLocation location) { - return new EncoderValidator(_logger, _processFactory).ValidateVersion(path, logOutput); - } + bool rc = false; - private void ConfigureEncoderPaths() - { - if (_hasExternalEncoder) + if (!string.IsNullOrEmpty(path)) { - return; - } - - var appPath = GetEncodingOptions().EncoderAppPath; - - if (string.IsNullOrWhiteSpace(appPath)) - { - appPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg"); - } - - var newPaths = GetEncoderPaths(appPath); - if (string.IsNullOrWhiteSpace(newPaths.Item1) || string.IsNullOrWhiteSpace(newPaths.Item2) || IsSystemInstalledPath(appPath)) - { - newPaths = TestForInstalledVersions(); - } - - if (!string.IsNullOrWhiteSpace(newPaths.Item1) && !string.IsNullOrWhiteSpace(newPaths.Item2)) - { - FFMpegPath = newPaths.Item1; - FFProbePath = newPaths.Item2; - } + if (File.Exists(path)) + { + rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true); - LogPaths(); - } + if (!rc) + { + _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path); + } - private Tuple<string, string> GetEncoderPaths(string configuredPath) - { - var appPath = configuredPath; + // ToDo - Enable the ffmpeg validator. At the moment any version can be used. + rc = true; - if (!string.IsNullOrWhiteSpace(appPath)) - { - if (Directory.Exists(appPath)) - { - return GetPathsFromDirectory(appPath); + FFmpegPath = path; + EncoderLocation = location; } - - if (File.Exists(appPath)) + else { - return new Tuple<string, string>(appPath, GetProbePathFromEncoderPath(appPath)); + _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path); } } - return new Tuple<string, string>(null, null); + return rc; } - private Tuple<string, string> TestForInstalledVersions() + private string GetEncoderPathFromDirectory(string path, string filename) { - string encoderPath = null; - string probePath = null; - - if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath, true)) + try { - encoderPath = _originalFFMpegPath; - probePath = _originalFFProbePath; - } + var files = FileSystem.GetFilePaths(path); + + var excludeExtensions = new[] { ".c" }; - if (string.IsNullOrWhiteSpace(encoderPath)) + return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase) + && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); + } + catch (Exception) { - if (ValidateVersion("ffmpeg", true) && ValidateVersion("ffprobe", false)) - { - encoderPath = "ffmpeg"; - probePath = "ffprobe"; - } + // Trap all exceptions, like DirNotExists, and return null + return null; } - - return new Tuple<string, string>(encoderPath, probePath); } - private Tuple<string, string> GetPathsFromDirectory(string path) + /// <summary> + /// Search the system $PATH environment variable looking for given filename. + /// </summary> + /// <param name="fileName"></param> + /// <returns></returns> + private string ExistsOnSystemPath(string filename) { - // 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 = FileSystem.GetFilePaths(path); + var values = Environment.GetEnvironmentVariable("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)) + foreach (var path in values.Split(Path.PathSeparator)) { - files = FileSystem.GetFilePaths(path, true); - - ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); + var candidatePath = GetEncoderPathFromDirectory(path, filename); - if (!string.IsNullOrWhiteSpace(ffmpegPath)) + if (!string.IsNullOrEmpty(candidatePath)) { - ffprobePath = GetProbePathFromEncoderPath(ffmpegPath); + return candidatePath; } } - - return new Tuple<string, string>(ffmpegPath, ffprobePath); - } - - private string GetProbePathFromEncoderPath(string appPath) - { - return FileSystem.GetFilePaths(Path.GetDirectoryName(appPath)) - .FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase)); - } - - private void LogPaths() - { - _logger.LogInformation("FFMpeg: {0}", FFMpegPath ?? "not found"); - _logger.LogInformation("FFProbe: {0}", FFProbePath ?? "not found"); - } - - private EncodingOptions GetEncodingOptions() - { - return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding"); + return null; } private List<string> _encoders = new List<string>(); @@ -413,12 +294,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } /// <summary> - /// Gets the encoder path. - /// </summary> - /// <value>The encoder path.</value> - public string EncoderPath => FFMpegPath; - - /// <summary> /// Gets the media info. /// </summary> /// <param name="request">The request.</param> @@ -489,7 +364,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 = string.Format(args, probeSizeArgument, inputPath).Trim(), IsHidden = true, @@ -691,7 +566,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFMpegPath, + FileName = FFmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, @@ -814,7 +689,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFMpegPath, + FileName = FFmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, |
