This commit is contained in:
Malte Rosenbjerg 2021-03-07 00:26:08 +01:00
parent 7444899106
commit df0205fb11
18 changed files with 203 additions and 190 deletions

View file

@ -10,39 +10,39 @@ public class FFMpegOptionsTest
[TestMethod] [TestMethod]
public void Options_Initialized() public void Options_Initialized()
{ {
Assert.IsNotNull(FFMpegOptions.Options); Assert.IsNotNull(GlobalFFOptions.Current);
} }
[TestMethod] [TestMethod]
public void Options_Defaults_Configured() public void Options_Defaults_Configured()
{ {
Assert.AreEqual(new FFMpegOptions().RootDirectory, $""); Assert.AreEqual(new FFOptions().BinaryFolder, $"");
} }
[TestMethod] [TestMethod]
public void Options_Loaded_From_File() public void Options_Loaded_From_File()
{ {
Assert.AreEqual( Assert.AreEqual(
FFMpegOptions.Options.RootDirectory, GlobalFFOptions.Current.BinaryFolder,
JsonConvert.DeserializeObject<FFMpegOptions>(File.ReadAllText("ffmpeg.config.json")).RootDirectory JsonConvert.DeserializeObject<FFOptions>(File.ReadAllText("ffmpeg.config.json")).BinaryFolder
); );
} }
[TestMethod] [TestMethod]
public void Options_Set_Programmatically() public void Options_Set_Programmatically()
{ {
var original = FFMpegOptions.Options; var original = GlobalFFOptions.Current;
try try
{ {
FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "Whatever" }); GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" });
Assert.AreEqual( Assert.AreEqual(
FFMpegOptions.Options.RootDirectory, GlobalFFOptions.Current.BinaryFolder,
"Whatever" "Whatever"
); );
} }
finally finally
{ {
FFMpegOptions.Configure(original); GlobalFFOptions.Configure(original);
} }
} }
} }

View file

