Merge pull request #274 from BobSilent/add-fluent-configuration-on-ffmpeg-argument-processor

Add fluent configuration of ffoptions per run

Former-commit-id: 7461407f92
This commit is contained in:
Malte Rosenbjerg 2021-11-28 11:21:48 +01:00 committed by GitHub
commit c49a9b2721
6 changed files with 157 additions and 16 deletions

View file

@ -98,7 +98,7 @@ IEnumerable<IVideoFrame> CreateFrames(int count)
yield return GetNextFrame(); //method of generating new frames yield return GetNextFrame(); //method of generating new frames
} }
} }
var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable<IVideoFrame> or IEnumerator<IVideoFrame> to constructor of RawVideoPipeSource var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable<IVideoFrame> or IEnumerator<IVideoFrame> to constructor of RawVideoPipeSource
{ {
FrameRate = 30 //set source frame rate FrameRate = 30 //set source frame rate
@ -115,10 +115,20 @@ await FFMpegArguments
GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
// or // or
GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin"); GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin");
// or individual, per-run options // or individual, per-run options
await FFMpegArguments await FFMpegArguments
.FromFileInput(inputPath) .FromFileInput(inputPath)
.OutputToFile(outputPath) .OutputToFile(outputPath)
.ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); .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();
} }

View file

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

View file

@ -39,6 +39,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" /> <PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" /> <PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />

View file

@ -14,6 +14,7 @@ namespace FFMpegCore
public class FFMpegArgumentProcessor public class FFMpegArgumentProcessor
{ {
private static readonly Regex ProgressRegex = new Regex(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled); private static readonly Regex ProgressRegex = new Regex(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled);
private readonly List<Action<FFOptions>> _configurations;
private readonly FFMpegArguments _ffMpegArguments; private readonly FFMpegArguments _ffMpegArguments;
private Action<double>? _onPercentageProgress; private Action<double>? _onPercentageProgress;
private Action<TimeSpan>? _onTimeProgress; private Action<TimeSpan>? _onTimeProgress;
@ -22,12 +23,13 @@ public class FFMpegArgumentProcessor
internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments)
{ {
_configurations = new List<Action<FFOptions>>();
_ffMpegArguments = ffMpegArguments; _ffMpegArguments = ffMpegArguments;
} }
public string Arguments => _ffMpegArguments.Text; public string Arguments => _ffMpegArguments.Text;
private event EventHandler<int> CancelEvent = null!; private event EventHandler<int> CancelEvent = null!;
/// <summary> /// <summary>
/// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed and progress percentage is calculated. /// 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)); token.Register(() => CancelEvent?.Invoke(this, timeout));
return this; return this;
} }
public FFMpegArgumentProcessor Configure(Action<FFOptions> configureOptions)
{
_configurations.Add(configureOptions);
return this;
}
public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{ {
using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource); var options = GetConfiguredOptions(ffMpegOptions);
var errorCode = -1; using var instance = PrepareInstance(options, out var cancellationTokenSource);
void OnCancelEvent(object sender, int timeout) void OnCancelEvent(object sender, int timeout)
{ {
@ -87,7 +94,8 @@ void OnCancelEvent(object sender, int timeout)
} }
CancelEvent += OnCancelEvent; CancelEvent += OnCancelEvent;
instance.Exited += delegate { cancellationTokenSource.Cancel(); }; instance.Exited += delegate { cancellationTokenSource.Cancel(); };
var errorCode = -1;
try try
{ {
errorCode = Process(instance, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); errorCode = Process(instance, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult();
@ -100,14 +108,14 @@ void OnCancelEvent(object sender, int timeout)
{ {
CancelEvent -= OnCancelEvent; CancelEvent -= OnCancelEvent;
} }
return HandleCompletion(throwOnError, errorCode, instance.ErrorData); return HandleCompletion(throwOnError, errorCode, instance.ErrorData);
} }
public async Task<bool> ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) public async Task<bool> ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{ {
using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource); var options = GetConfiguredOptions(ffMpegOptions);
var errorCode = -1; using var instance = PrepareInstance(options, out var cancellationTokenSource);
void OnCancelEvent(object sender, int timeout) void OnCancelEvent(object sender, int timeout)
{ {
@ -120,7 +128,8 @@ void OnCancelEvent(object sender, int timeout)
} }
} }
CancelEvent += OnCancelEvent; CancelEvent += OnCancelEvent;
var errorCode = -1;
try try
{ {
errorCode = await Process(instance, cancellationTokenSource).ConfigureAwait(false); errorCode = await Process(instance, cancellationTokenSource).ConfigureAwait(false);
@ -163,6 +172,18 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<str
return exitCode == 0; return exitCode == 0;
} }
internal FFOptions GetConfiguredOptions(FFOptions? ffOptions)
{
var options = ffOptions ?? GlobalFFOptions.Current.Clone();
foreach (var configureOptions in _configurations)
{
configureOptions(options);
}
return options;
}
private Instance PrepareInstance(FFOptions ffOptions, private Instance PrepareInstance(FFOptions ffOptions,
out CancellationTokenSource cancellationTokenSource) out CancellationTokenSource cancellationTokenSource)
{ {
@ -185,7 +206,7 @@ private Instance PrepareInstance(FFOptions ffOptions,
return instance; return instance;
} }
private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList<string> errorData) private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList<string> errorData)
{ {
if (!throwOnError) if (!throwOnError)

View file

@ -1,10 +1,11 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
namespace FFMpegCore namespace FFMpegCore
{ {
public class FFOptions public class FFOptions : ICloneable
{ {
/// <summary> /// <summary>
/// Working directory for the ffmpeg/ffprobe instance /// Working directory for the ffmpeg/ffprobe instance
@ -27,16 +28,24 @@ public class FFOptions
public Encoding Encoding { get; set; } = Encoding.Default; public Encoding Encoding { get; set; } = Encoding.Default;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public Dictionary<string, string> ExtensionOverrides { get; set; } = new Dictionary<string, string> public Dictionary<string, string> ExtensionOverrides { get; set; } = new Dictionary<string, string>
{ {
{ "mpegts", ".ts" }, { "mpegts", ".ts" },
}; };
/// <summary> /// <summary>
/// Whether to cache calls to get ffmpeg codec, pixel- and container-formats /// Whether to cache calls to get ffmpeg codec, pixel- and container-formats
/// </summary> /// </summary>
public bool UseCache { get; set; } = true; public bool UseCache { get; set; } = true;
/// <inheritdoc/>
object ICloneable.Clone() => Clone();
/// <summary>
/// Creates a new object that is a copy of the current instance.
/// </summary>
public FFOptions Clone() => (FFOptions)MemberwiseClone();
} }
} }

View file

@ -182,7 +182,17 @@ await FFMpegArguments
.FromFileInput(inputPath) .FromFileInput(inputPath)
.OutputToFile(outputPath) .OutputToFile(outputPath)
.ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); .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 ### Option 2