瀏覽代碼

Merge pull request #844 from ploughpuff/ffmpeg

Reworked FFmpeg path discovery and always display to user
Vasily 6 年之前
父節點
當前提交
a4b52b7264

+ 1 - 0
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
 

+ 14 - 83
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);
-        }
-
-        /// <summary>
-        /// Registers the media encoder.
-        /// </summary>
-        /// <returns>Task.</returns>
-        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);
-        }
-
         /// <summary>
         /// Gets the user repository.
         /// </summary>
@@ -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

+ 0 - 24
Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs

@@ -1,24 +0,0 @@
-namespace Emby.Server.Implementations.FFMpeg
-{
-    /// <summary>
-    /// Class FFMpegInfo
-    /// </summary>
-    public class FFMpegInfo
-    {
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        public string EncoderPath { get; set; }
-        /// <summary>
-        /// Gets or sets the probe path.
-        /// </summary>
-        /// <value>The probe path.</value>
-        public string ProbePath { get; set; }
-        /// <summary>
-        /// Gets or sets the version.
-        /// </summary>
-        /// <value>The version.</value>
-        public string Version { get; set; }
-    }
-}

+ 0 - 17
Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs

@@ -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";
-        }
-    }
-}

+ 0 - 132
Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs

@@ -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<string> { 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;
-        }
-    }
-}

+ 2 - 2
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.")]

+ 3 - 2
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
     /// </summary>
     public interface IMediaEncoder : ITranscoderSupport
     {
-        string EncoderLocationType { get; }
+        FFmpegLocation EncoderLocation { get; }
 
         /// <summary>
         /// Gets the encoder path.
@@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <returns>System.String.</returns>
         string EscapeSubtitleFilterPath(string path);
 
-        void Init();
+        void SetFFmpegPath();
 
         void UpdateEncoderPath(string path, string pathType);
         bool SupportsEncoder(string encoder);

+ 9 - 1
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;
             }
 

+ 127 - 252
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>();
@@ -412,12 +293,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return true;
         }
 
-        /// <summary>
-        /// Gets the encoder path.
-        /// </summary>
-        /// <value>The encoder path.</value>
-        public string EncoderPath => FFMpegPath;
-
         /// <summary>
         /// Gets the media info.
         /// </summary>
@@ -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,

+ 7 - 0
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; }
+        /// <summary>
+        /// FFmpeg path as set by the user via the UI
+        /// </summary>
         public string EncoderAppPath { get; set; }
+        /// <summary>
+        /// The current FFmpeg path being used by the system and displayed on the transcode page
+        /// </summary>
+        public string EncoderAppPathDisplay { get; set; }
         public string VaapiDevice { get; set; }
         public int H264Crf { get; set; }
         public string H264Preset { get; set; }

+ 16 - 1
MediaBrowser.Model/System/SystemInfo.cs

@@ -4,6 +4,21 @@ using MediaBrowser.Model.Updates;
 
 namespace MediaBrowser.Model.System
 {
+    /// <summary>
+    /// Enum describing the location of the FFmpeg tool.
+    /// </summary>
+    public enum FFmpegLocation
+    {
+        /// <summary>No path to FFmpeg found.</summary>
+        NotFound,
+        /// <summary>Path supplied via command line using switch --ffmpeg.</summary>
+        SetByArgument,
+        /// <summary>User has supplied path via Transcoding UI page.</summary>
+        Custom,
+        /// <summary>FFmpeg tool found on system $PATH.</summary>
+        System
+    };
+
     /// <summary>
     /// Class SystemInfo
     /// </summary>
@@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System
         /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
         public bool HasUpdateAvailable { get; set; }
 
-        public string EncoderLocationType { get; set; }
+        public FFmpegLocation EncoderLocation { get; set; }
 
         public Architecture SystemArchitecture { get; set; }