@ -104,7 +104,7 @@ public void Video_ToMP4_Args_StreamPipe()
[TestMethod, Timeout(10000)] [TestMethod, Timeout(10000)]
public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure() public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure()
{ {
await Assert.ThrowsExceptionAsync<FFMpegProcessException>(async () => await Assert.ThrowsExceptionAsync<FFMpegException>(async () =>
{ {
await using var ms = new MemoryStream(); await using var ms = new MemoryStream();
var pipeSource = new StreamPipeSink(ms); var pipeSource = new StreamPipeSink(ms);
@ -134,7 +134,7 @@ public void Video_StreamFile_OutputToMemoryStream()
[TestMethod, Timeout(10000)] [TestMethod, Timeout(10000)]
public void Video_ToMP4_Args_StreamOutputPipe_Failure() public void Video_ToMP4_Args_StreamOutputPipe_Failure()
{ {
Assert.ThrowsException<FFMpegProcessException>(() => Assert.ThrowsException<FFMpegException>(() =>
{ {
using var ms = new MemoryStream(); using var ms = new MemoryStream();
FFMpegArguments FFMpegArguments
@ -435,7 +435,7 @@ public void Video_OutputsData()
var outputFile = new TemporaryFile("out.mp4"); var outputFile = new TemporaryFile("out.mp4");
var dataReceived = false; var dataReceived = false;
FFMpegOptions.Configure(opt => opt.Encoding = Encoding.UTF8); GlobalFFOptions.Configure(opt => opt.Encoding = Encoding.UTF8);
var success = FFMpegArguments var success = FFMpegArguments
.FromFileInput(TestResources.Mp4Video) .FromFileInput(TestResources.Mp4Video)
.WithGlobalOptions(options => options .WithGlobalOptions(options => options

View file

@ -18,7 +18,7 @@ public DemuxConcatArgument(IEnumerable<string> values)
{ {
Values = values.Select(value => $"file '{value}'"); Values = values.Select(value => $"file '{value}'");
} }
private readonly string _tempFileName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid() + ".txt"); private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"concat_{Guid.NewGuid()}.txt");
public void Pre() => File.WriteAllLines(_tempFileName, Values); public void Pre() => File.WriteAllLines(_tempFileName, Values);
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;

View file

@ -15,8 +15,8 @@ public string Extension
{ {
get get
{ {
if (FFMpegOptions.Options.ExtensionOverrides.ContainsKey(Name)) if (GlobalFFOptions.Current.ExtensionOverrides.ContainsKey(Name))
return FFMpegOptions.Options.ExtensionOverrides[Name]; return GlobalFFOptions.Current.ExtensionOverrides[Name];
return "." + Name; return "." + Name;
} }
} }

View file

@ -4,53 +4,43 @@ namespace FFMpegCore.Exceptions
{ {
public enum FFMpegExceptionType public enum FFMpegExceptionType
{ {
Dependency,
Conversion, Conversion,
File, File,
Operation, Operation,
Process Process
} }
public abstract class FFException : Exception
{
protected FFException(string message) : base(message) { }
protected FFException(string message, Exception innerException) : base(message, innerException) { }
}
public abstract class FFProcessException : FFException
{
protected FFProcessException(string process, int exitCode, string errorOutput)
: base($"{process} exited with non-zero exit-code {exitCode}\n{errorOutput}")
{
ExitCode = exitCode;
ErrorOutput = errorOutput;
}
public int ExitCode { get; }
public string ErrorOutput { get; }
}
public class FFMpegProcessException : FFProcessException
{
public FFMpegProcessException(int exitCode, string errorOutput)
: base("ffmpeg", exitCode, errorOutput) { }
}
public class FFProbeProcessException : FFProcessException
{
public FFProbeProcessException(int exitCode, string errorOutput)
: base("ffprobe", exitCode, errorOutput) { }
}
public class FFMpegException : Exception public class FFMpegException : Exception
{ {
public FFMpegException(FFMpegExceptionType type, string? message = null, Exception? innerException = null, string ffMpegErrorOutput = "") public FFMpegException(FFMpegExceptionType type, string message, Exception? innerException = null, string ffMpegErrorOutput = "")
: base(message, innerException) : base(message, innerException)
{ {
FFMpegErrorOutput = ffMpegErrorOutput; FFMpegErrorOutput = ffMpegErrorOutput;
Type = type; Type = type;
} }
public FFMpegException(FFMpegExceptionType type, string message, string ffMpegErrorOutput = "")
: base(message)
{
FFMpegErrorOutput = ffMpegErrorOutput;
Type = type;
}
public FFMpegException(FFMpegExceptionType type, string message)
: base(message)
{
FFMpegErrorOutput = string.Empty;
Type = type;
}
public FFMpegExceptionType Type { get; } public FFMpegExceptionType Type { get; }
public string FFMpegErrorOutput { get; } public string FFMpegErrorOutput { get; }
} }
public class FFOptionsException : Exception
{
public FFOptionsException(string message, Exception? innerException = null)
: base(message, innerException)
{
}
}
public class FFMpegArgumentException : Exception public class FFMpegArgumentException : Exception
{ {

View file

@ -254,8 +254,8 @@ public static bool Join(string output, params string[] videos)
{ {
var video = FFProbe.Analyse(videoPath); var video = FFProbe.Analyse(videoPath);
FFMpegHelper.ConversionSizeExceptionCheck(video); FFMpegHelper.ConversionSizeExceptionCheck(video);
var destinationPath = Path.Combine(FFMpegOptions.Options.TempDirectory, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}"); var destinationPath = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}");
Directory.CreateDirectory(FFMpegOptions.Options.TempDirectory); Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder);
Convert(videoPath, destinationPath, VideoType.Ts); Convert(videoPath, destinationPath, VideoType.Ts);
return destinationPath; return destinationPath;
}).ToArray(); }).ToArray();
@ -284,7 +284,7 @@ public static bool Join(string output, params string[] videos)
/// <returns>Output video information.</returns> /// <returns>Output video information.</returns>
public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images) public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images)
{ {
var tempFolderName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid().ToString()); var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString());
var temporaryImageFiles = images.Select((image, index) => var temporaryImageFiles = images.Select((image, index) =>
{ {
FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName)); FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName));
@ -398,7 +398,7 @@ internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal()
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
var list = new List<PixelFormat>(); var list = new List<PixelFormat>();
using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), "-pix_fmts"); using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts");
instance.DataReceived += (e, args) => instance.DataReceived += (e, args) =>
{ {
if (PixelFormat.TryParse(args.Data, out var format)) if (PixelFormat.TryParse(args.Data, out var format))
@ -413,14 +413,14 @@ internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal()
public static IReadOnlyList<PixelFormat> GetPixelFormats() public static IReadOnlyList<PixelFormat> GetPixelFormats()
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
return GetPixelFormatsInternal(); return GetPixelFormatsInternal();
return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly(); return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly();
} }
public static bool TryGetPixelFormat(string name, out PixelFormat fmt) public static bool TryGetPixelFormat(string name, out PixelFormat fmt)
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
{ {
fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return fmt != null; return fmt != null;
@ -443,7 +443,7 @@ private static void ParsePartOfCodecs(Dictionary<string, Codec> codecs, string a
{ {
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), arguments); using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), arguments);
instance.DataReceived += (e, args) => instance.DataReceived += (e, args) =>
{ {
var codec = parser(args.Data); var codec = parser(args.Data);
@ -485,14 +485,14 @@ internal static Dictionary<string, Codec> GetCodecsInternal()
public static IReadOnlyList<Codec> GetCodecs() public static IReadOnlyList<Codec> GetCodecs()
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
return GetCodecsInternal().Values.ToList().AsReadOnly(); return GetCodecsInternal().Values.ToList().AsReadOnly();
return FFMpegCache.Codecs.Values.ToList().AsReadOnly(); return FFMpegCache.Codecs.Values.ToList().AsReadOnly();
} }
public static IReadOnlyList<Codec> GetCodecs(CodecType type) public static IReadOnlyList<Codec> GetCodecs(CodecType type)
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly(); return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly();
return FFMpegCache.Codecs.Values.Where(x=>x.Type == type).ToList().AsReadOnly(); return FFMpegCache.Codecs.Values.Where(x=>x.Type == type).ToList().AsReadOnly();
} }
@ -504,7 +504,7 @@ public static IReadOnlyList<Codec> GetCodecs(CodecType type)
public static bool TryGetCodec(string name, out Codec codec) public static bool TryGetCodec(string name, out Codec codec)
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
{ {
codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return codec != null; return codec != null;
@ -527,7 +527,7 @@ internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
var list = new List<ContainerFormat>(); var list = new List<ContainerFormat>();
using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), "-formats"); using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats");
instance.DataReceived += (e, args) => instance.DataReceived += (e, args) =>
{ {
if (ContainerFormat.TryParse(args.Data, out var fmt)) if (ContainerFormat.TryParse(args.Data, out var fmt))
@ -542,14 +542,14 @@ internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
public static IReadOnlyList<ContainerFormat> GetContainerFormats() public static IReadOnlyList<ContainerFormat> GetContainerFormats()
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
return GetContainersFormatsInternal(); return GetContainersFormatsInternal();
return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly(); return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly();
} }
public static bool TryGetContainerFormat(string name, out ContainerFormat fmt) public static bool TryGetContainerFormat(string name, out ContainerFormat fmt)
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
{ {
fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return fmt != null; return fmt != null;

View file

@ -5,7 +5,7 @@
namespace FFMpegCore namespace FFMpegCore
{ {
public class FFMpegArgumentOptions : FFMpegOptionsBase public class FFMpegArgumentOptions : FFMpegArgumentsBase
{ {
internal FFMpegArgumentOptions() { } internal FFMpegArgumentOptions() { }

View file

@ -50,9 +50,9 @@ public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout
cancel = () => CancelEvent?.Invoke(this, timeout); cancel = () => CancelEvent?.Invoke(this, timeout);
return this; return this;
} }
public bool ProcessSynchronously(bool throwOnError = true) public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{ {
using var instance = PrepareInstance(out var cancellationTokenSource); using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource);
var errorCode = -1; var errorCode = -1;
void OnCancelEvent(object sender, int timeout) void OnCancelEvent(object sender, int timeout)
@ -90,9 +90,9 @@ void OnCancelEvent(object sender, int timeout)
return HandleCompletion(throwOnError, errorCode, instance.ErrorData); return HandleCompletion(throwOnError, errorCode, instance.ErrorData);
} }
public async Task<bool> ProcessAsynchronously(bool throwOnError = true) public async Task<bool> ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{ {
using var instance = PrepareInstance(out var cancellationTokenSource); using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource);
var errorCode = -1; var errorCode = -1;
void OnCancelEvent(object sender, int timeout) void OnCancelEvent(object sender, int timeout)
@ -132,7 +132,7 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t =>
private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<string> errorData) private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<string> errorData)
{ {
if (throwOnError && exitCode != 0) if (throwOnError && exitCode != 0)
throw new FFMpegProcessException(exitCode, string.Join("\n", errorData)); throw new FFMpegException(FFMpegExceptionType.Process, $"ffmpeg exited with non-zero exit-code ({exitCode} - {string.Join("\n", errorData)})", null, string.Join("\n", errorData));
_onPercentageProgress?.Invoke(100.0); _onPercentageProgress?.Invoke(100.0);
if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value); if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value);
@ -140,16 +140,17 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<str
return exitCode == 0; return exitCode == 0;
} }
private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSource) private Instance PrepareInstance(FFOptions ffMpegOptions,
out CancellationTokenSource cancellationTokenSource)
{ {
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
FFMpegHelper.VerifyFFMpegExists(); FFMpegHelper.VerifyFFMpegExists(ffMpegOptions);
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
FileName = FFMpegOptions.Options.FFMpegBinary(), FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions),
Arguments = _ffMpegArguments.Text, Arguments = _ffMpegArguments.Text,
StandardOutputEncoding = FFMpegOptions.Options.Encoding, StandardOutputEncoding = ffMpegOptions.Encoding,
StandardErrorEncoding = FFMpegOptions.Options.Encoding, StandardErrorEncoding = ffMpegOptions.Encoding,
}; };
var instance = new Instance(startInfo); var instance = new Instance(startInfo);
cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource = new CancellationTokenSource();

View file

@ -9,13 +9,13 @@
namespace FFMpegCore namespace FFMpegCore
{ {
public sealed class FFMpegArguments : FFMpegOptionsBase public sealed class FFMpegArguments : FFMpegArgumentsBase
{ {
private readonly FFMpegGlobalOptions _globalOptions = new FFMpegGlobalOptions(); private readonly FFMpegGlobalArguments _globalArguments = new FFMpegGlobalArguments();
private FFMpegArguments() { } private FFMpegArguments() { }
public string Text => string.Join(" ", _globalOptions.Arguments.Concat(Arguments).Select(arg => arg.Text)); public string Text => string.Join(" ", _globalArguments.Arguments.Concat(Arguments).Select(arg => arg.Text));
public static FFMpegArguments FromConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments); public static FFMpegArguments FromConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments);
public static FFMpegArguments FromDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments); public static FFMpegArguments FromDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments);
@ -26,9 +26,9 @@ private FFMpegArguments() { }
public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments); public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments);
public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalOptions> configureOptions) public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalArguments> configureOptions)
{ {
configureOptions(_globalOptions); configureOptions(_globalArguments);
return this; return this;
} }

View file

@ -3,7 +3,7 @@
namespace FFMpegCore namespace FFMpegCore
{ {
public abstract class FFMpegOptionsBase public abstract class FFMpegArgumentsBase
{ {
internal readonly List<IArgument> Arguments = new List<IArgument>(); internal readonly List<IArgument> Arguments = new List<IArgument>();
} }

View file

@ -0,0 +1,18 @@
using FFMpegCore.Arguments;
namespace FFMpegCore
{
public sealed class FFMpegGlobalArguments : FFMpegArgumentsBase
{
internal FFMpegGlobalArguments() { }
public FFMpegGlobalArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel));
private FFMpegGlobalArguments WithOption(IArgument argument)
{
Arguments.Add(argument);
return this;
}
}
}

View file

@ -1,18 +0,0 @@
using FFMpegCore.Arguments;
namespace FFMpegCore
{
public sealed class FFMpegGlobalOptions : FFMpegOptionsBase
{
internal FFMpegGlobalOptions() { }
public FFMpegGlobalOptions WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel));
private FFMpegGlobalOptions WithOption(IArgument argument)
{
Arguments.Add(argument);
return this;
}
}
}

