From c96fdc490a66e087ead22383e762a081365e5c8e Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 11:07:06 +0100 Subject: [PATCH 01/53] Updated package description for SkiaSharp instead of Aspose.Drawing --- FFMpegCore/FFMpegCore.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 7c3f7bb..b08f00b 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -2,12 +2,12 @@ true - A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications + A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications. Uses SkiaSharp instead of System.Drawing.Common. 5.0.0 ffmpeg ffprobe convert video audio mediafile resize analyze muxing - Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken README.md From f464be430bd05ec70ca884e0b53dc5cbb97e96a1 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 11:25:45 +0100 Subject: [PATCH 02/53] Replaced System.Drawing.Common with SkiaSharp --- FFMpegCore.Examples/Program.cs | 3 +- .../BitmapExtensions.cs | 10 ++- .../BitmapVideoFrameWrapper.cs | 65 +++++-------------- ...re.Extensions.System.Drawing.Common.csproj | 2 +- .../FFMpegImage.cs | 11 ++-- 5 files changed, 34 insertions(+), 57 deletions(-) diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs index ac4bce5..f926d94 100644 --- a/FFMpegCore.Examples/Program.cs +++ b/FFMpegCore.Examples/Program.cs @@ -3,6 +3,7 @@ using FFMpegCore.Enums; using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; +using SkiaSharp; var inputPath = "/path/to/input"; var outputPath = "/path/to/output"; @@ -79,7 +80,7 @@ await FFMpegArguments FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath); // or #pragma warning disable CA1416 - using var image = Image.FromFile(inputImagePath); + using var image = SKBitmap.Decode(inputImagePath); image.AddAudio(inputAudioPath, outputPath); #pragma warning restore CA1416 } diff --git a/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs index 14cecaa..b8a0c83 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs @@ -1,13 +1,17 @@ -using System.Drawing; +using SkiaSharp; namespace FFMpegCore.Extensions.System.Drawing.Common { public static class BitmapExtensions { - public static bool AddAudio(this Image poster, string audio, string output) + public static bool AddAudio(this SKBitmap poster, string audio, string output) { var destination = $"{Environment.TickCount}.png"; - poster.Save(destination); + using (var fileStream = File.OpenWrite(destination)) + { + poster.Encode(fileStream, SKEncodedImageFormat.Png, default); // PNG does not respect the quality parameter + } + try { return FFMpeg.PosterWithAudio(destination, audio, output); diff --git a/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs index 5462ca2..0439721 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs @@ -1,7 +1,5 @@ -using System.Drawing; -using System.Drawing.Imaging; -using System.Runtime.InteropServices; -using FFMpegCore.Pipes; +using FFMpegCore.Pipes; +using SkiaSharp; namespace FFMpegCore.Extensions.System.Drawing.Common { @@ -13,44 +11,24 @@ public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable public string Format { get; private set; } - public Bitmap Source { get; private set; } + public SKBitmap Source { get; private set; } - public BitmapVideoFrameWrapper(Bitmap bitmap) + public BitmapVideoFrameWrapper(SKBitmap bitmap) { Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap)); - Format = ConvertStreamFormat(bitmap.PixelFormat); + Format = ConvertStreamFormat(bitmap.ColorType); } public void Serialize(Stream stream) { - var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); - - try - { - var buffer = new byte[data.Stride * data.Height]; - Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); - stream.Write(buffer, 0, buffer.Length); - } - finally - { - Source.UnlockBits(data); - } + var data = Source.Bytes; + stream.Write(data, 0, data.Length); } public async Task SerializeAsync(Stream stream, CancellationToken token) { - var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); - - try - { - var buffer = new byte[data.Stride * data.Height]; - Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); - await stream.WriteAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); - } - finally - { - Source.UnlockBits(data); - } + var data = Source.Bytes; + await stream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); } public void Dispose() @@ -58,27 +36,20 @@ public void Dispose() Source.Dispose(); } - private static string ConvertStreamFormat(PixelFormat fmt) + private static string ConvertStreamFormat(SKColorType fmt) { switch (fmt) { - case PixelFormat.Format16bppGrayScale: - return "gray16le"; - case PixelFormat.Format16bppRgb555: - return "bgr555le"; - case PixelFormat.Format16bppRgb565: - return "bgr565le"; - case PixelFormat.Format24bppRgb: - return "bgr24"; - case PixelFormat.Format32bppArgb: + case SKColorType.Gray8: + return "gray8"; + case SKColorType.Bgra8888: return "bgra"; - case PixelFormat.Format32bppPArgb: - //This is not really same as argb32 - return "argb"; - case PixelFormat.Format32bppRgb: + case SKColorType.Rgb888x: + return "rgb"; + case SKColorType.Rgba8888: return "rgba"; - case PixelFormat.Format48bppRgb: - return "rgb48le"; + case SKColorType.Rgb565: + return "rgb565"; default: throw new NotSupportedException($"Not supported pixel format {fmt}"); } 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 index aafb577..b0d6349 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj @@ -11,7 +11,7 @@ - + diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs index f36f83d..8cd0f26 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs @@ -1,5 +1,6 @@ using System.Drawing; using FFMpegCore.Pipes; +using SkiaSharp; namespace FFMpegCore.Extensions.System.Drawing.Common { @@ -14,7 +15,7 @@ public static class FFMpegImage /// 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) + public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { var source = FFProbe.Analyse(input); var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); @@ -26,8 +27,8 @@ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? capture .ProcessSynchronously(); ms.Position = 0; - using var bitmap = new Bitmap(ms); - return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat); + using var bitmap = SKBitmap.Decode(ms); + return bitmap.Copy(); } /// /// Saves a 'png' thumbnail to an in-memory bitmap @@ -38,7 +39,7 @@ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? capture /// 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) + 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) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); @@ -50,7 +51,7 @@ await arguments .ProcessAsynchronously(); ms.Position = 0; - return new Bitmap(ms); + return SKBitmap.Decode(ms); } } } From 7f17d68a52fcc08863ff8f0c709fff72cf6f9c8f Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 11:39:03 +0100 Subject: [PATCH 03/53] Updated tests for SkiaSharp --- FFMpegCore.Test/Utilities/BitmapSources.cs | 11 ++--- FFMpegCore.Test/VideoTest.cs | 49 +++++++++++----------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs index b7ecb45..7e8d1a9 100644 --- a/FFMpegCore.Test/Utilities/BitmapSources.cs +++ b/FFMpegCore.Test/Utilities/BitmapSources.cs @@ -4,13 +4,14 @@ using System.Runtime.Versioning; using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; +using SkiaSharp; namespace FFMpegCore.Test.Utilities { [SupportedOSPlatform("windows")] internal static class BitmapSource { - public static IEnumerable CreateBitmaps(int count, PixelFormat fmt, int w, int h) + public static IEnumerable CreateBitmaps(int count, SKColorType fmt, int w, int h) { for (var i = 0; i < count; i++) { @@ -21,9 +22,9 @@ public static IEnumerable CreateBitmaps(int count, PixelFormat fmt, } } - public static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset) + public static BitmapVideoFrameWrapper CreateVideoFrame(int index, SKColorType fmt, int w, int h, float scaleNoise, float offset) { - var bitmap = new Bitmap(w, h, fmt); + var bitmap = new SKBitmap(w, h, fmt, SKAlphaType.Opaque); offset = offset * index; @@ -36,9 +37,9 @@ public static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fm var nx = x * scaleNoise + offset; var ny = y * scaleNoise + offset; - var value = (int)((Perlin.Noise(nx, ny) + 1.0f) / 2.0f * 255); + var value = (byte)((Perlin.Noise(nx, ny) + 1.0f) / 2.0f * 255); - var color = Color.FromArgb((int)(value * xf), (int)(value * yf), value); + var color = new SKColor((byte)(value * xf), (byte)(value * yf), value); bitmap.SetPixel(x, y, color); } diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index e3e4b6b..23cc1b5 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -1,5 +1,4 @@ -using System.Drawing.Imaging; -using System.Runtime.Versioning; +using System.Runtime.Versioning; using System.Text; using FFMpegCore.Arguments; using FFMpegCore.Enums; @@ -9,6 +8,7 @@ using FFMpegCore.Test.Resources; using FFMpegCore.Test.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; +using SkiaSharp; namespace FFMpegCore.Test { @@ -83,9 +83,9 @@ public void Video_ToH265_MKV_Args() [SupportedOSPlatform("windows")] [WindowsOnlyDataTestMethod, Timeout(10000)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) + [DataRow(SKColorType.Rgb565)] + [DataRow(SKColorType.Bgra8888)] + public void Video_ToMP4_Args_Pipe(SKColorType pixelFormat) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -106,8 +106,8 @@ public void Video_ToMP4_Args_Pipe_DifferentImageSizes() var frames = new List { - BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 256, 256, 1, 0) + BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 256, 256, 1, 0) }; var videoFramesSource = new RawVideoPipeSource(frames); @@ -126,8 +126,8 @@ public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() var frames = new List { - BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 256, 256, 1, 0) + BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 256, 256, 1, 0) }; var videoFramesSource = new RawVideoPipeSource(frames); @@ -146,8 +146,8 @@ public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() var frames = new List { - BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format32bppRgb, 255, 255, 1, 0) + BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, SKColorType.Bgra8888, 255, 255, 1, 0) }; var videoFramesSource = new RawVideoPipeSource(frames); @@ -166,8 +166,8 @@ public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() var frames = new List { - BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format32bppRgb, 255, 255, 1, 0) + BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, SKColorType.Bgra8888, 255, 255, 1, 0) }; var videoFramesSource = new RawVideoPipeSource(frames); @@ -313,9 +313,9 @@ public void Video_ToTS_Args() [SupportedOSPlatform("windows")] [WindowsOnlyDataTestMethod, Timeout(10000)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - public async Task Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) + [DataRow(SKColorType.Rgb565)] + [DataRow(SKColorType.Bgra8888)] + public async Task Video_ToTS_Args_Pipe(SKColorType pixelFormat) { using var output = new TemporaryFile($"out{VideoType.Ts.Extension}"); var input = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); @@ -346,10 +346,9 @@ public async Task Video_ToOGV_Resize() [SupportedOSPlatform("windows")] [WindowsOnlyDataTestMethod, Timeout(10000)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format48bppRgb)] - public void RawVideoPipeSource_Ogv_Scale(System.Drawing.Imaging.PixelFormat pixelFormat) + [DataRow(SKColorType.Rgb565)] + [DataRow(SKColorType.Bgra8888)] + public void RawVideoPipeSource_Ogv_Scale(SKColorType pixelFormat) { using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); @@ -382,10 +381,10 @@ public void Scale_Mp4_Multithreaded() [SupportedOSPlatform("windows")] [WindowsOnlyDataTestMethod, Timeout(10000)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] + [DataRow(SKColorType.Rgb565)] + [DataRow(SKColorType.Bgra8888)] // [DataRow(PixelFormat.Format48bppRgb)] - public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) + public void Video_ToMP4_Resize_Args_Pipe(SKColorType pixelFormat) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); @@ -407,7 +406,7 @@ public void Video_Snapshot_InMemory() var input = FFProbe.Analyse(TestResources.Mp4Video); Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); - Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); + Assert.AreEqual(bitmap.ColorType, SKColorType.Bgra8888); } [TestMethod, Timeout(10000)] @@ -568,7 +567,7 @@ public void Video_TranscodeInMemory() { using var resStream = new MemoryStream(); var reader = new StreamPipeSink(resStream); - var writer = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 128, 128)); + var writer = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, SKColorType.Rgb565, 128, 128)); FFMpegArguments .FromPipeInput(writer) From 72c76c20f0a7e8aafbdf008cc782b784e91f4461 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 11:39:34 +0100 Subject: [PATCH 04/53] refactor: Centralized test timeout duration Changes can be made in a single place if neeeded --- FFMpegCore.Test/VideoTest.cs | 84 ++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 23cc1b5..dd205e0 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -15,7 +15,9 @@ namespace FFMpegCore.Test [TestClass] public class VideoTest { - [TestMethod, Timeout(10000)] + private const int BaseTimeoutMilliseconds = 10_000; + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToOGV() { using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); @@ -27,7 +29,7 @@ public void Video_ToOGV() Assert.IsTrue(success); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToMP4() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -39,7 +41,7 @@ public void Video_ToMP4() Assert.IsTrue(success); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToMP4_YUV444p() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -55,7 +57,7 @@ public void Video_ToMP4_YUV444p() Assert.IsTrue(analysis.VideoStreams.First().PixelFormat == "yuv444p"); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToMP4_Args() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -68,7 +70,7 @@ public void Video_ToMP4_Args() Assert.IsTrue(success); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToH265_MKV_Args() { using var outputFile = new TemporaryFile($"out.mkv"); @@ -82,7 +84,7 @@ public void Video_ToH265_MKV_Args() } [SupportedOSPlatform("windows")] - [WindowsOnlyDataTestMethod, Timeout(10000)] + [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] [DataRow(SKColorType.Rgb565)] [DataRow(SKColorType.Bgra8888)] public void Video_ToMP4_Args_Pipe(SKColorType pixelFormat) @@ -99,7 +101,7 @@ public void Video_ToMP4_Args_Pipe(SKColorType pixelFormat) } [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(10000)] + [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToMP4_Args_Pipe_DifferentImageSizes() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -119,7 +121,7 @@ public void Video_ToMP4_Args_Pipe_DifferentImageSizes() } [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(10000)] + [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -139,7 +141,7 @@ public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() } [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(10000)] + [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -159,7 +161,7 @@ public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() } [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(10000)] + [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -178,7 +180,7 @@ public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() .ProcessAsynchronously()); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToMP4_Args_StreamPipe() { using var input = File.OpenRead(TestResources.WebmVideo); @@ -192,7 +194,7 @@ public void Video_ToMP4_Args_StreamPipe() Assert.IsTrue(success); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure() { await Assert.ThrowsExceptionAsync(async () => @@ -206,7 +208,7 @@ await FFMpegArguments }); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_StreamFile_OutputToMemoryStream() { var output = new MemoryStream(); @@ -223,7 +225,7 @@ public void Video_StreamFile_OutputToMemoryStream() Console.WriteLine(result.Duration); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToMP4_Args_StreamOutputPipe_Failure() { Assert.ThrowsException(() => @@ -237,7 +239,7 @@ public void Video_ToMP4_Args_StreamOutputPipe_Failure() }); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_ToMP4_Args_StreamOutputPipe_Async() { await using var ms = new MemoryStream(); @@ -250,7 +252,7 @@ await FFMpegArguments .ProcessAsynchronously(); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task TestDuplicateRun() { FFMpegArguments @@ -266,7 +268,7 @@ await FFMpegArguments File.Delete("temporary.mp4"); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void TranscodeToMemoryStream_Success() { using var output = new MemoryStream(); @@ -284,7 +286,7 @@ public void TranscodeToMemoryStream_Success() Assert.AreEqual(inputAnalysis.Duration.TotalSeconds, outputAnalysis.Duration.TotalSeconds, 0.3); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToTS() { using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}"); @@ -296,7 +298,7 @@ public void Video_ToTS() Assert.IsTrue(success); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToTS_Args() { using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}"); @@ -312,7 +314,7 @@ public void Video_ToTS_Args() } [SupportedOSPlatform("windows")] - [WindowsOnlyDataTestMethod, Timeout(10000)] + [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] [DataRow(SKColorType.Rgb565)] [DataRow(SKColorType.Bgra8888)] public async Task Video_ToTS_Args_Pipe(SKColorType pixelFormat) @@ -331,7 +333,7 @@ public async Task Video_ToTS_Args_Pipe(SKColorType pixelFormat) Assert.AreEqual(VideoType.Ts.Name, analysis.Format.FormatName); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_ToOGV_Resize() { using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); @@ -345,7 +347,7 @@ public async Task Video_ToOGV_Resize() } [SupportedOSPlatform("windows")] - [WindowsOnlyDataTestMethod, Timeout(10000)] + [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] [DataRow(SKColorType.Rgb565)] [DataRow(SKColorType.Bgra8888)] public void RawVideoPipeSource_Ogv_Scale(SKColorType pixelFormat) @@ -365,7 +367,7 @@ public void RawVideoPipeSource_Ogv_Scale(SKColorType pixelFormat) Assert.AreEqual((int)VideoSize.Ed, analysis.PrimaryVideoStream!.Width); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Scale_Mp4_Multithreaded() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -380,7 +382,7 @@ public void Scale_Mp4_Multithreaded() } [SupportedOSPlatform("windows")] - [WindowsOnlyDataTestMethod, Timeout(10000)] + [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] [DataRow(SKColorType.Rgb565)] [DataRow(SKColorType.Bgra8888)] // [DataRow(PixelFormat.Format48bppRgb)] @@ -398,7 +400,7 @@ public void Video_ToMP4_Resize_Args_Pipe(SKColorType pixelFormat) } [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(10000)] + [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_Snapshot_InMemory() { using var bitmap = FFMpegImage.Snapshot(TestResources.Mp4Video); @@ -409,7 +411,7 @@ public void Video_Snapshot_InMemory() Assert.AreEqual(bitmap.ColorType, SKColorType.Bgra8888); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_Snapshot_PersistSnapshot() { var outputPath = new TemporaryFile("out.png"); @@ -423,7 +425,7 @@ public void Video_Snapshot_PersistSnapshot() Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_Join() { var inputCopy = new TemporaryFile("copy-input.mp4"); @@ -445,7 +447,7 @@ public void Video_Join() Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width); } - [TestMethod, Timeout(20000)] + [TestMethod, Timeout(2 * BaseTimeoutMilliseconds)] public void Video_Join_Image_Sequence() { var imageSet = new List(); @@ -470,7 +472,7 @@ public void Video_Join_Image_Sequence() Assert.AreEqual(imageAnalysis.PrimaryVideoStream!.Height, result.PrimaryVideoStream.Height); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_With_Only_Audio_Should_Extract_Metadata() { var video = FFProbe.Analyse(TestResources.Mp4WithoutVideo); @@ -479,7 +481,7 @@ public void Video_With_Only_Audio_Should_Extract_Metadata() Assert.AreEqual(10, video.Duration.TotalSeconds, 0.5); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_Duration() { var video = FFProbe.Analyse(TestResources.Mp4Video); @@ -499,7 +501,7 @@ public void Video_Duration() Assert.AreEqual(video.Duration.Seconds - 2, outputVideo.Duration.Seconds); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_UpdatesProgress() { var outputFile = new TemporaryFile("out.mp4"); @@ -540,7 +542,7 @@ void OnTimeProgess(TimeSpan time) Assert.AreNotEqual(analysis.Duration, timeDone); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_OutputsData() { var outputFile = new TemporaryFile("out.mp4"); @@ -562,7 +564,7 @@ public void Video_OutputsData() } [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(10000)] + [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_TranscodeInMemory() { using var resStream = new MemoryStream(); @@ -582,7 +584,7 @@ public void Video_TranscodeInMemory() Assert.AreEqual(vi.PrimaryVideoStream.Height, 128); } - [TestMethod, Timeout(20000)] + [TestMethod, Timeout(2 * BaseTimeoutMilliseconds)] public void Video_TranscodeToMemory() { using var memoryStream = new MemoryStream(); @@ -600,7 +602,7 @@ public void Video_TranscodeToMemory() Assert.AreEqual(vi.PrimaryVideoStream.Height, 360); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_Cancel_Async() { var outputFile = new TemporaryFile("out.mp4"); @@ -624,7 +626,7 @@ public async Task Video_Cancel_Async() Assert.IsFalse(result); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_Cancel() { var outputFile = new TemporaryFile("out.mp4"); @@ -645,7 +647,7 @@ public void Video_Cancel() Assert.IsFalse(result); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_Cancel_Async_With_Timeout() { var outputFile = new TemporaryFile("out.mp4"); @@ -675,7 +677,7 @@ public async Task Video_Cancel_Async_With_Timeout() Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_Cancel_CancellationToken_Async() { var outputFile = new TemporaryFile("out.mp4"); @@ -700,7 +702,7 @@ public async Task Video_Cancel_CancellationToken_Async() Assert.IsFalse(result); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_Cancel_CancellationToken_Async_Throws() { var outputFile = new TemporaryFile("out.mp4"); @@ -723,7 +725,7 @@ public async Task Video_Cancel_CancellationToken_Async_Throws() await Assert.ThrowsExceptionAsync(() => task); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_Cancel_CancellationToken_Throws() { var outputFile = new TemporaryFile("out.mp4"); @@ -745,7 +747,7 @@ public void Video_Cancel_CancellationToken_Throws() Assert.ThrowsException(() => task.ProcessSynchronously()); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public async Task Video_Cancel_CancellationToken_Async_With_Timeout() { var outputFile = new TemporaryFile("out.mp4"); From 6e38b45445847ce272c1253e52722992affe7569 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 11:40:11 +0100 Subject: [PATCH 05/53] chore: Increased test timeout duration Required for successful test execution on my rather slow machine --- FFMpegCore.Test/VideoTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index dd205e0..930cd99 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -15,7 +15,7 @@ namespace FFMpegCore.Test [TestClass] public class VideoTest { - private const int BaseTimeoutMilliseconds = 10_000; + private const int BaseTimeoutMilliseconds = 60_000; [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToOGV() From f95bba5aa2aef925455b045e8dec4a603c991e35 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 11:54:41 +0100 Subject: [PATCH 06/53] Added configuration for running tests in WSL on windows See https://learn.microsoft.com/en-us/visualstudio/test/remote-testing?view=vs-2022 --- testenvironments.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 testenvironments.json diff --git a/testenvironments.json b/testenvironments.json new file mode 100644 index 0000000..14b2763 --- /dev/null +++ b/testenvironments.json @@ -0,0 +1,10 @@ +{ + "version": "1", + "environments": [ + { + "name": "Ubuntu", + "type": "wsl", + "wslDistribution": "Ubuntu" + } + ] +} \ No newline at end of file From bdfe87be163032d346117f4836e824da3a53d72e Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 12:09:35 +0100 Subject: [PATCH 07/53] Enabled windows-only tests on all plattforms They are now also supported on Linux because we are using SkiaSharp instead of System.Drawing.Common --- .../Utilities/WindowsOnlyDataTestMethod.cs | 24 +++++++++---------- .../Utilities/WindowsOnlyTestMethod.cs | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs b/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs index 84a779a..e39921a 100644 --- a/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs +++ b/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs @@ -1,5 +1,4 @@ -using System.Runtime.InteropServices; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test.Utilities; @@ -7,16 +6,17 @@ public class WindowsOnlyDataTestMethod : DataTestMethodAttribute { public override TestResult[] Execute(ITestMethod testMethod) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var message = $"Test not executed on other platforms than Windows"; - { - return new[] - { - new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) } - }; - } - } + // Commented out because this edition of FFMpegCore fully supports Linux + //if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + //{ + // var message = $"Test not executed on other platforms than Windows"; + // { + // return new[] + // { + // new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) } + // }; + // } + //} return base.Execute(testMethod); } diff --git a/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs b/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs index 7e817bf..5143194 100644 --- a/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs +++ b/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs @@ -1,5 +1,4 @@ -using System.Runtime.InteropServices; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test.Utilities; @@ -7,16 +6,17 @@ public class WindowsOnlyTestMethod : TestMethodAttribute { public override TestResult[] Execute(ITestMethod testMethod) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var message = $"Test not executed on other platforms than Windows"; - { - return new[] - { - new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) } - }; - } - } + // Commented out because this edition of FFMpegCore fully supports Linux + //if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + //{ + // var message = $"Test not executed on other platforms than Windows"; + // { + // return new[] + // { + // new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) } + // }; + // } + //} return base.Execute(testMethod); } From 29fc9246d11fb0b570296825d51e69f4b517ce1c Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 12:11:03 +0100 Subject: [PATCH 08/53] Added dependency SkiaSharp.NativeAssets.Linux.NoDependencies Required for execution on Linux, otherwise we get the following error: Unable to load shared library 'libSkiaSharp' or one of its dependencies See https://github.com/mono/SkiaSharp/issues/1341 for more information --- .../FFMpegCore.Extensions.System.Drawing.Common.csproj | 1 + 1 file changed, 1 insertion(+) 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 index b0d6349..25e820a 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj @@ -12,6 +12,7 @@ + From fb19f453318df7bf0d442536b05c1387389ad0ec Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 12:22:36 +0100 Subject: [PATCH 09/53] Updated package properties for fork --- FFMpegCore/FFMpegCore.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index b08f00b..a30bf76 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -9,6 +9,9 @@ ffmpeg ffprobe convert video audio mediafile resize analyze muxing Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken README.md + FFMpegCore.SkiaSharp + https://github.com/drasive/FFMpegCore + https://github.com/drasive/FFMpegCore From db215ff4c7c70b51d58fbd25684572de4415c8d9 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 12:23:07 +0100 Subject: [PATCH 10/53] Revert "Updated package description for SkiaSharp instead of Aspose.Drawing" This reverts commit c96fdc490a66e087ead22383e762a081365e5c8e. --- FFMpegCore/FFMpegCore.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index a30bf76..1bb2305 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -2,12 +2,12 @@ true - A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications. Uses SkiaSharp instead of System.Drawing.Common. + A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications 5.0.0 ffmpeg ffprobe convert video audio mediafile resize analyze muxing - Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev README.md FFMpegCore.SkiaSharp https://github.com/drasive/FFMpegCore From 8afe1e0c9eca7fdf17196d53e796c5e254ca0493 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 13 Feb 2023 12:23:12 +0100 Subject: [PATCH 11/53] Revert "Updated package properties for fork" This reverts commit fb19f453318df7bf0d442536b05c1387389ad0ec. --- FFMpegCore/FFMpegCore.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 1bb2305..7c3f7bb 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -9,9 +9,6 @@ ffmpeg ffprobe convert video audio mediafile resize analyze muxing Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev README.md - FFMpegCore.SkiaSharp - https://github.com/drasive/FFMpegCore - https://github.com/drasive/FFMpegCore From cc22d15061ac62922ccb120729864aa5a4f1619a Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Thu, 16 Feb 2023 09:37:01 +0100 Subject: [PATCH 12/53] Moved SkiaSharp implemented to its own extension --- .../BitmapExtensions.cs | 28 ++++++++ .../BitmapVideoFrameWrapper.cs | 58 +++++++++++++++++ .../FFMpegCore.Extensions.SkiaSharp.csproj | 22 +++++++ .../FFMpegImage.cs | 57 ++++++++++++++++ .../BitmapExtensions.cs | 10 +-- .../BitmapVideoFrameWrapper.cs | 65 ++++++++++++++----- ...re.Extensions.System.Drawing.Common.csproj | 3 +- .../FFMpegImage.cs | 11 ++-- FFMpegCore.sln | 8 ++- 9 files changed, 228 insertions(+), 34 deletions(-) create mode 100644 FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs create mode 100644 FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs create mode 100644 FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj create mode 100644 FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs diff --git a/FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs b/FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs new file mode 100644 index 0000000..34e303a --- /dev/null +++ b/FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs @@ -0,0 +1,28 @@ +using SkiaSharp; + +namespace FFMpegCore.Extensions.SkiaSharp +{ + public static class BitmapExtensions + { + public static bool AddAudio(this SKBitmap poster, string audio, string output) + { + var destination = $"{Environment.TickCount}.png"; + using (var fileStream = File.OpenWrite(destination)) + { + poster.Encode(fileStream, SKEncodedImageFormat.Png, default); // PNG does not respect the quality parameter + } + + try + { + return FFMpeg.PosterWithAudio(destination, audio, output); + } + finally + { + if (File.Exists(destination)) + { + File.Delete(destination); + } + } + } + } +} diff --git a/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs b/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs new file mode 100644 index 0000000..2556883 --- /dev/null +++ b/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs @@ -0,0 +1,58 @@ +using FFMpegCore.Pipes; +using SkiaSharp; + +namespace FFMpegCore.Extensions.SkiaSharp +{ + public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable + { + public int Width => Source.Width; + + public int Height => Source.Height; + + public string Format { get; private set; } + + public SKBitmap Source { get; private set; } + + public BitmapVideoFrameWrapper(SKBitmap bitmap) + { + Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap)); + Format = ConvertStreamFormat(bitmap.ColorType); + } + + public void Serialize(Stream stream) + { + var data = Source.Bytes; + stream.Write(data, 0, data.Length); + } + + public async Task SerializeAsync(Stream stream, CancellationToken token) + { + var data = Source.Bytes; + await stream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); + } + + public void Dispose() + { + Source.Dispose(); + } + + private static string ConvertStreamFormat(SKColorType fmt) + { + switch (fmt) + { + case SKColorType.Gray8: + return "gray8"; + case SKColorType.Bgra8888: + return "bgra"; + case SKColorType.Rgb888x: + return "rgb"; + case SKColorType.Rgba8888: + return "rgba"; + case SKColorType.Rgb565: + return "rgb565"; + default: + throw new NotSupportedException($"Not supported pixel format {fmt}"); + } + } + } +} diff --git a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj new file mode 100644 index 0000000..25e820a --- /dev/null +++ b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj @@ -0,0 +1,22 @@ + + + + true + Image extension for FFMpegCore using System.Common.Drawing + 5.0.0 + + + ffmpeg ffprobe convert video audio mediafile resize analyze muxing + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev + + + + + + + + + + + + diff --git a/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs b/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs new file mode 100644 index 0000000..69929d3 --- /dev/null +++ b/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs @@ -0,0 +1,57 @@ +using System.Drawing; +using FFMpegCore.Pipes; +using SkiaSharp; + +namespace FFMpegCore.Extensions.SkiaSharp +{ + public static class FFMpegImage + { + /// + /// 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 SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) + { + var source = FFProbe.Analyse(input); + var (arguments, outputOptions) = SnapshotArgumentBuilder.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 = SKBitmap.Decode(ms); + return bitmap.Copy(); + } + /// + /// 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) = SnapshotArgumentBuilder.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 SKBitmap.Decode(ms); + } + } +} diff --git a/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs index b8a0c83..14cecaa 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs @@ -1,17 +1,13 @@ -using SkiaSharp; +using System.Drawing; namespace FFMpegCore.Extensions.System.Drawing.Common { public static class BitmapExtensions { - public static bool AddAudio(this SKBitmap poster, string audio, string output) + public static bool AddAudio(this Image poster, string audio, string output) { var destination = $"{Environment.TickCount}.png"; - using (var fileStream = File.OpenWrite(destination)) - { - poster.Encode(fileStream, SKEncodedImageFormat.Png, default); // PNG does not respect the quality parameter - } - + poster.Save(destination); try { return FFMpeg.PosterWithAudio(destination, audio, output); diff --git a/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs index 0439721..5462ca2 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs @@ -1,5 +1,7 @@ -using FFMpegCore.Pipes; -using SkiaSharp; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using FFMpegCore.Pipes; namespace FFMpegCore.Extensions.System.Drawing.Common { @@ -11,24 +13,44 @@ public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable public string Format { get; private set; } - public SKBitmap Source { get; private set; } + public Bitmap Source { get; private set; } - public BitmapVideoFrameWrapper(SKBitmap bitmap) + public BitmapVideoFrameWrapper(Bitmap bitmap) { Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap)); - Format = ConvertStreamFormat(bitmap.ColorType); + Format = ConvertStreamFormat(bitmap.PixelFormat); } public void Serialize(Stream stream) { - var data = Source.Bytes; - stream.Write(data, 0, data.Length); + var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); + + try + { + var buffer = new byte[data.Stride * data.Height]; + Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); + stream.Write(buffer, 0, buffer.Length); + } + finally + { + Source.UnlockBits(data); + } } public async Task SerializeAsync(Stream stream, CancellationToken token) { - var data = Source.Bytes; - await stream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); + var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); + + try + { + var buffer = new byte[data.Stride * data.Height]; + Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); + await stream.WriteAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); + } + finally + { + Source.UnlockBits(data); + } } public void Dispose() @@ -36,20 +58,27 @@ public void Dispose() Source.Dispose(); } - private static string ConvertStreamFormat(SKColorType fmt) + private static string ConvertStreamFormat(PixelFormat fmt) { switch (fmt) { - case SKColorType.Gray8: - return "gray8"; - case SKColorType.Bgra8888: + case PixelFormat.Format16bppGrayScale: + return "gray16le"; + case PixelFormat.Format16bppRgb555: + return "bgr555le"; + case PixelFormat.Format16bppRgb565: + return "bgr565le"; + case PixelFormat.Format24bppRgb: + return "bgr24"; + case PixelFormat.Format32bppArgb: return "bgra"; - case SKColorType.Rgb888x: - return "rgb"; - case SKColorType.Rgba8888: + case PixelFormat.Format32bppPArgb: + //This is not really same as argb32 + return "argb"; + case PixelFormat.Format32bppRgb: return "rgba"; - case SKColorType.Rgb565: - return "rgb565"; + case PixelFormat.Format48bppRgb: + return "rgb48le"; default: throw new NotSupportedException($"Not supported pixel format {fmt}"); } 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 index 25e820a..aafb577 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj @@ -11,8 +11,7 @@ - - + diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs index 8cd0f26..f36f83d 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs @@ -1,6 +1,5 @@ using System.Drawing; using FFMpegCore.Pipes; -using SkiaSharp; namespace FFMpegCore.Extensions.System.Drawing.Common { @@ -15,7 +14,7 @@ public static class FFMpegImage /// Selected video stream index. /// Input file index /// Bitmap with the requested snapshot. - public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) + 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) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); @@ -27,8 +26,8 @@ public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captu .ProcessSynchronously(); ms.Position = 0; - using var bitmap = SKBitmap.Decode(ms); - return bitmap.Copy(); + 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 @@ -39,7 +38,7 @@ public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captu /// 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) + 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) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); @@ -51,7 +50,7 @@ await arguments .ProcessAsynchronously(); ms.Position = 0; - return SKBitmap.Decode(ms); + return new Bitmap(ms); } } } diff --git a/FFMpegCore.sln b/FFMpegCore.sln index 5a9faa8..7ab0929 100644 --- a/FFMpegCore.sln +++ b/FFMpegCore.sln @@ -9,7 +9,9 @@ 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.System.Drawing.Common", "FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj", "{9C1A4930-9369-4A18-AD98-929A2A510D80}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.SkiaSharp", "FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj", "{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,6 +35,10 @@ Global {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 + {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 315c1e1a914000e835e9b6a2b0e2fbae24458efc Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Thu, 16 Feb 2023 10:25:42 +0100 Subject: [PATCH 13/53] Adjusted FFMpegCore.Examples for separate FFMpegCore.Extensions.SkiaSharp project --- FFMpegCore.Examples/FFMpegCore.Examples.csproj | 1 + FFMpegCore.Examples/Program.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj index db3c66e..a1193a8 100644 --- a/FFMpegCore.Examples/FFMpegCore.Examples.csproj +++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj @@ -7,6 +7,7 @@ + diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs index f926d94..24d29d5 100644 --- a/FFMpegCore.Examples/Program.cs +++ b/FFMpegCore.Examples/Program.cs @@ -3,7 +3,6 @@ using FFMpegCore.Enums; using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; -using SkiaSharp; var inputPath = "/path/to/input"; var outputPath = "/path/to/output"; @@ -80,7 +79,8 @@ await FFMpegArguments FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath); // or #pragma warning disable CA1416 - using var image = SKBitmap.Decode(inputImagePath); + using var image = Image.FromFile(inputImagePath); // Using FFMpegCore.Extensions.System.Drawing.Common + //using var image = SKBitmap.Decode(inputImagePath); // Using FFMpegCore.Extensions.SkiaSharp image.AddAudio(inputAudioPath, outputPath); #pragma warning restore CA1416 } From 2458b4ae9c54732afeabc35a40229c72d277a2d5 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Thu, 16 Feb 2023 14:34:35 +0100 Subject: [PATCH 14/53] Adjusted FFMpegCore.Test for separate FFMpegCore.Extensions.SkiaSharp project --- FFMpegCore.Test/FFMpegCore.Test.csproj | 2 + FFMpegCore.Test/Utilities/BitmapSources.cs | 53 ++++++-- .../Utilities/WindowsOnlyDataTestMethod.cs | 24 ++-- .../Utilities/WindowsOnlyTestMethod.cs | 24 ++-- FFMpegCore.Test/VideoTest.cs | 124 +++++++++++++----- 5 files changed, 162 insertions(+), 65 deletions(-) diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index def07d2..67096f5 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -20,9 +20,11 @@ + + diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs index 7e8d1a9..755f781 100644 --- a/FFMpegCore.Test/Utilities/BitmapSources.cs +++ b/FFMpegCore.Test/Utilities/BitmapSources.cs @@ -2,15 +2,25 @@ using System.Drawing.Imaging; using System.Numerics; using System.Runtime.Versioning; -using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; using SkiaSharp; namespace FFMpegCore.Test.Utilities { - [SupportedOSPlatform("windows")] internal static class BitmapSource { + [SupportedOSPlatform("windows")] + public static IEnumerable CreateBitmaps(int count, PixelFormat fmt, int w, int h) + { + for (var i = 0; i < count; i++) + { + using (var frame = CreateVideoFrame(i, fmt, w, h, 0.025f, 0.025f * w * 0.03f)) + { + yield return frame; + } + } + } + public static IEnumerable CreateBitmaps(int count, SKColorType fmt, int w, int h) { for (var i = 0; i < count; i++) @@ -22,10 +32,41 @@ public static IEnumerable CreateBitmaps(int count, SKColorType fmt, } } - public static BitmapVideoFrameWrapper CreateVideoFrame(int index, SKColorType fmt, int w, int h, float scaleNoise, float offset) + [SupportedOSPlatform("windows")] + public static Extensions.System.Drawing.Common.BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset) + { + var bitmap = new Bitmap(w, h, fmt); + + SetVideoFramePixels(index, w, h, scaleNoise, offset, ((int x, int y, byte red, byte green, byte blue) args) => + { + var color = Color.FromArgb(args.red, args.blue, args.green); + bitmap.SetPixel(args.x, args.y, color); + }); + + return new Extensions.System.Drawing.Common.BitmapVideoFrameWrapper(bitmap); + } + + public static Extensions.SkiaSharp.BitmapVideoFrameWrapper CreateVideoFrame(int index, SKColorType fmt, int w, int h, float scaleNoise, float offset) { var bitmap = new SKBitmap(w, h, fmt, SKAlphaType.Opaque); + SetVideoFramePixels(index, w, h, scaleNoise, offset, ((int x, int y, byte red, byte green, byte blue) args) => + { + var color = new SKColor(args.red, args.blue, args.green); + bitmap.SetPixel(args.x, args.y, color); + }); + + return new Extensions.SkiaSharp.BitmapVideoFrameWrapper(bitmap); + } + + private static void SetVideoFramePixels( + int index, + int w, + int h, + float scaleNoise, + float offset, + Action<(int x, int y, byte red, byte green, byte blue)> setPixel) + { offset = offset * index; for (var y = 0; y < h; y++) @@ -39,13 +80,9 @@ public static BitmapVideoFrameWrapper CreateVideoFrame(int index, SKColorType fm var value = (byte)((Perlin.Noise(nx, ny) + 1.0f) / 2.0f * 255); - var color = new SKColor((byte)(value * xf), (byte)(value * yf), value); - - bitmap.SetPixel(x, y, color); + setPixel((x, y, (byte)(value * xf), (byte)(value * yf), value)); } } - - return new BitmapVideoFrameWrapper(bitmap); } // diff --git a/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs b/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs index e39921a..84a779a 100644 --- a/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs +++ b/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Runtime.InteropServices; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test.Utilities; @@ -6,17 +7,16 @@ public class WindowsOnlyDataTestMethod : DataTestMethodAttribute { public override TestResult[] Execute(ITestMethod testMethod) { - // Commented out because this edition of FFMpegCore fully supports Linux - //if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - //{ - // var message = $"Test not executed on other platforms than Windows"; - // { - // return new[] - // { - // new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) } - // }; - // } - //} + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var message = $"Test not executed on other platforms than Windows"; + { + return new[] + { + new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) } + }; + } + } return base.Execute(testMethod); } diff --git a/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs b/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs index 5143194..7e817bf 100644 --- a/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs +++ b/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Runtime.InteropServices; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test.Utilities; @@ -6,17 +7,16 @@ public class WindowsOnlyTestMethod : TestMethodAttribute { public override TestResult[] Execute(ITestMethod testMethod) { - // Commented out because this edition of FFMpegCore fully supports Linux - //if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - //{ - // var message = $"Test not executed on other platforms than Windows"; - // { - // return new[] - // { - // new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) } - // }; - // } - //} + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var message = $"Test not executed on other platforms than Windows"; + { + return new[] + { + new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) } + }; + } + } return base.Execute(testMethod); } diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 930cd99..90a4b89 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -1,21 +1,20 @@ -using System.Runtime.Versioning; +using System.Drawing.Imaging; +using System.Runtime.Versioning; using System.Text; using FFMpegCore.Arguments; using FFMpegCore.Enums; using FFMpegCore.Exceptions; -using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; using FFMpegCore.Test.Resources; using FFMpegCore.Test.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; -using SkiaSharp; namespace FFMpegCore.Test { [TestClass] public class VideoTest { - private const int BaseTimeoutMilliseconds = 60_000; + private const int BaseTimeoutMilliseconds = 10_000; [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToOGV() @@ -85,9 +84,16 @@ public void Video_ToH265_MKV_Args() [SupportedOSPlatform("windows")] [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(SKColorType.Rgb565)] - [DataRow(SKColorType.Bgra8888)] - public void Video_ToMP4_Args_Pipe(SKColorType pixelFormat) + [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] + public void Video_ToMP4_Args_Pipe_WindowsOnly(System.Drawing.Imaging.PixelFormat pixelFormat) => Video_ToMP4_Args_Pipe_Internal(pixelFormat); + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + [DataRow(SkiaSharp.SKColorType.Rgb565)] + [DataRow(SkiaSharp.SKColorType.Bgra8888)] + public void Video_ToMP4_Args_Pipe(SkiaSharp.SKColorType pixelFormat) => Video_ToMP4_Args_Pipe_Internal(pixelFormat); + + private static void Video_ToMP4_Args_Pipe_Internal(dynamic pixelFormat) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -102,14 +108,19 @@ public void Video_ToMP4_Args_Pipe(SKColorType pixelFormat) [SupportedOSPlatform("windows")] [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_Args_Pipe_DifferentImageSizes() + public void Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly() => Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(System.Drawing.Imaging.PixelFormat.Format24bppRgb); + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_ToMP4_Args_Pipe_DifferentImageSizes() => Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(SkiaSharp.SKColorType.Rgb565); + + private static void Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(dynamic pixelFormat) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); var frames = new List { - BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 256, 256, 1, 0) + BitmapSource.CreateVideoFrame(0, pixelFormat, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, pixelFormat, 256, 256, 1, 0) }; var videoFramesSource = new RawVideoPipeSource(frames); @@ -122,14 +133,19 @@ public void Video_ToMP4_Args_Pipe_DifferentImageSizes() [SupportedOSPlatform("windows")] [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() + public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly_Async() => await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(System.Drawing.Imaging.PixelFormat.Format24bppRgb); + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() => await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(SkiaSharp.SKColorType.Rgb565); + + private static async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(dynamic pixelFormat) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); var frames = new List { - BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 256, 256, 1, 0) + BitmapSource.CreateVideoFrame(0, pixelFormat, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, pixelFormat, 256, 256, 1, 0) }; var videoFramesSource = new RawVideoPipeSource(frames); @@ -142,14 +158,20 @@ public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() [SupportedOSPlatform("windows")] [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() + public void Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly() => + Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(System.Drawing.Imaging.PixelFormat.Format24bppRgb, System.Drawing.Imaging.PixelFormat.Format32bppRgb); + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() => Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(SkiaSharp.SKColorType.Rgb565, SkiaSharp.SKColorType.Bgra8888); + + private static void Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); var frames = new List { - BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, SKColorType.Bgra8888, 255, 255, 1, 0) + BitmapSource.CreateVideoFrame(0, pixelFormatFrame1, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, pixelFormatFrame2, 255, 255, 1, 0) }; var videoFramesSource = new RawVideoPipeSource(frames); @@ -162,14 +184,20 @@ public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() [SupportedOSPlatform("windows")] [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() + public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly_Async() => + await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(System.Drawing.Imaging.PixelFormat.Format24bppRgb, System.Drawing.Imaging.PixelFormat.Format32bppRgb); + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() => await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(SkiaSharp.SKColorType.Rgb565, SkiaSharp.SKColorType.Bgra8888); + + private static async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); var frames = new List { - BitmapSource.CreateVideoFrame(0, SKColorType.Rgb565, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, SKColorType.Bgra8888, 255, 255, 1, 0) + BitmapSource.CreateVideoFrame(0, pixelFormatFrame1, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, pixelFormatFrame2, 255, 255, 1, 0) }; var videoFramesSource = new RawVideoPipeSource(frames); @@ -315,9 +343,16 @@ public void Video_ToTS_Args() [SupportedOSPlatform("windows")] [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(SKColorType.Rgb565)] - [DataRow(SKColorType.Bgra8888)] - public async Task Video_ToTS_Args_Pipe(SKColorType pixelFormat) + [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] + public async Task Video_ToTS_Args_Pipe_WindowsOnly(System.Drawing.Imaging.PixelFormat pixelFormat) => await Video_ToTS_Args_Pipe_Internal(pixelFormat); + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + [DataRow(SkiaSharp.SKColorType.Rgb565)] + [DataRow(SkiaSharp.SKColorType.Bgra8888)] + public async Task Video_ToTS_Args_Pipe(SkiaSharp.SKColorType pixelFormat) => await Video_ToTS_Args_Pipe_Internal(pixelFormat); + + private static async Task Video_ToTS_Args_Pipe_Internal(dynamic pixelFormat) { using var output = new TemporaryFile($"out{VideoType.Ts.Extension}"); var input = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); @@ -348,9 +383,9 @@ public async Task Video_ToOGV_Resize() [SupportedOSPlatform("windows")] [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(SKColorType.Rgb565)] - [DataRow(SKColorType.Bgra8888)] - public void RawVideoPipeSource_Ogv_Scale(SKColorType pixelFormat) + [DataRow(SkiaSharp.SKColorType.Rgb565)] + [DataRow(SkiaSharp.SKColorType.Bgra8888)] + public void RawVideoPipeSource_Ogv_Scale(SkiaSharp.SKColorType pixelFormat) { using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); @@ -383,10 +418,17 @@ public void Scale_Mp4_Multithreaded() [SupportedOSPlatform("windows")] [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(SKColorType.Rgb565)] - [DataRow(SKColorType.Bgra8888)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] // [DataRow(PixelFormat.Format48bppRgb)] - public void Video_ToMP4_Resize_Args_Pipe(SKColorType pixelFormat) + public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) => Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat); + + [DataTestMethod, Timeout(BaseTimeoutMilliseconds)] + [DataRow(SkiaSharp.SKColorType.Rgb565)] + [DataRow(SkiaSharp.SKColorType.Bgra8888)] + public void Video_ToMP4_Resize_Args_Pipe(SkiaSharp.SKColorType pixelFormat) => Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat); + + private static void Video_ToMP4_Resize_Args_Pipe_Internal(dynamic pixelFormat) { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); @@ -401,14 +443,25 @@ public void Video_ToMP4_Resize_Args_Pipe(SKColorType pixelFormat) [SupportedOSPlatform("windows")] [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Snapshot_InMemory() + public void Video_Snapshot_InMemory_SystemDrawingCommon() { - using var bitmap = FFMpegImage.Snapshot(TestResources.Mp4Video); + using var bitmap = Extensions.System.Drawing.Common.FFMpegImage.Snapshot(TestResources.Mp4Video); var input = FFProbe.Analyse(TestResources.Mp4Video); Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); - Assert.AreEqual(bitmap.ColorType, SKColorType.Bgra8888); + Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_InMemory_SkiaSharp() + { + using var bitmap = Extensions.SkiaSharp.FFMpegImage.Snapshot(TestResources.Mp4Video); + + var input = FFProbe.Analyse(TestResources.Mp4Video); + Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); + Assert.AreEqual(bitmap.ColorType, SkiaSharp.SKColorType.Bgra8888); } [TestMethod, Timeout(BaseTimeoutMilliseconds)] @@ -565,11 +618,16 @@ public void Video_OutputsData() [SupportedOSPlatform("windows")] [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_TranscodeInMemory() + public void Video_TranscodeInMemory_WindowsOnly() => Video_TranscodeInMemory_Internal(System.Drawing.Imaging.PixelFormat.Format24bppRgb); + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_TranscodeInMemory() => Video_TranscodeInMemory_Internal(SkiaSharp.SKColorType.Rgb565); + + private static void Video_TranscodeInMemory_Internal(dynamic pixelFormat) { using var resStream = new MemoryStream(); var reader = new StreamPipeSink(resStream); - var writer = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, SKColorType.Rgb565, 128, 128)); + var writer = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 128, 128)); FFMpegArguments .FromPipeInput(writer) From a66bdba21146d82e35eda94666344d3aa15bc077 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Thu, 16 Feb 2023 14:35:00 +0100 Subject: [PATCH 15/53] Improved SkiaSharp CreateVideoFrame performance --- FFMpegCore.Test/Utilities/BitmapSources.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs index 755f781..8965435 100644 --- a/FFMpegCore.Test/Utilities/BitmapSources.cs +++ b/FFMpegCore.Test/Utilities/BitmapSources.cs @@ -50,10 +50,11 @@ public static Extensions.SkiaSharp.BitmapVideoFrameWrapper CreateVideoFrame(int { var bitmap = new SKBitmap(w, h, fmt, SKAlphaType.Opaque); + using var bitmapCanvas = new SKCanvas(bitmap); SetVideoFramePixels(index, w, h, scaleNoise, offset, ((int x, int y, byte red, byte green, byte blue) args) => { var color = new SKColor(args.red, args.blue, args.green); - bitmap.SetPixel(args.x, args.y, color); + bitmapCanvas.DrawPoint(args.x, args.y, color); }); return new Extensions.SkiaSharp.BitmapVideoFrameWrapper(bitmap); From 55b652a77f8f3a0a7a4d916b7cf0ab2e5e71b077 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Thu, 16 Feb 2023 14:39:09 +0100 Subject: [PATCH 16/53] Added TODO for additional SKColorType support --- FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs b/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs index 2556883..7bb98fb 100644 --- a/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs +++ b/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs @@ -38,6 +38,7 @@ public void Dispose() private static string ConvertStreamFormat(SKColorType fmt) { + // TODO: Add support for additional formats switch (fmt) { case SKColorType.Gray8: From f3c7df1ff516e1b27f037b5f7333d812816e602d Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Thu, 16 Feb 2023 14:39:14 +0100 Subject: [PATCH 17/53] Code formatting --- FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs index f36f83d..c946507 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs @@ -29,6 +29,7 @@ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? capture 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 /// From 7b32ba5a27fd64ab09f1d3270b5e36928163cb4b Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Thu, 16 Feb 2023 14:48:06 +0100 Subject: [PATCH 18/53] Fixed ambiguous method call The two constructors were causing issues with ambiguous method calls but are a good idea, so this is more of a workaround. Error message: The call is ambiguous between the following methods or properties: 'FFMpegCore.Pipes.RawVideoPipeSource.RawVideoPipeSource(System.Collections.Generic.IEnumerator)' and 'FFMpegCore.Pipes.RawVideoPipeSource.RawVideoPipeSource(System.Collections.Generic.IEnumerable)' --- FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs index fe4c881..2f3028f 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs @@ -15,13 +15,11 @@ public class RawVideoPipeSource : IPipeSource private bool _formatInitialized; private readonly IEnumerator _framesEnumerator; - public RawVideoPipeSource(IEnumerator framesEnumerator) + public RawVideoPipeSource(IEnumerable framesEnumerator) { - _framesEnumerator = framesEnumerator; + _framesEnumerator = framesEnumerator.GetEnumerator(); } - public RawVideoPipeSource(IEnumerable framesEnumerator) : this(framesEnumerator.GetEnumerator()) { } - public string GetStreamArguments() { if (!_formatInitialized) From ad5dca3cd64944467cd69a186a2c79e390334f3e Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 16 Feb 2023 23:55:10 +0100 Subject: [PATCH 19/53] Ensure all images have same file extension and handle that --- FFMpegCore/FFMpeg/FFMpeg.cs | 38 ++++++++++++++++++++++-------------- FFMpegCore/FFMpegCore.csproj | 2 +- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 58526b8..94f35c0 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -66,25 +66,34 @@ public static async Task SnapshotAsync(string input, string output, Size? /// Output video information. public static bool JoinImageSequence(string output, double frameRate = 30, params string[] images) { - int? width = null, height = null; - var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); - var temporaryImageFiles = images.Select((imagePath, index) => + var fileExtensions = images.Select(Path.GetExtension).Distinct().ToArray(); + if (fileExtensions.Length != 1) { - var analysis = FFProbe.Analyse(imagePath); - FFMpegHelper.ConversionSizeExceptionCheck(analysis.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Height); - width ??= analysis.PrimaryVideoStream.Width; - height ??= analysis.PrimaryVideoStream.Height; + throw new ArgumentException("All images must have the same extension", nameof(images)); + } - var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{Path.GetExtension(imagePath)}"); - Directory.CreateDirectory(tempFolderName); - File.Copy(imagePath, destinationPath); - return destinationPath; - }).ToArray(); + var fileExtension = fileExtensions[0].ToLowerInvariant(); + int? width = null, height = null; + + var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempFolderName); try { + var index = 0; + foreach (var imagePath in images) + { + var analysis = FFProbe.Analyse(imagePath); + FFMpegHelper.ConversionSizeExceptionCheck(analysis.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Height); + width ??= analysis.PrimaryVideoStream.Width; + height ??= analysis.PrimaryVideoStream.Height; + + var destinationPath = Path.Combine(tempFolderName, $"{index++.ToString().PadLeft(9, '0')}{fileExtension}"); + File.Copy(imagePath, destinationPath); + } + return FFMpegArguments - .FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false) + .FromFileInput(Path.Combine(tempFolderName, $"%09d{fileExtension}"), false) .OutputToFile(output, true, options => options .ForcePixelFormat("yuv420p") .Resize(width!.Value, height!.Value) @@ -93,8 +102,7 @@ public static bool JoinImageSequence(string output, double frameRate = 30, param } finally { - Cleanup(temporaryImageFiles); - Directory.Delete(tempFolderName); + Directory.Delete(tempFolderName, true); } } diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 7c3f7bb..5db9cc8 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -3,7 +3,7 @@ true A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 5.0.0 + 5.0.1 ffmpeg ffprobe convert video audio mediafile resize analyze muxing From 02d2b4261b0f5519c0c0137b9f0b2f44c89113d2 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 16 Feb 2023 23:55:20 +0100 Subject: [PATCH 20/53] Add missing usings on tests --- FFMpegCore.Test/VideoTest.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index e3e4b6b..fec8386 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -413,7 +413,7 @@ public void Video_Snapshot_InMemory() [TestMethod, Timeout(10000)] public void Video_Snapshot_PersistSnapshot() { - var outputPath = new TemporaryFile("out.png"); + using var outputPath = new TemporaryFile("out.png"); var input = FFProbe.Analyse(TestResources.Mp4Video); FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); @@ -427,10 +427,10 @@ public void Video_Snapshot_PersistSnapshot() [TestMethod, Timeout(10000)] public void Video_Join() { - var inputCopy = new TemporaryFile("copy-input.mp4"); + using var inputCopy = new TemporaryFile("copy-input.mp4"); File.Copy(TestResources.Mp4Video, inputCopy); - var outputPath = new TemporaryFile("out.mp4"); + using var outputPath = new TemporaryFile("out.mp4"); var input = FFProbe.Analyse(TestResources.Mp4Video); var success = FFMpeg.Join(outputPath, TestResources.Mp4Video, inputCopy); Assert.IsTrue(success); @@ -461,7 +461,7 @@ public void Video_Join_Image_Sequence() }); var imageAnalysis = FFProbe.Analyse(imageSet.First()); - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var success = FFMpeg.JoinImageSequence(outputFile, frameRate: 10, images: imageSet.ToArray()); Assert.IsTrue(success); var result = FFProbe.Analyse(outputFile); @@ -484,7 +484,7 @@ public void Video_With_Only_Audio_Should_Extract_Metadata() public void Video_Duration() { var video = FFProbe.Analyse(TestResources.Mp4Video); - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); FFMpegArguments .FromFileInput(TestResources.Mp4Video) @@ -503,7 +503,7 @@ public void Video_Duration() [TestMethod, Timeout(10000)] public void Video_UpdatesProgress() { - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var percentageDone = 0.0; var timeDone = TimeSpan.Zero; @@ -544,7 +544,7 @@ void OnTimeProgess(TimeSpan time) [TestMethod, Timeout(10000)] public void Video_OutputsData() { - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var dataReceived = false; GlobalFFOptions.Configure(opt => opt.Encoding = Encoding.UTF8); @@ -604,7 +604,7 @@ public void Video_TranscodeToMemory() [TestMethod, Timeout(10000)] public async Task Video_Cancel_Async() { - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var task = FFMpegArguments .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args @@ -628,7 +628,7 @@ public async Task Video_Cancel_Async() [TestMethod, Timeout(10000)] public void Video_Cancel() { - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var task = FFMpegArguments .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args .WithCustomArgument("-re") @@ -649,7 +649,7 @@ public void Video_Cancel() [TestMethod, Timeout(10000)] public async Task Video_Cancel_Async_With_Timeout() { - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var task = FFMpegArguments .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args @@ -679,7 +679,7 @@ public async Task Video_Cancel_Async_With_Timeout() [TestMethod, Timeout(10000)] public async Task Video_Cancel_CancellationToken_Async() { - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var cts = new CancellationTokenSource(); @@ -704,7 +704,7 @@ public async Task Video_Cancel_CancellationToken_Async() [TestMethod, Timeout(10000)] public async Task Video_Cancel_CancellationToken_Async_Throws() { - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var cts = new CancellationTokenSource(); @@ -727,7 +727,7 @@ public async Task Video_Cancel_CancellationToken_Async_Throws() [TestMethod, Timeout(10000)] public void Video_Cancel_CancellationToken_Throws() { - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var cts = new CancellationTokenSource(); @@ -749,7 +749,7 @@ public void Video_Cancel_CancellationToken_Throws() [TestMethod, Timeout(10000)] public async Task Video_Cancel_CancellationToken_Async_With_Timeout() { - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var cts = new CancellationTokenSource(); From 8b69ed104aef42203f96dc72bfc87b4c12b00424 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 16 Feb 2023 23:55:28 +0100 Subject: [PATCH 21/53] Remove unused enums --- FFMpegCore.Test/Resources/TestResources.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/FFMpegCore.Test/Resources/TestResources.cs b/FFMpegCore.Test/Resources/TestResources.cs index de84080..b958b80 100644 --- a/FFMpegCore.Test/Resources/TestResources.cs +++ b/FFMpegCore.Test/Resources/TestResources.cs @@ -1,15 +1,5 @@ namespace FFMpegCore.Test.Resources { - public enum AudioType - { - Mp3 - } - - public enum ImageType - { - Png - } - public static class TestResources { public static readonly string Mp4Video = "./Resources/input_3sec.mp4"; From d6517fa1ef54517069e7f817ccbe1b8e7cf314fd Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 16 Feb 2023 23:58:20 +0100 Subject: [PATCH 22/53] Fix CI trigger after rename --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6d3529..d00c29b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: - FFMpegCore.Test/** pull_request: branches: - - master + - main - release paths: - .github/workflows/ci.yml From 5b34d04a3585ef2e08a3d37aaf77a4f23184a909 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Feb 2023 00:12:32 +0100 Subject: [PATCH 23/53] Add PackageOutputPath and format --- .github/workflows/release.yml | 2 +- ...re.Extensions.System.Drawing.Common.csproj | 31 ++++++++-------- FFMpegCore/FFMpegCore.csproj | 35 ++++++++++--------- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c7725b..03504f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,5 +19,5 @@ jobs: run: dotnet pack FFMpegCore.sln --output build -c Release - name: Publish NuGet package - run: dotnet nuget push build/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} + run: dotnet nuget push *.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} 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 index aafb577..13cdc1a 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj @@ -1,21 +1,22 @@ - - true - Image extension for FFMpegCore using System.Common.Drawing - 5.0.0 - - - ffmpeg ffprobe convert video audio mediafile resize analyze muxing - Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev - + + true + Image extension for FFMpegCore using System.Common.Drawing + 5.0.0 + ../nupkg + + + ffmpeg ffprobe convert video audio mediafile resize analyze muxing + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev + - - - + + + - - - + + + diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 5db9cc8..843bdba 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -1,23 +1,24 @@  - - true - A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 5.0.1 - - - ffmpeg ffprobe convert video audio mediafile resize analyze muxing - Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev - README.md - + + true + A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications + 5.0.1 + ../nupkg + + + ffmpeg ffprobe convert video audio mediafile resize analyze muxing + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev + README.md + - - - + + + - - - - + + + + From daa512c294f3ec2021b6d30c5f8a0cae9cc793aa Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Feb 2023 00:13:34 +0100 Subject: [PATCH 24/53] Format remaining csproj files --- FFMpegCore.Examples/FFMpegCore.Examples.csproj | 18 +++++++++--------- FFMpegCore.Test/FFMpegCore.Test.csproj | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj index db3c66e..555f4b7 100644 --- a/FFMpegCore.Examples/FFMpegCore.Examples.csproj +++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj @@ -1,14 +1,14 @@ - - Exe - net6.0 - false - + + Exe + net6.0 + false + - - - - + + + + diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index def07d2..0243372 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -12,19 +12,19 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - - + + From 8fff02d8d6ab09c5045fa4f2c75466804720a44e Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Feb 2023 00:14:23 +0100 Subject: [PATCH 25/53] Add --skip-duplicate --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03504f0..9ec3258 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,5 +19,5 @@ jobs: run: dotnet pack FFMpegCore.sln --output build -c Release - name: Publish NuGet package - run: dotnet nuget push *.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} + run: dotnet nuget push *.nupkg --skip-duplicate --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} From 2e9f5f117090f410682957cb945e7bed787b40e3 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Feb 2023 00:23:17 +0100 Subject: [PATCH 26/53] Remove --output build --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c7725b..5356f8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: dotnet-version: '7.0.x' - name: Build solution - run: dotnet pack FFMpegCore.sln --output build -c Release + run: dotnet pack FFMpegCore.sln -c Release - name: Publish NuGet package run: dotnet nuget push build/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} From 55c90fb081820dd01f81ddabe18cda69275a60a2 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 17 Feb 2023 00:27:12 +0100 Subject: [PATCH 27/53] Fix nupkg directory --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 676cfd2..00a1ea7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,5 +19,5 @@ jobs: run: dotnet pack FFMpegCore.sln -c Release - name: Publish NuGet package - run: dotnet nuget push *.nupkg --skip-duplicate --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} + run: dotnet nuget push nupkg/*.nupkg --skip-duplicate --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} From fe5c1f5b58d4089d87036783dd302ca319256563 Mon Sep 17 00:00:00 2001 From: Kevin Heritage Date: Mon, 23 May 2022 07:32:32 +0200 Subject: [PATCH 28/53] Create end seek argument --- FFMpegCore.Test/ArgumentBuilderTest.cs | 7 ++++ .../FFMpeg/Arguments/EndSeekArgument.cs | 35 +++++++++++++++++++ FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 1 + 3 files changed, 43 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index f676a44..2c550c9 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -258,6 +258,13 @@ public void Builder_BuildString_Seek() Assert.AreEqual("-ss 00:00:10.000 -i \"input.mp4\" -ss 00:00:10.000 \"output.mp4\"", str); } + [TestMethod] + public void Builder_BuildString_EndSeek() + { + var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.EndSeek(TimeSpan.FromSeconds(10))).OutputToFile("output.mp4", false, opt => opt.EndSeek(TimeSpan.FromSeconds(10))).Arguments; + Assert.AreEqual("-to 00:00:10.000 -i \"input.mp4\" -to 00:00:10.000 \"output.mp4\"", str); + } + [TestMethod] public void Builder_BuildString_Shortest() { diff --git a/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs new file mode 100644 index 0000000..5ced1c4 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs @@ -0,0 +1,35 @@ +using System; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents seek parameter + /// + public class EndSeekArgument : IArgument + { + public readonly TimeSpan? SeekTo; + + public EndSeekArgument(TimeSpan? seekTo) + { + SeekTo = seekTo; + } + + public string Text { + get { + if(SeekTo.HasValue) + { + int hours = SeekTo.Value.Hours; + if(SeekTo.Value.Days > 0) + { + hours += SeekTo.Value.Days * 24; + } + return $"-to {hours.ToString("00")}:{SeekTo.Value.Minutes.ToString("00")}:{SeekTo.Value.Seconds.ToString("00")}.{SeekTo.Value.Milliseconds.ToString("000")}"; + } + else + { + return string.Empty; + } + } + } + } +} diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 0f54b8c..cc49c5f 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -54,6 +54,7 @@ public FFMpegArgumentOptions WithAudioFilters(Action audioFi public FFMpegArgumentOptions WithCustomArgument(string argument) => WithArgument(new CustomArgument(argument)); public FFMpegArgumentOptions Seek(TimeSpan? seekTo) => WithArgument(new SeekArgument(seekTo)); + public FFMpegArgumentOptions EndSeek(TimeSpan? seekTo) => WithArgument(new EndSeekArgument(seekTo)); public FFMpegArgumentOptions Loop(int times) => WithArgument(new LoopArgument(times)); public FFMpegArgumentOptions OverwriteExisting() => WithArgument(new OverwriteArgument()); public FFMpegArgumentOptions SelectStream(int streamIndex, int inputFileIndex = 0, From c7c591b451b76ce0162b7d6d53cbd529ea267570 Mon Sep 17 00:00:00 2001 From: Kevin Heritage Date: Mon, 23 May 2022 09:53:29 +0200 Subject: [PATCH 29/53] Create cut video function From a2c899d02e4384da7076a3a9b9d125755b79a672 Mon Sep 17 00:00:00 2001 From: Kevin Heritage Date: Fri, 17 Feb 2023 19:17:31 +0100 Subject: [PATCH 30/53] feat: create sub video function --- FFMpegCore/FFMpeg/FFMpeg.cs | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 58526b8..3939502 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -239,6 +239,46 @@ public static bool Join(string output, params string[] videos) } } + private static FFMpegArgumentProcessor BaseSubVideo(string input, string output, TimeSpan startTime, TimeSpan endTime) + { + if (Path.GetExtension(input) != Path.GetExtension(output)) + { + output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output), Path.GetExtension(input)); + } + + return FFMpegArguments + .FromFileInput(input, true, options => options.Seek(startTime).EndSeek(endTime)) + .OutputToFile(output, true, options => options.CopyChannel()); + } + + /// + /// Creates a new video starting and ending at the specified times + /// + /// Input video file. + /// Output video file. + /// The start time of when the sub video needs to start + /// The end time of where the sub video needs to end + /// Output video information. + public static bool SubVideo(string input, string output, TimeSpan startTime, TimeSpan endTime) + { + return BaseSubVideo(input, output, startTime, endTime) + .ProcessSynchronously(); + } + + /// + /// Creates a new video starting and ending at the specified times + /// + /// Input video file. + /// Output video file. + /// The start time of when the sub video needs to start + /// The end time of where the sub video needs to end + /// Output video information. + public static async Task SubVideoAsync(string input, string output, TimeSpan startTime, TimeSpan endTime) + { + return await BaseSubVideo(input, output, startTime, endTime) + .ProcessAsynchronously(); + } + /// /// Records M3U8 streams to the specified output. /// From a31f3886fd5a21d657b7bf62f944c69a45e18d8f Mon Sep 17 00:00:00 2001 From: Kevin Heritage Date: Fri, 17 Feb 2023 19:17:44 +0100 Subject: [PATCH 31/53] docs: update documentation to include sub video function --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 990361e..d2a9633 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,15 @@ FFMpeg.Join(@"..\joined_video.mp4", ); ``` +### Create a sub video +``` csharp +FFMpeg.SubVideo(inputPath, + outputPath, + TimeSpan.FromSeconds(0) + TimeSpan.FromSeconds(30) +); +``` + ### Join images into a video: ```csharp FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, From 59d43bb1c36ccae186c31c2529dce9e73a8be2c6 Mon Sep 17 00:00:00 2001 From: Kevin Heritage Date: Fri, 17 Feb 2023 19:27:30 +0100 Subject: [PATCH 32/53] chore: apply changes to format end seek --- FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs index 5ced1c4..ea57339 100644 --- a/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Represents seek parameter @@ -14,15 +12,18 @@ public EndSeekArgument(TimeSpan? seekTo) SeekTo = seekTo; } - public string Text { - get { - if(SeekTo.HasValue) + public string Text + { + get + { + if (SeekTo.HasValue) { - int hours = SeekTo.Value.Hours; - if(SeekTo.Value.Days > 0) + var hours = SeekTo.Value.Hours; + if (SeekTo.Value.Days > 0) { hours += SeekTo.Value.Days * 24; } + return $"-to {hours.ToString("00")}:{SeekTo.Value.Minutes.ToString("00")}:{SeekTo.Value.Seconds.ToString("00")}.{SeekTo.Value.Milliseconds.ToString("000")}"; } else From 01a33f9a1f0f443fbeb9c320ca9e44eaf092badd Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 20 Feb 2023 13:18:01 +0100 Subject: [PATCH 33/53] Updated package properties for SkiaSharp --- .../FFMpegCore.Extensions.SkiaSharp.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj index 25e820a..f055149 100644 --- a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj +++ b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj @@ -2,12 +2,12 @@ true - Image extension for FFMpegCore using System.Common.Drawing + Image extension for FFMpegCore using SkiaSharp 5.0.0 - ffmpeg ffprobe convert video audio mediafile resize analyze muxing - Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev + ffmpeg ffprobe convert video audio mediafile resize analyze muxing skiasharp + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken From b63258217d4bffa4f6ab525acf73c8eb2ea8a4a0 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 20 Feb 2023 13:18:14 +0100 Subject: [PATCH 34/53] Updated example program to avoid commented-out code --- FFMpegCore.Examples/Program.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs index 24d29d5..e89de17 100644 --- a/FFMpegCore.Examples/Program.cs +++ b/FFMpegCore.Examples/Program.cs @@ -2,7 +2,10 @@ using FFMpegCore; using FFMpegCore.Enums; using FFMpegCore.Extensions.System.Drawing.Common; +using FFMpegCore.Extensions.SkiaSharp; using FFMpegCore.Pipes; +using SkiaSharp; +using FFMpegImage = FFMpegCore.Extensions.System.Drawing.Common.FFMpegImage; var inputPath = "/path/to/input"; var outputPath = "/path/to/output"; @@ -77,12 +80,14 @@ await FFMpegArguments var inputImagePath = "/path/to/input/image"; { FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath); - // or + // or using FFMpegCore.Extensions.System.Drawing.Common #pragma warning disable CA1416 - using var image = Image.FromFile(inputImagePath); // Using FFMpegCore.Extensions.System.Drawing.Common - //using var image = SKBitmap.Decode(inputImagePath); // Using FFMpegCore.Extensions.SkiaSharp + using var image = Image.FromFile(inputImagePath); image.AddAudio(inputAudioPath, outputPath); #pragma warning restore CA1416 + // or using FFMpegCore.Extensions.SkiaSharp + using var skiaSharpImage = SKBitmap.Decode(inputImagePath); + skiaSharpImage.AddAudio(inputAudioPath, outputPath); } IVideoFrame GetNextFrame() => throw new NotImplementedException(); From 9646c440bb549046775b3f52ecb62add58eef76a Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 20 Feb 2023 13:23:06 +0100 Subject: [PATCH 35/53] Fixed code style issues --- FFMpegCore.Examples/Program.cs | 2 +- FFMpegCore.Test/VideoTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs index e89de17..d7abde4 100644 --- a/FFMpegCore.Examples/Program.cs +++ b/FFMpegCore.Examples/Program.cs @@ -1,8 +1,8 @@ using System.Drawing; using FFMpegCore; using FFMpegCore.Enums; -using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Extensions.SkiaSharp; +using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; using SkiaSharp; using FFMpegImage = FFMpegCore.Extensions.System.Drawing.Common.FFMpegImage; diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 03e0b1a..a6be018 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -184,7 +184,7 @@ private static void Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(dynamic [SupportedOSPlatform("windows")] [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly_Async() => + public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly_Async() => await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(System.Drawing.Imaging.PixelFormat.Format24bppRgb, System.Drawing.Imaging.PixelFormat.Format32bppRgb); [TestMethod, Timeout(BaseTimeoutMilliseconds)] From 317ba47dd282792da0b782330c6c7461833f2b13 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 20 Feb 2023 13:46:58 +0100 Subject: [PATCH 36/53] Refactored video frame generator to pure function --- FFMpegCore.Test/Utilities/BitmapSources.cs | 26 +++++++++------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs index 8965435..6c15710 100644 --- a/FFMpegCore.Test/Utilities/BitmapSources.cs +++ b/FFMpegCore.Test/Utilities/BitmapSources.cs @@ -37,11 +37,11 @@ public static Extensions.System.Drawing.Common.BitmapVideoFrameWrapper CreateVid { var bitmap = new Bitmap(w, h, fmt); - SetVideoFramePixels(index, w, h, scaleNoise, offset, ((int x, int y, byte red, byte green, byte blue) args) => + foreach (var (x, y, red, green, blue) in GenerateVideoFramePixels(index, w, h, scaleNoise, offset)) { - var color = Color.FromArgb(args.red, args.blue, args.green); - bitmap.SetPixel(args.x, args.y, color); - }); + var color = Color.FromArgb(red, blue, green); + bitmap.SetPixel(x, y, color); + } return new Extensions.System.Drawing.Common.BitmapVideoFrameWrapper(bitmap); } @@ -51,22 +51,16 @@ public static Extensions.SkiaSharp.BitmapVideoFrameWrapper CreateVideoFrame(int var bitmap = new SKBitmap(w, h, fmt, SKAlphaType.Opaque); using var bitmapCanvas = new SKCanvas(bitmap); - SetVideoFramePixels(index, w, h, scaleNoise, offset, ((int x, int y, byte red, byte green, byte blue) args) => + foreach (var (x, y, red, green, blue) in GenerateVideoFramePixels(index, w, h, scaleNoise, offset)) { - var color = new SKColor(args.red, args.blue, args.green); - bitmapCanvas.DrawPoint(args.x, args.y, color); - }); + var color = new SKColor(red, blue, green); + bitmapCanvas.DrawPoint(x, y, color); + } return new Extensions.SkiaSharp.BitmapVideoFrameWrapper(bitmap); } - private static void SetVideoFramePixels( - int index, - int w, - int h, - float scaleNoise, - float offset, - Action<(int x, int y, byte red, byte green, byte blue)> setPixel) + private static IEnumerable<(int x, int y, byte red, byte green, byte blue)> GenerateVideoFramePixels(int index, int w, int h, float scaleNoise, float offset) { offset = offset * index; @@ -81,7 +75,7 @@ private static void SetVideoFramePixels( var value = (byte)((Perlin.Noise(nx, ny) + 1.0f) / 2.0f * 255); - setPixel((x, y, (byte)(value * xf), (byte)(value * yf), value)); + yield return ((x, y, (byte)(value * xf), (byte)(value * yf), value)); } } } From 5593bc4a4b119ca1a56ef5400383a6de4f0cde18 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 20 Feb 2023 13:50:19 +0100 Subject: [PATCH 37/53] Improved SkiaSharp CreateVideoFrame performance Execution time is now approximately one fifth of what it was previously --- FFMpegCore.Test/Utilities/BitmapSources.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs index 6c15710..f3b657a 100644 --- a/FFMpegCore.Test/Utilities/BitmapSources.cs +++ b/FFMpegCore.Test/Utilities/BitmapSources.cs @@ -50,12 +50,9 @@ public static Extensions.SkiaSharp.BitmapVideoFrameWrapper CreateVideoFrame(int { var bitmap = new SKBitmap(w, h, fmt, SKAlphaType.Opaque); - using var bitmapCanvas = new SKCanvas(bitmap); - foreach (var (x, y, red, green, blue) in GenerateVideoFramePixels(index, w, h, scaleNoise, offset)) - { - var color = new SKColor(red, blue, green); - bitmapCanvas.DrawPoint(x, y, color); - } + bitmap.Pixels = GenerateVideoFramePixels(index, w, h, scaleNoise, offset) + .Select(args => new SKColor(args.red, args.blue, args.green)) + .ToArray(); return new Extensions.SkiaSharp.BitmapVideoFrameWrapper(bitmap); } From e3aa7a5eae21a2e5f69150278f76f57732e2a28a Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 20 Feb 2023 13:57:48 +0100 Subject: [PATCH 38/53] Removed invalid assertion --- FFMpegCore.Test/VideoTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index a6be018..9814666 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -461,7 +461,8 @@ public void Video_Snapshot_InMemory_SkiaSharp() var input = FFProbe.Analyse(TestResources.Mp4Video); Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); - Assert.AreEqual(bitmap.ColorType, SkiaSharp.SKColorType.Bgra8888); + // Note: The resulting ColorType is dependent on the execution environment and therefore not assessed, + // e.g. Bgra8888 on Windows and Rgba8888 on macOS. } [TestMethod, Timeout(BaseTimeoutMilliseconds)] From 46a8ec7da5dcdd609a1a49d037502f4c68f9cd82 Mon Sep 17 00:00:00 2001 From: Dimitri Vranken Date: Mon, 20 Feb 2023 14:10:18 +0100 Subject: [PATCH 39/53] Increased unit test timeout Existing (untouched) tests are not stable in CI test execution --- FFMpegCore.Test/VideoTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 9814666..4403065 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -14,7 +14,7 @@ namespace FFMpegCore.Test [TestClass] public class VideoTest { - private const int BaseTimeoutMilliseconds = 10_000; + private const int BaseTimeoutMilliseconds = 15_000; [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToOGV() From e3a9ef3defc25f4f10f6158c2bcb82e3851ffdec Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Tue, 21 Feb 2023 16:49:47 +0100 Subject: [PATCH 40/53] Only upload to codecov on windows --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d00c29b..94a858f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,8 @@ jobs: - name: Test with dotnet run: dotnet test FFMpegCore.sln --collect "XPlat Code Coverage" --logger GitHubActions - - name: Upload coverage reports to Codecov + - if: matrix.os == 'windows-latest' + name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 with: directory: FFMpegCore.Test/TestResults From 54e216d3e0a389c7c6cf69aa159fd9ae7fc50842 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Tue, 21 Feb 2023 18:27:26 +0100 Subject: [PATCH 41/53] Bump nuget version and cleanup --- .../FFMpegCore.Extensions.SkiaSharp.csproj | 1 + FFMpegCore.Test/FFMpegCore.Test.csproj | 12 ++++----- FFMpegCore/Extend/TimeSpanExtensions.cs | 15 +++++++++++ .../FFMpeg/Arguments/EndSeekArgument.cs | 25 +++---------------- FFMpegCore/FFMpeg/Arguments/SeekArgument.cs | 25 +++---------------- FFMpegCore/FFMpegCore.csproj | 8 +++--- FFMpegCore/FFProbe/MediaAnalysis.cs | 9 ++----- FFMpegCore/Helpers/FFProbeHelper.cs | 19 +------------- 8 files changed, 37 insertions(+), 77 deletions(-) create mode 100644 FFMpegCore/Extend/TimeSpanExtensions.cs diff --git a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj index f055149..d15a7bd 100644 --- a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj +++ b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj @@ -4,6 +4,7 @@ true Image extension for FFMpegCore using SkiaSharp 5.0.0 + ../nupkg ffmpeg ffprobe convert video audio mediafile resize analyze muxing skiasharp diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 219315b..b78af1b 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -12,21 +12,21 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - - + + diff --git a/FFMpegCore/Extend/TimeSpanExtensions.cs b/FFMpegCore/Extend/TimeSpanExtensions.cs new file mode 100644 index 0000000..2d69237 --- /dev/null +++ b/FFMpegCore/Extend/TimeSpanExtensions.cs @@ -0,0 +1,15 @@ +namespace FFMpegCore.Extend; + +public static class TimeSpanExtensions +{ + public static string ToLongString(this TimeSpan timeSpan) + { + var hours = timeSpan.Hours; + if (timeSpan.Days > 0) + { + hours += timeSpan.Days * 24; + } + + return $"-ss {hours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}.{timeSpan.Milliseconds:000}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs index ea57339..e4e8f5d 100644 --- a/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs @@ -1,4 +1,6 @@ -namespace FFMpegCore.Arguments +using FFMpegCore.Extend; + +namespace FFMpegCore.Arguments { /// /// Represents seek parameter @@ -12,25 +14,6 @@ public EndSeekArgument(TimeSpan? seekTo) SeekTo = seekTo; } - public string Text - { - get - { - if (SeekTo.HasValue) - { - var hours = SeekTo.Value.Hours; - if (SeekTo.Value.Days > 0) - { - hours += SeekTo.Value.Days * 24; - } - - return $"-to {hours.ToString("00")}:{SeekTo.Value.Minutes.ToString("00")}:{SeekTo.Value.Seconds.ToString("00")}.{SeekTo.Value.Milliseconds.ToString("000")}"; - } - else - { - return string.Empty; - } - } - } + public string Text => SeekTo.HasValue ? $"-to {SeekTo.Value.ToLongString()}" : string.Empty; } } diff --git a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs index 8862e76..29cda7f 100644 --- a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs @@ -1,4 +1,6 @@ -namespace FFMpegCore.Arguments +using FFMpegCore.Extend; + +namespace FFMpegCore.Arguments { /// /// Represents seek parameter @@ -12,25 +14,6 @@ public SeekArgument(TimeSpan? seekTo) SeekTo = seekTo; } - public string Text - { - get - { - if (SeekTo.HasValue) - { - var hours = SeekTo.Value.Hours; - if (SeekTo.Value.Days > 0) - { - hours += SeekTo.Value.Days * 24; - } - - return $"-ss {hours.ToString("00")}:{SeekTo.Value.Minutes.ToString("00")}:{SeekTo.Value.Seconds.ToString("00")}.{SeekTo.Value.Milliseconds.ToString("000")}"; - } - else - { - return string.Empty; - } - } - } + public string Text => SeekTo.HasValue ? $"-ss {SeekTo.Value.ToLongString()}" : string.Empty; } } diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 843bdba..db5abd1 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -3,7 +3,7 @@ true A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 5.0.1 + 5.0.2 ../nupkg @@ -13,12 +13,12 @@ - + - - + + diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 53943dc..9fce0fe 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -50,7 +50,7 @@ private MediaFormat ParseFormat(Format analysisFormat) { var bitDepth = int.TryParse(stream.BitsPerRawSample, out var bprs) ? bprs : stream.BitsPerSample; - return bitDepth == 0 ? null : (int?)bitDepth; + return bitDepth == 0 ? null : bitDepth; } private VideoStream ParseVideoStream(FFProbeStream stream) @@ -126,7 +126,7 @@ public static class MediaAnalysisUtils { private static readonly Regex DurationRegex = new(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled); - internal static Dictionary? ToCaseInsensitive(this Dictionary? dictionary) + internal static Dictionary ToCaseInsensitive(this Dictionary? dictionary) { return dictionary?.ToDictionary(tag => tag.Key, tag => tag.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary(); } @@ -195,11 +195,6 @@ public static TimeSpan ParseDuration(string duration) } } - public static TimeSpan ParseDuration(FFProbeStream ffProbeStream) - { - return ParseDuration(ffProbeStream.Duration); - } - public static int ParseRotation(FFProbeStream fFProbeStream) { var displayMatrixSideData = fFProbeStream.SideData?.Find(item => item.TryGetValue("side_data_type", out var rawSideDataType) && rawSideDataType.ToString() == "Display Matrix"); diff --git a/FFMpegCore/Helpers/FFProbeHelper.cs b/FFMpegCore/Helpers/FFProbeHelper.cs index 0c44ab6..ff1ff20 100644 --- a/FFMpegCore/Helpers/FFProbeHelper.cs +++ b/FFMpegCore/Helpers/FFProbeHelper.cs @@ -3,27 +3,10 @@ namespace FFMpegCore.Helpers { - public class FFProbeHelper + public static class FFProbeHelper { private static bool _ffprobeVerified; - public static int Gcd(int first, int second) - { - while (first != 0 && second != 0) - { - if (first > second) - { - first -= second; - } - else - { - second -= first; - } - } - - return first == 0 ? second : first; - } - public static void RootExceptionCheck() { if (GlobalFFOptions.Current.BinaryFolder == null) From 7ebf948f931ce13c38bfd8fa12c6783cd1d984ea Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Tue, 21 Feb 2023 18:37:56 +0100 Subject: [PATCH 42/53] Fix TimeSpanExtensions --- FFMpegCore/Extend/TimeSpanExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/Extend/TimeSpanExtensions.cs b/FFMpegCore/Extend/TimeSpanExtensions.cs index 2d69237..3e70d5c 100644 --- a/FFMpegCore/Extend/TimeSpanExtensions.cs +++ b/FFMpegCore/Extend/TimeSpanExtensions.cs @@ -10,6 +10,6 @@ public static string ToLongString(this TimeSpan timeSpan) hours += timeSpan.Days * 24; } - return $"-ss {hours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}.{timeSpan.Milliseconds:000}"; + return $"{hours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}.{timeSpan.Milliseconds:000}"; } } From 718371505cb78f1bc14ee78f413be88af53e6167 Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Mon, 6 Mar 2023 17:22:08 +1300 Subject: [PATCH 43/53] Add .gif file extension --- FFMpegCore/FFMpeg/Enums/FileExtension.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/FFMpegCore/FFMpeg/Enums/FileExtension.cs b/FFMpegCore/FFMpeg/Enums/FileExtension.cs index b5e775d..f3067ba 100644 --- a/FFMpegCore/FFMpeg/Enums/FileExtension.cs +++ b/FFMpegCore/FFMpeg/Enums/FileExtension.cs @@ -20,5 +20,6 @@ public static string Extension(this Codec type) 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"; } } From 4dbbf345d4912283dc63a92d71d7400b4501783c Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Mon, 6 Mar 2023 17:25:08 +1300 Subject: [PATCH 44/53] Add "GifPalettArgument" for outputting GIFs --- .../FFMpeg/Arguments/GifPalettArgument.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs diff --git a/FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs b/FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs new file mode 100644 index 0000000..408832d --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs @@ -0,0 +1,21 @@ +using System.Drawing; + +namespace FFMpegCore.Arguments +{ + public class GifPalettArgument : IArgument + { + private readonly int _fps; + + private readonly Size? _size; + + public GifPalettArgument(int fps, Size? size) + { + _fps = fps; + _size = size; + } + + private string ScaleText => _size.HasValue ? $"scale=w={_size.Value.Width}:h={_size.Value.Height}," : string.Empty; + + public string Text => $"-filter_complex \"[0:v] fps={_fps},{ScaleText}split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer\""; + } +} From b7fd9890da19e32d9a14c8169d535f66cc2a63ae Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Tue, 7 Mar 2023 16:07:30 +1300 Subject: [PATCH 45/53] Add GifPallet argument builder --- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index cc49c5f..635ef29 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -76,6 +76,7 @@ public FFMpegArgumentOptions DeselectStreams(IEnumerable streamIndices, int public FFMpegArgumentOptions WithAudibleEncryptionKeys(string key, string iv) => WithArgument(new AudibleEncryptionKeyArgument(key, iv)); public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) => WithArgument(new AudibleEncryptionKeyArgument(activationBytes)); public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) => WithArgument(new ID3V2VersionArgument(id3v2Version)); + public FFMpegArgumentOptions WithGifPalettArgument(Size? size, int fps = 12) => WithArgument(new GifPalettArgument(fps, size)); public FFMpegArgumentOptions WithArgument(IArgument argument) { From 19c177a248d7a627671b2162534089446855fc6b Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Tue, 7 Mar 2023 16:19:03 +1300 Subject: [PATCH 46/53] Receive stream index as an argument for generating GIFs --- FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs | 7 +++++-- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs b/FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs index 408832d..9c95045 100644 --- a/FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs @@ -4,18 +4,21 @@ namespace FFMpegCore.Arguments { public class GifPalettArgument : IArgument { + private readonly int _streamIndex; + private readonly int _fps; private readonly Size? _size; - public GifPalettArgument(int fps, Size? size) + public GifPalettArgument(int streamIndex, int fps, Size? size) { + _streamIndex = streamIndex; _fps = fps; _size = size; } private string ScaleText => _size.HasValue ? $"scale=w={_size.Value.Width}:h={_size.Value.Height}," : string.Empty; - public string Text => $"-filter_complex \"[0:v] fps={_fps},{ScaleText}split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer\""; + public string Text => $"-filter_complex \"[{_streamIndex}:v] fps={_fps},{ScaleText}split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer\""; } } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 635ef29..25394e2 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -76,7 +76,7 @@ public FFMpegArgumentOptions DeselectStreams(IEnumerable streamIndices, int public FFMpegArgumentOptions WithAudibleEncryptionKeys(string key, string iv) => WithArgument(new AudibleEncryptionKeyArgument(key, iv)); public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) => WithArgument(new AudibleEncryptionKeyArgument(activationBytes)); public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) => WithArgument(new ID3V2VersionArgument(id3v2Version)); - public FFMpegArgumentOptions WithGifPalettArgument(Size? size, int fps = 12) => WithArgument(new GifPalettArgument(fps, size)); + public FFMpegArgumentOptions WithGifPalettArgument(int streamIndex, Size? size, int fps = 12) => WithArgument(new GifPalettArgument(streamIndex, fps, size)); public FFMpegArgumentOptions WithArgument(IArgument argument) { From d14ef2268f6b5ae9b9027d181db85dd1d08fe4ba Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Tue, 7 Mar 2023 16:31:45 +1300 Subject: [PATCH 47/53] Add "BuildGifSnapshotArguments" method --- FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs index 4456837..2645867 100644 --- a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs +++ b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs @@ -31,6 +31,31 @@ public static (FFMpegArguments, Action outputOptions) Bui .Resize(size)); } + public static (FFMpegArguments, Action outputOptions) BuildGifSnapshotArguments( + string input, + IMediaAnalysis source, + Size? size = null, + TimeSpan? captureTime = null, + TimeSpan? duration = null, + int? streamIndex = null, + int fps = 12) + { + var defaultGifOutputSize = new Size(480, -1); + + captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); + size = PrepareSnapshotSize(source, size) ?? defaultGifOutputSize; + streamIndex ??= source.PrimaryVideoStream?.Index + ?? source.VideoStreams.FirstOrDefault()?.Index + ?? 0; + + return (FFMpegArguments + .FromFileInput(input, false, options => options + .Seek(captureTime) + .WithDuration(duration)), + options => options + .WithGifPalettArgument((int)streamIndex, size, fps)); + } + private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize) { if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0) || source.PrimaryVideoStream == null) From a90918eac69500f914fb171a757d82f70d20f1f9 Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Tue, 7 Mar 2023 16:41:52 +1300 Subject: [PATCH 48/53] Add "GifSnapshot" and "GifSnapshotAsync" methods --- FFMpegCore/FFMpeg/FFMpeg.cs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 362a865..6cd0cd0 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -57,6 +57,36 @@ public static async Task SnapshotAsync(string input, string output, Size? .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); + } + + var source = FFProbe.Analyse(input); + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); + + return arguments + .OutputToFile(output, true, outputOptions) + .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); + } + + var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); + + return await arguments + .OutputToFile(output, true, outputOptions) + .ProcessAsynchronously(); + } + /// /// Converts an image sequence to a video. /// From c218b3592bc9e70511d8788e6ad674b3d311efcc Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Tue, 7 Mar 2023 16:44:33 +1300 Subject: [PATCH 49/53] Add unit tests --- FFMpegCore.Test/ArgumentBuilderTest.cs | 36 ++++++++++++++- FFMpegCore.Test/VideoTest.cs | 61 +++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 2c550c9..efe7f7b 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -1,4 +1,5 @@ -using FFMpegCore.Arguments; +using System.Drawing; +using FFMpegCore.Arguments; using FFMpegCore.Enums; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -537,5 +538,38 @@ public void Builder_BuildString_PadFilter_Alt() "-i \"input.mp4\" -vf \"pad=aspect=4/3:x=(ow-iw)/2:y=(oh-ih)/2:color=violet:eval=frame\" \"output.mp4\"", str); } + + [TestMethod] + public void Builder_BuildString_GifPallet() + { + var streamIndex = 0; + var size = new Size(640, 480); + + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.gif", false, opt => opt + .WithGifPalettArgument(streamIndex, size)) + .Arguments; + + Assert.AreEqual($""" + -i "input.mp4" -filter_complex "[0:v] fps=12,scale=w={size.Width}:h={size.Height},split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer" "output.gif" + """, str); + } + + [TestMethod] + public void Builder_BuildString_GifPallet_NullSize_FpsSupplied() + { + var streamIndex = 1; + + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.gif", false, opt => opt + .WithGifPalettArgument(streamIndex, null, 10)) + .Arguments; + + Assert.AreEqual($""" + -i "input.mp4" -filter_complex "[{streamIndex}:v] fps=10,split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer" "output.gif" + """, str); + } } } diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 4403065..5071a48 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -1,4 +1,5 @@ -using System.Drawing.Imaging; +using System.Drawing; +using System.Drawing.Imaging; using System.Runtime.Versioning; using System.Text; using FFMpegCore.Arguments; @@ -479,6 +480,64 @@ public void Video_Snapshot_PersistSnapshot() Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); } + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_GifSnapshot_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.gif"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.GifSnapshot(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0)); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreNotEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreNotEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_GifSnapshot_PersistSnapshot_SizeSupplied() + { + using var outputPath = new TemporaryFile("out.gif"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + var desiredGifSize = new Size(320, 240); + + FFMpeg.GifSnapshot(TestResources.Mp4Video, outputPath, desiredGifSize, captureTime: TimeSpan.FromSeconds(0)); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreNotEqual(input.PrimaryVideoStream!.Width, desiredGifSize.Width); + Assert.AreNotEqual(input.PrimaryVideoStream.Height, desiredGifSize.Height); + Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public async Task Video_GifSnapshot_PersistSnapshotAsync() + { + using var outputPath = new TemporaryFile("out.gif"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0)); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreNotEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreNotEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public async Task Video_GifSnapshot_PersistSnapshotAsync_SizeSupplied() + { + using var outputPath = new TemporaryFile("out.gif"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + var desiredGifSize = new Size(320, 240); + + await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, desiredGifSize, captureTime: TimeSpan.FromSeconds(0)); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreNotEqual(input.PrimaryVideoStream!.Width, desiredGifSize.Width); + Assert.AreNotEqual(input.PrimaryVideoStream.Height, desiredGifSize.Height); + Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); + } + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_Join() { From 62a9ed628185b2d7a0ab9dc32e61fa5784b2ef6f Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Tue, 7 Mar 2023 16:49:18 +1300 Subject: [PATCH 50/53] Update README to add information about GIF Snapshots --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index d2a9633..365d0be 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,17 @@ var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); ``` +### You can also capture GIF snapshots from a video file: +```csharp +FFMpeg.GifSnapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromSeconds(10)); + +// or async +await FFMpeg.GifSnapshotAsync(inputPath, outputPath, new Size(200, 400), TimeSpan.FromSeconds(10)); + +// you can also supply -1 to either one of Width/Height Size properties if you'd like FFMPEG to resize while maintaining the aspect ratio +await FFMpeg.GifSnapshotAsync(inputPath, outputPath, new Size(480, -1), TimeSpan.FromSeconds(10)); +``` + ### Join video parts into one single file: ```csharp FFMpeg.Join(@"..\joined_video.mp4", From 7569669f526baafcc0f23421d917d60376156231 Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Tue, 7 Mar 2023 16:55:11 +1300 Subject: [PATCH 51/53] Fix Palette typos --- FFMpegCore.Test/ArgumentBuilderTest.cs | 8 ++++---- .../{GifPalettArgument.cs => GifPaletteArgument.cs} | 4 ++-- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 2 +- FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename FFMpegCore/FFMpeg/Arguments/{GifPalettArgument.cs => GifPaletteArgument.cs} (83%) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index efe7f7b..30adabd 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -540,7 +540,7 @@ public void Builder_BuildString_PadFilter_Alt() } [TestMethod] - public void Builder_BuildString_GifPallet() + public void Builder_BuildString_GifPalette() { var streamIndex = 0; var size = new Size(640, 480); @@ -548,7 +548,7 @@ public void Builder_BuildString_GifPallet() var str = FFMpegArguments .FromFileInput("input.mp4") .OutputToFile("output.gif", false, opt => opt - .WithGifPalettArgument(streamIndex, size)) + .WithGifPaletteArgument(streamIndex, size)) .Arguments; Assert.AreEqual($""" @@ -557,14 +557,14 @@ public void Builder_BuildString_GifPallet() } [TestMethod] - public void Builder_BuildString_GifPallet_NullSize_FpsSupplied() + public void Builder_BuildString_GifPalette_NullSize_FpsSupplied() { var streamIndex = 1; var str = FFMpegArguments .FromFileInput("input.mp4") .OutputToFile("output.gif", false, opt => opt - .WithGifPalettArgument(streamIndex, null, 10)) + .WithGifPaletteArgument(streamIndex, null, 10)) .Arguments; Assert.AreEqual($""" diff --git a/FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs b/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs similarity index 83% rename from FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs rename to FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs index 9c95045..ac67fcd 100644 --- a/FFMpegCore/FFMpeg/Arguments/GifPalettArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs @@ -2,7 +2,7 @@ namespace FFMpegCore.Arguments { - public class GifPalettArgument : IArgument + public class GifPaletteArgument : IArgument { private readonly int _streamIndex; @@ -10,7 +10,7 @@ public class GifPalettArgument : IArgument private readonly Size? _size; - public GifPalettArgument(int streamIndex, int fps, Size? size) + public GifPaletteArgument(int streamIndex, int fps, Size? size) { _streamIndex = streamIndex; _fps = fps; diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 25394e2..4930b52 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -76,7 +76,7 @@ public FFMpegArgumentOptions DeselectStreams(IEnumerable streamIndices, int public FFMpegArgumentOptions WithAudibleEncryptionKeys(string key, string iv) => WithArgument(new AudibleEncryptionKeyArgument(key, iv)); public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) => WithArgument(new AudibleEncryptionKeyArgument(activationBytes)); public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) => WithArgument(new ID3V2VersionArgument(id3v2Version)); - public FFMpegArgumentOptions WithGifPalettArgument(int streamIndex, Size? size, int fps = 12) => WithArgument(new GifPalettArgument(streamIndex, fps, size)); + public FFMpegArgumentOptions WithGifPaletteArgument(int streamIndex, Size? size, int fps = 12) => WithArgument(new GifPaletteArgument(streamIndex, fps, size)); public FFMpegArgumentOptions WithArgument(IArgument argument) { diff --git a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs index 2645867..0d9b414 100644 --- a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs +++ b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs @@ -53,7 +53,7 @@ public static (FFMpegArguments, Action outputOptions) Bui .Seek(captureTime) .WithDuration(duration)), options => options - .WithGifPalettArgument((int)streamIndex, size, fps)); + .WithGifPaletteArgument((int)streamIndex, size, fps)); } private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize) From 3dc2fff0ac8c67567ab5e12cfba2822ac9fbbaf6 Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Tue, 7 Mar 2023 17:04:23 +1300 Subject: [PATCH 52/53] Fix whitespace formatting linting error --- FFMpegCore/FFMpeg/FFMpeg.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 6cd0cd0..a8de12b 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -57,7 +57,7 @@ public static async Task SnapshotAsync(string input, string output, Size? .ProcessAsynchronously(); } - public static bool GifSnapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int ? streamIndex = null) + 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) { From 943662aa15e8cf73dce0b1a8b93ffe8e8bae274b Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Wed, 15 Mar 2023 13:26:11 +0100 Subject: [PATCH 53/53] Bump nuget version --- FFMpegCore/FFMpegCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index db5abd1..2af7f16 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -3,7 +3,7 @@ true A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 5.0.2 + 5.1.0 ../nupkg