From b3c201b42e1379b5aaeb375bee5713e42f47953b Mon Sep 17 00:00:00 2001 From: Sergey Nechaev <6499856+snechaev@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:02:35 +0200 Subject: [PATCH 1/2] Add cancellation token support to SnapshotAsync, GifSnapshotAsync and SubVideoAsync methods. Fixes #592. --- .../FFMpegImage.cs | 9 ++++--- .../FFMpegImage.cs | 9 ++++--- FFMpegCore.Test/VideoTest.cs | 6 +++-- FFMpegCore/FFMpeg/FFMpeg.cs | 25 +++++++++++++------ 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs b/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs index f412844..41c6a1c 100644 --- a/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs +++ b/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs @@ -39,18 +39,21 @@ public static class FFMpegImage /// Thumbnail size. If width or height equal 0, the other will be computed automatically. /// Selected video stream index. /// Input file index + /// Cancellation token /// Bitmap with the requested snapshot. public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, - int inputFileIndex = 0) + int inputFileIndex = 0, CancellationToken cancellationToken = default) { - var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + var source = await FFProbe.AnalyseAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false); var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); using var ms = new MemoryStream(); await arguments .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options .ForceFormat("rawvideo"))) - .ProcessAsynchronously(); + .CancellableThrough(cancellationToken) + .ProcessAsynchronously() + .ConfigureAwait(false); ms.Position = 0; return SKBitmap.Decode(ms); diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs index 1c7f965..2dd2234 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs @@ -38,18 +38,21 @@ public static class FFMpegImage /// Thumbnail size. If width or height equal 0, the other will be computed automatically. /// Selected video stream index. /// Input file index + /// Cancellation token /// Bitmap with the requested snapshot. public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, - int inputFileIndex = 0) + int inputFileIndex = 0, CancellationToken cancellationToken = default) { - var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + var source = await FFProbe.AnalyseAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false); var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); using var ms = new MemoryStream(); await arguments .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options .ForceFormat("rawvideo"))) - .ProcessAsynchronously(); + .CancellableThrough(cancellationToken) + .ProcessAsynchronously() + .ConfigureAwait(false); ms.Position = 0; return new Bitmap(ms); diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 1f54d9f..07c435d 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -698,7 +698,8 @@ public class VideoTest using var outputPath = new TemporaryFile("out.gif"); var input = FFProbe.Analyse(TestResources.Mp4Video); - await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0)); + await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0), + cancellationToken: TestContext.CancellationToken); var analysis = FFProbe.Analyse(outputPath); Assert.AreNotEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); @@ -714,7 +715,8 @@ public class VideoTest var input = FFProbe.Analyse(TestResources.Mp4Video); var desiredGifSize = new Size(320, 240); - await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, desiredGifSize, TimeSpan.FromSeconds(0)); + await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, desiredGifSize, TimeSpan.FromSeconds(0), + cancellationToken: TestContext.CancellationToken); var analysis = FFProbe.Analyse(outputPath); Assert.AreNotEqual(input.PrimaryVideoStream!.Width, desiredGifSize.Width); diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 0b6de74..b9a0d5d 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -37,16 +37,19 @@ public static class FFMpeg /// Thumbnail size. If width or height equal 0, the other will be computed automatically. /// Selected video stream index. /// Input file index + /// Cancellation token /// Bitmap with the requested snapshot. public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, - int inputFileIndex = 0) + int inputFileIndex = 0, CancellationToken cancellationToken = default) { CheckSnapshotOutputExtension(output, FileExtension.Image.All); - var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + var source = await FFProbe.AnalyseAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false); return await SnapshotProcess(input, output, source, size, captureTime, streamIndex, inputFileIndex) - .ProcessAsynchronously(); + .CancellableThrough(cancellationToken) + .ProcessAsynchronously() + .ConfigureAwait(false); } public static bool GifSnapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, @@ -61,14 +64,16 @@ public static class FFMpeg } public static async Task GifSnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, - int? streamIndex = null) + int? streamIndex = null, CancellationToken cancellationToken = default) { CheckSnapshotOutputExtension(output, [FileExtension.Gif]); - var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + var source = await FFProbe.AnalyseAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false); return await GifSnapshotProcess(input, output, source, size, captureTime, duration, streamIndex) - .ProcessAsynchronously(); + .CancellableThrough(cancellationToken) + .ProcessAsynchronously() + .ConfigureAwait(false); } private static FFMpegArgumentProcessor SnapshotProcess(string input, string output, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, @@ -321,11 +326,15 @@ public static class FFMpeg /// Output video file. /// The start time of when the sub video needs to start /// The end time of where the sub video needs to end + /// Cancellation token /// Output video information. - public static async Task SubVideoAsync(string input, string output, TimeSpan startTime, TimeSpan endTime) + public static async Task SubVideoAsync(string input, string output, TimeSpan startTime, TimeSpan endTime, + CancellationToken cancellationToken = default) { return await BaseSubVideo(input, output, startTime, endTime) - .ProcessAsynchronously(); + .CancellableThrough(cancellationToken) + .ProcessAsynchronously() + .ConfigureAwait(false); } /// From f5ecbaee68876131f7783a06f5755607128900b4 Mon Sep 17 00:00:00 2001 From: Sergey Nechaev <6499856+snechaev@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:29:27 +0200 Subject: [PATCH 2/2] Fixed a race condition that occurred when handling the cancellation of an asynchronous operation after the FFmpeg process had already exited. Fixes #348. Related: #592 --- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 2f350ce..8191223 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -166,12 +166,28 @@ public class FFMpegArgumentProcessor void OnCancelEvent(object sender, int timeout) { - instance.SendInput("q"); + ExecuteIgnoringFinishedProcessExceptions(() => instance.SendInput("q")); if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) { cancellationTokenSource.Cancel(); - instance.Kill(); + ExecuteIgnoringFinishedProcessExceptions(() => instance.Kill()); + } + + static void ExecuteIgnoringFinishedProcessExceptions(Action action) + { + try + { + action(); + } + catch (Instances.Exceptions.InstanceProcessAlreadyExitedException) + { + //ignore + } + catch (ObjectDisposedException) + { + //ignore + } } }