From aa1051b2686136e6ff9dede89bc96f695eb0ecdd Mon Sep 17 00:00:00 2001 From: Victor Horobchuk Date: Thu, 17 Apr 2025 12:29:06 +0300 Subject: [PATCH] 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)); }