View file

@ -1,66 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
namespace FFMpegCore
{
public class FFMpegOptions
{
private static readonly string ConfigFile = "ffmpeg.config.json";
private static readonly string DefaultRoot = "";
private static readonly string DefaultTemp = Path.GetTempPath();
private static readonly Dictionary<string, string> DefaultExtensionsOverrides = new Dictionary<string, string>
{
{ "mpegts", ".ts" },
};
public static FFMpegOptions Options { get; private set; } = new FFMpegOptions();
public static void Configure(Action<FFMpegOptions> optionsAction)
{
optionsAction?.Invoke(Options);
}
public static void Configure(FFMpegOptions options)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
}
static FFMpegOptions()
{
if (File.Exists(ConfigFile))
{
Options = JsonSerializer.Deserialize<FFMpegOptions>(File.ReadAllText(ConfigFile))!;
foreach (var pair in DefaultExtensionsOverrides)
if (!Options.ExtensionOverrides.ContainsKey(pair.Key)) Options.ExtensionOverrides.Add(pair.Key, pair.Value);
}
}
public string RootDirectory { get; set; } = DefaultRoot;
public string TempDirectory { get; set; } = DefaultTemp;
public bool UseCache { get; set; } = true;
public Encoding Encoding { get; set; } = Encoding.Default;
public string FFMpegBinary() => FFBinary("FFMpeg");
public string FFProbeBinary() => FFBinary("FFProbe");
public Dictionary<string, string> ExtensionOverrides { get; private set; } = new Dictionary<string, string>();
private static string FFBinary(string name)
{
var ffName = name.ToLowerInvariant();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
ffName += ".exe";
var target = Environment.Is64BitProcess ? "x64" : "x86";
if (Directory.Exists(Path.Combine(Options.RootDirectory, target)))
ffName = Path.Combine(target, ffName);
return Path.Combine(Options.RootDirectory, ffName);
}
}
}

