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 7c3e9ca..89f905d 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -18,16 +18,28 @@ 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)); 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 = 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)); + + 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); + 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)); @@ -38,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(); @@ -62,16 +74,26 @@ 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); var exitCode = await instance.FinishedRunning().ConfigureAwait(false); 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 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 = 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); var exitCode = await instance.FinishedRunning().ConfigureAwait(false); 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)); @@ -82,7 +104,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(); @@ -118,12 +140,28 @@ 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 + }) ; - private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions) + return ffprobeAnalysis; + } + + + 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, diff --git a/FFMpegCore/FFProbe/FrameAnalysis.cs b/FFMpegCore/FFProbe/FrameAnalysis.cs new file mode 100644 index 0000000..a22cd24 --- /dev/null +++ b/FFMpegCore/FFProbe/FrameAnalysis.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FFMpegCore +{ + public class FFProbeFrameAnalysis + { + [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 + { + [JsonPropertyName("frames")] + public List Frames { get; set; } + } +}