Improve cancellation handling

This commit is contained in:
Malte Rosenbjerg 2025-10-17 21:48:30 +02:00
parent fc5e8a66e3
commit e01b73787d

View file

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