diff --git a/FFMpegCore.Test/FFMpegOptionsTests.cs b/FFMpegCore.Test/FFMpegOptionsTests.cs index d175644..2be810f 100644 --- a/FFMpegCore.Test/FFMpegOptionsTests.cs +++ b/FFMpegCore.Test/FFMpegOptionsTests.cs @@ -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(File.ReadAllText("ffmpeg.config.json")).RootDirectory + GlobalFFOptions.Current.BinaryFolder, + JsonConvert.DeserializeObject(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); } } } diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 45072c1..30e6a9a 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -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(async () => + await Assert.ThrowsExceptionAsync(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(() => + Assert.ThrowsException(() => { 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 diff --git a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs index 5651802..c672c74 100644 --- a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs @@ -18,7 +18,7 @@ public DemuxConcatArgument(IEnumerable 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; diff --git a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs index 90da0d2..2da1572 100644 --- a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs +++ b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs @@ -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; } } diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs index bf7c41f..dad6ef1 100644 --- a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs +++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs @@ -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 { diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index b242c7b..3f3d162 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -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) /// Output video information. 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 GetPixelFormatsInternal() FFMpegHelper.RootExceptionCheck(); var list = new List(); - 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 GetPixelFormatsInternal() public static IReadOnlyList 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 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 GetCodecsInternal() public static IReadOnlyList GetCodecs() { - if (!FFMpegOptions.Options.UseCache) + if (!GlobalFFOptions.Current.UseCache) return GetCodecsInternal().Values.ToList().AsReadOnly(); return FFMpegCache.Codecs.Values.ToList().AsReadOnly(); } public static IReadOnlyList 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 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 GetContainersFormatsInternal() FFMpegHelper.RootExceptionCheck(); var list = new List(); - 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 GetContainersFormatsInternal() public static IReadOnlyList 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; diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index c87e029..be342c4 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -5,7 +5,7 @@ namespace FFMpegCore { - public class FFMpegArgumentOptions : FFMpegOptionsBase + public class FFMpegArgumentOptions : FFMpegArgumentsBase { internal FFMpegArgumentOptions() { } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 06fca1a..67607af 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -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 ProcessAsynchronously(bool throwOnError = true) + public async Task 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 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 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 filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments); public static FFMpegArguments FromDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments); @@ -26,9 +26,9 @@ private FFMpegArguments() { } public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments); - public FFMpegArguments WithGlobalOptions(Action configureOptions) + public FFMpegArguments WithGlobalOptions(Action configureOptions) { - configureOptions(_globalOptions); + configureOptions(_globalArguments); return this; } diff --git a/FFMpegCore/FFMpeg/FFMpegOptionsBase.cs b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs similarity index 79% rename from FFMpegCore/FFMpeg/FFMpegOptionsBase.cs rename to FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs index 015e609..fc51ab1 100644 --- a/FFMpegCore/FFMpeg/FFMpegOptionsBase.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs @@ -3,7 +3,7 @@ namespace FFMpegCore { - public abstract class FFMpegOptionsBase + public abstract class FFMpegArgumentsBase { internal readonly List Arguments = new List(); } diff --git a/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs new file mode 100644 index 0000000..e7d6e24 --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs @@ -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; + } + + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs b/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs deleted file mode 100644 index 00dc66f..0000000 --- a/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs +++ /dev/null @@ -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; - } - - } -} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegOptions.cs b/FFMpegCore/FFMpeg/FFMpegOptions.cs deleted file mode 100644 index 4a09d7a..0000000 --- a/FFMpegCore/FFMpeg/FFMpegOptions.cs +++ /dev/null @@ -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 DefaultExtensionsOverrides = new Dictionary - { - { "mpegts", ".ts" }, - }; - - public static FFMpegOptions Options { get; private set; } = new FFMpegOptions(); - - public static void Configure(Action 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(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 ExtensionOverrides { get; private set; } = new Dictionary(); - - 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); - } - } -} diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs new file mode 100644 index 0000000..1f7e497 --- /dev/null +++ b/FFMpegCore/FFOptions.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace FFMpegCore +{ + public class FFOptions + { + /// + /// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH + /// + public string BinaryFolder { get; set; } = string.Empty; + + /// + /// Folder used for temporary files necessary for static methods on FFMpeg class + /// + public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); + + /// + /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes + /// + public Encoding Encoding { get; set; } = Encoding.Default; + + /// + /// + /// + public Dictionary ExtensionOverrides { get; set; } = new Dictionary + { + { "mpegts", ".ts" }, + }; + + /// + /// Whether to cache calls to get ffmpeg codec, pixel- and container-formats + /// + public bool UseCache { get; set; } = true; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index fab4b2e..ab35457 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -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 AnalyseAsync(string filePath, int outputCapacity = int.MaxValue) + public static async Task 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 AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue) + public static async Task 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 AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue) + public static async Task 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; } } diff --git a/FFMpegCore/GlobalFFOptions.cs b/FFMpegCore/GlobalFFOptions.cs new file mode 100644 index 0000000..358787a --- /dev/null +++ b/FFMpegCore/GlobalFFOptions.cs @@ -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(File.ReadAllText(ConfigFile))!; + } + else + { + Current = new FFOptions(); + } + } + + public static void Configure(Action 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); + } + } +} diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs index 676f6d3..12e52c3 100644 --- a/FFMpegCore/Helpers/FFMpegHelper.cs +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -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"); } } } diff --git a/FFMpegCore/Helpers/FFProbeHelper.cs b/FFMpegCore/Helpers/FFProbeHelper.cs index 1e833e0..d0064e4 100644 --- a/FFMpegCore/Helpers/FFProbeHelper.cs +++ b/FFMpegCore/Helpers/FFProbeHelper.cs @@ -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"); } } }