From dbccc116fe05db82863d7107b68f2d2e1072c385 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 24 Sep 2021 10:23:59 +0100 Subject: [PATCH 1/2] Adding support for ffprobe show frames Former-commit-id: 93131a7cd09f95e5958d46179a56c3edd32a026e --- FFMpegCore/FFProbe/FFProbe.cs | 46 +++++++++++++++++++++++++++++ FFMpegCore/FFProbe/FrameAnalysis.cs | 39 ++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 FFMpegCore/FFProbe/FrameAnalysis.cs diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 7d043a6..fe1e1fe 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -25,6 +25,18 @@ public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.M return ParseOutput(instance); } + public static FFProbeFrames GetFrames(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + { + if (!File.Exists(filePath)) + throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + + using var instance = PrepareProbeInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + var exitCode = instance.BlockUntilFinished(); + if (exitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); + + return ParseFramesOutput(instance); + } public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); @@ -66,6 +78,16 @@ public static async Task AnalyseAsync(string filePath, int outpu await instance.FinishedRunning().ConfigureAwait(false); return ParseOutput(instance); } + + public static async Task GetFramesAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + { + if (!File.Exists(filePath)) + throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + + using var instance = PrepareProbeInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + await instance.FinishedRunning().ConfigureAwait(false); + return ParseFramesOutput(instance); + } public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); @@ -112,6 +134,17 @@ private static IMediaAnalysis ParseOutput(Instance instance) return new MediaAnalysis(ffprobeAnalysis); } + private static FFProbeFrames ParseFramesOutput(Instance instance) + { + var json = string.Join(string.Empty, instance.OutputData); + var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString + }) ; + + return ffprobeAnalysis; + } private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions) { @@ -126,5 +159,18 @@ private static Instance PrepareInstance(string filePath, int outputCapacity, FFO var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; return instance; } + private static Instance PrepareProbeInstance(string filePath, int outputCapacity, FFOptions ffOptions) + { + FFProbeHelper.RootExceptionCheck(); + FFProbeHelper.VerifyFFProbeExists(ffOptions); + var arguments = $"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\""; + var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) + { + StandardOutputEncoding = ffOptions.Encoding, + StandardErrorEncoding = ffOptions.Encoding + }; + var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; + return instance; + } } } diff --git a/FFMpegCore/FFProbe/FrameAnalysis.cs b/FFMpegCore/FFProbe/FrameAnalysis.cs new file mode 100644 index 0000000..6ac2b19 --- /dev/null +++ b/FFMpegCore/FFProbe/FrameAnalysis.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace FFMpegCore +{ + public class Frame + { + public string media_type { get; set; } + public int stream_index { get; set; } + public int key_frame { get; set; } + public long pkt_pts { get; set; } + public string pkt_pts_time { get; set; } + public long pkt_dts { get; set; } + public string pkt_dts_time { get; set; } + public long best_effort_timestamp { get; set; } + public string best_effort_timestamp_time { get; set; } + public int pkt_duration { get; set; } + public string pkt_duration_time { get; set; } + public long pkt_pos { get; set; } + public int pkt_size { get; set; } + public long width { get; set; } + public long height { get; set; } + public string pix_fmt { get; set; } + public string pict_type { get; set; } + public long coded_picture_number { get; set; } + public long display_picture_number { get; set; } + public int interlaced_frame { get; set; } + public int top_field_first { get; set; } + public int repeat_pict { get; set; } + public string chroma_location { get; set; } + } + + public class FFProbeFrames + { + public List frames { get; set; } + } +} From 75fa1db64f0026b10213385133cb7f7e175f7bc1 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 21 Oct 2021 20:36:54 +0200 Subject: [PATCH 2/2] Minor improvements Former-commit-id: 79bc9189175c7998b738bf26ca8d0dee7fe4a08d --- FFMpegCore.Test/FFProbeTests.cs | 25 +++++++ FFMpegCore/FFProbe/FFProbe.cs | 38 +++++------ FFMpegCore/FFProbe/FrameAnalysis.cs | 100 ++++++++++++++++++++-------- 3 files changed, 112 insertions(+), 51 deletions(-) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index aaadd4c..f990c7f 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Threading.Tasks; using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -25,6 +26,30 @@ public async Task Audio_FromStream_Duration() Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); } + [TestMethod] + public void FrameAnalysis_Sync() + { + var frameAnalysis = FFProbe.GetFrames(TestResources.WebmVideo); + + Assert.AreEqual(90, frameAnalysis.Frames.Count); + 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() + { + var frameAnalysis = await FFProbe.GetFramesAsync(TestResources.WebmVideo); + + Assert.AreEqual(90, frameAnalysis.Frames.Count); + 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")); + } + [DataTestMethod] [DataRow("0:00:03.008000", 0, 0, 0, 3, 8)] [DataRow("05:12:59.177", 0, 5, 12, 59, 177)] diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index fe1e1fe..09dde4b 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -18,7 +18,7 @@ public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.M if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var exitCode = instance.BlockUntilFinished(); if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); @@ -30,7 +30,7 @@ public static FFProbeFrames GetFrames(string filePath, int outputCapacity = int. if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareProbeInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareFrameAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var exitCode = instance.BlockUntilFinished(); if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); @@ -39,7 +39,7 @@ public static FFProbeFrames GetFrames(string filePath, int outputCapacity = int. } public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { - using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var exitCode = instance.BlockUntilFinished(); if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); @@ -50,7 +50,7 @@ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.Max { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); - using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); var task = instance.FinishedRunning(); @@ -74,7 +74,7 @@ public static async Task AnalyseAsync(string filePath, int outpu if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); await instance.FinishedRunning().ConfigureAwait(false); return ParseOutput(instance); } @@ -84,13 +84,13 @@ public static async Task GetFramesAsync(string filePath, int outp if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareProbeInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareFrameAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); await instance.FinishedRunning().ConfigureAwait(false); return ParseFramesOutput(instance); } public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { - using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); await instance.FinishedRunning().ConfigureAwait(false); return ParseOutput(instance); } @@ -98,7 +98,7 @@ public static async Task AnalyseAsync(Stream stream, int outputC { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); - using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); var task = instance.FinishedRunning(); @@ -146,24 +146,16 @@ private static FFProbeFrames ParseFramesOutput(Instance instance) return ffprobeAnalysis; } - private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions) + + private static Instance PrepareStreamAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) + => PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"", outputCapacity, ffOptions); + private static Instance PrepareFrameAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) + => PrepareInstance($"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\"", outputCapacity, ffOptions); + + private static Instance PrepareInstance(string arguments, int outputCapacity, FFOptions ffOptions) { FFProbeHelper.RootExceptionCheck(); FFProbeHelper.VerifyFFProbeExists(ffOptions); - var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\""; - var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) - { - StandardOutputEncoding = ffOptions.Encoding, - StandardErrorEncoding = ffOptions.Encoding - }; - var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; - return instance; - } - private static Instance PrepareProbeInstance(string filePath, int outputCapacity, FFOptions ffOptions) - { - FFProbeHelper.RootExceptionCheck(); - FFProbeHelper.VerifyFFProbeExists(ffOptions); - var arguments = $"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\""; var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) { StandardOutputEncoding = ffOptions.Encoding, diff --git a/FFMpegCore/FFProbe/FrameAnalysis.cs b/FFMpegCore/FFProbe/FrameAnalysis.cs index 6ac2b19..a22cd24 100644 --- a/FFMpegCore/FFProbe/FrameAnalysis.cs +++ b/FFMpegCore/FFProbe/FrameAnalysis.cs @@ -1,39 +1,83 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; using System.Text.Json.Serialization; namespace FFMpegCore { - public class Frame + public class FFProbeFrameAnalysis { - public string media_type { get; set; } - public int stream_index { get; set; } - public int key_frame { get; set; } - public long pkt_pts { get; set; } - public string pkt_pts_time { get; set; } - public long pkt_dts { get; set; } - public string pkt_dts_time { get; set; } - public long best_effort_timestamp { get; set; } - public string best_effort_timestamp_time { get; set; } - public int pkt_duration { get; set; } - public string pkt_duration_time { get; set; } - public long pkt_pos { get; set; } - public int pkt_size { get; set; } - public long width { get; set; } - public long height { get; set; } - public string pix_fmt { get; set; } - public string pict_type { get; set; } - public long coded_picture_number { get; set; } - public long display_picture_number { get; set; } - public int interlaced_frame { get; set; } - public int top_field_first { get; set; } - public int repeat_pict { get; set; } - public string chroma_location { get; set; } + [JsonPropertyName("media_type")] + public string MediaType { get; set; } + + [JsonPropertyName("stream_index")] + public int StreamIndex { get; set; } + + [JsonPropertyName("key_frame")] + public int KeyFrame { get; set; } + + [JsonPropertyName("pkt_pts")] + public long PacketPts { get; set; } + + [JsonPropertyName("pkt_pts_time")] + public string PacketPtsTime { get; set; } + + [JsonPropertyName("pkt_dts")] + public long PacketDts { get; set; } + + [JsonPropertyName("pkt_dts_time")] + public string PacketDtsTime { get; set; } + + [JsonPropertyName("best_effort_timestamp")] + public long BestEffortTimestamp { get; set; } + + [JsonPropertyName("best_effort_timestamp_time")] + public string BestEffortTimestampTime { get; set; } + + [JsonPropertyName("pkt_duration")] + public int PacketDuration { get; set; } + + [JsonPropertyName("pkt_duration_time")] + public string PacketDurationTime { get; set; } + + [JsonPropertyName("pkt_pos")] + public long PacketPos { get; set; } + + [JsonPropertyName("pkt_size")] + public int PacketSize { get; set; } + + [JsonPropertyName("width")] + public long Width { get; set; } + + [JsonPropertyName("height")] + public long Height { get; set; } + + [JsonPropertyName("pix_fmt")] + public string PixelFormat { get; set; } + + [JsonPropertyName("pict_type")] + public string PictureType { get; set; } + + [JsonPropertyName("coded_picture_number")] + public long CodedPictureNumber { get; set; } + + [JsonPropertyName("display_picture_number")] + public long DisplayPictureNumber { get; set; } + + [JsonPropertyName("interlaced_frame")] + public int InterlacedFrame { get; set; } + + [JsonPropertyName("top_field_first")] + public int TopFieldFirst { get; set; } + + [JsonPropertyName("repeat_pict")] + public int RepeatPicture { get; set; } + + [JsonPropertyName("chroma_location")] + public string ChromaLocation { get; set; } } public class FFProbeFrames { - public List frames { get; set; } + [JsonPropertyName("frames")] + public List Frames { get; set; } } }