Merge pull request #844 from ploughpuff/ffmpeg

Reworked FFmpeg path discovery and always display to user
This commit is contained in:
Vasily 2019-03-07 17:23:06 +03:00 committed by GitHub
commit a4b52b7264
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 199 additions and 534 deletions

View File

@ -23,6 +23,7 @@
- [fruhnow](https://github.com/fruhnow) - [fruhnow](https://github.com/fruhnow)
- [Lynxy](https://github.com/Lynxy) - [Lynxy](https://github.com/Lynxy)
- [fasheng](https://github.com/fasheng) - [fasheng](https://github.com/fasheng)
- [ploughpuff](https://github.com/ploughpuff)
# Emby Contributors # Emby Contributors

View File

@ -28,7 +28,6 @@ using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Diagnostics; using Emby.Server.Implementations.Diagnostics;
using Emby.Server.Implementations.Dto; using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.FFMpeg;
using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO; using Emby.Server.Implementations.IO;
@ -535,7 +534,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
MediaEncoder.Init(); MediaEncoder.SetFFmpegPath();
//if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) //if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
//{ //{
@ -790,7 +789,18 @@ namespace Emby.Server.Implementations
ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
serviceCollection.AddSingleton(ChapterManager); 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); EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
serviceCollection.AddSingleton(EncodingManager); serviceCollection.AddSingleton(EncodingManager);
@ -906,85 +916,6 @@ namespace Emby.Server.Implementations
return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); 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> /// <summary>
/// Gets the user repository. /// Gets the user repository.
/// </summary> /// </summary>
@ -1460,7 +1391,7 @@ namespace Emby.Server.Implementations
ServerName = FriendlyName, ServerName = FriendlyName,
LocalAddress = localAddress, LocalAddress = localAddress,
SupportsLibraryMonitor = true, SupportsLibraryMonitor = true,
EncoderLocationType = MediaEncoder.EncoderLocationType, EncoderLocation = MediaEncoder.EncoderLocation,
SystemArchitecture = EnvironmentInfo.SystemArchitecture, SystemArchitecture = EnvironmentInfo.SystemArchitecture,
SystemUpdateLevel = SystemUpdateLevel, SystemUpdateLevel = SystemUpdateLevel,
PackageName = StartupOptions.PackageName PackageName = StartupOptions.PackageName

View File

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

View File

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

View File

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

View File

@ -20,10 +20,10 @@ namespace Jellyfin.Server
[Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")] [Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")]
public string LogDir { get; set; } 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; } 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; } public string FFprobePath { get; set; }
[Option("service", Required = false, HelpText = "Run as headless service.")] [Option("service", Required = false, HelpText = "Run as headless service.")]

View File