37
FFMpegCore/FFOptions.cs Normal file
View file

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace FFMpegCore
{
public class FFOptions
{
/// <summary>
/// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH
/// </summary>
public string BinaryFolder { get; set; } = string.Empty;
/// <summary>
/// Folder used for temporary files necessary for static methods on FFMpeg class
/// </summary>
public string TemporaryFilesFolder { get; set; } = Path.GetTempPath();
/// <summary>
/// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes
/// </summary>
public Encoding Encoding { get; set; } = Encoding.Default;
/// <summary>
///
/// </summary>
public Dictionary<string, string> ExtensionOverrides { get; set; } = new Dictionary<string, string>
{
{ "mpegts", ".ts" },
};
/// <summary>
/// Whether to cache calls to get ffmpeg codec, pixel- and container-formats
/// </summary>
public bool UseCache { get; set; } = true;
}
}

View file

@ -12,32 +12,32 @@ namespace FFMpegCore
{ {
public static class FFProbe public static class FFProbe
{ {
public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue) public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
if (!File.Exists(filePath)) if (!File.Exists(filePath))
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PrepareInstance(filePath, outputCapacity); using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
var exitCode = instance.BlockUntilFinished(); var exitCode = instance.BlockUntilFinished();
if (exitCode != 0) if (exitCode != 0)
throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData)); throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
return ParseOutput(instance); return ParseOutput(instance);
} }
public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue) public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
var exitCode = instance.BlockUntilFinished(); var exitCode = instance.BlockUntilFinished();
if (exitCode != 0) if (exitCode != 0)
throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData)); throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
return ParseOutput(instance); return ParseOutput(instance);
} }
public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue) public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
var streamPipeSource = new StreamPipeSource(stream); var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource); var pipeArgument = new InputPipeArgument(streamPipeSource);
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity); using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
pipeArgument.Pre(); pipeArgument.Pre();
var task = instance.FinishedRunning(); var task = instance.FinishedRunning();
@ -52,30 +52,30 @@ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.Max
} }
var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult(); var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult();
if (exitCode != 0) if (exitCode != 0)
throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData)); throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
return ParseOutput(instance); return ParseOutput(instance);
} }
public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, int outputCapacity = int.MaxValue) public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
if (!File.Exists(filePath)) if (!File.Exists(filePath))
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PrepareInstance(filePath, outputCapacity); using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
await instance.FinishedRunning().ConfigureAwait(false); await instance.FinishedRunning().ConfigureAwait(false);
return ParseOutput(instance); return ParseOutput(instance);
} }
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue) public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
await instance.FinishedRunning().ConfigureAwait(false); await instance.FinishedRunning().ConfigureAwait(false);
return ParseOutput(instance); return ParseOutput(instance);
} }
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue) public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
var streamPipeSource = new StreamPipeSource(stream); var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource); var pipeArgument = new InputPipeArgument(streamPipeSource);
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity); using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
pipeArgument.Pre(); pipeArgument.Pre();
var task = instance.FinishedRunning(); var task = instance.FinishedRunning();
@ -112,12 +112,12 @@ private static IMediaAnalysis ParseOutput(Instance instance)
return new MediaAnalysis(ffprobeAnalysis); return new MediaAnalysis(ffprobeAnalysis);
} }
private static Instance PrepareInstance(string filePath, int outputCapacity) private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions)
{ {
FFProbeHelper.RootExceptionCheck(); FFProbeHelper.RootExceptionCheck();
FFProbeHelper.VerifyFFProbeExists(); FFProbeHelper.VerifyFFProbeExists(ffOptions);
var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\""; var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"";
var instance = new Instance(FFMpegOptions.Options.FFProbeBinary(), arguments) {DataBufferCapacity = outputCapacity}; var instance = new Instance(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) {DataBufferCapacity = outputCapacity};
return instance; return instance;
} }
} }

