From e01b73787dbe8ff26e1577880ce3907dcb966277 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:48:30 +0200 Subject: [PATCH 1/4] Improve cancellation handling --- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 51 +++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 590c099..18ee9dd 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -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> _configurations; private readonly FFMpegArguments _ffMpegArguments; + private CancellationTokenRegistration? _cancellationTokenRegistration; + private bool _cancelled; private FFMpegLogLevel? _logLevel; private Action? _onError; private Action? _onOutput; @@ -29,6 +32,12 @@ public class FFMpegArgumentProcessor private event EventHandler CancelEvent = null!; + ~FFMpegArgumentProcessor() + { + _cancellationTokenSource.Dispose(); + _cancellationTokenRegistration?.Dispose(); + } + /// /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed and progress percentage is /// calculated. @@ -71,13 +80,21 @@ public class FFMpegArgumentProcessor public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0) { - cancel = () => CancelEvent?.Invoke(this, timeout); + cancel = () => + { + _cancelled = true; + CancelEvent?.Invoke(this, timeout); + }; return this; } 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; } @@ -101,12 +118,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 +139,12 @@ public class FFMpegArgumentProcessor public async Task 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 +157,25 @@ public class FFMpegArgumentProcessor return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); } - private async Task Process(ProcessArguments processArguments, CancellationTokenSource cancellationTokenSource) + private async Task 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 +187,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 +233,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 +263,6 @@ public class FFMpegArgumentProcessor WorkingDirectory = ffOptions.WorkingDirectory }; var processArguments = new ProcessArguments(startInfo); - cancellationTokenSource = new CancellationTokenSource(); if (_onOutput != null) { From 4baddaab7fa7f724724134ac21ab0378db40200a Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:51:11 +0200 Subject: [PATCH 2/4] Add test verifying cancellation before processing starts --- FFMpegCore.Test/VideoTest.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 7946552..5921a6e 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -1043,6 +1043,28 @@ public class VideoTest Assert.ThrowsExactly(() => 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(() => task.ProcessSynchronously()); + } + [TestMethod] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public async Task Video_Cancel_CancellationToken_Async_With_Timeout() From 670986dcb2404d3ad159337ebd2c8ffd7c35d763 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:54:19 +0200 Subject: [PATCH 3/4] Extract method for reuse --- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 18ee9dd..163e113 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -78,23 +78,22 @@ 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 = () => - { - _cancelled = true; - CancelEvent?.Invoke(this, timeout); - }; + cancel = () => Cancel(timeout); return this; } + public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0) { - _cancellationTokenRegistration = token.Register(() => - { - _cancelled = true; - CancelEvent?.Invoke(this, timeout); - }); + _cancellationTokenRegistration = token.Register(() => Cancel(timeout)); return this; } From d8904292691bca19a0bfabc0472db9ac67a33871 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:56:40 +0200 Subject: [PATCH 4/4] Remove extranous blank line --- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 163e113..0aa31ca 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -90,7 +90,6 @@ public class FFMpegArgumentProcessor return this; } - public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0) { _cancellationTokenRegistration = token.Register(() => Cancel(timeout));