diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs index 256ef3c..a718a21 100644 --- a/FFMpegCore.Examples/Program.cs +++ b/FFMpegCore.Examples/Program.cs @@ -98,7 +98,7 @@ IEnumerable CreateFrames(int count) yield return GetNextFrame(); //method of generating new frames } } - + var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable or IEnumerator to constructor of RawVideoPipeSource { FrameRate = 30 //set source frame rate @@ -115,10 +115,20 @@ await FFMpegArguments GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); // or GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin"); - + // or individual, per-run options await FFMpegArguments .FromFileInput(inputPath) .OutputToFile(outputPath) .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); + + // or combined, setting global defaults and adapting per-run options + GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "./globalTmp", WorkingDirectory = "./" }); + + await FFMpegArguments + .FromFileInput(inputPath) + .OutputToFile(outputPath) + .Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir") + .Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder") + .ProcessAsynchronously(); } \ No newline at end of file diff --git a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs new file mode 100644 index 0000000..dc65489 --- /dev/null +++ b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs @@ -0,0 +1,90 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using FluentAssertions; +using System.Reflection; + +namespace FFMpegCore.Test +{ + [TestClass] + public class FFMpegArgumentProcessorTest + { + [TestCleanup] + public void TestInitialize() + + { + // After testing reset global configuration to null, to be not wrong for other test relying on configuration + typeof(GlobalFFOptions).GetField("_current", BindingFlags.NonPublic | BindingFlags.Static).SetValue(GlobalFFOptions.Current, null); + } + + private static FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments + .FromFileInput("") + .OutputToFile(""); + + + [TestMethod] + public void Processor_GlobalOptions_GetUsed() + { + var globalWorkingDir = "Whatever"; + GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); + + var processor = CreateArgumentProcessor(); + var options2 = processor.GetConfiguredOptions(null); + options2.WorkingDirectory.Should().Be(globalWorkingDir); + } + + [TestMethod] + public void Processor_SessionOptions_GetUsed() + { + var sessionWorkingDir = "./CurrentRunWorkingDir"; + + var processor = CreateArgumentProcessor(); + processor.Configure(options => options.WorkingDirectory = sessionWorkingDir); + var options = processor.GetConfiguredOptions(null); + + options.WorkingDirectory.Should().Be(sessionWorkingDir); + } + + + [TestMethod] + public void Processor_Options_CanBeOverridden_And_Configured() + { + var globalConfig = "Whatever"; + GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalConfig, TemporaryFilesFolder = globalConfig, BinaryFolder = globalConfig }); + + + var processor = CreateArgumentProcessor(); + + var sessionTempDir = "./CurrentRunWorkingDir"; + processor.Configure(options => options.TemporaryFilesFolder = sessionTempDir); + + var overrideOptions = new FFOptions() { WorkingDirectory = "override" }; + var options = processor.GetConfiguredOptions(overrideOptions); + + options.Should().BeEquivalentTo(overrideOptions); + options.TemporaryFilesFolder.Should().BeEquivalentTo(sessionTempDir); + options.BinaryFolder.Should().NotBeEquivalentTo(globalConfig); + } + + + [TestMethod] + public void Options_Global_And_Session_Options_Can_Differ() + { + FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments + .FromFileInput("") + .OutputToFile(""); + + var globalWorkingDir = "Whatever"; + GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); + + var processor1 = CreateArgumentProcessor(); + var sessionWorkingDir = "./CurrentRunWorkingDir"; + processor1.Configure(options => options.WorkingDirectory = sessionWorkingDir); + var options1 = processor1.GetConfiguredOptions(null); + options1.WorkingDirectory.Should().Be(sessionWorkingDir); + + + var processor2 = CreateArgumentProcessor(); + var options2 = processor2.GetConfiguredOptions(null); + options2.WorkingDirectory.Should().Be(globalWorkingDir); + } + } +} \ No newline at end of file diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index e6831e6..6388724 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -39,6 +39,7 @@ + diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 68738be..fdbdcc8 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -14,6 +14,7 @@ namespace FFMpegCore public class FFMpegArgumentProcessor { private static readonly Regex ProgressRegex = new Regex(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled); + private readonly List> _configurations; private readonly FFMpegArguments _ffMpegArguments; private Action? _onPercentageProgress; private Action? _onTimeProgress; @@ -22,12 +23,13 @@ public class FFMpegArgumentProcessor internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) { + _configurations = new List>(); _ffMpegArguments = ffMpegArguments; } public string Arguments => _ffMpegArguments.Text; - private event EventHandler CancelEvent = null!; + private event EventHandler CancelEvent = null!; /// /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed and progress percentage is calculated. @@ -70,10 +72,15 @@ public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int t token.Register(() => CancelEvent?.Invoke(this, timeout)); return this; } + public FFMpegArgumentProcessor Configure(Action configureOptions) + { + _configurations.Add(configureOptions); + return this; + } public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { - using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource); - var errorCode = -1; + var options = GetConfiguredOptions(ffMpegOptions); + using var instance = PrepareInstance(options, out var cancellationTokenSource); void OnCancelEvent(object sender, int timeout) { @@ -87,7 +94,8 @@ void OnCancelEvent(object sender, int timeout) } CancelEvent += OnCancelEvent; instance.Exited += delegate { cancellationTokenSource.Cancel(); }; - + + var errorCode = -1; try { errorCode = Process(instance, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); @@ -100,14 +108,14 @@ void OnCancelEvent(object sender, int timeout) { CancelEvent -= OnCancelEvent; } - + return HandleCompletion(throwOnError, errorCode, instance.ErrorData); } public async Task ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { - using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource); - var errorCode = -1; + var options = GetConfiguredOptions(ffMpegOptions); + using var instance = PrepareInstance(options, out var cancellationTokenSource); void OnCancelEvent(object sender, int timeout) { @@ -120,7 +128,8 @@ void OnCancelEvent(object sender, int timeout) } } CancelEvent += OnCancelEvent; - + + var errorCode = -1; try { errorCode = await Process(instance, cancellationTokenSource).ConfigureAwait(false); @@ -163,6 +172,18 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) { if (!throwOnError) diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs index 94ce212..a34bca2 100644 --- a/FFMpegCore/FFOptions.cs +++ b/FFMpegCore/FFOptions.cs @@ -1,10 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Text; namespace FFMpegCore { - public class FFOptions + public class FFOptions : ICloneable { /// /// Working directory for the ffmpeg/ffprobe instance @@ -27,16 +28,24 @@ public class FFOptions 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; + + /// + object ICloneable.Clone() => Clone(); + + /// + /// Creates a new object that is a copy of the current instance. + /// + public FFOptions Clone() => (FFOptions)MemberwiseClone(); } } \ No newline at end of file diff --git a/README.md b/README.md index a8ab510..2c55520 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,17 @@ await FFMpegArguments .FromFileInput(inputPath) .OutputToFile(outputPath) .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); -``` + +// or combined, setting global defaults and adapting per-run options +GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "./globalTmp", WorkingDirectory = "./" }); + +await FFMpegArguments + .FromFileInput(inputPath) + .OutputToFile(outputPath) + .Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir") + .Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder") + .ProcessAsynchronously(); + ``` ### Option 2