View file

@ -0,0 +1,52 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
namespace FFMpegCore
{
public static class GlobalFFOptions
{
private static readonly string ConfigFile = "ffmpeg.config.json";
public static FFOptions Current { get; private set; }
static GlobalFFOptions()
{
if (File.Exists(ConfigFile))
{
Current = JsonSerializer.Deserialize<FFOptions>(File.ReadAllText(ConfigFile))!;
}
else
{
Current = new FFOptions();
}
}
public static void Configure(Action<FFOptions> optionsAction)
{
optionsAction?.Invoke(Current);
}
public static void Configure(FFOptions ffOptions)
{
Current = ffOptions ?? throw new ArgumentNullException(nameof(ffOptions));
}
public static string GetFFMpegBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFMpeg", ffOptions ?? Current);
public static string GetFFProbeBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFProbe", ffOptions ?? Current);
private static string GetFFBinaryPath(string name, FFOptions ffOptions)
{
var ffName = name.ToLowerInvariant();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
ffName += ".exe";
var target = Environment.Is64BitProcess ? "x64" : "x86";
if (Directory.Exists(Path.Combine(ffOptions.BinaryFolder, target)))
ffName = Path.Combine(target, ffName);
return Path.Combine(ffOptions.BinaryFolder, ffName);
}
}
}

