mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-10 00:24:14 +01:00
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:
commit
c49a9b2721
6 changed files with 157 additions and 16 deletions
|
@ -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();
|
||||||
}
|
}
|
90
FFMpegCore.Test/FFMpegArgumentProcessorTest.cs
Normal file
90
FFMpegCore.Test/FFMpegArgumentProcessorTest.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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" />
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
12
README.md
12
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue