From c9d7b663ae7307861ed5fdeb807d0b2df65306f7 Mon Sep 17 00:00:00 2001 From: Emem Adegbola <43295492+ememadegbola@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:52:09 +0000 Subject: [PATCH 1/4] Adds overloads to FFProbe.GetFrames methods that take streams --- FFMpegCore.Test/FFProbeTests.cs | 26 ++++++++++++++++++++++++++ FFMpegCore/FFProbe/FFProbe.cs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 2ee5a92..ba1c869 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -30,6 +30,19 @@ public class FFProbeTests Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); } + [TestMethod] + public void FrameAnalysis_FromStream_Sync() + { + using var stream = File.OpenRead(TestResources.WebmVideo); + var frameAnalysis = FFProbe.GetFrames(stream); + + Assert.HasCount(90, frameAnalysis.Frames); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p")); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); + } + [TestMethod] public async Task FrameAnalysis_Async() { @@ -42,6 +55,19 @@ public class FFProbeTests Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); } + [TestMethod] + public async Task FrameAnalysis_FromStream_Async() + { + using var stream = File.OpenRead(TestResources.WebmVideo); + var frameAnalysis = await FFProbe.GetFramesAsync(stream, cancellationToken: TestContext.CancellationToken); + + Assert.HasCount(90, frameAnalysis.Frames); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p")); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); + } + [TestMethod] public async Task PacketAnalysis_Async() { diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 164ea72..a263b45 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -22,6 +22,11 @@ public static class FFProbe return ParseOutput(result); } + public static FFProbeFrames GetFrames(Stream stream, FFOptions? ffOptions = null, string? customArguments = null) + { + return GetFramesAsync(stream, ffOptions, CancellationToken.None, customArguments).ConfigureAwait(false).GetAwaiter().GetResult(); + } + public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null, string? customArguments = null) { ThrowIfInputFileDoesNotExist(filePath); @@ -167,6 +172,31 @@ public static class FFProbe return ParseFramesOutput(result); } + public static async Task GetFramesAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, + string? customArguments = null) + { + var streamPipeSource = new StreamPipeSource(stream); + var pipeArgument = new InputPipeArgument(streamPipeSource); + var instance = PrepareFrameAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + pipeArgument.Pre(); + + var task = instance.Start().WaitForExitAsync(cancellationToken); + try + { + await pipeArgument.During(cancellationToken).ConfigureAwait(false); + } + catch (IOException) { } + finally + { + pipeArgument.Post(); + } + + var result = task.ConfigureAwait(false).GetAwaiter().GetResult(); + ThrowIfExitCodeNotZero(result); + + return ParseFramesOutput(result); + } + private static IMediaAnalysis ParseOutput(IProcessResult instance) { var json = string.Join(string.Empty, instance.OutputData); From ee1c1c3ff884cd7903ce5760bb56db2664c9b5ca Mon Sep 17 00:00:00 2001 From: Emem Adegbola <43295492+ememadegbola@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:14:40 +0000 Subject: [PATCH 2/4] Reorder FFProbe methods and use overloads where possible - Reordered public methods so Analyse, GetFrames and GetPackets (and their overloads) are grouped together for readability - Updated synchronous overloads to call the corresponding async implementations and wait with ConfigureAwait(false).GetAwaiter().GetResult() to avoid duplicated logic --- FFMpegCore/FFProbe/FFProbe.cs | 131 +++++++++++----------------------- 1 file changed, 43 insertions(+), 88 deletions(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index a263b45..24300b8 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -13,73 +13,17 @@ public static class FFProbe { public static IMediaAnalysis Analyse(string filePath, FFOptions? ffOptions = null, string? customArguments = null) { - ThrowIfInputFileDoesNotExist(filePath); - - var processArguments = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = processArguments.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); - } - - public static FFProbeFrames GetFrames(Stream stream, FFOptions? ffOptions = null, string? customArguments = null) - { - return GetFramesAsync(stream, ffOptions, CancellationToken.None, customArguments).ConfigureAwait(false).GetAwaiter().GetResult(); - } - - public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null, string? customArguments = null) - { - ThrowIfInputFileDoesNotExist(filePath); - - var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = instance.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParseFramesOutput(result); - } - - public static FFProbePackets GetPackets(string filePath, FFOptions? ffOptions = null, string? customArguments = null) - { - ThrowIfInputFileDoesNotExist(filePath); - - var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = instance.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParsePacketsOutput(result); + return AnalyseAsync(filePath, ffOptions, CancellationToken.None, customArguments).ConfigureAwait(false).GetAwaiter().GetResult(); } public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null, string? customArguments = null) { - var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = instance.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); + return AnalyseAsync(uri, ffOptions, CancellationToken.None, customArguments).ConfigureAwait(false).GetAwaiter().GetResult(); } public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null, string? customArguments = null) { - var streamPipeSource = new StreamPipeSource(stream); - var pipeArgument = new InputPipeArgument(streamPipeSource); - var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - pipeArgument.Pre(); - - var task = instance.StartAndWaitForExitAsync(); - try - { - pipeArgument.During().ConfigureAwait(false).GetAwaiter().GetResult(); - } - catch (IOException) { } - finally - { - pipeArgument.Post(); - } - - var result = task.ConfigureAwait(false).GetAwaiter().GetResult(); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); + return AnalyseAsync(stream, ffOptions, CancellationToken.None, customArguments).ConfigureAwait(false).GetAwaiter().GetResult(); } public static async Task AnalyseAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, @@ -95,35 +39,6 @@ public static class FFProbe return ParseOutput(result); } - public static FFProbeFrames GetFrames(Uri uri, FFOptions? ffOptions = null, string? customArguments = null) - { - var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = instance.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParseFramesOutput(result); - } - - public static async Task GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, - string? customArguments = null) - { - ThrowIfInputFileDoesNotExist(filePath); - - var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - return ParseFramesOutput(result); - } - - public static async Task GetPacketsAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, - string? customArguments = null) - { - ThrowIfInputFileDoesNotExist(filePath); - - var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - return ParsePacketsOutput(result); - } - public static async Task AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) { @@ -164,6 +79,31 @@ public static class FFProbe return ParseOutput(result); } + public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null, string? customArguments = null) + { + return GetFramesAsync(filePath, ffOptions, CancellationToken.None, customArguments).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public static FFProbeFrames GetFrames(Uri uri, FFOptions? ffOptions = null, string? customArguments = null) + { + return GetFramesAsync(uri, ffOptions, CancellationToken.None, customArguments).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public static FFProbeFrames GetFrames(Stream stream, FFOptions? ffOptions = null, string? customArguments = null) + { + return GetFramesAsync(stream, ffOptions, CancellationToken.None, customArguments).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public static async Task GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, + string? customArguments = null) + { + ThrowIfInputFileDoesNotExist(filePath); + + var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + return ParseFramesOutput(result); + } + public static async Task GetFramesAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) { @@ -197,6 +137,21 @@ public static class FFProbe return ParseFramesOutput(result); } + public static FFProbePackets GetPackets(string filePath, FFOptions? ffOptions = null, string? customArguments = null) + { + return GetPacketsAsync(filePath, ffOptions, CancellationToken.None, customArguments).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public static async Task GetPacketsAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, + string? customArguments = null) + { + ThrowIfInputFileDoesNotExist(filePath); + + var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + return ParsePacketsOutput(result); + } + private static IMediaAnalysis ParseOutput(IProcessResult instance) { var json = string.Join(string.Empty, instance.OutputData); From c66ad474e01d3c388b1027ecd1f0ea2a79e96ebe Mon Sep 17 00:00:00 2001 From: Emem Adegbola <43295492+ememadegbola@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:26:04 +0000 Subject: [PATCH 3/4] Refactor Analyse and GetFrames overloads to share backing logic --- FFMpegCore/FFProbe/FFProbe.cs | 62 ++++++++++++++++------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 24300b8..2a017e9 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -30,24 +30,13 @@ public static class FFProbe string? customArguments = null) { ThrowIfInputFileDoesNotExist(filePath); - - var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); + return await AnalyseCoreAsync(filePath, ffOptions, cancellationToken, customArguments).ConfigureAwait(false); } public static async Task AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) { - var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); + return await AnalyseCoreAsync(uri.AbsoluteUri, ffOptions, cancellationToken, customArguments).ConfigureAwait(false); } public static async Task AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, @@ -55,10 +44,10 @@ public static class FFProbe { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); - var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - pipeArgument.Pre(); - var task = instance.StartAndWaitForExitAsync(cancellationToken); + var task = AnalyseCoreAsync(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, cancellationToken, customArguments); + + pipeArgument.Pre(); try { await pipeArgument.During(cancellationToken).ConfigureAwait(false); @@ -71,12 +60,7 @@ public static class FFProbe pipeArgument.Post(); } - var result = await task.ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfExitCodeNotZero(result); - - pipeArgument.Post(); - return ParseOutput(result); + return await task.ConfigureAwait(false); } public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null, string? customArguments = null) @@ -98,18 +82,13 @@ public static class FFProbe string? customArguments = null) { ThrowIfInputFileDoesNotExist(filePath); - - var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - return ParseFramesOutput(result); + return await GetFramesCoreAsync(filePath, ffOptions, cancellationToken, customArguments).ConfigureAwait(false); } public static async Task GetFramesAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) { - var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - return ParseFramesOutput(result); + return await GetFramesCoreAsync(uri.AbsoluteUri, ffOptions, cancellationToken, customArguments).ConfigureAwait(false); } public static async Task GetFramesAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, @@ -117,10 +96,10 @@ public static class FFProbe { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); - var instance = PrepareFrameAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - pipeArgument.Pre(); - var task = instance.Start().WaitForExitAsync(cancellationToken); + var task = GetFramesCoreAsync(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, cancellationToken, customArguments); + + pipeArgument.Pre(); try { await pipeArgument.During(cancellationToken).ConfigureAwait(false); @@ -131,7 +110,24 @@ public static class FFProbe pipeArgument.Post(); } - var result = task.ConfigureAwait(false).GetAwaiter().GetResult(); + return await task.ConfigureAwait(false); + } + + private static async Task AnalyseCoreAsync(string inputPath, FFOptions? ffOptions, CancellationToken cancellationToken, string? customArguments) + { + var instance = PrepareStreamAnalysisInstance(inputPath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfExitCodeNotZero(result); + + return ParseOutput(result); + } + + private static async Task GetFramesCoreAsync(string inputPath, FFOptions? ffOptions, CancellationToken cancellationToken, string? customArguments) + { + var instance = PrepareFrameAnalysisInstance(inputPath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); ThrowIfExitCodeNotZero(result); return ParseFramesOutput(result); From 68d359b57ed5385ba8f9753926738d110d7e80c7 Mon Sep 17 00:00:00 2001 From: Emem Adegbola <43295492+ememadegbola@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:22:07 +0000 Subject: [PATCH 4/4] Update FFProbe URI overload to handle file based URIs --- FFMpegCore.Test/FFProbeTests.cs | 12 ++++++++++++ FFMpegCore/FFProbe/FFProbe.cs | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index ba1c869..b808072 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -43,6 +43,18 @@ public class FFProbeTests Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); } + [TestMethod] + public void FrameAnalysis_FromUri_Sync() + { + var frameAnalysis = FFProbe.GetFrames(new Uri(Path.GetFullPath(TestResources.WebmVideo))); + + Assert.HasCount(90, frameAnalysis.Frames); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p")); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); + } + [TestMethod] public async Task FrameAnalysis_Async() { diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 2a017e9..4ca37a1 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -36,7 +36,7 @@ public static class FFProbe public static async Task AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) { - return await AnalyseCoreAsync(uri.AbsoluteUri, ffOptions, cancellationToken, customArguments).ConfigureAwait(false); + return await AnalyseCoreAsync(uri.IsFile ? uri.LocalPath : uri.AbsoluteUri, ffOptions, cancellationToken, customArguments).ConfigureAwait(false); } public static async Task AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, @@ -88,7 +88,7 @@ public static class FFProbe public static async Task GetFramesAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) { - return await GetFramesCoreAsync(uri.AbsoluteUri, ffOptions, cancellationToken, customArguments).ConfigureAwait(false); + return await GetFramesCoreAsync(uri.IsFile ? uri.LocalPath : uri.AbsoluteUri, ffOptions, cancellationToken, customArguments).ConfigureAwait(false); } public static async Task GetFramesAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default,