From 844753b1ed178fafb4b8ffb36259bdf2801f331d Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 20:19:37 +0100 Subject: [PATCH] Init Former-commit-id: 53651a02756adfb17f73f7c196b1a6fd6274ac9f --- .../FFMpegCore.Examples.csproj | 1 + FFMpegCore.Examples/Program.cs | 8 +- .../BitmapExtensions.cs | 2 +- .../BitmapVideoFrameWrapper.cs | 2 +- ...re.Extensions.System.Drawing.Common.csproj | 28 ++++ .../FFMpegImage.cs | 134 ++++++++++++++++++ .../ImageInfo.cs | 0 FFMpegCore.Test/AudioTest.cs | 3 +- FFMpegCore.Test/FFMpegCore.Test.csproj | 1 + FFMpegCore.Test/Utilities/BitmapSources.cs | 4 +- FFMpegCore.Test/VideoTest.cs | 9 +- FFMpegCore.sln | 6 + FFMpegCore/FFMpeg/FFMpeg.cs | 48 ------- FFMpegCore/FFMpegCore.csproj | 1 - FFMpegCore/Helpers/FFMpegHelper.cs | 5 +- 15 files changed, 186 insertions(+), 66 deletions(-) rename {FFMpegCore/Extend => FFMpegCore.Extensions.System.Drawing.Common}/BitmapExtensions.cs (91%) rename {FFMpegCore/Extend => FFMpegCore.Extensions.System.Drawing.Common}/BitmapVideoFrameWrapper.cs (98%) create mode 100644 FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj create mode 100644 FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs rename {FFMpegCore => FFMpegCore.Extensions.System.Drawing.Common}/ImageInfo.cs (100%) diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj index 68e7b5c..347607f 100644 --- a/FFMpegCore.Examples/FFMpegCore.Examples.csproj +++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj @@ -6,6 +6,7 @@ + diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs index a718a21..ea343f2 100644 --- a/FFMpegCore.Examples/Program.cs +++ b/FFMpegCore.Examples/Program.cs @@ -5,7 +5,7 @@ using FFMpegCore; using FFMpegCore.Enums; using FFMpegCore.Pipes; -using FFMpegCore.Extend; +using FFMpegCore.Extensions.System.Drawing.Common; var inputPath = "/path/to/input"; var outputPath = "/path/to/output"; @@ -34,7 +34,7 @@ { // process the snapshot in-memory and use the Bitmap directly - var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); + var bitmap = FFMpegImage.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); // or persists the image on the drive FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); @@ -61,7 +61,7 @@ await FFMpegArguments } { - FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, + FFMpegImage.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, ImageInfo.FromPath(@"..\1.png"), ImageInfo.FromPath(@"..\2.png"), ImageInfo.FromPath(@"..\3.png") @@ -83,7 +83,7 @@ await FFMpegArguments var inputImagePath = "/path/to/input/image"; { - FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath); + FFMpegImage.PosterWithAudio(inputPath, inputAudioPath, outputPath); // or var image = Image.FromFile(inputImagePath); image.AddAudio(inputAudioPath, outputPath); diff --git a/FFMpegCore/Extend/BitmapExtensions.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs similarity index 91% rename from FFMpegCore/Extend/BitmapExtensions.cs rename to FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs index e2f5505..e549580 100644 --- a/FFMpegCore/Extend/BitmapExtensions.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs @@ -2,7 +2,7 @@ using System.Drawing; using System.IO; -namespace FFMpegCore.Extend +namespace FFMpegCore.Extensions.System.Drawing.Common { public static class BitmapExtensions { diff --git a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs similarity index 98% rename from FFMpegCore/Extend/BitmapVideoFrameWrapper.cs rename to FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs index 2222db6..2259fea 100644 --- a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using FFMpegCore.Pipes; -namespace FFMpegCore.Extend +namespace FFMpegCore.Extensions.System.Drawing.Common { public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable { diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj new file mode 100644 index 0000000..beeb939 --- /dev/null +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj @@ -0,0 +1,28 @@ + + + + en + https://github.com/rosenbjerg/FFMpegCore + https://github.com/rosenbjerg/FFMpegCore + + Image extension for FFMpegCore, using System.Common.Drawing + + + 8 + 4.0.0.0 + 4.0.0 + MIT + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev + ffmpeg ffprobe convert video audio mediafile resize analyze muxing + GitHub + true + enable + netstandard2.0 + + + + + + + + diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs new file mode 100644 index 0000000..19fd16e --- /dev/null +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs @@ -0,0 +1,134 @@ +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using FFMpegCore.Enums; +using FFMpegCore.Helpers; +using FFMpegCore.Pipes; + +namespace FFMpegCore.Extensions.System.Drawing.Common +{ + public static class FFMpegImage + { + public static void ConversionSizeExceptionCheck(Image image) + => FFMpegHelper.ConversionSizeExceptionCheck(image.Size.Width, image.Size.Height); + + /// + /// Converts an image sequence to a video. + /// + /// Output video file. + /// FPS + /// Image sequence collection + /// Output video information. + public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images) + { + var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); + var temporaryImageFiles = images.Select((imageInfo, index) => + { + using var image = Image.FromFile(imageInfo.FullName); + FFMpegHelper.ConversionSizeExceptionCheck(image); + var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{imageInfo.Extension}"); + Directory.CreateDirectory(tempFolderName); + File.Copy(imageInfo.FullName, destinationPath); + return destinationPath; + }).ToArray(); + + var firstImage = images.First(); + try + { + return FFMpegArguments + .FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false) + .OutputToFile(output, true, options => options + .Resize(firstImage.Width, firstImage.Height) + .WithFramerate(frameRate)) + .ProcessSynchronously(); + } + finally + { + Cleanup(temporaryImageFiles); + Directory.Delete(tempFolderName); + } + } + /// + /// Adds a poster image to an audio file. + /// + /// Source image file. + /// Source audio file. + /// Output video file. + /// + public static bool PosterWithAudio(string image, string audio, string output) + { + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); + using (var img = Image.FromFile(image)) + FFMpegHelper.ConversionSizeExceptionCheck(img); + + return FFMpegArguments + .FromFileInput(image, false, options => options + .Loop(1)) + .AddFileInput(audio) + .OutputToFile(output, true, options => options + .WithVideoCodec(VideoCodec.LibX264) + .CopyChannel() + .WithConstantRateFactor(21) + .WithAudioBitrate(AudioQuality.Normal) + .UsingShortest()) + .ProcessSynchronously(); + } + /// + /// Saves a 'png' thumbnail to an in-memory bitmap + /// + /// Source video file. + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. + /// Input file index + /// Bitmap with the requested snapshot. + public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) + { + var source = FFProbe.Analyse(input); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); + using var ms = new MemoryStream(); + + arguments + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) + .ProcessSynchronously(); + + ms.Position = 0; + using var bitmap = new Bitmap(ms); + return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat); + } + /// + /// Saves a 'png' thumbnail to an in-memory bitmap + /// + /// Source video file. + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. + /// Input file index + /// Bitmap with the requested snapshot. + public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) + { + var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); + using var ms = new MemoryStream(); + + await arguments + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) + .ProcessAsynchronously(); + + ms.Position = 0; + return new Bitmap(ms); + } + private static void Cleanup(IEnumerable pathList) + { + foreach (var path in pathList) + { + if (File.Exists(path)) + File.Delete(path); + } + } + } +} \ No newline at end of file diff --git a/FFMpegCore/ImageInfo.cs b/FFMpegCore.Extensions.System.Drawing.Common/ImageInfo.cs similarity index 100% rename from FFMpegCore/ImageInfo.cs rename to FFMpegCore.Extensions.System.Drawing.Common/ImageInfo.cs diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index 795fedf..23c4e79 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using FFMpegCore.Extensions.System.Drawing.Common; namespace FFMpegCore.Test { @@ -68,7 +69,7 @@ public void Audio_Add() public void Image_AddAudio() { using var outputFile = new TemporaryFile("out.mp4"); - FFMpeg.PosterWithAudio(TestResources.PngImage, TestResources.Mp3Audio, outputFile); + FFMpegImage.PosterWithAudio(TestResources.PngImage, TestResources.Mp3Audio, outputFile); var analysis = FFProbe.Analyse(TestResources.Mp3Audio); Assert.IsTrue(analysis.Duration.TotalSeconds > 0); Assert.IsTrue(File.Exists(outputFile)); diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 4ac890c..c84f3fe 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -50,6 +50,7 @@ + diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs index 8ea02e8..50dd691 100644 --- a/FFMpegCore.Test/Utilities/BitmapSources.cs +++ b/FFMpegCore.Test/Utilities/BitmapSources.cs @@ -1,9 +1,9 @@ -using FFMpegCore.Extend; -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Numerics; +using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; namespace FFMpegCore.Test diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 8f73575..ad72e57 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -13,6 +13,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using FFMpegCore.Extensions.System.Drawing.Common; namespace FFMpegCore.Test { @@ -402,8 +403,8 @@ public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixe public void Video_Snapshot_InMemory() { var input = FFProbe.Analyse(TestResources.Mp4Video); - using var bitmap = FFMpeg.Snapshot(TestResources.Mp4Video); - + using var bitmap = FFMpegImage.Snapshot(TestResources.Mp4Video); + Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); @@ -460,8 +461,8 @@ public void Video_Join_Image_Sequence() } }); - using var outputFile = new TemporaryFile("out.mp4"); - var success = FFMpeg.JoinImageSequence(outputFile, images: imageSet.ToArray()); + var outputFile = new TemporaryFile("out.mp4"); + var success = FFMpegImage.JoinImageSequence(outputFile, images: imageSet.ToArray()); Assert.IsTrue(success); var result = FFProbe.Analyse(outputFile); Assert.AreEqual(3, result.Duration.Seconds); diff --git a/FFMpegCore.sln b/FFMpegCore.sln index 7a27980..5a9faa8 100644 --- a/FFMpegCore.sln +++ b/FFMpegCore.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Test", "FFMpegCo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Examples", "FFMpegCore.Examples\FFMpegCore.Examples.csproj", "{3125CF91-FFBD-4E4E-8930-247116AFE772}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore.Extensions.System.Drawing.Common", "FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj", "{9C1A4930-9369-4A18-AD98-929A2A510D80}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {3125CF91-FFBD-4E4E-8930-247116AFE772}.Debug|Any CPU.Build.0 = Debug|Any CPU {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.ActiveCfg = Release|Any CPU {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.Build.0 = Release|Any CPU + {9C1A4930-9369-4A18-AD98-929A2A510D80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C1A4930-9369-4A18-AD98-929A2A510D80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 9e9e0ce..165ec31 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -59,54 +59,6 @@ public static async Task SnapshotAsync(string input, string output, Size? .ProcessAsynchronously(); } - /// - /// Saves a 'png' thumbnail to an in-memory bitmap - /// - /// Source video file. - /// Seek position where the thumbnail should be taken. - /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Selected video stream index. - /// Input file index - /// Bitmap with the requested snapshot. - public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) - { - var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - using var ms = new MemoryStream(); - - arguments - .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options - .ForceFormat("rawvideo"))) - .ProcessSynchronously(); - - ms.Position = 0; - using var bitmap = new Bitmap(ms); - return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat); - } - /// - /// Saves a 'png' thumbnail to an in-memory bitmap - /// - /// Source video file. - /// Seek position where the thumbnail should be taken. - /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Selected video stream index. - /// Input file index - /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) - { - var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - using var ms = new MemoryStream(); - - await arguments - .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options - .ForceFormat("rawvideo"))) - .ProcessAsynchronously(); - - ms.Position = 0; - return new Bitmap(ms); - } - private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( string input, IMediaAnalysis source, diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 79d191e..a3251db 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -38,7 +38,6 @@ - diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs index cb3b4cf..ecea946 100644 --- a/FFMpegCore/Helpers/FFMpegHelper.cs +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -10,13 +10,10 @@ public static class FFMpegHelper { private static bool _ffmpegVerified; - public static void ConversionSizeExceptionCheck(Image image) - => ConversionSizeExceptionCheck(image.Size.Width, image.Size.Height); - public static void ConversionSizeExceptionCheck(IMediaAnalysis info) => ConversionSizeExceptionCheck(info.PrimaryVideoStream!.Width, info.PrimaryVideoStream.Height); - private static void ConversionSizeExceptionCheck(int width, int height) + public static void ConversionSizeExceptionCheck(int width, int height) { if (height % 2 != 0 || width % 2 != 0 ) throw new ArgumentException("FFMpeg yuv420p encoding requires the width and height to be a multiple of 2!");