View file

@ -31,17 +31,17 @@ public static void ExtensionExceptionCheck(string filename, string extension)
public static void RootExceptionCheck() public static void RootExceptionCheck()
{ {
if (FFMpegOptions.Options.RootDirectory == null) if (GlobalFFOptions.Current.BinaryFolder == null)
throw new FFMpegException(FFMpegExceptionType.Dependency, throw new FFOptionsException("FFMpeg root is not configured in app config. Missing key 'BinaryFolder'.");
"FFMpeg root is not configured in app config. Missing key 'ffmpegRoot'.");
} }
public static void VerifyFFMpegExists() public static void VerifyFFMpegExists(FFOptions ffMpegOptions)
{ {
if (_ffmpegVerified) return; if (_ffmpegVerified) return;
var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFMpegBinary(), "-version"); var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version");
_ffmpegVerified = exitCode == 0; _ffmpegVerified = exitCode == 0;
if (!_ffmpegVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system"); if (!_ffmpegVerified)
throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system");
} }
} }
} }

View file

@ -20,18 +20,17 @@ public static int Gcd(int first, int second)
public static void RootExceptionCheck() public static void RootExceptionCheck()
{ {
if (FFMpegOptions.Options.RootDirectory == null) if (GlobalFFOptions.Current.BinaryFolder == null)
throw new FFMpegException(FFMpegExceptionType.Dependency, throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'.");
"FFProbe root is not configured in app config. Missing key 'ffmpegRoot'.");
} }
public static void VerifyFFProbeExists() public static void VerifyFFProbeExists(FFOptions ffMpegOptions)
{ {
if (_ffprobeVerified) return; if (_ffprobeVerified) return;
var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFProbeBinary(), "-version"); var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version");
_ffprobeVerified = exitCode == 0; _ffprobeVerified = exitCode == 0;
if (!_ffprobeVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system"); if (!_ffprobeVerified)
throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system");
} }
} }
} }