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

View file

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

View file

@ -18,7 +18,7 @@ public DemuxConcatArgument(IEnumerable<string> values)
{
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 Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;

View file

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

View file

@ -4,53 +4,43 @@ namespace FFMpegCore.Exceptions
{
public enum FFMpegExceptionType
{
Dependency,
Conversion,
File,
Operation,
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 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)
{
FFMpegErrorOutput = ffMpegErrorOutput;
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 string FFMpegErrorOutput { get; }
}
public class FFOptionsException : Exception
{
public FFOptionsException(string message, Exception? innerException = null)
: base(message, innerException)
{
}
}
public class FFMpegArgumentException : Exception
{

View file

@ -254,8 +254,8 @@ public static bool Join(string output, params string[] videos)
{
var video = FFProbe.Analyse(videoPath);
FFMpegHelper.ConversionSizeExceptionCheck(video);
var destinationPath = Path.Combine(FFMpegOptions.Options.TempDirectory, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}");
Directory.CreateDirectory(FFMpegOptions.Options.TempDirectory);
var destinationPath = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}");
Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder);
Convert(videoPath, destinationPath, VideoType.Ts);
return destinationPath;
}).ToArray();
@ -284,7 +284,7 @@ public static bool Join(string output, params string[] videos)
/// <returns>Output video information.</returns>
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) =>
{
FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName));
@ -398,7 +398,7 @@ internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal()
FFMpegHelper.RootExceptionCheck();
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) =>
{
if (PixelFormat.TryParse(args.Data, out var format))
@ -413,14 +413,14 @@ internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal()
public static IReadOnlyList<PixelFormat> GetPixelFormats()
{
if (!FFMpegOptions.Options.UseCache)
if (!GlobalFFOptions.Current.UseCache)
return GetPixelFormatsInternal();
return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly();
}
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());
return fmt != null;
@ -443,7 +443,7 @@ private static void ParsePartOfCodecs(Dictionary<string, Codec> codecs, string a
{
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) =>
{
var codec = parser(args.Data);
@ -485,14 +485,14 @@ internal static Dictionary<string, Codec> GetCodecsInternal()
public static IReadOnlyList<Codec> GetCodecs()
{
if (!FFMpegOptions.Options.UseCache)
if (!GlobalFFOptions.Current.UseCache)
return GetCodecsInternal().Values.ToList().AsReadOnly();
return FFMpegCache.Codecs.Values.ToList().AsReadOnly();
}
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 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)
{
if (!FFMpegOptions.Options.UseCache)
if (!GlobalFFOptions.Current.UseCache)
{
codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return codec != null;
@ -527,7 +527,7 @@ internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
FFMpegHelper.RootExceptionCheck();
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) =>
{
if (ContainerFormat.TryParse(args.Data, out var fmt))
@ -542,14 +542,14 @@ internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
public static IReadOnlyList<ContainerFormat> GetContainerFormats()
{
if (!FFMpegOptions.Options.UseCache)
if (!GlobalFFOptions.Current.UseCache)
return GetContainersFormatsInternal();
return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly();
}
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());
return fmt != null;

View file

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

View file

@ -50,9 +50,9 @@ public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout
cancel = () => CancelEvent?.Invoke(this, timeout);
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;
void OnCancelEvent(object sender, int timeout)
@ -90,9 +90,9 @@ void OnCancelEvent(object sender, int timeout)
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;
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)
{
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);
if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value);
@ -140,16 +140,17 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<str
return exitCode == 0;
}
private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSource)
private Instance PrepareInstance(FFOptions ffMpegOptions,
out CancellationTokenSource cancellationTokenSource)
{
FFMpegHelper.RootExceptionCheck();
FFMpegHelper.VerifyFFMpegExists();
FFMpegHelper.VerifyFFMpegExists(ffMpegOptions);
var startInfo = new ProcessStartInfo
{
FileName = FFMpegOptions.Options.FFMpegBinary(),
FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions),
Arguments = _ffMpegArguments.Text,
StandardOutputEncoding = FFMpegOptions.Options.Encoding,
StandardErrorEncoding = FFMpegOptions.Options.Encoding,
StandardOutputEncoding = ffMpegOptions.Encoding,
StandardErrorEncoding = ffMpegOptions.Encoding,
};
var instance = new Instance(startInfo);
cancellationTokenSource = new CancellationTokenSource();

View file

@ -9,13 +9,13 @@
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() { }
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 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 FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalOptions> configureOptions)
public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalArguments> configureOptions)
{
configureOptions(_globalOptions);
configureOptions(_globalArguments);
return this;
}

View file

@ -3,7 +3,7 @@
namespace FFMpegCore
{
public abstract class FFMpegOptionsBase
public abstract class FFMpegArgumentsBase
{
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 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))
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();
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);
}
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();
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);
}
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 pipeArgument = new InputPipeArgument(streamPipeSource);
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity);
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
pipeArgument.Pre();
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();
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);
}
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))
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);
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);
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 pipeArgument = new InputPipeArgument(streamPipeSource);
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity);
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
pipeArgument.Pre();
var task = instance.FinishedRunning();
@ -112,12 +112,12 @@ private static IMediaAnalysis ParseOutput(Instance instance)
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.VerifyFFProbeExists();
FFProbeHelper.VerifyFFProbeExists(ffOptions);
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;
}
}

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()
{
if (FFMpegOptions.Options.RootDirectory == null)
throw new FFMpegException(FFMpegExceptionType.Dependency,
"FFMpeg root is not configured in app config. Missing key 'ffmpegRoot'.");
if (GlobalFFOptions.Current.BinaryFolder == null)
throw new FFOptionsException("FFMpeg root is not configured in app config. Missing key 'BinaryFolder'.");
}
public static void VerifyFFMpegExists()
public static void VerifyFFMpegExists(FFOptions ffMpegOptions)
{
if (_ffmpegVerified) return;
var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFMpegBinary(), "-version");
var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version");
_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()
{
if (FFMpegOptions.Options.RootDirectory == null)
throw new FFMpegException(FFMpegExceptionType.Dependency,
"FFProbe root is not configured in app config. Missing key 'ffmpegRoot'.");
if (GlobalFFOptions.Current.BinaryFolder == null)
throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'.");
}
public static void VerifyFFProbeExists()
public static void VerifyFFProbeExists(FFOptions ffMpegOptions)
{
if (_ffprobeVerified) return;
var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFProbeBinary(), "-version");
var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version");
_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");
}
}
}