diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4b397b328..81857e57c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -23,6 +23,7 @@ - [fruhnow](https://github.com/fruhnow) - [Lynxy](https://github.com/Lynxy) - [fasheng](https://github.com/fasheng) + - [ploughpuff](https://github.com/ploughpuff) # Emby Contributors diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index b3bb4f740..dc971ea59 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -28,7 +28,6 @@ using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.Diagnostics; using Emby.Server.Implementations.Dto; -using Emby.Server.Implementations.FFMpeg; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.IO; @@ -535,7 +534,7 @@ namespace Emby.Server.Implementations ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; - MediaEncoder.Init(); + MediaEncoder.SetFFmpegPath(); //if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) //{ @@ -790,7 +789,18 @@ namespace Emby.Server.Implementations ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); serviceCollection.AddSingleton(ChapterManager); - RegisterMediaEncoder(serviceCollection); + MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( + LoggerFactory, + JsonSerializer, + StartupOptions.FFmpegPath, + StartupOptions.FFprobePath, + ServerConfigurationManager, + FileSystemManager, + () => SubtitleEncoder, + () => MediaSourceManager, + ProcessFactory, + 5000); + serviceCollection.AddSingleton(MediaEncoder); EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager); serviceCollection.AddSingleton(EncodingManager); @@ -906,85 +916,6 @@ namespace Emby.Server.Implementations return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); } - protected virtual FFMpegInstallInfo GetFfmpegInstallInfo() - { - var info = new FFMpegInstallInfo(); - - // Windows builds: http://ffmpeg.zeranoe.com/builds/ - // Linux builds: http://johnvansickle.com/ffmpeg/ - // OS X builds: http://ffmpegmac.net/ - // OS X x64: http://www.evermeet.cx/ffmpeg/ - - if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux) - { - info.FFMpegFilename = "ffmpeg"; - info.FFProbeFilename = "ffprobe"; - info.ArchiveType = "7z"; - info.Version = "20170308"; - } - else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) - { - info.FFMpegFilename = "ffmpeg.exe"; - info.FFProbeFilename = "ffprobe.exe"; - info.Version = "20170308"; - info.ArchiveType = "7z"; - } - else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX) - { - info.FFMpegFilename = "ffmpeg"; - info.FFProbeFilename = "ffprobe"; - info.ArchiveType = "7z"; - info.Version = "20170308"; - } - - return info; - } - - protected virtual FFMpegInfo GetFFMpegInfo() - { - return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo()) - .GetFFMpegInfo(StartupOptions); - } - - /// - /// Registers the media encoder. - /// - /// Task. - private void RegisterMediaEncoder(IServiceCollection serviceCollection) - { - string encoderPath = null; - string probePath = null; - - var info = GetFFMpegInfo(); - - encoderPath = info.EncoderPath; - probePath = info.ProbePath; - var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase); - - var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( - LoggerFactory, - JsonSerializer, - encoderPath, - probePath, - hasExternalEncoder, - ServerConfigurationManager, - FileSystemManager, - LiveTvManager, - IsoManager, - LibraryManager, - ChannelManager, - SessionManager, - () => SubtitleEncoder, - () => MediaSourceManager, - HttpClient, - ZipClient, - ProcessFactory, - 5000); - - MediaEncoder = mediaEncoder; - serviceCollection.AddSingleton(MediaEncoder); - } - /// /// Gets the user repository. /// @@ -1460,7 +1391,7 @@ namespace Emby.Server.Implementations ServerName = FriendlyName, LocalAddress = localAddress, SupportsLibraryMonitor = true, - EncoderLocationType = MediaEncoder.EncoderLocationType, + EncoderLocation = MediaEncoder.EncoderLocation, SystemArchitecture = EnvironmentInfo.SystemArchitecture, SystemUpdateLevel = SystemUpdateLevel, PackageName = StartupOptions.PackageName diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs deleted file mode 100644 index 60cd7b3d7..000000000 --- a/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Emby.Server.Implementations.FFMpeg -{ - /// - /// Class FFMpegInfo - /// - public class FFMpegInfo - { - /// - /// Gets or sets the path. - /// - /// The path. - public string EncoderPath { get; set; } - /// - /// Gets or sets the probe path. - /// - /// The probe path. - public string ProbePath { get; set; } - /// - /// Gets or sets the version. - /// - /// The version. - public string Version { get; set; } - } -} diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs deleted file mode 100644 index fa9cb5e01..000000000 --- a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Emby.Server.Implementations.FFMpeg -{ - public class FFMpegInstallInfo - { - public string Version { get; set; } - public string FFMpegFilename { get; set; } - public string FFProbeFilename { get; set; } - public string ArchiveType { get; set; } - - public FFMpegInstallInfo() - { - Version = "Path"; - FFMpegFilename = "ffmpeg"; - FFProbeFilename = "ffprobe"; - } - } -} diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs deleted file mode 100644 index bbf51dd24..000000000 --- a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.IO; - -namespace Emby.Server.Implementations.FFMpeg -{ - public class FFMpegLoader - { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly FFMpegInstallInfo _ffmpegInstallInfo; - - public FFMpegLoader(IApplicationPaths appPaths, IFileSystem fileSystem, FFMpegInstallInfo ffmpegInstallInfo) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - _ffmpegInstallInfo = ffmpegInstallInfo; - } - - public FFMpegInfo GetFFMpegInfo(IStartupOptions options) - { - var customffMpegPath = options.FFmpegPath; - var customffProbePath = options.FFprobePath; - - if (!string.IsNullOrWhiteSpace(customffMpegPath) && !string.IsNullOrWhiteSpace(customffProbePath)) - { - return new FFMpegInfo - { - ProbePath = customffProbePath, - EncoderPath = customffMpegPath, - Version = "external" - }; - } - - var downloadInfo = _ffmpegInstallInfo; - - var prebuiltFolder = _appPaths.ProgramSystemPath; - var prebuiltffmpeg = Path.Combine(prebuiltFolder, downloadInfo.FFMpegFilename); - var prebuiltffprobe = Path.Combine(prebuiltFolder, downloadInfo.FFProbeFilename); - if (File.Exists(prebuiltffmpeg) && File.Exists(prebuiltffprobe)) - { - return new FFMpegInfo - { - ProbePath = prebuiltffprobe, - EncoderPath = prebuiltffmpeg, - Version = "external" - }; - } - - var version = downloadInfo.Version; - - if (string.Equals(version, "0", StringComparison.OrdinalIgnoreCase)) - { - return new FFMpegInfo(); - } - - var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg"); - var versionedDirectoryPath = Path.Combine(rootEncoderPath, version); - - var info = new FFMpegInfo - { - ProbePath = Path.Combine(versionedDirectoryPath, downloadInfo.FFProbeFilename), - EncoderPath = Path.Combine(versionedDirectoryPath, downloadInfo.FFMpegFilename), - Version = version - }; - - Directory.CreateDirectory(versionedDirectoryPath); - - var excludeFromDeletions = new List { versionedDirectoryPath }; - - if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath)) - { - // ffmpeg not present. See if there's an older version we can start with - var existingVersion = GetExistingVersion(info, rootEncoderPath); - - // No older version. Need to download and block until complete - if (existingVersion == null) - { - return new FFMpegInfo(); - } - else - { - info = existingVersion; - versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath); - excludeFromDeletions.Add(versionedDirectoryPath); - } - } - - // Allow just one of these to be overridden, if desired. - if (!string.IsNullOrWhiteSpace(customffMpegPath)) - { - info.EncoderPath = customffMpegPath; - } - if (!string.IsNullOrWhiteSpace(customffProbePath)) - { - info.ProbePath = customffProbePath; - } - - return info; - } - - private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath) - { - var encoderFilename = Path.GetFileName(info.EncoderPath); - var probeFilename = Path.GetFileName(info.ProbePath); - - foreach (var directory in _fileSystem.GetDirectoryPaths(rootEncoderPath)) - { - var allFiles = _fileSystem.GetFilePaths(directory, true).ToList(); - - var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase)); - var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase)); - - if (!string.IsNullOrWhiteSpace(encoder) && - !string.IsNullOrWhiteSpace(probe)) - { - return new FFMpegInfo - { - EncoderPath = encoder, - ProbePath = probe, - Version = Path.GetFileName(Path.GetDirectoryName(probe)) - }; - } - } - - return null; - } - } -} diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 5d3f7b171..c8cdb984d 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -20,10 +20,10 @@ namespace Jellyfin.Server [Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")] public string LogDir { get; set; } - [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH. Must be specified along with --ffprobe.")] + [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")] public string FFmpegPath { get; set; } - [Option("ffprobe", Required = false, HelpText = "Path to external FFprobe executable to use in place of default found in PATH. Must be specified along with --ffmpeg.")] + [Option("ffprobe", Required = false, HelpText = "(deprecated) Option has no effect and shall be removed in next release.")] public string FFprobePath { get; set; } [Option("service", Required = false, HelpText = "Run as headless service.")] diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 057e43910..d4ac3b7c3 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -6,6 +6,7 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.System; namespace MediaBrowser.Controller.MediaEncoding { @@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// public interface IMediaEncoder : ITranscoderSupport { - string EncoderLocationType { get; } + FFmpegLocation EncoderLocation { get; } /// /// Gets the encoder path. @@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// System.String. string EscapeSubtitleFilterPath(string path); - void Init(); + void SetFFmpegPath(); void UpdateEncoderPath(string path, string pathType); bool SupportsEncoder(string encoder); 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 decoders, IEnumerable encoders) Validate(string encoderPath) + public (IEnumerable decoders, IEnumerable 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 { /// - /// The _logger + /// Gets the encoder path. /// + /// The encoder path. + public string EncoderPath => FFmpegPath; + + /// + /// The location of the discovered FFmpeg tool. + /// + public FFmpegLocation EncoderLocation { get; private set; } + private readonly ILogger _logger; - - /// - /// Gets the json serializer. - /// - /// The json serializer. private readonly IJsonSerializer _jsonSerializer; - - /// - /// The _thumbnail resource pool - /// - private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); - - public string FFMpegPath { get; private set; } - - public string FFProbePath { get; private set; } - + 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 SubtitleEncoder; protected readonly Func MediaSourceManager; - private readonly IHttpClient _httpClient; - private readonly IZipClient _zipClient; private readonly IProcessFactory _processFactory; - - private readonly List _runningProcesses = new List(); - private readonly bool _hasExternalEncoder; - private readonly string _originalFFMpegPath; - private readonly string _originalFFProbePath; private readonly int DefaultImageExtractionTimeoutMs; + private readonly string StartupOptionFFmpegPath; + private readonly string StartupOptionFFprobePath; + + private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); + private readonly List _runningProcesses = new List(); 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 subtitleEncoder, Func 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 + /// + /// Run at startup or if the user removes a Custom path from transcode page. + /// Sets global variables FFmpegPath. + /// Precedence is: Config > CLI > $PATH + /// + 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"; - } - } - - private bool IsSystemInstalledPath(string path) - { - if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1) - { - return true; + _logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release"); } - return false; - } - - public void Init() - { - InitPaths(); - - if (!string.IsNullOrWhiteSpace(FFMpegPath)) + // 1) Custom path stored in config/encoding xml file under tag takes precedence + if (!ValidatePath(ConfigurationManager.GetConfiguration("encoding").EncoderAppPath, FFmpegLocation.Custom)) { - var result = new EncoderValidator(_logger, _processFactory).Validate(FFMpegPath); + // 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; + } + } + } + + // Write the FFmpeg path to the config/encoding.xml file as so it appears in UI + var config = ConfigurationManager.GetConfiguration("encoding"); + config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty; + ConfigurationManager.SaveConfiguration("encoding", config); + + // Only if mpeg path is set, try and set path to probe + if (FFmpegPath != null) + { + // 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); } + + _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty); } - 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); - } - } - + /// + /// 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. + /// + /// + /// 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 newPaths; - - if (string.Equals(pathType, "system", 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); - } - else + if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Unexpected pathType value"); } - - if (string.IsNullOrWhiteSpace(newPaths.Item1)) + else if (string.IsNullOrWhiteSpace(path)) { - throw new ResourceNotFoundException("ffmpeg not found"); + // User had cleared the custom path in UI + newPath = string.Empty; } - if (string.IsNullOrWhiteSpace(newPaths.Item2)) + else if (File.Exists(path)) { - throw new ResourceNotFoundException("ffprobe not found"); + newPath = path; + } + else if (Directory.Exists(path)) + { + // Given path is directory, so resolve down to filename + newPath = GetEncoderPathFromDirectory(path, "ffmpeg"); + } + else + { + throw new ResourceNotFoundException(); } - path = newPaths.Item1; - - if (!ValidateVersion(path, true)) - { - throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required."); - } - - var config = GetEncodingOptions(); - config.EncoderAppPath = path; + // Write the new ffmpeg path to the xml as + // This ensures its not lost on next startup + var config = ConfigurationManager.GetConfiguration("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) + /// + /// Validates the supplied FQPN to ensure it is a ffmpeg utility. + /// If checks pass, global variable FFmpegPath and EncoderLocation are updated. + /// + /// FQPN to test + /// Location (External, Custom, System) of tool + /// + 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; - } - - LogPaths(); - } - - private Tuple GetEncoderPaths(string configuredPath) - { - var appPath = configuredPath; - - if (!string.IsNullOrWhiteSpace(appPath)) - { - if (Directory.Exists(appPath)) + if (File.Exists(path)) { - return GetPathsFromDirectory(appPath); + rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true); + + if (!rc) + { + _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path); + } + + // ToDo - Enable the ffmpeg validator. At the moment any version can be used. + rc = true; + + FFmpegPath = path; + EncoderLocation = location; } - - if (File.Exists(appPath)) + else { - return new Tuple(appPath, GetProbePathFromEncoderPath(appPath)); + _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path); } } - return new Tuple(null, null); + return rc; } - private Tuple 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" }; + + return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase) + && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); } - - if (string.IsNullOrWhiteSpace(encoderPath)) + catch (Exception) { - if (ValidateVersion("ffmpeg", true) && ValidateVersion("ffprobe", false)) + // Trap all exceptions, like DirNotExists, and return null + return null; + } + } + + /// + /// Search the system $PATH environment variable looking for given filename. + /// + /// + /// + private string ExistsOnSystemPath(string filename) + { + var values = Environment.GetEnvironmentVariable("PATH"); + + foreach (var path in values.Split(Path.PathSeparator)) + { + var candidatePath = GetEncoderPathFromDirectory(path, filename); + + if (!string.IsNullOrEmpty(candidatePath)) { - encoderPath = "ffmpeg"; - probePath = "ffprobe"; + return candidatePath; } } - - return new Tuple(encoderPath, probePath); - } - - private Tuple GetPathsFromDirectory(string path) - { - // 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 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)) - { - files = FileSystem.GetFilePaths(path, true); - - ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); - - if (!string.IsNullOrWhiteSpace(ffmpegPath)) - { - ffprobePath = GetProbePathFromEncoderPath(ffmpegPath); - } - } - - return new Tuple(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("encoding"); + return null; } private List _encoders = new List(); @@ -412,12 +293,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return true; } - /// - /// Gets the encoder path. - /// - /// The encoder path. - public string EncoderPath => FFMpegPath; - /// /// Gets the media info. /// @@ -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, diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 8584bd3dd..285ff4ba5 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -8,7 +8,14 @@ namespace MediaBrowser.Model.Configuration public bool EnableThrottling { get; set; } public int ThrottleDelaySeconds { get; set; } public string HardwareAccelerationType { get; set; } + /// + /// FFmpeg path as set by the user via the UI + /// public string EncoderAppPath { get; set; } + /// + /// The current FFmpeg path being used by the system and displayed on the transcode page + /// + public string EncoderAppPathDisplay { get; set; } public string VaapiDevice { get; set; } public int H264Crf { get; set; } public string H264Preset { get; set; } diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 581a1069c..6482f2c84 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -4,6 +4,21 @@ using MediaBrowser.Model.Updates; namespace MediaBrowser.Model.System { + /// + /// Enum describing the location of the FFmpeg tool. + /// + public enum FFmpegLocation + { + /// No path to FFmpeg found. + NotFound, + /// Path supplied via command line using switch --ffmpeg. + SetByArgument, + /// User has supplied path via Transcoding UI page. + Custom, + /// FFmpeg tool found on system $PATH. + System + }; + /// /// Class SystemInfo /// @@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System /// true if this instance has update available; otherwise, false. public bool HasUpdateAvailable { get; set; } - public string EncoderLocationType { get; set; } + public FFmpegLocation EncoderLocation { get; set; } public Architecture SystemArchitecture { get; set; }