From aa1051b2686136e6ff9dede89bc96f695eb0ecdd Mon Sep 17 00:00:00 2001 From: Victor Horobchuk Date: Thu, 17 Apr 2025 12:29:06 +0300 Subject: [PATCH 1/3] FEAT: added more extensions for snapshot(jpg, bmp, webp) --- FFMpegCore.Test/VideoTest.cs | 59 +++++++++++++++++++- FFMpegCore/FFMpeg/Enums/Enums.cs | 26 ++++++++- FFMpegCore/FFMpeg/Enums/FileExtension.cs | 15 ++++- FFMpegCore/FFMpeg/FFMpeg.cs | 38 ++++++++----- FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs | 26 ++++++++- 5 files changed, 144 insertions(+), 20 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 8da9c19..ce23660 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -467,7 +467,7 @@ namespace FFMpegCore.Test } [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Snapshot_PersistSnapshot() + public void Video_Snapshot_Png_PersistSnapshot() { using var outputPath = new TemporaryFile("out.png"); var input = FFProbe.Analyse(TestResources.Mp4Video); @@ -480,6 +480,63 @@ namespace FFMpegCore.Test Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); } + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_Jpg_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.jpg"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("mjpeg", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_Bmp_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.bmp"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("bmp", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_Webp_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.webp"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("webp", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_Exception_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.asd"); + + try + { + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + } + catch (Exception ex) + { + Assert.IsTrue(ex is ArgumentException); + } + } + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_Snapshot_Rotated_PersistSnapshot() { diff --git a/FFMpegCore/FFMpeg/Enums/Enums.cs b/FFMpegCore/FFMpeg/Enums/Enums.cs index 1f00203..d777ecb 100644 --- a/FFMpegCore/FFMpeg/Enums/Enums.cs +++ b/FFMpegCore/FFMpeg/Enums/Enums.cs @@ -15,9 +15,33 @@ public static Codec LibX265 => FFMpeg.GetCodec("libx265"); public static Codec LibVpx => FFMpeg.GetCodec("libvpx"); public static Codec LibTheora => FFMpeg.GetCodec("libtheora"); - public static Codec Png => FFMpeg.GetCodec("png"); public static Codec MpegTs => FFMpeg.GetCodec("mpegts"); public static Codec LibaomAv1 => FFMpeg.GetCodec("libaom-av1"); + + public static class Image + { + public static Codec Png => FFMpeg.GetCodec("png"); + public static Codec Jpg => FFMpeg.GetCodec("mjpeg"); + public static Codec Bmp => FFMpeg.GetCodec("bmp"); + public static Codec Webp => FFMpeg.GetCodec("webp"); + + public static Codec GetByExtension(string path) + { + var ext = Path.GetExtension(path); + switch (ext) + { + case FileExtension.Image.Png: + return Png; + case FileExtension.Image.Jpg: + return Jpg; + case FileExtension.Image.Bmp: + return Bmp; + case FileExtension.Image.Webp: + return Webp; + default: throw new NotSupportedException($"Unsupported image extension: {ext}"); + } + } + } } public static class AudioCodec diff --git a/FFMpegCore/FFMpeg/Enums/FileExtension.cs b/FFMpegCore/FFMpeg/Enums/FileExtension.cs index f3067ba..386ef33 100644 --- a/FFMpegCore/FFMpeg/Enums/FileExtension.cs +++ b/FFMpegCore/FFMpeg/Enums/FileExtension.cs @@ -10,7 +10,10 @@ "libxvpx" => WebM, "libxtheora" => Ogv, "mpegts" => Ts, - "png" => Png, + "png" => Image.Png, + "jpg" => Image.Jpg, + "bmp" => Image.Bmp, + "webp" => Image.Webp, _ => throw new Exception("The extension for this video type is not defined.") }; } @@ -18,8 +21,16 @@ public static readonly string Ts = VideoType.MpegTs.Extension; public static readonly string Ogv = VideoType.Ogv.Extension; public static readonly string WebM = VideoType.WebM.Extension; - public static readonly string Png = ".png"; public static readonly string Mp3 = ".mp3"; public static readonly string Gif = ".gif"; + + public static class Image + { + public const string Png = ".png"; + public const string Jpg = ".jpg"; + public const string Bmp = ".bmp"; + public const string Webp = ".webp"; + public static readonly List All = [Png, Jpg, Bmp, Webp]; + } } } diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 820d9fb..beb815c 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -20,16 +20,11 @@ namespace FFMpegCore /// Bitmap with the requested snapshot. public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { - if (Path.GetExtension(output) != FileExtension.Png) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Png); - } + CheckSnapshotOutputExtension(ref output); var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - return arguments - .OutputToFile(output, true, outputOptions) + return SnapshotProcess(input, output, source, size, captureTime, streamIndex, inputFileIndex) .ProcessSynchronously(); } /// @@ -44,24 +39,37 @@ namespace FFMpegCore /// 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) { - if (Path.GetExtension(output) != FileExtension.Png) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Png); - } + CheckSnapshotOutputExtension(ref output); var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - return await arguments - .OutputToFile(output, true, outputOptions) + return await SnapshotProcess(input, output, source, size, captureTime, streamIndex, inputFileIndex) .ProcessAsynchronously(); } + private static FFMpegArgumentProcessor SnapshotProcess(string input, string output, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) + { + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, output, source, size, captureTime, streamIndex, inputFileIndex); + + return arguments + .OutputToFile(output, true, outputOptions); + } + + private static void CheckSnapshotOutputExtension(ref string output) + { + if (!FileExtension.Image.All.Contains(Path.GetExtension(output).ToLower())) + { + throw new ArgumentException( + $"Invalid snapshot output extension: {output}, needed: {string.Join(",", FileExtension.Image.All)}"); + } + } + public static bool GifSnapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) { if (Path.GetExtension(output)?.ToLower() != FileExtension.Gif) { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Gif); + throw new ArgumentException( + $"Invalid snapshot output extension: {output}, needed: {FileExtension.Gif}"); } var source = FFProbe.Analyse(input); diff --git a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs index 7d83183..3204bcc 100644 --- a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs +++ b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs @@ -9,6 +9,30 @@ public static class SnapshotArgumentBuilder { public static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( string input, + string output, + IMediaAnalysis source, + Size? size = null, + TimeSpan? captureTime = null, + int? streamIndex = null, + int inputFileIndex = 0) + { + return BuildSnapshotArguments(input, VideoCodec.Image.GetByExtension(output), source, size, captureTime, streamIndex, inputFileIndex); + } + + public static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( + string input, + IMediaAnalysis source, + Size? size = null, + TimeSpan? captureTime = null, + int? streamIndex = null, + int inputFileIndex = 0) + { + return BuildSnapshotArguments(input, VideoCodec.Image.Png, source, size, captureTime, streamIndex, inputFileIndex); + } + + private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( + string input, + Codec codec, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, @@ -26,7 +50,7 @@ public static class SnapshotArgumentBuilder .Seek(captureTime)), options => options .SelectStream((int)streamIndex, inputFileIndex) - .WithVideoCodec(VideoCodec.Png) + .WithVideoCodec(codec) .WithFrameOutputCount(1) .Resize(size)); } From 4025b82fbfe6dc5e47d7680975ffa5b83cc48705 Mon Sep 17 00:00:00 2001 From: Victor Horobchuk Date: Thu, 17 Apr 2025 13:02:27 +0300 Subject: [PATCH 2/3] FIX: small moments --- FFMpegCore/FFMpeg/FFMpeg.cs | 63 +++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index beb815c..cc9f94a 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -20,7 +20,7 @@ namespace FFMpegCore /// Bitmap with the requested snapshot. public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { - CheckSnapshotOutputExtension(ref output); + CheckSnapshotOutputExtension(output, FileExtension.Image.All); var source = FFProbe.Analyse(input); @@ -39,7 +39,7 @@ namespace FFMpegCore /// 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) { - CheckSnapshotOutputExtension(ref output); + CheckSnapshotOutputExtension(output, FileExtension.Image.All); var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); @@ -47,52 +47,47 @@ namespace FFMpegCore .ProcessAsynchronously(); } - private static FFMpegArgumentProcessor SnapshotProcess(string input, string output, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) - { - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, output, source, size, captureTime, streamIndex, inputFileIndex); - - return arguments - .OutputToFile(output, true, outputOptions); - } - - private static void CheckSnapshotOutputExtension(ref string output) - { - if (!FileExtension.Image.All.Contains(Path.GetExtension(output).ToLower())) - { - throw new ArgumentException( - $"Invalid snapshot output extension: {output}, needed: {string.Join(",", FileExtension.Image.All)}"); - } - } - public static bool GifSnapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) { - if (Path.GetExtension(output)?.ToLower() != FileExtension.Gif) - { - throw new ArgumentException( - $"Invalid snapshot output extension: {output}, needed: {FileExtension.Gif}"); - } + CheckSnapshotOutputExtension(output, [FileExtension.Gif]); var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); - return arguments - .OutputToFile(output, true, outputOptions) + return GifSnapshotProcess(input, output, source, size, captureTime, duration, streamIndex) .ProcessSynchronously(); } public static async Task GifSnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) { - if (Path.GetExtension(output)?.ToLower() != FileExtension.Gif) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Gif); - } + CheckSnapshotOutputExtension(output, [FileExtension.Gif]); var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + + return await GifSnapshotProcess(input, output, source, size, captureTime, duration, streamIndex) + .ProcessAsynchronously(); + } + + private static FFMpegArgumentProcessor SnapshotProcess(string input, string output, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) + { + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, output, source, size, captureTime, streamIndex, inputFileIndex); + + return arguments.OutputToFile(output, true, outputOptions); + } + + private static FFMpegArgumentProcessor GifSnapshotProcess(string input, string output, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) + { var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); - return await arguments - .OutputToFile(output, true, outputOptions) - .ProcessAsynchronously(); + return arguments.OutputToFile(output, true, outputOptions); + } + + private static void CheckSnapshotOutputExtension(string output, List extensions) + { + if (!extensions.Contains(Path.GetExtension(output).ToLower())) + { + throw new ArgumentException( + $"Invalid snapshot output extension: {output}, needed: {string.Join(",", FileExtension.Image.All)}"); + } } /// From 3d21599c5d420a812317b92f6cc8fb63382eebc7 Mon Sep 17 00:00:00 2001 From: Victor Horobchuk Date: Thu, 17 Apr 2025 13:17:52 +0300 Subject: [PATCH 3/3] FIX: for dotnet format --- FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs | 2 +- FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs index d44d18d..c7b6b56 100644 --- a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs @@ -8,6 +8,6 @@ /// /// //public string GetText(StringBuilder context); - public string GetText(IEnumerable context); + string GetText(IEnumerable context); } } diff --git a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs index f7a9e4a..4a43c9d 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs @@ -36,8 +36,8 @@ namespace FFMpegCore.Arguments public interface IVideoFilterArgument { - public string Key { get; } - public string Value { get; } + string Key { get; } + string Value { get; } } public class VideoFilterOptions