diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs
index 2c550c9..30adabd 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_GifPalette()
+ {
+ var streamIndex = 0;
+ var size = new Size(640, 480);
+
+ var str = FFMpegArguments
+ .FromFileInput("input.mp4")
+ .OutputToFile("output.gif", false, opt => opt
+ .WithGifPaletteArgument(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_GifPalette_NullSize_FpsSupplied()
+ {
+ var streamIndex = 1;
+
+ var str = FFMpegArguments
+ .FromFileInput("input.mp4")
+ .OutputToFile("output.gif", false, opt => opt
+ .WithGifPaletteArgument(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()
{
diff --git a/FFMpegCore/FFMpeg/Arguments/CopyCodecArgument.cs b/FFMpegCore/FFMpeg/Arguments/CopyCodecArgument.cs
new file mode 100644
index 0000000..8ea3484
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/CopyCodecArgument.cs
@@ -0,0 +1,10 @@
+namespace FFMpegCore.Arguments
+{
+ ///
+ /// Represents a copy codec parameter
+ ///
+ public class CopyCodecArgument : IArgument
+ {
+ public string Text => $"-codec copy";
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs b/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs
new file mode 100644
index 0000000..ac67fcd
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs
@@ -0,0 +1,24 @@
+using System.Drawing;
+
+namespace FFMpegCore.Arguments
+{
+ public class GifPaletteArgument : IArgument
+ {
+ private readonly int _streamIndex;
+
+ private readonly int _fps;
+
+ private readonly Size? _size;
+
+ public GifPaletteArgument(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 \"[{_streamIndex}:v] fps={_fps},{ScaleText}split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer\"";
+ }
+}
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";
}
}
diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs
index 362a865..820d9fb 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.
///
@@ -303,7 +333,10 @@ public static bool SaveM3U8Stream(Uri uri, string output)
}
return FFMpegArguments
- .FromUrlInput(uri)
+ .FromUrlInput(uri, options =>
+ {
+ options.WithCopyCodec();
+ })
.OutputToFile(output)
.ProcessSynchronously();
}
diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs
index cc49c5f..6a6586c 100644
--- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs
+++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs
@@ -76,6 +76,8 @@ 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 WithGifPaletteArgument(int streamIndex, Size? size, int fps = 12) => WithArgument(new GifPaletteArgument(streamIndex, fps, size));
+ public FFMpegArgumentOptions WithCopyCodec() => WithArgument(new CopyCodecArgument());
public FFMpegArgumentOptions WithArgument(IArgument argument)
{
diff --git a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs
index 4456837..0d9b414 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
+ .WithGifPaletteArgument((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)
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
diff --git a/README.md b/README.md
index d2a9633..33f7ddf 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",
@@ -76,7 +87,7 @@ FFMpeg.Join(@"..\joined_video.mp4",
``` csharp
FFMpeg.SubVideo(inputPath,
outputPath,
- TimeSpan.FromSeconds(0)
+ TimeSpan.FromSeconds(0),
TimeSpan.FromSeconds(30)
);
```