From e01b73787dbe8ff26e1577880ce3907dcb966277 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:48:30 +0200 Subject: [PATCH 01/34] 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 0f800c4333395b160fa518f4cd364e4d5e56bad2 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:49:42 +0200 Subject: [PATCH 02/34] Provide fps argument to input parameter as well --- FFMpegCore/FFMpeg/FFMpeg.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 7076a49..0b6de74 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -132,7 +132,8 @@ public static class FFMpeg } return FFMpegArguments - .FromFileInput(Path.Combine(tempFolderName, $"%09d{fileExtension}"), false) + .FromFileInput(Path.Combine(tempFolderName, $"%09d{fileExtension}"), false, options => options + .WithFramerate(frameRate)) .OutputToFile(output, true, options => options .ForcePixelFormat("yuv420p") .Resize(width!.Value, height!.Value) From 3c3da28a99bb347c66766ccdd077410c34b2ff70 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:50:34 +0200 Subject: [PATCH 03/34] Update test assertion on video duration --- FFMpegCore.Test/VideoTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 7946552..8ecbf6e 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -739,7 +739,7 @@ public class VideoTest Assert.IsTrue(success); var result = FFProbe.Analyse(outputFile); - Assert.AreEqual(1, result.Duration.Seconds); + Assert.AreEqual(3, result.Duration.Seconds); Assert.AreEqual(imageAnalysis.PrimaryVideoStream!.Width, result.PrimaryVideoStream!.Width); Assert.AreEqual(imageAnalysis.PrimaryVideoStream!.Height, result.PrimaryVideoStream.Height); } From 6b1e34ce08b10a84f953bfd81663307bc1a444c3 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:50:49 +0200 Subject: [PATCH 04/34] Change fps parameter to double --- FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs | 5 ++--- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 2 +- FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs b/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs index 7e4db86..8e62139 100644 --- a/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs @@ -4,12 +4,11 @@ namespace FFMpegCore.Arguments; public class GifPaletteArgument : IArgument { - private readonly int _fps; - + private readonly double _fps; private readonly Size? _size; private readonly int _streamIndex; - public GifPaletteArgument(int streamIndex, int fps, Size? size) + public GifPaletteArgument(int streamIndex, double fps, Size? size) { _streamIndex = streamIndex; _fps = fps; diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 7ee10e2..4b6a170 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -258,7 +258,7 @@ public class FFMpegArgumentOptions : FFMpegArgumentsBase return WithArgument(new ID3V2VersionArgument(id3v2Version)); } - public FFMpegArgumentOptions WithGifPaletteArgument(int streamIndex, Size? size, int fps = 12) + public FFMpegArgumentOptions WithGifPaletteArgument(int streamIndex, Size? size, double fps = 12) { return WithArgument(new GifPaletteArgument(streamIndex, fps, size)); } diff --git a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs index 3204bcc..6237320 100644 --- a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs +++ b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs @@ -62,7 +62,7 @@ public static class SnapshotArgumentBuilder TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null, - int fps = 12) + double fps = 12) { var defaultGifOutputSize = new Size(480, -1); From 4baddaab7fa7f724724134ac21ab0378db40200a Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:51:11 +0200 Subject: [PATCH 05/34] 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 7c070765b83bf3a16c246e64f2dfa7faa15d13d3 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:51:43 +0200 Subject: [PATCH 06/34] Remove stray space --- FFMpegCore/FFProbe/FFProbeAnalysis.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs index 3812ecf..7176ce1 100644 --- a/FFMpegCore/FFProbe/FFProbeAnalysis.cs +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -151,7 +151,7 @@ public static class TagExtensions public static string? GetCreationTime(this ITagsContainer tagsContainer) { - return TryGetTagValue(tagsContainer, "creation_time "); + return TryGetTagValue(tagsContainer, "creation_time"); } public static string? GetRotate(this ITagsContainer tagsContainer) From 670986dcb2404d3ad159337ebd2c8ffd7c35d763 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 21:54:19 +0200 Subject: [PATCH 07/34] 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 08/34] 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)); From 20031009096f75a4f673bb22bd081cd997076f34 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 22:14:00 +0200 Subject: [PATCH 09/34] Improve test for percentage progress events --- FFMpegCore.Test/VideoTest.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 7946552..295f751 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -785,12 +785,12 @@ public class VideoTest var timeDone = TimeSpan.Zero; var analysis = FFProbe.Analyse(TestResources.Mp4Video); + var events = new List(); + void OnPercentageProgess(double percentage) { - if (percentage < 100) - { - percentageDone = percentage; - } + events.Add(percentage); + percentageDone = percentage; } void OnTimeProgess(TimeSpan time) @@ -812,7 +812,9 @@ public class VideoTest Assert.IsTrue(success); Assert.IsTrue(File.Exists(outputFile)); Assert.AreNotEqual(0.0, percentageDone); - Assert.AreNotEqual(100.0, percentageDone); + CollectionAssert.AllItemsAreUnique(events); + Assert.AreNotEqual(100.0, events.First()); + Assert.AreEqual(100.0, events.Last()); Assert.AreNotEqual(TimeSpan.Zero, timeDone); Assert.AreNotEqual(analysis.Duration, timeDone); } From fab7ff0aaba6f02f5e464a02d4dc35afafba77e1 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 22:37:39 +0200 Subject: [PATCH 10/34] Ensure TestContext.CancellationToken is used --- FFMpegCore.Test/AudioTest.cs | 17 +++++++ FFMpegCore.Test/VideoTest.cs | 99 ++++++++++++++++++++++++++---------- 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index 3960b9f..3f0f25e 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -9,6 +9,8 @@ namespace FFMpegCore.Test; [TestClass] public class AudioTest { + public TestContext TestContext { get; set; } + [TestMethod] public void Audio_Remove() { @@ -41,6 +43,7 @@ public class AudioTest await FFMpegArguments .FromPipeInput(new StreamPipeSource(file), options => options.ForceFormat("s16le")) .OutputToPipe(new StreamPipeSink(memoryStream), options => options.ForceFormat("mp3")) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(); } @@ -83,6 +86,7 @@ public class AudioTest .FromPipeInput(audioSamplesSource) .OutputToFile(outputFile, false, opt => opt .WithAudioCodec(AudioCodec.Aac)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -101,6 +105,7 @@ public class AudioTest .FromPipeInput(audioSamplesSource) .OutputToFile(outputFile, false, opt => opt .WithAudioCodec(AudioCodec.LibVorbis)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -119,6 +124,7 @@ public class AudioTest .FromPipeInput(audioSamplesSource) .OutputToFile(outputFile, false, opt => opt .WithAudioCodec(AudioCodec.Aac)) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(); Assert.IsTrue(success); } @@ -137,6 +143,7 @@ public class AudioTest .FromPipeInput(audioSamplesSource) .OutputToFile(outputFile, false, opt => opt .WithAudioCodec(AudioCodec.Aac)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -153,6 +160,7 @@ public class AudioTest .FromPipeInput(audioSamplesSource) .OutputToFile(outputFile, false, opt => opt .WithAudioCodec(AudioCodec.Aac)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously()); } @@ -168,6 +176,7 @@ public class AudioTest .FromPipeInput(audioSamplesSource) .OutputToFile(outputFile, false, opt => opt .WithAudioCodec(AudioCodec.Aac)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously()); } @@ -183,6 +192,7 @@ public class AudioTest .FromPipeInput(audioSamplesSource) .OutputToFile(outputFile, false, opt => opt .WithAudioCodec(AudioCodec.Aac)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously()); } @@ -196,6 +206,7 @@ public class AudioTest .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.Pan(1, "c0 < 0.9 * c0 + 0.1 * c1"))) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); var mediaAnalysis = FFProbe.Analyse(outputFile); @@ -215,6 +226,7 @@ public class AudioTest .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.Pan(1))) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); var mediaAnalysis = FFProbe.Analyse(outputFile); @@ -234,6 +246,7 @@ public class AudioTest .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1"))) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously()); } @@ -247,6 +260,7 @@ public class AudioTest .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1"))) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously()); } @@ -260,6 +274,7 @@ public class AudioTest .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.DynamicNormalizer())) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); @@ -275,6 +290,7 @@ public class AudioTest .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.DynamicNormalizer(250, 7, 0.9, 2, 1, false, true, true, 0.5))) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); @@ -294,6 +310,7 @@ public class AudioTest .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.DynamicNormalizer(filterWindow: filterWindow))) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously()); } } diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 5921a6e..522c925 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -30,6 +30,7 @@ public class VideoTest var success = FFMpegArguments .FromFileInput(TestResources.WebmVideo) .OutputToFile(outputFile, false) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -43,6 +44,7 @@ public class VideoTest var success = FFMpegArguments .FromFileInput(TestResources.WebmVideo) .OutputToFile(outputFile, false) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -58,6 +60,7 @@ public class VideoTest .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264) .ForcePixelFormat("yuv444p")) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); var analysis = FFProbe.Analyse(outputFile); @@ -74,6 +77,7 @@ public class VideoTest .FromFileInput(TestResources.WebmVideo) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -88,6 +92,7 @@ public class VideoTest .FromFileInput(TestResources.WebmVideo) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX265)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -99,7 +104,7 @@ public class VideoTest [DataRow(PixelFormat.Format32bppArgb)] public void Video_ToMP4_Args_Pipe_WindowsOnly(PixelFormat pixelFormat) { - Video_ToMP4_Args_Pipe_Internal(pixelFormat); + Video_ToMP4_Args_Pipe_Internal(pixelFormat, TestContext.CancellationToken); } [TestMethod] @@ -108,10 +113,10 @@ public class VideoTest [DataRow(SKColorType.Bgra8888)] public void Video_ToMP4_Args_Pipe(SKColorType pixelFormat) { - Video_ToMP4_Args_Pipe_Internal(pixelFormat); + Video_ToMP4_Args_Pipe_Internal(pixelFormat, TestContext.CancellationToken); } - private static void Video_ToMP4_Args_Pipe_Internal(dynamic pixelFormat) + private static void Video_ToMP4_Args_Pipe_Internal(dynamic pixelFormat, CancellationToken cancellationToken) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -120,6 +125,7 @@ public class VideoTest .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(cancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -129,17 +135,17 @@ public class VideoTest [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly() { - Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(PixelFormat.Format24bppRgb); + Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(PixelFormat.Format24bppRgb, TestContext.CancellationToken); } [TestMethod] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Video_ToMP4_Args_Pipe_DifferentImageSizes() { - Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(SKColorType.Rgb565); + Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(SKColorType.Rgb565, TestContext.CancellationToken); } - private static void Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(dynamic pixelFormat) + private static void Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(dynamic pixelFormat, CancellationToken cancellationToken) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -153,6 +159,7 @@ public class VideoTest .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(cancellationToken) .ProcessSynchronously()); } @@ -161,17 +168,17 @@ public class VideoTest [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly_Async() { - await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(PixelFormat.Format24bppRgb); + await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(PixelFormat.Format24bppRgb, TestContext.CancellationToken); } [TestMethod] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() { - await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(SKColorType.Rgb565); + await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(SKColorType.Rgb565, TestContext.CancellationToken); } - private static async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(dynamic pixelFormat) + private static async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(dynamic pixelFormat, CancellationToken cancellationToken) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -185,6 +192,7 @@ public class VideoTest .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(cancellationToken) .ProcessAsynchronously()); } @@ -194,20 +202,22 @@ public class VideoTest public void Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly() { Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(PixelFormat.Format24bppRgb, - PixelFormat.Format32bppRgb); + PixelFormat.Format32bppRgb, TestContext.CancellationToken); } [TestMethod] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() { - Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(SKColorType.Rgb565, SKColorType.Bgra8888); + Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(SKColorType.Rgb565, SKColorType.Bgra8888, TestContext.CancellationToken); } - private static void Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2) + private static void Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2, + CancellationToken cancellationToken) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + var frames = new List { BitmapSource.CreateVideoFrame(0, pixelFormatFrame1, 255, 255, 1, 0), BitmapSource.CreateVideoFrame(0, pixelFormatFrame2, 255, 255, 1, 0) @@ -218,6 +228,7 @@ public class VideoTest .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(cancellationToken) .ProcessSynchronously()); } @@ -227,17 +238,18 @@ public class VideoTest public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly_Async() { await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(PixelFormat.Format24bppRgb, - PixelFormat.Format32bppRgb); + PixelFormat.Format32bppRgb, TestContext.CancellationToken); } [TestMethod] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() { - await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(SKColorType.Rgb565, SKColorType.Bgra8888); + await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(SKColorType.Rgb565, SKColorType.Bgra8888, TestContext.CancellationToken); } - private static async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2) + private static async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2, + CancellationToken cancellationToken) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -251,6 +263,7 @@ public class VideoTest .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(cancellationToken) .ProcessAsynchronously()); } @@ -265,6 +278,7 @@ public class VideoTest .FromPipeInput(new StreamPipeSource(input)) .OutputToFile(output, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -280,6 +294,7 @@ public class VideoTest await FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToPipe(pipeSource, opt => opt.ForceFormat("mp4")) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(); }); } @@ -295,6 +310,7 @@ public class VideoTest .ForceFormat("webm")) .OutputToPipe(new StreamPipeSink(output), opt => opt .ForceFormat("mpegts")) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); output.Position = 0; @@ -313,6 +329,7 @@ public class VideoTest .FromFileInput(TestResources.Mp4Video) .OutputToPipe(new StreamPipeSink(ms), opt => opt .ForceFormat("mkv")) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); }); } @@ -328,6 +345,7 @@ public class VideoTest .OutputToPipe(pipeSource, opt => opt .WithVideoCodec(VideoCodec.LibX264) .ForceFormat("matroska")) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(); } @@ -338,11 +356,13 @@ public class VideoTest FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToFile("temporary.mp4") + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); await FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToFile("temporary.mp4") + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(); File.Delete("temporary.mp4"); @@ -358,6 +378,7 @@ public class VideoTest .OutputToPipe(new StreamPipeSink(output), opt => opt .WithVideoCodec(VideoCodec.LibVpx) .ForceFormat("matroska")) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); @@ -376,6 +397,7 @@ public class VideoTest var success = FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToFile(outputFile, false) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -392,6 +414,7 @@ public class VideoTest .CopyChannel() .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) .ForceFormat(VideoType.MpegTs)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -403,7 +426,7 @@ public class VideoTest [DataRow(PixelFormat.Format32bppArgb)] public async Task Video_ToTS_Args_Pipe_WindowsOnly(PixelFormat pixelFormat) { - await Video_ToTS_Args_Pipe_Internal(pixelFormat); + await Video_ToTS_Args_Pipe_Internal(pixelFormat, TestContext.CancellationToken); } [TestMethod] @@ -412,10 +435,10 @@ public class VideoTest [DataRow(SKColorType.Bgra8888)] public async Task Video_ToTS_Args_Pipe(SKColorType pixelFormat) { - await Video_ToTS_Args_Pipe_Internal(pixelFormat); + await Video_ToTS_Args_Pipe_Internal(pixelFormat, TestContext.CancellationToken); } - private static async Task Video_ToTS_Args_Pipe_Internal(dynamic pixelFormat) + private static async Task Video_ToTS_Args_Pipe_Internal(dynamic pixelFormat, CancellationToken cancellationToken) { using var output = new TemporaryFile($"out{VideoType.Ts.Extension}"); var input = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); @@ -424,6 +447,7 @@ public class VideoTest .FromPipeInput(input) .OutputToFile(output, false, opt => opt .ForceFormat(VideoType.Ts)) + .CancellableThrough(cancellationToken) .ProcessAsynchronously(); Assert.IsTrue(success); @@ -441,6 +465,7 @@ public class VideoTest .OutputToFile(outputFile, false, opt => opt .Resize(200, 200) .WithVideoCodec(VideoCodec.LibTheora)) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(); Assert.IsTrue(success); } @@ -461,6 +486,7 @@ public class VideoTest .WithVideoFilters(filterOptions => filterOptions .Scale(VideoSize.Ed)) .WithVideoCodec(VideoCodec.LibTheora)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); var analysis = FFProbe.Analyse(outputFile); @@ -478,6 +504,7 @@ public class VideoTest .OutputToFile(outputFile, false, opt => opt .UsingMultithreading(true) .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -490,7 +517,7 @@ public class VideoTest // [DataRow(PixelFormat.Format48bppRgb)] public void Video_ToMP4_Resize_Args_Pipe(PixelFormat pixelFormat) { - Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat); + Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat, TestContext.CancellationToken); } [TestMethod] @@ -499,10 +526,10 @@ public class VideoTest [DataRow(SKColorType.Bgra8888)] public void Video_ToMP4_Resize_Args_Pipe(SKColorType pixelFormat) { - Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat); + Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat, TestContext.CancellationToken); } - private static void Video_ToMP4_Resize_Args_Pipe_Internal(dynamic pixelFormat) + private static void Video_ToMP4_Resize_Args_Pipe_Internal(dynamic pixelFormat, CancellationToken cancellationToken) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); @@ -511,6 +538,7 @@ public class VideoTest .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(cancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); } @@ -764,6 +792,7 @@ public class VideoTest FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToFile(outputFile, false, opt => opt.WithDuration(TimeSpan.FromSeconds(video.Duration.TotalSeconds - 2))) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(File.Exists(outputFile)); @@ -807,6 +836,7 @@ public class VideoTest .WithDuration(analysis.Duration)) .NotifyOnProgress(OnPercentageProgess, analysis.Duration) .NotifyOnProgress(OnTimeProgess) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(success); @@ -832,6 +862,7 @@ public class VideoTest .WithDuration(TimeSpan.FromSeconds(2))) .NotifyOnError(_ => dataReceived = true) .Configure(opt => opt.Encoding = Encoding.UTF8) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); Assert.IsTrue(dataReceived); @@ -844,17 +875,17 @@ public class VideoTest [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Video_TranscodeInMemory_WindowsOnly() { - Video_TranscodeInMemory_Internal(PixelFormat.Format24bppRgb); + Video_TranscodeInMemory_Internal(PixelFormat.Format24bppRgb, TestContext.CancellationToken); } [TestMethod] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Video_TranscodeInMemory() { - Video_TranscodeInMemory_Internal(SKColorType.Rgb565); + Video_TranscodeInMemory_Internal(SKColorType.Rgb565, TestContext.CancellationToken); } - private static void Video_TranscodeInMemory_Internal(dynamic pixelFormat) + private static void Video_TranscodeInMemory_Internal(dynamic pixelFormat, CancellationToken cancellationToken) { using var resStream = new MemoryStream(); var reader = new StreamPipeSink(resStream); @@ -865,6 +896,7 @@ public class VideoTest .OutputToPipe(reader, opt => opt .WithVideoCodec("vp9") .ForceFormat("webm")) + .CancellableThrough(cancellationToken) .ProcessSynchronously(); resStream.Position = 0; @@ -884,6 +916,7 @@ public class VideoTest .OutputToPipe(new StreamPipeSink(memoryStream), opt => opt .WithVideoCodec("vp9") .ForceFormat("webm")) + .CancellableThrough(TestContext.CancellationToken) .ProcessSynchronously(); memoryStream.Position = 0; @@ -907,6 +940,8 @@ public class VideoTest .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(out var cancel) + .CancellableThrough(TestContext.CancellationToken) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(false); await Task.Delay(300, TestContext.CancellationToken); @@ -930,11 +965,13 @@ public class VideoTest .WithAudioCodec(AudioCodec.Aac) .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) - .CancellableThrough(out var cancel); + .CancellableThrough(out var cancel) + .CancellableThrough(TestContext.CancellationToken); Task.Delay(300, TestContext.CancellationToken).ContinueWith(_ => cancel(), TestContext.CancellationToken); - var result = task.ProcessSynchronously(false); + var result = task.CancellableThrough(TestContext.CancellationToken) + .ProcessSynchronously(false); Assert.IsFalse(result); } @@ -954,6 +991,7 @@ public class VideoTest .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(out var cancel, 10000) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(false); await Task.Delay(300, TestContext.CancellationToken); @@ -987,6 +1025,7 @@ public class VideoTest .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(cts.Token) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(false); cts.CancelAfter(300); @@ -1013,6 +1052,7 @@ public class VideoTest .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(cts.Token) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(); cts.CancelAfter(300); @@ -1040,7 +1080,8 @@ public class VideoTest cts.CancelAfter(300); - Assert.ThrowsExactly(() => task.ProcessSynchronously()); + Assert.ThrowsExactly(() => task.CancellableThrough(TestContext.CancellationToken) + .ProcessSynchronously()); } [TestMethod] @@ -1062,7 +1103,8 @@ public class VideoTest .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(cts.Token); - Assert.ThrowsExactly(() => task.ProcessSynchronously()); + Assert.ThrowsExactly(() => task.CancellableThrough(TestContext.CancellationToken) + .ProcessSynchronously()); } [TestMethod] @@ -1082,6 +1124,7 @@ public class VideoTest .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(cts.Token, 8000) + .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(false); cts.CancelAfter(300); From 326b3e271989e9f8dec6ab19ffaff4bd35262cb3 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 22:43:25 +0200 Subject: [PATCH 11/34] Use local CancellationTokenSource --- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 27 ++++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 0aa31ca..2f350ce 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -10,7 +10,6 @@ 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; @@ -32,12 +31,6 @@ 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. @@ -92,6 +85,7 @@ public class FFMpegArgumentProcessor public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0) { + _cancellationTokenRegistration?.Dispose(); _cancellationTokenRegistration = token.Register(() => Cancel(timeout)); return this; } @@ -117,11 +111,12 @@ public class FFMpegArgumentProcessor { var options = GetConfiguredOptions(ffMpegOptions); var processArguments = PrepareProcessArguments(options); + using var cancellationTokenSource = new CancellationTokenSource(); IProcessResult? processResult = null; try { - processResult = Process(processArguments).ConfigureAwait(false).GetAwaiter().GetResult(); + processResult = Process(processArguments, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); } catch (OperationCanceledException) { @@ -138,11 +133,12 @@ public class FFMpegArgumentProcessor { var options = GetConfiguredOptions(ffMpegOptions); var processArguments = PrepareProcessArguments(options); + using var cancellationTokenSource = new CancellationTokenSource(); IProcessResult? processResult = null; try { - processResult = await Process(processArguments).ConfigureAwait(false); + processResult = await Process(processArguments, cancellationTokenSource).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -155,11 +151,12 @@ public class FFMpegArgumentProcessor return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); } - private async Task Process(ProcessArguments processArguments) + private async Task Process(ProcessArguments processArguments, CancellationTokenSource cancellationTokenSource) { IProcessResult processResult = null!; if (_cancelled) { + _cancellationTokenRegistration?.Dispose(); throw new OperationCanceledException("cancelled before starting processing"); } @@ -171,9 +168,9 @@ public class FFMpegArgumentProcessor { instance.SendInput("q"); - if (!_cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) + if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) { - _cancellationTokenSource.Cancel(); + cancellationTokenSource.Cancel(); instance.Kill(); } } @@ -185,12 +182,13 @@ 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) { + _cancellationTokenRegistration?.Dispose(); throw new OperationCanceledException("ffmpeg processing was cancelled"); } @@ -199,6 +197,7 @@ public class FFMpegArgumentProcessor finally { CancelEvent -= OnCancelEvent; + _cancellationTokenRegistration?.Dispose(); } } From 0ea445cdb8be9384ad322d3c5e3bf87e907023de Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 22:46:03 +0200 Subject: [PATCH 12/34] Apply suggestions from Copilot review --- FFMpegCore.Test/VideoTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 295f751..46161ae 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -812,9 +812,10 @@ public class VideoTest Assert.IsTrue(success); Assert.IsTrue(File.Exists(outputFile)); Assert.AreNotEqual(0.0, percentageDone); + Assert.IsGreaterThan(1, events.Count); CollectionAssert.AllItemsAreUnique(events); Assert.AreNotEqual(100.0, events.First()); - Assert.AreEqual(100.0, events.Last()); + Assert.AreEqual(100.0, events.Last(), 0.001); Assert.AreNotEqual(TimeSpan.Zero, timeDone); Assert.AreNotEqual(analysis.Duration, timeDone); } From 40414ad008b2b21e3b727507d7d4e79fc9f6e7a9 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 22:46:45 +0200 Subject: [PATCH 13/34] Remove extranous blank line --- FFMpegCore.Test/VideoTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 522c925..ec97cec 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -217,7 +217,6 @@ public class VideoTest { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var frames = new List { BitmapSource.CreateVideoFrame(0, pixelFormatFrame1, 255, 255, 1, 0), BitmapSource.CreateVideoFrame(0, pixelFormatFrame2, 255, 255, 1, 0) From d0f6db1a2ab2a516e3e3be3ca0e1fa1e881ee45c Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 22:52:33 +0200 Subject: [PATCH 14/34] Dont override in test using cancellationtoken --- FFMpegCore.Test/VideoTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index ec97cec..edcf911 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -1123,7 +1123,6 @@ public class VideoTest .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(cts.Token, 8000) - .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(false); cts.CancelAfter(300); From f11b168ed9002b986b4e4e259f50f12ea1eed7c6 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 23:15:16 +0200 Subject: [PATCH 15/34] Increase timeout because of slow windows CI agents --- FFMpegCore.Test/VideoTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index edcf911..cf88c64 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -17,7 +17,7 @@ namespace FFMpegCore.Test; [TestClass] public class VideoTest { - private const int BaseTimeoutMilliseconds = 15_000; + private const int BaseTimeoutMilliseconds = 30_000; public TestContext TestContext { get; set; } From 90be0888e8578e1689a821f7c1cce25cb417cea6 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 23:15:28 +0200 Subject: [PATCH 16/34] Create BaseTimeoutMilliseconds in AudioTests --- FFMpegCore.Test/AudioTest.cs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index 3f0f25e..d552621 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -9,6 +9,8 @@ namespace FFMpegCore.Test; [TestClass] public class AudioTest { + private const int BaseTimeoutMilliseconds = 15_000; + public TestContext TestContext { get; set; } [TestMethod] @@ -73,7 +75,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_ToAAC_Args_Pipe() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -92,7 +94,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_ToLibVorbis_Args_Pipe() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -111,7 +113,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public async Task Audio_ToAAC_Args_Pipe_Async() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -130,7 +132,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_ToAAC_Args_Pipe_ValidDefaultConfiguration() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -149,7 +151,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_ToAAC_Args_Pipe_InvalidChannels() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -165,7 +167,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_ToAAC_Args_Pipe_InvalidFormat() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -181,7 +183,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_ToAAC_Args_Pipe_InvalidSampleRate() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -197,7 +199,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_Pan_ToMono() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -217,7 +219,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_Pan_ToMonoNoDefinitions() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -237,7 +239,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -251,7 +253,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -265,7 +267,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_DynamicNormalizer_WithDefaultValues() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -281,7 +283,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Audio_DynamicNormalizer_WithNonDefaultValues() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -297,7 +299,7 @@ public class AudioTest } [TestMethod] - [Timeout(10000, CooperativeCancellation = true)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [DataRow(2)] [DataRow(32)] [DataRow(8)] From 9a0f784c710084f087bb409b32fe59ba9d725bf2 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 23:29:13 +0200 Subject: [PATCH 17/34] Increate timeout further due to slow windows CI .... --- FFMpegCore.Test/VideoTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index cf88c64..626e00c 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -17,7 +17,7 @@ namespace FFMpegCore.Test; [TestClass] public class VideoTest { - private const int BaseTimeoutMilliseconds = 30_000; + private const int BaseTimeoutMilliseconds = 60_000; public TestContext TestContext { get; set; } From 90786394a60ec5f30228ff4d6458a00e0f0c0e43 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Oct 2025 23:49:53 +0200 Subject: [PATCH 18/34] Increase timeout due to slow windows CI agent ... --- FFMpegCore.Test/AudioTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index d552621..3172af7 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -9,7 +9,7 @@ namespace FFMpegCore.Test; [TestClass] public class AudioTest { - private const int BaseTimeoutMilliseconds = 15_000; + private const int BaseTimeoutMilliseconds = 30_000; public TestContext TestContext { get; set; } 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 19/34] 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 20/34] 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 + } } } From 97053929a9d6eebf758e3ae121a0b82fcabca9a4 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:25:16 +0200 Subject: [PATCH 21/34] Add FFMetadataBuilder for easily constructing metadata text --- FFMpegCore/FFMetadataInputArgument.cs | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 FFMpegCore/FFMetadataInputArgument.cs diff --git a/FFMpegCore/FFMetadataInputArgument.cs b/FFMpegCore/FFMetadataInputArgument.cs new file mode 100644 index 0000000..68a2c2e --- /dev/null +++ b/FFMpegCore/FFMetadataInputArgument.cs @@ -0,0 +1,63 @@ +using System.Text; + +namespace FFMpegCore; + +public class FFMetadataBuilder +{ + private Dictionary Tags { get; } = new(); + private List Chapters { get; } = []; + + public static FFMetadataBuilder Empty() + { + return new FFMetadataBuilder(); + } + + public FFMetadataBuilder WithTag(string key, string value) + { + Tags.Add(key, value); + return this; + } + + public FFMetadataBuilder WithChapter(string title, long durationMs) + { + Chapters.Add(new FFMetadataChapter(title, durationMs)); + return this; + } + + public FFMetadataBuilder WithChapter(string title, double durationSeconds) + { + Chapters.Add(new FFMetadataChapter(title, Convert.ToInt64(durationSeconds * 1000))); + return this; + } + + public string GetMetadataFileContent() + { + var sb = new StringBuilder(); + sb.AppendLine(";FFMETADATA1"); + + foreach (var tag in Tags) + { + sb.AppendLine($"{tag.Key}={tag.Value}"); + } + + long totalDurationMs = 0; + foreach (var chapter in Chapters) + { + sb.AppendLine("[CHAPTER]"); + sb.AppendLine("TIMEBASE=1/1000"); + sb.AppendLine($"START={totalDurationMs}"); + sb.AppendLine($"END={totalDurationMs + chapter.DurationMs}"); + sb.AppendLine($"title={chapter.Title}"); + totalDurationMs += chapter.DurationMs; + } + + return sb.ToString(); + } + + + private class FFMetadataChapter(string title, long durationMs) + { + public string Title { get; } = title; + public long DurationMs { get; } = durationMs; + } +} From 62e829d9b484052ffea58a1b3302df72ac36b3d7 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:25:43 +0200 Subject: [PATCH 22/34] Add AddMetaData overload accepting FFMetadataBuilder instance --- FFMpegCore/FFMpeg/FFMpegArguments.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 4c3b7bf..927442d 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -109,6 +109,11 @@ public sealed class FFMpegArguments : FFMpegArgumentsBase return WithInput(new MetaDataArgument(content), addArguments); } + public FFMpegArguments AddMetaData(FFMetadataBuilder metaDataBuilder, Action? addArguments = null) + { + return WithInput(new MetaDataArgument(metaDataBuilder.GetMetadataFileContent()), addArguments); + } + public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action? addArguments = null) { return WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); From ef313ea411f5600a5d87ee5f5a494a20f9d6b12f Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:25:52 +0200 Subject: [PATCH 23/34] Add test verifying functionality --- FFMpegCore.Test/VideoTest.cs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 1f54d9f..accba60 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -82,6 +82,40 @@ public class VideoTest Assert.IsTrue(success); } + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_MetadataBuilder() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + await FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .AddMetaData(FFMetadataBuilder.Empty() + .WithTag("title", "noname") + .WithTag("artist", "unknown") + .WithChapter("Chapter 1", 1.1) + .WithChapter("Chapter 2", 1.23)) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(TestContext.CancellationToken) + .ProcessAsynchronously(); + + var analysis = await FFProbe.AnalyseAsync(outputFile, cancellationToken: TestContext.CancellationToken); + Assert.IsTrue(analysis.Format.Tags!.TryGetValue("title", out var title)); + Assert.IsTrue(analysis.Format.Tags!.TryGetValue("artist", out var artist)); + Assert.AreEqual("noname", title); + Assert.AreEqual("unknown", artist); + + Assert.HasCount(2, analysis.Chapters); + Assert.AreEqual("Chapter 1", analysis.Chapters.First().Title); + Assert.AreEqual(1.1, analysis.Chapters.First().Duration.TotalSeconds); + Assert.AreEqual(1.1, analysis.Chapters.First().End.TotalSeconds); + + Assert.AreEqual("Chapter 2", analysis.Chapters.Last().Title); + Assert.AreEqual(1.23, analysis.Chapters.Last().Duration.TotalSeconds); + Assert.AreEqual(1.1 + 1.23, analysis.Chapters.Last().End.TotalSeconds); + } + [TestMethod] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Video_ToH265_MKV_Args() From 15acd9f0dac97f5795f7215611400d30cf748145 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:28:47 +0200 Subject: [PATCH 24/34] Add BOM --- FFMpegCore/FFMetadataInputArgument.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMetadataInputArgument.cs b/FFMpegCore/FFMetadataInputArgument.cs index 68a2c2e..cfd16c9 100644 --- a/FFMpegCore/FFMetadataInputArgument.cs +++ b/FFMpegCore/FFMetadataInputArgument.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; namespace FFMpegCore; From 53445322e41d341513ce3afc9eb5cc6cc575f2a1 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:36:40 +0200 Subject: [PATCH 25/34] Fix linting --- FFMpegCore/FFMetadataInputArgument.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/FFMpegCore/FFMetadataInputArgument.cs b/FFMpegCore/FFMetadataInputArgument.cs index cfd16c9..f2fce59 100644 --- a/FFMpegCore/FFMetadataInputArgument.cs +++ b/FFMpegCore/FFMetadataInputArgument.cs @@ -54,7 +54,6 @@ public class FFMetadataBuilder return sb.ToString(); } - private class FFMetadataChapter(string title, long durationMs) { public string Title { get; } = title; From 930d493b8c0a2735537922dd1d53a69829d7e6f5 Mon Sep 17 00:00:00 2001 From: Sergey Nechaev <6499856+snechaev@users.noreply.github.com> Date: Wed, 22 Oct 2025 20:32:30 +0200 Subject: [PATCH 26/34] Add test to verify unexpected exception on FFProbe operations cancellation. Ref.: #594 --- FFMpegCore.Test/FFProbeTests.cs | 57 +++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index a702eed..16f3b4c 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -285,4 +285,61 @@ public class FFProbeTests var info = FFProbe.Analyse(TestResources.Mp4Video, customArguments: "-headers \"Hello: World\""); Assert.AreEqual(3, info.Duration.Seconds); } + + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Parallel_FFProbe_Cancellation_Should_Throw_Only_OperationCanceledException() + { + // Warm up FFMpegCore environment + Helpers.FFProbeHelper.VerifyFFProbeExists(GlobalFFOptions.Current); + + var mp4 = TestResources.Mp4Video; + if (!File.Exists(mp4)) + { + Assert.Inconclusive($"Test video not found: {mp4}"); + return; + } + + var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken); + var token = cts.Token; + using var semaphore = new SemaphoreSlim(Environment.ProcessorCount, Environment.ProcessorCount); + var tasks = Enumerable.Range(0, 50).Select(x => Task.Run(async () => + { + await semaphore.WaitAsync(token); + try + { + var analysis = await FFProbe.AnalyseAsync(mp4, cancellationToken: token); + return analysis; + } + finally + { + semaphore.Release(); + } + }, token)).ToList(); + + // Wait for 2 tasks to finish, then cancel all + await Task.WhenAny(tasks); + await Task.WhenAny(tasks); + await cts.CancelAsync(); + cts.Dispose(); + + var exceptions = new List(); + foreach (var task in tasks) + { + try + { + await task; + } + catch (Exception e) + { + exceptions.Add(e); + } + } + + Assert.IsNotEmpty(exceptions, "No exceptions were thrown on cancellation. Test was useless. " + + ".Try adjust cancellation timings to make cancellation at the moment, when ffprobe is still running."); + + // Check that all exceptions are OperationCanceledException + CollectionAssert.AllItemsAreInstancesOfType(exceptions, typeof(OperationCanceledException)); + } } From b863f5d19e876cb3527e05436b3ef4377406dcd0 Mon Sep 17 00:00:00 2001 From: Sergey Nechaev <6499856+snechaev@users.noreply.github.com> Date: Wed, 22 Oct 2025 20:41:26 +0200 Subject: [PATCH 27/34] FFProbe: Do not throw FFMpegException if cancellation was requested. Throw OperationCancelledException in this case to provide more uniform and expected behavior. Fixes #594 --- FFMpegCore/FFProbe/FFProbe.cs | 11 +++++++---- FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index e199376..ba08346 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -84,7 +84,7 @@ public static class FFProbe var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - ThrowIfExitCodeNotZero(result); + ThrowIfExitCodeNotZero(result, cancellationToken); return ParseOutput(result); } @@ -123,7 +123,7 @@ public static class FFProbe { var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - ThrowIfExitCodeNotZero(result); + ThrowIfExitCodeNotZero(result, cancellationToken); return ParseOutput(result); } @@ -150,7 +150,7 @@ public static class FFProbe } var result = await task.ConfigureAwait(false); - ThrowIfExitCodeNotZero(result); + ThrowIfExitCodeNotZero(result, cancellationToken); pipeArgument.Post(); return ParseOutput(result); @@ -212,8 +212,11 @@ public static class FFProbe } } - private static void ThrowIfExitCodeNotZero(IProcessResult result) + private static void ThrowIfExitCodeNotZero(IProcessResult result, CancellationToken cancellationToken = default) { + // if cancellation requested, then we are not interested in the exit code, just throw the cancellation exception + // to get consistent and expected behavior. + cancellationToken.ThrowIfCancellationRequested(); if (result.ExitCode != 0) { var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})"; diff --git a/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs b/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs index ef7140a..6682e94 100644 --- a/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs +++ b/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs @@ -13,6 +13,6 @@ public static class ProcessArgumentsExtensions public static async Task StartAndWaitForExitAsync(this ProcessArguments processArguments, CancellationToken cancellationToken = default) { using var instance = processArguments.Start(); - return await instance.WaitForExitAsync(cancellationToken); + return await instance.WaitForExitAsync(cancellationToken).ConfigureAwait(false); } } From e44611bd25360df725d9331ca6bbffef3e71ad72 Mon Sep 17 00:00:00 2001 From: Sergey Nechaev <6499856+snechaev@users.noreply.github.com> Date: Wed, 22 Oct 2025 21:23:30 +0200 Subject: [PATCH 28/34] Additional test to verify that FFProbeHelper still throws FFMpegException when FFProbe exits with non-zero code and no cancellation was requested. Ref.: #594 --- FFMpegCore.Test/FFProbeTests.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 16f3b4c..ee837bf 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -1,4 +1,5 @@ -using FFMpegCore.Test.Resources; +using FFMpegCore.Exceptions; +using FFMpegCore.Test.Resources; namespace FFMpegCore.Test; @@ -342,4 +343,13 @@ public class FFProbeTests // Check that all exceptions are OperationCanceledException CollectionAssert.AllItemsAreInstancesOfType(exceptions, typeof(OperationCanceledException)); } + + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task FFProbe_Should_Throw_FFMpegException_When_Exits_With_Non_Zero_Code() + { + var input = TestResources.SrtSubtitle; //non media file + await Assert.ThrowsAsync(async () => await FFProbe.AnalyseAsync(input, + cancellationToken: TestContext.CancellationToken, customArguments: "--some-invalid-argument")); + } } From 560c791802d3c7b799aced9ab9dca2f7b76e6968 Mon Sep 17 00:00:00 2001 From: Sergey Nechaev <6499856+snechaev@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:06:55 +0200 Subject: [PATCH 29/34] Update the `ThrowIfExitCodeNotZero()` to check the exit code before handling cancellation. This preserves the original semantics and contract (throw only if the ffprobe exits with a non-zero code). --- FFMpegCore/FFProbe/FFProbe.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index ba08346..5f275d2 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -214,11 +214,11 @@ public static class FFProbe private static void ThrowIfExitCodeNotZero(IProcessResult result, CancellationToken cancellationToken = default) { - // if cancellation requested, then we are not interested in the exit code, just throw the cancellation exception - // to get consistent and expected behavior. - cancellationToken.ThrowIfCancellationRequested(); if (result.ExitCode != 0) { + // if cancellation requested, then we are not interested in the exit code, just throw the cancellation exception + // to get consistent and expected behavior. + cancellationToken.ThrowIfCancellationRequested(); var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})"; throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData)); } From 67af2aa01dff975fd5c328a8a2025b6a109fd687 Mon Sep 17 00:00:00 2001 From: Sergey Nechaev <6499856+snechaev@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:35:36 +0100 Subject: [PATCH 30/34] Move cancellation check outside of the `ThrowIfExitCodeNotZero()` and call it separately in all the async code paths. --- FFMpegCore/FFProbe/FFProbe.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 5f275d2..164ea72 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -84,7 +84,8 @@ public static class FFProbe var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - ThrowIfExitCodeNotZero(result, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfExitCodeNotZero(result); return ParseOutput(result); } @@ -123,7 +124,8 @@ public static class FFProbe { var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - ThrowIfExitCodeNotZero(result, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfExitCodeNotZero(result); return ParseOutput(result); } @@ -150,7 +152,8 @@ public static class FFProbe } var result = await task.ConfigureAwait(false); - ThrowIfExitCodeNotZero(result, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfExitCodeNotZero(result); pipeArgument.Post(); return ParseOutput(result); @@ -212,13 +215,10 @@ public static class FFProbe } } - private static void ThrowIfExitCodeNotZero(IProcessResult result, CancellationToken cancellationToken = default) + private static void ThrowIfExitCodeNotZero(IProcessResult result) { if (result.ExitCode != 0) { - // if cancellation requested, then we are not interested in the exit code, just throw the cancellation exception - // to get consistent and expected behavior. - cancellationToken.ThrowIfCancellationRequested(); var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})"; throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData)); } From 919c6ef526e39c6fcaf28c4073c8cfcbeeed4eff Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 27 Oct 2025 19:38:58 +0100 Subject: [PATCH 31/34] Use CreateLinkedTokenSource to bind to TestContext cancellationtoken --- FFMpegCore.Test/VideoTest.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index accba60..e338324 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -1050,7 +1050,7 @@ public class VideoTest { using var outputFile = new TemporaryFile("out.mp4"); - var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken); var task = FFMpegArguments .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args @@ -1061,7 +1061,6 @@ public class VideoTest .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(cts.Token) - .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(false); cts.CancelAfter(300); @@ -1077,7 +1076,7 @@ public class VideoTest { using var outputFile = new TemporaryFile("out.mp4"); - var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken); var task = FFMpegArguments .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args @@ -1088,7 +1087,6 @@ public class VideoTest .WithVideoCodec(VideoCodec.LibX264) .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(cts.Token) - .CancellableThrough(TestContext.CancellationToken) .ProcessAsynchronously(); cts.CancelAfter(300); @@ -1102,7 +1100,7 @@ public class VideoTest { using var outputFile = new TemporaryFile("out.mp4"); - var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken); var task = FFMpegArguments .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args @@ -1126,7 +1124,7 @@ public class VideoTest { using var outputFile = new TemporaryFile("out.mp4"); - var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken); cts.Cancel(); var task = FFMpegArguments @@ -1149,7 +1147,7 @@ public class VideoTest { using var outputFile = new TemporaryFile("out.mp4"); - var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken); var task = FFMpegArguments .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args From 3c8d2c23c1f923c255ddd378f7cfa2c5b92f0663 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 27 Oct 2025 19:39:13 +0100 Subject: [PATCH 32/34] Use using for CancellationTokenSource --- FFMpegCore.Test/FFProbeTests.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index ee837bf..2ee5a92 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -1,4 +1,5 @@ using FFMpegCore.Exceptions; +using FFMpegCore.Helpers; using FFMpegCore.Test.Resources; namespace FFMpegCore.Test; @@ -292,7 +293,7 @@ public class FFProbeTests public async Task Parallel_FFProbe_Cancellation_Should_Throw_Only_OperationCanceledException() { // Warm up FFMpegCore environment - Helpers.FFProbeHelper.VerifyFFProbeExists(GlobalFFOptions.Current); + FFProbeHelper.VerifyFFProbeExists(GlobalFFOptions.Current); var mp4 = TestResources.Mp4Video; if (!File.Exists(mp4)) @@ -301,28 +302,26 @@ public class FFProbeTests return; } - var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken); - var token = cts.Token; + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken); using var semaphore = new SemaphoreSlim(Environment.ProcessorCount, Environment.ProcessorCount); var tasks = Enumerable.Range(0, 50).Select(x => Task.Run(async () => { - await semaphore.WaitAsync(token); + await semaphore.WaitAsync(cts.Token); try { - var analysis = await FFProbe.AnalyseAsync(mp4, cancellationToken: token); + var analysis = await FFProbe.AnalyseAsync(mp4, cancellationToken: cts.Token); return analysis; } finally { semaphore.Release(); } - }, token)).ToList(); + }, cts.Token)).ToList(); // Wait for 2 tasks to finish, then cancel all await Task.WhenAny(tasks); await Task.WhenAny(tasks); await cts.CancelAsync(); - cts.Dispose(); var exceptions = new List(); foreach (var task in tasks) From d916fd3be4e41ae60f54dfddaf3d0e2ebe4b5281 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 27 Oct 2025 19:53:39 +0100 Subject: [PATCH 33/34] Update nuget details --- FFMpegCore/FFMpegCore.csproj | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 671239d..df567ad 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -3,13 +3,14 @@ true A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 5.3.0 + 5.4.0 ../nupkg - - **Fixed race condition on Named pipe dispose/disconnect** by techtel-pstevens - - **More extensions for snapshot function(jpg, bmp, webp)** by GorobVictor - - **Include more GUID characters in pipe path** by reima, rosenbjerg - - **Updated dependencies and minor cleanup**: by rosenbjerg + - Fixed exception thrown on cancelling ffprobe analysis - by snechaev + - Added FFMetadataBuilder - by rosenbjerg + - Fix JoinImageSequence by passing framerate argument to input as well as output - by rosenbjerg + - Change fps input from int to double - by rosenbjerg + - Fix GetCreationTime method on ITagsContainer - by rosenbjerg ffmpeg ffprobe convert video audio mediafile resize analyze muxing Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev From 3b1a1438bbae73b227e8576d7af3139626b4a8c3 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 27 Oct 2025 20:13:05 +0100 Subject: [PATCH 34/34] Update nuget details --- FFMpegCore/FFMpegCore.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index df567ad..48bce0f 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -7,6 +7,7 @@ ../nupkg - Fixed exception thrown on cancelling ffprobe analysis - by snechaev + - Support for cancellationtoken in SnapsnotAsync methods - by snechaev - Added FFMetadataBuilder - by rosenbjerg - Fix JoinImageSequence by passing framerate argument to input as well as output - by rosenbjerg - Change fps input from int to double - by rosenbjerg