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