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..cc9f94a 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(output, FileExtension.Image.All); 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,47 +39,55 @@ 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(output, FileExtension.Image.All); 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(); } 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); - } + 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)}"); + } } /// 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)); }