@ -6,6 +6,7 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.System;
namespace MediaBrowser.Controller.MediaEncoding namespace MediaBrowser.Controller.MediaEncoding
{ {
@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary> /// </summary>
public interface IMediaEncoder : ITranscoderSupport public interface IMediaEncoder : ITranscoderSupport
{ {
string EncoderLocationType { get; } FFmpegLocation EncoderLocation { get; }
/// <summary> /// <summary>
/// Gets the encoder path. /// Gets the encoder path.
@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
string EscapeSubtitleFilterPath(string path); string EscapeSubtitleFilterPath(string path);
void Init(); void SetFFmpegPath();
void UpdateEncoderPath(string path, string pathType); void UpdateEncoderPath(string path, string pathType);
bool SupportsEncoder(string encoder); bool SupportsEncoder(string encoder);

View File

@ -19,7 +19,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_processFactory = processFactory; _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); _logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath);
@ -48,6 +48,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (string.IsNullOrWhiteSpace(output)) if (string.IsNullOrWhiteSpace(output))
{ {
if (logOutput)
{
_logger.LogError("FFmpeg validation: The process returned no result");
}
return false; return false;
} }
@ -55,6 +59,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
{ {
if (logOutput)
{
_logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
}
return false; return false;
} }

View File

@ -3,17 +3,14 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Diagnostics;
@ -22,6 +19,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Encoder namespace MediaBrowser.MediaEncoding.Encoder
@ -32,340 +30,223 @@ namespace MediaBrowser.MediaEncoding.Encoder
public class MediaEncoder : IMediaEncoder, IDisposable public class MediaEncoder : IMediaEncoder, IDisposable
{ {
/// <summary> /// <summary>
/// The _logger /// Gets the encoder path.
/// </summary> /// </summary>
/// <value>The encoder path.</value>
public string EncoderPath => FFmpegPath;
/// <summary>
/// The location of the discovered FFmpeg tool.
/// </summary>
public FFmpegLocation EncoderLocation { get; private set; }
private readonly ILogger _logger; private readonly ILogger _logger;
/// <summary>
/// Gets the json serializer.
/// </summary>
/// <value>The json serializer.</value>
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private string FFmpegPath;
/// <summary> private string FFprobePath;
/// The _thumbnail resource pool
/// </summary>
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1);
public string FFMpegPath { get; private set; }
public string FFProbePath { get; private set; }
protected readonly IServerConfigurationManager ConfigurationManager; protected readonly IServerConfigurationManager ConfigurationManager;
protected readonly IFileSystem FileSystem; 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<ISubtitleEncoder> SubtitleEncoder;
protected readonly Func<IMediaSourceManager> MediaSourceManager; protected readonly Func<IMediaSourceManager> MediaSourceManager;
private readonly IHttpClient _httpClient;
private readonly IZipClient _zipClient;
private readonly IProcessFactory _processFactory; private readonly IProcessFactory _processFactory;
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
private readonly bool _hasExternalEncoder;
private readonly string _originalFFMpegPath;
private readonly string _originalFFProbePath;
private readonly int DefaultImageExtractionTimeoutMs; 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>();
public MediaEncoder( public MediaEncoder(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
string ffMpegPath, string startupOptionsFFmpegPath,
string ffProbePath, string startupOptionsFFprobePath,
bool hasExternalEncoder,
IServerConfigurationManager configurationManager, IServerConfigurationManager configurationManager,
IFileSystem fileSystem, IFileSystem fileSystem,
ILiveTvManager liveTvManager,
IIsoManager isoManager,
ILibraryManager libraryManager,
IChannelManager channelManager,
ISessionManager sessionManager,
Func<ISubtitleEncoder> subtitleEncoder, Func<ISubtitleEncoder> subtitleEncoder,
Func<IMediaSourceManager> mediaSourceManager, Func<IMediaSourceManager> mediaSourceManager,
IHttpClient httpClient,
IZipClient zipClient,
IProcessFactory processFactory, IProcessFactory processFactory,
int defaultImageExtractionTimeoutMs) int defaultImageExtractionTimeoutMs)
{ {
_logger = loggerFactory.CreateLogger(nameof(MediaEncoder)); _logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
StartupOptionFFmpegPath = startupOptionsFFmpegPath;
StartupOptionFFprobePath = startupOptionsFFprobePath;
ConfigurationManager = configurationManager; ConfigurationManager = configurationManager;
FileSystem = fileSystem; FileSystem = fileSystem;
LiveTvManager = liveTvManager;
IsoManager = isoManager;
LibraryManager = libraryManager;
ChannelManager = channelManager;
SessionManager = sessionManager;
SubtitleEncoder = subtitleEncoder; SubtitleEncoder = subtitleEncoder;
MediaSourceManager = mediaSourceManager;
_httpClient = httpClient;
_zipClient = zipClient;
_processFactory = processFactory; _processFactory = processFactory;
DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs; 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) _logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release");
{
return "External";
} }
if (string.IsNullOrWhiteSpace(FFMpegPath)) // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
if (!ValidatePath(ConfigurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
{ {
return null; // 2) Check if the --ffmpeg CLI switch has been given
} if (!ValidatePath(StartupOptionFFmpegPath, FFmpegLocation.SetByArgument))
if (IsSystemInstalledPath(FFMpegPath))
{ {
return "System"; // 3) Search system $PATH environment variable for valid FFmpeg
if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System))
{
EncoderLocation = FFmpegLocation.NotFound;
FFmpegPath = null;
} }
return "Custom";
} }
} }
private bool IsSystemInstalledPath(string path) // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
{ var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1) config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty;
{ ConfigurationManager.SaveConfiguration("encoding", config);
return true;
}
return false; // Only if mpeg path is set, try and set path to probe
} if (FFmpegPath != null)
public void Init()
{ {
InitPaths(); // Determine a probe path from the mpeg path
FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1");
if (!string.IsNullOrWhiteSpace(FFMpegPath)) // Interrogate to understand what coders are supported
{ var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath);
var result = new EncoderValidator(_logger, _processFactory).Validate(FFMpegPath);
SetAvailableDecoders(result.decoders); SetAvailableDecoders(result.decoders);
SetAvailableEncoders(result.encoders); SetAvailableEncoders(result.encoders);
} }
_logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty);
} }
private void InitPaths() /// <summary>
{ /// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use.
ConfigureEncoderPaths(); /// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here.
/// </summary>
if (_hasExternalEncoder) /// <param name="path"></param>
{ /// <param name="pathType"></param>
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);
}
}
public void UpdateEncoderPath(string path, string pathType) public void UpdateEncoderPath(string path, string pathType)
{ {
if (_hasExternalEncoder) string newPath;
{
return;
}
_logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty); _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, "custom", StringComparison.OrdinalIgnoreCase))
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
{ {
throw new ArgumentException("Unexpected pathType value"); throw new ArgumentException("Unexpected pathType value");
} }
else if (string.IsNullOrWhiteSpace(path))
if (string.IsNullOrWhiteSpace(newPaths.Item1))
{ {
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; // Write the new ffmpeg path to the xml as <EncoderAppPath>
// This ensures its not lost on next startup
if (!ValidateVersion(path, true)) var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
{ config.EncoderAppPath = newPath;
throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required.");
}
var config = GetEncodingOptions();
config.EncoderAppPath = path;
ConfigurationManager.SaveConfiguration("encoding", config); 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;
if (!string.IsNullOrEmpty(path))
{
if (File.Exists(path))
{
rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true);
if (!rc)
{
_logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path);
} }
private void ConfigureEncoderPaths() // ToDo - Enable the ffmpeg validator. At the moment any version can be used.
{ rc = true;
if (_hasExternalEncoder)
{ FFmpegPath = path;
return; EncoderLocation = location;
} }
else
var appPath = GetEncodingOptions().EncoderAppPath;
if (string.IsNullOrWhiteSpace(appPath))
{ {
appPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg"); _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path);
}
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<string, string> GetEncoderPaths(string configuredPath)
{
var appPath = configuredPath;
if (!string.IsNullOrWhiteSpace(appPath))
{
if (Directory.Exists(appPath))
{
return GetPathsFromDirectory(appPath);
}
if (File.Exists(appPath))
{
return new Tuple<string, string>(appPath, GetProbePathFromEncoderPath(appPath));
} }
} }
return new Tuple<string, string>(null, null); return rc;
} }
private Tuple<string, string> TestForInstalledVersions() private string GetEncoderPathFromDirectory(string path, string filename)
{ {
string encoderPath = null; try
string probePath = null;
if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath, true))
{ {
encoderPath = _originalFFMpegPath;
probePath = _originalFFProbePath;
}
if (string.IsNullOrWhiteSpace(encoderPath))
{
if (ValidateVersion("ffmpeg", true) && ValidateVersion("ffprobe", false))
{
encoderPath = "ffmpeg";
probePath = "ffprobe";
}
}
return new Tuple<string, string>(encoderPath, probePath);
}
private Tuple<string, string> 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 files = FileSystem.GetFilePaths(path);
var excludeExtensions = new[] { ".c" }; var excludeExtensions = new[] { ".c" };
var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
}
if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath)) catch (Exception)
{ {
files = FileSystem.GetFilePaths(path, true); // Trap all exceptions, like DirNotExists, and return null
return null;
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<string, string>(ffmpegPath, ffprobePath); /// <summary>
} /// Search the system $PATH environment variable looking for given filename.
/// </summary>
private string GetProbePathFromEncoderPath(string appPath) /// <param name="fileName"></param>
/// <returns></returns>
private string ExistsOnSystemPath(string filename)
{ {
return FileSystem.GetFilePaths(Path.GetDirectoryName(appPath)) var values = Environment.GetEnvironmentVariable("PATH");
.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase));
}
private void LogPaths() foreach (var path in values.Split(Path.PathSeparator))
{ {
_logger.LogInformation("FFMpeg: {0}", FFMpegPath ?? "not found"); var candidatePath = GetEncoderPathFromDirectory(path, filename);
_logger.LogInformation("FFProbe: {0}", FFProbePath ?? "not found");
}
private EncodingOptions GetEncodingOptions() if (!string.IsNullOrEmpty(candidatePath))
{ {
return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding"); return candidatePath;
}
}
return null;
} }
private List<string> _encoders = new List<string>(); private List<string> _encoders = new List<string>();
@ -412,12 +293,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
return true; return true;
} }
/// <summary>
/// Gets the encoder path.
/// </summary>
/// <value>The encoder path.</value>
public string EncoderPath => FFMpegPath;
/// <summary> /// <summary>
/// Gets the media info. /// Gets the media info.
/// </summary> /// </summary>
@ -489,7 +364,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Must consume both or ffmpeg may hang due to deadlocks. See comments below. // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
RedirectStandardOutput = true, RedirectStandardOutput = true,
FileName = FFProbePath, FileName = FFprobePath,
Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(), Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(),
IsHidden = true, IsHidden = true,
@ -691,7 +566,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
CreateNoWindow = true, CreateNoWindow = true,
UseShellExecute = false, UseShellExecute = false,
FileName = FFMpegPath, FileName = FFmpegPath,
Arguments = args, Arguments = args,
IsHidden = true, IsHidden = true,
ErrorDialog = false, ErrorDialog = false,
@ -814,7 +689,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
CreateNoWindow = true, CreateNoWindow = true,
UseShellExecute = false, UseShellExecute = false,
FileName = FFMpegPath, FileName = FFmpegPath,
Arguments = args, Arguments = args,
IsHidden = true, IsHidden = true,
ErrorDialog = false, ErrorDialog = false,

View File

@ -8,7 +8,14 @@ namespace MediaBrowser.Model.Configuration
public bool EnableThrottling { get; set; } public bool EnableThrottling { get; set; }
public int ThrottleDelaySeconds { get; set; } public int ThrottleDelaySeconds { get; set; }
public string HardwareAccelerationType { get; set; } public string HardwareAccelerationType { get; set; }
/// <summary>
/// FFmpeg path as set by the user via the UI
/// </summary>
public string EncoderAppPath { get; set; } 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 string VaapiDevice { get; set; }
public int H264Crf { get; set; } public int H264Crf { get; set; }
public string H264Preset { get; set; } public string H264Preset { get; set; }

View File

@ -4,6 +4,21 @@ using MediaBrowser.Model.Updates;
namespace MediaBrowser.Model.System 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> /// <summary>
/// Class SystemInfo /// Class SystemInfo
/// </summary> /// </summary>
@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value> /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
public bool HasUpdateAvailable { get; set; } public bool HasUpdateAvailable { get; set; }
public string EncoderLocationType { get; set; } public FFmpegLocation EncoderLocation { get; set; }
public Architecture SystemArchitecture { get; set; } public Architecture SystemArchitecture { get; set; }