Merge pull request #584 from rosenbjerg/improve-cancellation-handling

Improve cancellation handling
This commit is contained in:
Malte Rosenbjerg 2025-10-17 22:12:50 +02:00 committed by GitHub
commit 0d07456c6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 54 additions and 17 deletions

View file

@ -1043,6 +1043,28 @@ public class VideoTest
Assert.ThrowsExactly<OperationCanceledException>(() => task.ProcessSynchronously());
}
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_Cancel_CancellationToken_Before_Throws()
{
using var outputFile = new TemporaryFile("out.mp4");
var cts = new CancellationTokenSource();
cts.Cancel();
var task = FFMpegArguments
.FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args
.WithCustomArgument("-re")
.ForceFormat("lavfi"))
.OutputToFile(outputFile, false, opt => opt
.WithAudioCodec(AudioCodec.Aac)
.WithVideoCodec(VideoCodec.LibX264)
.WithSpeedPreset(Speed.VeryFast))
.CancellableThrough(cts.Token);
Assert.ThrowsExactly<OperationCanceledException>(() => task.ProcessSynchronously());
}
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public async Task Video_Cancel_CancellationToken_Async_With_Timeout()

View file

@ -10,8 +10,11 @@ namespace FFMpegCore;
public class FFMpegArgumentProcessor
{
private static readonly Regex ProgressRegex = new(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled);
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly List<Action<FFOptions>> _configurations;
private readonly FFMpegArguments _ffMpegArguments;
private CancellationTokenRegistration? _cancellationTokenRegistration;
private bool _cancelled;
private FFMpegLogLevel? _logLevel;
private Action<string>? _onError;
private Action<string>? _onOutput;
@ -29,6 +32,12 @@ public class FFMpegArgumentProcessor
private event EventHandler<int> CancelEvent = null!;
~FFMpegArgumentProcessor()
{
_cancellationTokenSource.Dispose();
_cancellationTokenRegistration?.Dispose();
}
/// <summary>
/// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed and progress percentage is
/// calculated.
@ -69,15 +78,21 @@ public class FFMpegArgumentProcessor
return this;
}
private void Cancel(int timeout)
{
_cancelled = true;
CancelEvent?.Invoke(this, timeout);
}
public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0)
{
cancel = () => CancelEvent?.Invoke(this, timeout);
cancel = () => Cancel(timeout);
return this;
}
public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0)
{
token.Register(() => CancelEvent?.Invoke(this, timeout));
_cancellationTokenRegistration = token.Register(() => Cancel(timeout));
return this;
}
@ -101,12 +116,12 @@ public class FFMpegArgumentProcessor
public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{
var options = GetConfiguredOptions(ffMpegOptions);
var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource);
var processArguments = PrepareProcessArguments(options);
IProcessResult? processResult = null;
try
{
processResult = Process(processArguments, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult();
processResult = Process(processArguments).ConfigureAwait(false).GetAwaiter().GetResult();
}
catch (OperationCanceledException)
{
@ -122,12 +137,12 @@ public class FFMpegArgumentProcessor
public async Task<bool> ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{
var options = GetConfiguredOptions(ffMpegOptions);
var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource);
var processArguments = PrepareProcessArguments(options);
IProcessResult? processResult = null;
try
{
processResult = await Process(processArguments, cancellationTokenSource).ConfigureAwait(false);
processResult = await Process(processArguments).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@ -140,23 +155,25 @@ public class FFMpegArgumentProcessor
return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty<string>());
}
private async Task<IProcessResult> Process(ProcessArguments processArguments, CancellationTokenSource cancellationTokenSource)
private async Task<IProcessResult> Process(ProcessArguments processArguments)
{
IProcessResult processResult = null!;
if (_cancelled)
{
throw new OperationCanceledException("cancelled before starting processing");
}
_ffMpegArguments.Pre();
using var instance = processArguments.Start();
var cancelled = false;
void OnCancelEvent(object sender, int timeout)
{
cancelled = true;
instance.SendInput("q");
if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true))
if (!_cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true))
{
cancellationTokenSource.Cancel();
_cancellationTokenSource.Cancel();
instance.Kill();
}
}
@ -168,11 +185,11 @@ public class FFMpegArgumentProcessor
await Task.WhenAll(instance.WaitForExitAsync().ContinueWith(t =>
{
processResult = t.Result;
cancellationTokenSource.Cancel();
_cancellationTokenSource.Cancel();
_ffMpegArguments.Post();
}), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false);
}), _ffMpegArguments.During(_cancellationTokenSource.Token)).ConfigureAwait(false);
if (cancelled)
if (_cancelled)
{
throw new OperationCanceledException("ffmpeg processing was cancelled");
}
@ -214,8 +231,7 @@ public class FFMpegArgumentProcessor
return options;
}
private ProcessArguments PrepareProcessArguments(FFOptions ffOptions,
out CancellationTokenSource cancellationTokenSource)
private ProcessArguments PrepareProcessArguments(FFOptions ffOptions)
{
FFMpegHelper.RootExceptionCheck();
FFMpegHelper.VerifyFFMpegExists(ffOptions);
@ -245,7 +261,6 @@ public class FFMpegArgumentProcessor
WorkingDirectory = ffOptions.WorkingDirectory
};
var processArguments = new ProcessArguments(startInfo);
cancellationTokenSource = new CancellationTokenSource();
if (_onOutput != null)
{