diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a6fe6cb..6946920 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,12 +18,12 @@ jobs:
timeout-minutes: 6
steps:
- name: Checkout
- uses: actions/checkout@v1
+ uses: actions/checkout@v2
- name: Prepare .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '5.0.x'
- name: Prepare FFMpeg
- uses: FedericoCarboni/setup-ffmpeg@v1-beta
+ uses: FedericoCarboni/setup-ffmpeg@v1
- name: Test with dotnet
run: dotnet test --logger GitHubActions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0f10b27..5ef0a4c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v1
+ uses: actions/checkout@v2
- name: Prepare .NET
uses: actions/setup-dotnet@v1
with:
diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj
new file mode 100644
index 0000000..f9daae7
--- /dev/null
+++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs
new file mode 100644
index 0000000..256ef3c
--- /dev/null
+++ b/FFMpegCore.Examples/Program.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using FFMpegCore;
+using FFMpegCore.Enums;
+using FFMpegCore.Pipes;
+using FFMpegCore.Extend;
+
+var inputPath = "/path/to/input";
+var outputPath = "/path/to/output";
+
+{
+ var mediaInfo = FFProbe.Analyse(inputPath);
+}
+
+{
+ var mediaInfo = await FFProbe.AnalyseAsync(inputPath);
+}
+
+{
+ FFMpegArguments
+ .FromFileInput(inputPath)
+ .OutputToFile(outputPath, false, options => options
+ .WithVideoCodec(VideoCodec.LibX264)
+ .WithConstantRateFactor(21)
+ .WithAudioCodec(AudioCodec.Aac)
+ .WithVariableBitrate(4)
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(VideoSize.Hd))
+ .WithFastStart())
+ .ProcessSynchronously();
+}
+
+{
+ // process the snapshot in-memory and use the Bitmap directly
+ var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
+
+ // or persists the image on the drive
+ FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
+}
+
+var inputStream = new MemoryStream();
+var outputStream = new MemoryStream();
+
+{
+ await FFMpegArguments
+ .FromPipeInput(new StreamPipeSource(inputStream))
+ .OutputToPipe(new StreamPipeSink(outputStream), options => options
+ .WithVideoCodec("vp9")
+ .ForceFormat("webm"))
+ .ProcessAsynchronously();
+}
+
+{
+ FFMpeg.Join(@"..\joined_video.mp4",
+ @"..\part1.mp4",
+ @"..\part2.mp4",
+ @"..\part3.mp4"
+ );
+}
+
+{
+ FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1,
+ ImageInfo.FromPath(@"..\1.png"),
+ ImageInfo.FromPath(@"..\2.png"),
+ ImageInfo.FromPath(@"..\3.png")
+ );
+}
+
+{
+ FFMpeg.Mute(inputPath, outputPath);
+}
+
+{
+ FFMpeg.ExtractAudio(inputPath, outputPath);
+}
+
+var inputAudioPath = "/path/to/input/audio";
+{
+ FFMpeg.ReplaceAudio(inputPath, inputAudioPath, outputPath);
+}
+
+var inputImagePath = "/path/to/input/image";
+{
+ FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath);
+ // or
+ var image = Image.FromFile(inputImagePath);
+ image.AddAudio(inputAudioPath, outputPath);
+}
+
+IVideoFrame GetNextFrame() => throw new NotImplementedException();
+{
+ IEnumerable CreateFrames(int count)
+ {
+ for(int i = 0; i < count; i++)
+ {
+ yield return GetNextFrame(); //method of generating new frames
+ }
+ }
+
+ var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable or IEnumerator to constructor of RawVideoPipeSource
+ {
+ FrameRate = 30 //set source frame rate
+ };
+ await FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputPath, false, options => options
+ .WithVideoCodec(VideoCodec.LibVpx))
+ .ProcessAsynchronously();
+}
+
+{
+ // setting global options
+ GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
+ // or
+ GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin");
+
+ // or individual, per-run options
+ await FFMpegArguments
+ .FromFileInput(inputPath)
+ .OutputToFile(outputPath)
+ .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
+}
\ No newline at end of file
diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs
index f56c4d3..dda6c84 100644
--- a/FFMpegCore.Test/ArgumentBuilderTest.cs
+++ b/FFMpegCore.Test/ArgumentBuilderTest.cs
@@ -8,7 +8,7 @@ namespace FFMpegCore.Test
[TestClass]
public class ArgumentBuilderTest
{
- private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4"};
+ private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4" };
[TestMethod]
@@ -21,28 +21,35 @@ public void Builder_BuildString_IO_1()
[TestMethod]
public void Builder_BuildString_Scale()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.Scale(VideoSize.Hd)).Arguments;
- Assert.AreEqual("-i \"input.mp4\" -vf scale=-1:720 \"output.mp4\" -y", str);
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", true, opt => opt
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(VideoSize.Hd)))
+ .Arguments;
+ Assert.AreEqual("-i \"input.mp4\" -vf \"scale=-1:720\" \"output.mp4\" -y", str);
}
-
+
[TestMethod]
public void Builder_BuildString_AudioCodec()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.WithAudioCodec(AudioCodec.Aac)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", true, opt => opt.WithAudioCodec(AudioCodec.Aac)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -c:a aac \"output.mp4\" -y", str);
}
-
+
[TestMethod]
public void Builder_BuildString_AudioBitrate()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.WithAudioBitrate(AudioQuality.Normal)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", true, opt => opt.WithAudioBitrate(AudioQuality.Normal)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -b:a 128k \"output.mp4\" -y", str);
}
-
+
[TestMethod]
public void Builder_BuildString_Quiet()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").WithGlobalOptions(opt => opt.WithVerbosityLevel()).OutputToFile("output.mp4", false).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4").WithGlobalOptions(opt => opt.WithVerbosityLevel())
+ .OutputToFile("output.mp4", false).Arguments;
Assert.AreEqual("-hide_banner -loglevel error -i \"input.mp4\" \"output.mp4\"", str);
}
@@ -50,27 +57,32 @@ public void Builder_BuildString_Quiet()
[TestMethod]
public void Builder_BuildString_AudioCodec_Fluent()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithAudioCodec(AudioCodec.Aac).WithAudioBitrate(128)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false,
+ opt => opt.WithAudioCodec(AudioCodec.Aac).WithAudioBitrate(128)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -c:a aac -b:a 128k \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_BitStream()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithBitStreamFilter(Channel.Audio, Filter.H264_Mp4ToAnnexB)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false,
+ opt => opt.WithBitStreamFilter(Channel.Audio, Filter.H264_Mp4ToAnnexB)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -bsf:a h264_mp4toannexb \"output.mp4\"", str);
}
-
+
[TestMethod]
public void Builder_BuildString_HardwareAcceleration_Auto()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithHardwareAcceleration()).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithHardwareAcceleration()).Arguments;
Assert.AreEqual("-i \"input.mp4\" -hwaccel \"output.mp4\"", str);
}
+
[TestMethod]
public void Builder_BuildString_HardwareAcceleration_Specific()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithHardwareAcceleration(HardwareAccelerationDevice.CUVID)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false,
+ opt => opt.WithHardwareAcceleration(HardwareAccelerationDevice.CUVID)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -hwaccel cuvid \"output.mp4\"", str);
}
@@ -84,98 +96,138 @@ public void Builder_BuildString_Concat()
[TestMethod]
public void Builder_BuildString_Copy_Audio()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Audio)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Audio)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -c:a copy \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Copy_Video()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Video)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Video)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -c:v copy \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Copy_Both()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.CopyChannel()).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.CopyChannel()).Arguments;
Assert.AreEqual("-i \"input.mp4\" -c copy \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_DisableChannel_Audio()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Audio)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Audio)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -an \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_DisableChannel_Video()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Video)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Video)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -vn \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_AudioSamplingRate_Default()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate()).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate()).Arguments;
Assert.AreEqual("-i \"input.mp4\" -ar 48000 \"output.mp4\"", str);
}
-
+
[TestMethod]
public void Builder_BuildString_AudioSamplingRate()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate(44000)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate(44000)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -ar 44000 \"output.mp4\"", str);
}
-
+
[TestMethod]
public void Builder_BuildString_VariableBitrate()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithVariableBitrate(5)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithVariableBitrate(5)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -vbr 5 \"output.mp4\"", str);
}
-
+
[TestMethod]
public void Builder_BuildString_Faststart()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithFastStart()).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithFastStart()).Arguments;
Assert.AreEqual("-i \"input.mp4\" -movflags faststart \"output.mp4\"", str);
}
-
+
[TestMethod]
public void Builder_BuildString_Overwrite()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.OverwriteExisting()).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.OverwriteExisting()).Arguments;
Assert.AreEqual("-i \"input.mp4\" -y \"output.mp4\"", str);
}
-
+
[TestMethod]
public void Builder_BuildString_RemoveMetadata()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithoutMetadata()).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithoutMetadata()).Arguments;
Assert.AreEqual("-i \"input.mp4\" -map_metadata -1 \"output.mp4\"", str);
}
-
+
[TestMethod]
public void Builder_BuildString_Transpose()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Transpose(Transposition.CounterClockwise90)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt
+ .WithVideoFilters(filterOptions => filterOptions
+ .Transpose(Transposition.CounterClockwise90)))
+ .Arguments;
Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2\" \"output.mp4\"", str);
}
+ [TestMethod]
+ public void Builder_BuildString_Mirroring()
+ {
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt
+ .WithVideoFilters(filterOptions => filterOptions
+ .Mirror(Mirroring.Horizontal)))
+ .Arguments;
+ Assert.AreEqual("-i \"input.mp4\" -vf \"hflip\" \"output.mp4\"", str);
+ }
+
+ [TestMethod]
+ public void Builder_BuildString_TransposeScale()
+ {
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt
+ .WithVideoFilters(filterOptions => filterOptions
+ .Transpose(Transposition.CounterClockwise90)
+ .Scale(200, 300)))
+ .Arguments;
+ Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2, scale=200:300\" \"output.mp4\"", str);
+ }
+
[TestMethod]
public void Builder_BuildString_ForceFormat()
{
- var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)).OutputToFile("output.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.ForceFormat(VideoType.Mp4))
+ .OutputToFile("output.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)).Arguments;
Assert.AreEqual("-f mp4 -i \"input.mp4\" -f mp4 \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_FrameOutputCount()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithFrameOutputCount(50)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithFrameOutputCount(50)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -vframes 50 \"output.mp4\"", str);
}
@@ -189,14 +241,16 @@ public void Builder_BuildString_VideoStreamNumber()
[TestMethod]
public void Builder_BuildString_FrameRate()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithFramerate(50)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithFramerate(50)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -r 50 \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Loop()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Loop(50)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Loop(50))
+ .Arguments;
Assert.AreEqual("-i \"input.mp4\" -loop 50 \"output.mp4\"", str);
}
@@ -204,27 +258,30 @@ public void Builder_BuildString_Loop()
public void Builder_BuildString_Seek()
{
var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).OutputToFile("output.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).Arguments;
- Assert.AreEqual("-ss 00:00:10 -i \"input.mp4\" -ss 00:00:10 \"output.mp4\"", str);
+ Assert.AreEqual("-ss 00:00:10.000 -i \"input.mp4\" -ss 00:00:10.000 \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Shortest()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.UsingShortest()).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.UsingShortest()).Arguments;
Assert.AreEqual("-i \"input.mp4\" -shortest \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Size()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Resize(1920, 1080)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.Resize(1920, 1080)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -s 1920x1080 \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Speed()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithSpeedPreset(Speed.Fast)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithSpeedPreset(Speed.Fast)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -preset fast \"output.mp4\"", str);
}
@@ -234,18 +291,21 @@ public void Builder_BuildString_DrawtextFilter()
var str = FFMpegArguments
.FromFileInput("input.mp4")
.OutputToFile("output.mp4", false, opt => opt
- .DrawText(DrawTextOptions
- .Create("Stack Overflow", "/path/to/font.ttf")
- .WithParameter("fontcolor", "white")
- .WithParameter("fontsize", "24")
- .WithParameter("box", "1")
- .WithParameter("boxcolor", "black@0.5")
- .WithParameter("boxborderw", "5")
- .WithParameter("x", "(w-text_w)/2")
- .WithParameter("y", "(h-text_h)/2")))
+ .WithVideoFilters(filterOptions => filterOptions
+ .DrawText(DrawTextOptions
+ .Create("Stack Overflow", "/path/to/font.ttf")
+ .WithParameter("fontcolor", "white")
+ .WithParameter("fontsize", "24")
+ .WithParameter("box", "1")
+ .WithParameter("boxcolor", "black@0.5")
+ .WithParameter("boxborderw", "5")
+ .WithParameter("x", "(w-text_w)/2")
+ .WithParameter("y", "(h-text_h)/2"))))
.Arguments;
- Assert.AreEqual("-i \"input.mp4\" -vf drawtext=\"text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=(h-text_h)/2\" \"output.mp4\"", str);
+ Assert.AreEqual(
+ "-i \"input.mp4\" -vf \"drawtext=text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=(h-text_h)/2\" \"output.mp4\"",
+ str);
}
[TestMethod]
@@ -254,45 +314,53 @@ public void Builder_BuildString_DrawtextFilter_Alt()
var str = FFMpegArguments
.FromFileInput("input.mp4")
.OutputToFile("output.mp4", false, opt => opt
- .DrawText(DrawTextOptions
- .Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24"))))
+ .WithVideoFilters(filterOptions => filterOptions
+ .DrawText(DrawTextOptions
+ .Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24")))))
.Arguments;
- Assert.AreEqual("-i \"input.mp4\" -vf drawtext=\"text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24\" \"output.mp4\"", str);
+ Assert.AreEqual(
+ "-i \"input.mp4\" -vf \"drawtext=text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24\" \"output.mp4\"",
+ str);
}
-
+
[TestMethod]
public void Builder_BuildString_StartNumber()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithStartNumber(50)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithStartNumber(50)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -start_number 50 \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Threads_1()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.UsingThreads(50)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.UsingThreads(50)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -threads 50 \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Threads_2()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.UsingMultithreading(true)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.UsingMultithreading(true)).Arguments;
Assert.AreEqual($"-i \"input.mp4\" -threads {Environment.ProcessorCount} \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Codec()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithVideoCodec(VideoCodec.LibX264)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithVideoCodec(VideoCodec.LibX264)).Arguments;
Assert.AreEqual("-i \"input.mp4\" -c:v libx264 \"output.mp4\"", str);
}
[TestMethod]
public void Builder_BuildString_Codec_Override()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.WithVideoCodec(VideoCodec.LibX264).ForcePixelFormat("yuv420p")).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true,
+ opt => opt.WithVideoCodec(VideoCodec.LibX264).ForcePixelFormat("yuv420p")).Arguments;
Assert.AreEqual("-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\" -y", str);
}
@@ -300,17 +368,20 @@ public void Builder_BuildString_Codec_Override()
[TestMethod]
public void Builder_BuildString_Duration()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithDuration(TimeSpan.FromSeconds(20))).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithDuration(TimeSpan.FromSeconds(20))).Arguments;
Assert.AreEqual("-i \"input.mp4\" -t 00:00:20 \"output.mp4\"", str);
}
-
+
[TestMethod]
public void Builder_BuildString_Raw()
{
- var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.WithCustomArgument(null!)).OutputToFile("output.mp4", false, opt => opt.WithCustomArgument(null!)).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.WithCustomArgument(null!))
+ .OutputToFile("output.mp4", false, opt => opt.WithCustomArgument(null!)).Arguments;
Assert.AreEqual(" -i \"input.mp4\" \"output.mp4\"", str);
- str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithCustomArgument("-acodec copy")).Arguments;
+ str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.WithCustomArgument("-acodec copy")).Arguments;
Assert.AreEqual("-i \"input.mp4\" -acodec copy \"output.mp4\"", str);
}
@@ -318,7 +389,8 @@ public void Builder_BuildString_Raw()
[TestMethod]
public void Builder_BuildString_ForcePixelFormat()
{
- var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments;
+ var str = FFMpegArguments.FromFileInput("input.mp4")
+ .OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments;
Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str);
}
}
diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs
index bd53541..f1abb72 100644
--- a/FFMpegCore.Test/AudioTest.cs
+++ b/FFMpegCore.Test/AudioTest.cs
@@ -1,11 +1,13 @@
-using System;
-using FFMpegCore.Enums;
+using FFMpegCore.Enums;
+using FFMpegCore.Exceptions;
+using FFMpegCore.Pipes;
using FFMpegCore.Test.Resources;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
-using FFMpegCore.Pipes;
namespace FFMpegCore.Test
{
@@ -70,5 +72,155 @@ public void Image_AddAudio()
Assert.IsTrue(analysis.Duration.TotalSeconds > 0);
Assert.IsTrue(File.Exists(outputFile));
}
+
+ [TestMethod, Timeout(10000)]
+ public void Audio_ToAAC_Args_Pipe()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var samples = new List
+ {
+ new PcmAudioSampleWrapper(new byte[] { 0, 0 }),
+ new PcmAudioSampleWrapper(new byte[] { 0, 0 }),
+ };
+
+ var audioSamplesSource = new RawAudioPipeSource(samples)
+ {
+ Channels = 2,
+ Format = "s8",
+ SampleRate = 8000,
+ };
+
+ var success = FFMpegArguments
+ .FromPipeInput(audioSamplesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.Aac))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
+ }
+
+ [TestMethod, Timeout(10000)]
+ public void Audio_ToLibVorbis_Args_Pipe()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var samples = new List
+ {
+ new PcmAudioSampleWrapper(new byte[] { 0, 0 }),
+ new PcmAudioSampleWrapper(new byte[] { 0, 0 }),
+ };
+
+ var audioSamplesSource = new RawAudioPipeSource(samples)
+ {
+ Channels = 2,
+ Format = "s8",
+ SampleRate = 8000,
+ };
+
+ var success = FFMpegArguments
+ .FromPipeInput(audioSamplesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.LibVorbis))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
+ }
+
+ [TestMethod, Timeout(10000)]
+ public async Task Audio_ToAAC_Args_Pipe_Async()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var samples = new List
+ {
+ new PcmAudioSampleWrapper(new byte[] { 0, 0 }),
+ new PcmAudioSampleWrapper(new byte[] { 0, 0 }),
+ };
+
+ var audioSamplesSource = new RawAudioPipeSource(samples)
+ {
+ Channels = 2,
+ Format = "s8",
+ SampleRate = 8000,
+ };
+
+ var success = await FFMpegArguments
+ .FromPipeInput(audioSamplesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.Aac))
+ .ProcessAsynchronously();
+ Assert.IsTrue(success);
+ }
+
+ [TestMethod, Timeout(10000)]
+ public void Audio_ToAAC_Args_Pipe_ValidDefaultConfiguration()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var samples = new List
+ {
+ new PcmAudioSampleWrapper(new byte[] { 0, 0 }),
+ new PcmAudioSampleWrapper(new byte[] { 0, 0 }),
+ };
+
+ var audioSamplesSource = new RawAudioPipeSource(samples);
+
+ var success = FFMpegArguments
+ .FromPipeInput(audioSamplesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.Aac))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
+ }
+
+ [TestMethod, Timeout(10000)]
+ public void Audio_ToAAC_Args_Pipe_InvalidChannels()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var audioSamplesSource = new RawAudioPipeSource(new List())
+ {
+ Channels = 0,
+ };
+
+ var ex = Assert.ThrowsException(() => FFMpegArguments
+ .FromPipeInput(audioSamplesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.Aac))
+ .ProcessSynchronously());
+ }
+
+ [TestMethod, Timeout(10000)]
+ public void Audio_ToAAC_Args_Pipe_InvalidFormat()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var audioSamplesSource = new RawAudioPipeSource(new List())
+ {
+ Format = "s8le",
+ };
+
+ var ex = Assert.ThrowsException(() => FFMpegArguments
+ .FromPipeInput(audioSamplesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.Aac))
+ .ProcessSynchronously());
+ }
+
+ [TestMethod, Timeout(10000)]
+ public void Audio_ToAAC_Args_Pipe_InvalidSampleRate()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var audioSamplesSource = new RawAudioPipeSource(new List())
+ {
+ SampleRate = 0,
+ };
+
+ var ex = Assert.ThrowsException(() => FFMpegArguments
+ .FromPipeInput(audioSamplesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.Aac))
+ .ProcessSynchronously());
+ }
}
}
\ No newline at end of file
diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj
index 2cd97bb..98c9274 100644
--- a/FFMpegCore.Test/FFMpegCore.Test.csproj
+++ b/FFMpegCore.Test/FFMpegCore.Test.csproj
@@ -39,10 +39,10 @@
-
-
-
-
+
+
+
+
diff --git a/FFMpegCore.Test/FFMpegOptionsTests.cs b/FFMpegCore.Test/FFMpegOptionsTests.cs
index d175644..2be810f 100644
--- a/FFMpegCore.Test/FFMpegOptionsTests.cs
+++ b/FFMpegCore.Test/FFMpegOptionsTests.cs
@@ -10,39 +10,39 @@ public class FFMpegOptionsTest
[TestMethod]
public void Options_Initialized()
{
- Assert.IsNotNull(FFMpegOptions.Options);
+ Assert.IsNotNull(GlobalFFOptions.Current);
}
[TestMethod]
public void Options_Defaults_Configured()
{
- Assert.AreEqual(new FFMpegOptions().RootDirectory, $"");
+ Assert.AreEqual(new FFOptions().BinaryFolder, $"");
}
[TestMethod]
public void Options_Loaded_From_File()
{
Assert.AreEqual(
- FFMpegOptions.Options.RootDirectory,
- JsonConvert.DeserializeObject(File.ReadAllText("ffmpeg.config.json")).RootDirectory
+ GlobalFFOptions.Current.BinaryFolder,
+ JsonConvert.DeserializeObject(File.ReadAllText("ffmpeg.config.json")).BinaryFolder
);
}
[TestMethod]
public void Options_Set_Programmatically()
{
- var original = FFMpegOptions.Options;
+ var original = GlobalFFOptions.Current;
try
{
- FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "Whatever" });
+ GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" });
Assert.AreEqual(
- FFMpegOptions.Options.RootDirectory,
+ GlobalFFOptions.Current.BinaryFolder,
"Whatever"
);
}
finally
{
- FFMpegOptions.Configure(original);
+ GlobalFFOptions.Configure(original);
}
}
}
diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs
index 21d9d34..5cabc4e 100644
--- a/FFMpegCore.Test/FFProbeTests.cs
+++ b/FFMpegCore.Test/FFProbeTests.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System;
+using System.IO;
using System.Threading.Tasks;
using FFMpegCore.Test.Resources;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -23,16 +24,39 @@ public async Task Audio_FromStream_Duration()
var streamAnalysis = await FFProbe.AnalyseAsync(inputStream);
Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration);
}
+
+ [DataTestMethod]
+ [DataRow("0:00:03.008000", 0, 0, 0, 3, 8)]
+ [DataRow("05:12:59.177", 0, 5, 12, 59, 177)]
+ [DataRow("149:07:50.911750", 6, 5, 7, 50, 911)]
+ [DataRow("00:00:00.83", 0, 0, 0, 0, 830)]
+ public void MediaAnalysis_ParseDuration(string duration, int expectedDays, int expectedHours, int expectedMinutes, int expectedSeconds, int expectedMilliseconds)
+ {
+ var ffprobeStream = new FFProbeStream { Duration = duration };
+
+ var parsedDuration = MediaAnalysisUtils.ParseDuration(ffprobeStream);
+
+ Assert.AreEqual(expectedDays, parsedDuration.Days);
+ Assert.AreEqual(expectedHours, parsedDuration.Hours);
+ Assert.AreEqual(expectedMinutes, parsedDuration.Minutes);
+ Assert.AreEqual(expectedSeconds, parsedDuration.Seconds);
+ Assert.AreEqual(expectedMilliseconds, parsedDuration.Milliseconds);
+ }
+
+ [TestMethod]
+ public async Task Uri_Duration()
+ {
+ var fileAnalysis = await FFProbe.AnalyseAsync(new Uri("https://github.com/rosenbjerg/FFMpegCore/raw/master/FFMpegCore.Test/Resources/input_3sec.webm"));
+ Assert.IsNotNull(fileAnalysis);
+ }
[TestMethod]
public void Probe_Success()
{
var info = FFProbe.Analyse(TestResources.Mp4Video);
Assert.AreEqual(3, info.Duration.Seconds);
- Assert.AreEqual(".mp4", info.Extension);
- Assert.AreEqual(TestResources.Mp4Video, info.Path);
- Assert.AreEqual("5.1", info.PrimaryAudioStream.ChannelLayout);
+ Assert.AreEqual("5.1", info.PrimaryAudioStream!.ChannelLayout);
Assert.AreEqual(6, info.PrimaryAudioStream.Channels);
Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName);
Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName);
@@ -40,7 +64,7 @@ public void Probe_Success()
Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate);
Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz);
- Assert.AreEqual(1471810, info.PrimaryVideoStream.BitRate);
+ Assert.AreEqual(1471810, info.PrimaryVideoStream!.BitRate);
Assert.AreEqual(16, info.PrimaryVideoStream.DisplayAspectRatio.Width);
Assert.AreEqual(9, info.PrimaryVideoStream.DisplayAspectRatio.Height);
Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat);
diff --git a/FFMpegCore.Test/Resources/TestResources.cs b/FFMpegCore.Test/Resources/TestResources.cs
index f37ed0c..6277dd3 100644
--- a/FFMpegCore.Test/Resources/TestResources.cs
+++ b/FFMpegCore.Test/Resources/TestResources.cs
@@ -1,8 +1,4 @@
-using System;
-using System.IO;
-using FFMpegCore.Enums;
-
-namespace FFMpegCore.Test.Resources
+namespace FFMpegCore.Test.Resources
{
public enum AudioType
{
diff --git a/FFMpegCore.Test/TasksExtensions.cs b/FFMpegCore.Test/TasksExtensions.cs
deleted file mode 100644
index c9549ca..0000000
--- a/FFMpegCore.Test/TasksExtensions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Threading.Tasks;
-
-namespace FFMpegCore.Test
-{
- static class TasksExtensions
- {
- public static T WaitForResult(this Task task) =>
- task.ConfigureAwait(false).GetAwaiter().GetResult();
- }
-}
diff --git a/FFMpegCore.Test/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs
similarity index 98%
rename from FFMpegCore.Test/BitmapSources.cs
rename to FFMpegCore.Test/Utilities/BitmapSources.cs
index c3e8d40..8ea02e8 100644
--- a/FFMpegCore.Test/BitmapSources.cs
+++ b/FFMpegCore.Test/Utilities/BitmapSources.cs
@@ -21,7 +21,7 @@ public static IEnumerable CreateBitmaps(int count, PixelFormat fmt,
}
}
- private static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset)
+ public static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset)
{
var bitmap = new Bitmap(w, h, fmt);
diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs
index eb5b46b..45853ea 100644
--- a/FFMpegCore.Test/VideoTest.cs
+++ b/FFMpegCore.Test/VideoTest.cs
@@ -12,245 +12,64 @@
using FFMpegCore.Arguments;
using FFMpegCore.Exceptions;
using FFMpegCore.Pipes;
+using System.Threading;
namespace FFMpegCore.Test
{
[TestClass]
public class VideoTest
{
- public bool Convert(ContainerFormat type, bool multithreaded = false, VideoSize size = VideoSize.Original)
+ [TestMethod, Timeout(10000)]
+ public void Video_ToOGV()
{
- using var outputFile = new TemporaryFile($"out{type.Extension}");
-
- var input = FFProbe.Analyse(TestResources.Mp4Video);
- FFMpeg.Convert(input, outputFile, type, size: size, multithreaded: multithreaded);
- var outputVideo = FFProbe.Analyse(outputFile);
-
- Assert.IsTrue(File.Exists(outputFile));
- Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1);
- if (size == VideoSize.Original)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height);
- }
- else
- {
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height);
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, (int)size);
- }
-
- return File.Exists(outputFile) &&
- outputVideo.Duration == input.Duration &&
- (
- (
- size == VideoSize.Original &&
- outputVideo.PrimaryVideoStream.Width == input.PrimaryVideoStream.Width &&
- outputVideo.PrimaryVideoStream.Height == input.PrimaryVideoStream.Height
- ) ||
- (
- size != VideoSize.Original &&
- outputVideo.PrimaryVideoStream.Width != input.PrimaryVideoStream.Width &&
- outputVideo.PrimaryVideoStream.Height != input.PrimaryVideoStream.Height &&
- outputVideo.PrimaryVideoStream.Height == (int)size
- )
- );
- }
-
- private void ConvertFromStreamPipe(ContainerFormat type, params IArgument[] arguments)
- {
- using var outputFile = new TemporaryFile($"out{type.Extension}");
+ using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}");
- var input = FFProbe.Analyse(TestResources.WebmVideo);
- using var inputStream = File.OpenRead(input.Path);
- var processor = FFMpegArguments
- .FromPipeInput(new StreamPipeSource(inputStream))
- .OutputToFile(outputFile, false, opt =>
- {
- foreach (var arg in arguments)
- opt.WithArgument(arg);
- });
-
- var scaling = arguments.OfType().FirstOrDefault();
-
- var success = processor.ProcessSynchronously();
-
- var outputVideo = FFProbe.Analyse(outputFile);
-
+ var success = FFMpegArguments
+ .FromFileInput(TestResources.WebmVideo)
+ .OutputToFile(outputFile, false)
+ .ProcessSynchronously();
Assert.IsTrue(success);
- Assert.IsTrue(File.Exists(outputFile));
- Assert.IsTrue(Math.Abs((outputVideo.Duration - input.Duration).TotalMilliseconds) < 1000.0 / input.PrimaryVideoStream.FrameRate);
-
- if (scaling?.Size == null)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height);
- }
- else
- {
- if (scaling.Size.Value.Width != -1)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width);
- }
-
- if (scaling.Size.Value.Height != -1)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height);
- }
-
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height);
- }
- }
-
- private void ConvertToStreamPipe(params IArgument[] arguments)
- {
- using var ms = new MemoryStream();
- var processor = FFMpegArguments
- .FromFileInput(TestResources.Mp4Video)
- .OutputToPipe(new StreamPipeSink(ms), opt =>
- {
- foreach (var arg in arguments)
- opt.WithArgument(arg);
- });
-
- var scaling = arguments.OfType().FirstOrDefault();
-
- processor.ProcessSynchronously();
-
- ms.Position = 0;
- var outputVideo = FFProbe.Analyse(ms);
-
- var input = FFProbe.Analyse(TestResources.Mp4Video);
- // Assert.IsTrue(Math.Abs((outputVideo.Duration - input.Duration).TotalMilliseconds) < 1000.0 / input.PrimaryVideoStream.FrameRate);
-
- if (scaling?.Size == null)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height);
- }
- else
- {
- if (scaling.Size.Value.Width != -1)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width);
- }
-
- if (scaling.Size.Value.Height != -1)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height);
- }
-
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height);
- }
- }
-
- public void Convert(ContainerFormat type, Action validationMethod, params IArgument[] arguments)
- {
- using var outputFile = new TemporaryFile($"out{type.Extension}");
-
- var input = FFProbe.Analyse(TestResources.Mp4Video);
-
- var processor = FFMpegArguments
- .FromFileInput(TestResources.Mp4Video)
- .OutputToFile(outputFile, false, opt =>
- {
- foreach (var arg in arguments)
- opt.WithArgument(arg);
- });
-
- var scaling = arguments.OfType().FirstOrDefault();
- processor.ProcessSynchronously();
-
- var outputVideo = FFProbe.Analyse(outputFile);
-
- Assert.IsTrue(File.Exists(outputFile));
- Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1);
- validationMethod?.Invoke(outputVideo);
- if (scaling?.Size == null)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height);
- }
- else
- {
- if (scaling.Size.Value.Width != -1)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width);
- }
-
- if (scaling.Size.Value.Height != -1)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height);
- }
-
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width);
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height);
- }
- }
-
- public void Convert(ContainerFormat type, params IArgument[] inputArguments)
- {
- Convert(type, null, inputArguments);
- }
-
- public void ConvertFromPipe(ContainerFormat type, System.Drawing.Imaging.PixelFormat fmt, params IArgument[] arguments)
- {
- using var outputFile = new TemporaryFile($"out{type.Extension}");
-
- var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, fmt, 256, 256));
- var processor = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(outputFile, false, opt =>
- {
- foreach (var arg in arguments)
- opt.WithArgument(arg);
- });
- var scaling = arguments.OfType().FirstOrDefault();
- processor.ProcessSynchronously();
-
- var outputVideo = FFProbe.Analyse(outputFile);
-
- Assert.IsTrue(File.Exists(outputFile));
-
- if (scaling?.Size == null)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width);
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height);
- }
- else
- {
- if (scaling.Size.Value.Width != -1)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width);
- }
-
- if (scaling.Size.Value.Height != -1)
- {
- Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height);
- }
-
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width);
- Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height);
- }
}
[TestMethod, Timeout(10000)]
public void Video_ToMP4()
{
- Convert(VideoType.Mp4);
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var success = FFMpegArguments
+ .FromFileInput(TestResources.WebmVideo)
+ .OutputToFile(outputFile, false)
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
}
[TestMethod, Timeout(10000)]
public void Video_ToMP4_YUV444p()
{
- Convert(VideoType.Mp4, (a) => Assert.IsTrue(a.VideoStreams.First().PixelFormat == "yuv444p"),
- new ForcePixelFormat("yuv444p"));
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var success = FFMpegArguments
+ .FromFileInput(TestResources.WebmVideo)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264)
+ .ForcePixelFormat("yuv444p"))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
+ var analysis = FFProbe.Analyse(outputFile);
+ Assert.IsTrue(analysis.VideoStreams.First().PixelFormat == "yuv444p");
}
[TestMethod, Timeout(10000)]
public void Video_ToMP4_Args()
{
- Convert(VideoType.Mp4, new VideoCodecArgument(VideoCodec.LibX264));
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var success = FFMpegArguments
+ .FromFileInput(TestResources.WebmVideo)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
}
[DataTestMethod, Timeout(10000)]
@@ -258,13 +77,115 @@ public void Video_ToMP4_Args()
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
{
- ConvertFromPipe(VideoType.Mp4, pixelFormat, new VideoCodecArgument(VideoCodec.LibX264));
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256));
+ var success = FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
+ }
+
+ [TestMethod, Timeout(10000)]
+ public void Video_ToMP4_Args_Pipe_DifferentImageSizes()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ 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)
+ };
+
+ var videoFramesSource = new RawVideoPipeSource(frames);
+ var ex = Assert.ThrowsException(() => FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessSynchronously());
+
+ Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
+ }
+
+
+ [TestMethod, Timeout(10000)]
+ public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ 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)
+ };
+
+ var videoFramesSource = new RawVideoPipeSource(frames);
+ var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessAsynchronously());
+
+ Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
+ }
+
+ [TestMethod, Timeout(10000)]
+ public void Video_ToMP4_Args_Pipe_DifferentPixelFormats()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ 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)
+ };
+
+ var videoFramesSource = new RawVideoPipeSource(frames);
+ var ex = Assert.ThrowsException(() => FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessSynchronously());
+
+ Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
+ }
+
+
+ [TestMethod, Timeout(10000)]
+ public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ 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)
+ };
+
+ var videoFramesSource = new RawVideoPipeSource(frames);
+ var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessAsynchronously());
+
+ Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
}
[TestMethod, Timeout(10000)]
public void Video_ToMP4_Args_StreamPipe()
{
- ConvertFromStreamPipe(VideoType.Mp4, new VideoCodecArgument(VideoCodec.LibX264));
+ using var input = File.OpenRead(TestResources.WebmVideo);
+ using var output = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var success = FFMpegArguments
+ .FromPipeInput(new StreamPipeSource(input))
+ .OutputToFile(output, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
}
[TestMethod, Timeout(10000)]
@@ -276,18 +197,21 @@ await Assert.ThrowsExceptionAsync(async () =>
var pipeSource = new StreamPipeSink(ms);
await FFMpegArguments
.FromFileInput(TestResources.Mp4Video)
- .OutputToPipe(pipeSource, opt => opt.ForceFormat("mkv"))
+ .OutputToPipe(pipeSource, opt => opt.ForceFormat("mp4"))
.ProcessAsynchronously();
});
}
+
+
[TestMethod, Timeout(10000)]
public void Video_StreamFile_OutputToMemoryStream()
{
var output = new MemoryStream();
FFMpegArguments
- .FromPipeInput(new StreamPipeSource(File.OpenRead(TestResources.WebmVideo)), options => options.ForceFormat("webm"))
- .OutputToPipe(new StreamPipeSink(output), options => options
+ .FromPipeInput(new StreamPipeSource(File.OpenRead(TestResources.WebmVideo)), opt => opt
+ .ForceFormat("webm"))
+ .OutputToPipe(new StreamPipeSink(output), opt => opt
.ForceFormat("mpegts"))
.ProcessSynchronously();
@@ -299,32 +223,41 @@ public void Video_StreamFile_OutputToMemoryStream()
[TestMethod, Timeout(10000)]
public void Video_ToMP4_Args_StreamOutputPipe_Failure()
{
- Assert.ThrowsException(() => ConvertToStreamPipe(new ForceFormatArgument("mkv")));
+ Assert.ThrowsException(() =>
+ {
+ using var ms = new MemoryStream();
+ FFMpegArguments
+ .FromFileInput(TestResources.Mp4Video)
+ .OutputToPipe(new StreamPipeSink(ms), opt => opt
+ .ForceFormat("mkv"))
+ .ProcessSynchronously();
+ });
}
[TestMethod, Timeout(10000)]
- public void Video_ToMP4_Args_StreamOutputPipe_Async()
+ public async Task Video_ToMP4_Args_StreamOutputPipe_Async()
{
- using var ms = new MemoryStream();
+ await using var ms = new MemoryStream();
var pipeSource = new StreamPipeSink(ms);
- FFMpegArguments
+ await FFMpegArguments
.FromFileInput(TestResources.Mp4Video)
.OutputToPipe(pipeSource, opt => opt
.WithVideoCodec(VideoCodec.LibX264)
.ForceFormat("matroska"))
- .ProcessAsynchronously()
- .WaitForResult();
+ .ProcessAsynchronously();
}
[TestMethod, Timeout(10000)]
public async Task TestDuplicateRun()
{
- FFMpegArguments.FromFileInput(TestResources.Mp4Video)
+ FFMpegArguments
+ .FromFileInput(TestResources.Mp4Video)
.OutputToFile("temporary.mp4")
.ProcessSynchronously();
- await FFMpegArguments.FromFileInput(TestResources.Mp4Video)
+ await FFMpegArguments
+ .FromFileInput(TestResources.Mp4Video)
.OutputToFile("temporary.mp4")
.ProcessAsynchronously();
@@ -332,65 +265,115 @@ await FFMpegArguments.FromFileInput(TestResources.Mp4Video)
}
[TestMethod, Timeout(10000)]
- public void Video_ToMP4_Args_StreamOutputPipe()
+ public void TranscodeToMemoryStream_Success()
{
- ConvertToStreamPipe(new VideoCodecArgument(VideoCodec.LibX264), new ForceFormatArgument("matroska"));
+ using var output = new MemoryStream();
+ var success = FFMpegArguments
+ .FromFileInput(TestResources.WebmVideo)
+ .OutputToPipe(new StreamPipeSink(output), opt => opt
+ .WithVideoCodec(VideoCodec.LibVpx)
+ .ForceFormat("matroska"))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
+
+ output.Position = 0;
+ var inputAnalysis = FFProbe.Analyse(TestResources.WebmVideo);
+ var outputAnalysis = FFProbe.Analyse(output);
+ Assert.AreEqual(inputAnalysis.Duration.TotalSeconds, outputAnalysis.Duration.TotalSeconds, 0.3);
}
[TestMethod, Timeout(10000)]
public void Video_ToTS()
{
- Convert(VideoType.Ts);
+ using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}");
+
+ var success = FFMpegArguments
+ .FromFileInput(TestResources.Mp4Video)
+ .OutputToFile(outputFile, false)
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
}
[TestMethod, Timeout(10000)]
public void Video_ToTS_Args()
{
- Convert(VideoType.Ts,
- new CopyArgument(),
- new BitStreamFilterArgument(Channel.Video, Filter.H264_Mp4ToAnnexB),
- new ForceFormatArgument(VideoType.MpegTs));
+ using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}");
+
+ var success = FFMpegArguments
+ .FromFileInput(TestResources.Mp4Video)
+ .OutputToFile(outputFile, false, opt => opt
+ .CopyChannel()
+ .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB)
+ .ForceFormat(VideoType.MpegTs))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
}
[DataTestMethod, Timeout(10000)]
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
- public void Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
+ public async Task Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
{
- ConvertFromPipe(VideoType.Ts, pixelFormat, new ForceFormatArgument(VideoType.Ts));
+ using var output = new TemporaryFile($"out{VideoType.Ts.Extension}");
+ var input = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256));
+
+ var success = await FFMpegArguments
+ .FromPipeInput(input)
+ .OutputToFile(output, false, opt => opt
+ .ForceFormat(VideoType.Ts))
+ .ProcessAsynchronously();
+ Assert.IsTrue(success);
+
+ var analysis = await FFProbe.AnalyseAsync(output);
+ Assert.AreEqual(VideoType.Ts.Name, analysis.Format.FormatName);
}
[TestMethod, Timeout(10000)]
- public void Video_ToOGV_Resize()
+ public async Task Video_ToOGV_Resize()
{
- Convert(VideoType.Ogv, true, VideoSize.Ed);
- }
-
- [TestMethod, Timeout(10000)]
- public void Video_ToOGV_Resize_Args()
- {
- Convert(VideoType.Ogv, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora));
+ using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}");
+ var success = await FFMpegArguments
+ .FromFileInput(TestResources.Mp4Video)
+ .OutputToFile(outputFile, false, opt => opt
+ .Resize(200, 200)
+ .WithVideoCodec(VideoCodec.LibTheora))
+ .ProcessAsynchronously();
+ Assert.IsTrue(success);
}
[DataTestMethod, Timeout(10000)]
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
// [DataRow(PixelFormat.Format48bppRgb)]
- public void Video_ToOGV_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
+ public void RawVideoPipeSource_Ogv_Scale(System.Drawing.Imaging.PixelFormat pixelFormat)
{
- ConvertFromPipe(VideoType.Ogv, pixelFormat, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora));
+ using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}");
+ var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256));
+
+ FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(VideoSize.Ed))
+ .WithVideoCodec(VideoCodec.LibTheora))
+ .ProcessSynchronously();
+
+ var analysis = FFProbe.Analyse(outputFile);
+ Assert.AreEqual((int)VideoSize.Ed, analysis.PrimaryVideoStream!.Width);
}
[TestMethod, Timeout(10000)]
- public void Video_ToMP4_Resize()
+ public void Scale_Mp4_Multithreaded()
{
- Convert(VideoType.Mp4, true, VideoSize.Ed);
- }
-
- [TestMethod, Timeout(10000)]
- public void Video_ToMP4_Resize_Args()
- {
- Convert(VideoType.Mp4, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264));
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var success = FFMpegArguments
+ .FromFileInput(TestResources.Mp4Video)
+ .OutputToFile(outputFile, false, opt => opt
+ .UsingMultithreading(true)
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
}
[DataTestMethod, Timeout(10000)]
@@ -399,40 +382,24 @@ public void Video_ToMP4_Resize_Args()
// [DataRow(PixelFormat.Format48bppRgb)]
public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
{
- ConvertFromPipe(VideoType.Mp4, pixelFormat, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264));
- }
-
- [TestMethod, Timeout(10000)]
- public void Video_ToOGV()
- {
- Convert(VideoType.Ogv);
- }
-
- [TestMethod, Timeout(10000)]
- public void Video_ToMP4_MultiThread()
- {
- Convert(VideoType.Mp4, true);
- }
-
- [TestMethod, Timeout(10000)]
- public void Video_ToTS_MultiThread()
- {
- Convert(VideoType.Ts, true);
- }
-
- [TestMethod, Timeout(10000)]
- public void Video_ToOGV_MultiThread()
- {
- Convert(VideoType.Ogv, true);
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+ var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256));
+
+ var success = FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessSynchronously();
+ Assert.IsTrue(success);
}
[TestMethod, Timeout(10000)]
public void Video_Snapshot_InMemory()
{
var input = FFProbe.Analyse(TestResources.Mp4Video);
- using var bitmap = FFMpeg.Snapshot(input);
+ using var bitmap = FFMpeg.Snapshot(TestResources.Mp4Video);
- Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width);
+ Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width);
Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height);
Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png);
}
@@ -443,10 +410,10 @@ public void Video_Snapshot_PersistSnapshot()
var outputPath = new TemporaryFile("out.png");
var input = FFProbe.Analyse(TestResources.Mp4Video);
- FFMpeg.Snapshot(input, outputPath);
+ FFMpeg.Snapshot(TestResources.Mp4Video, outputPath);
using var bitmap = Image.FromFile(outputPath);
- Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width);
+ Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width);
Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height);
Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png);
}
@@ -469,7 +436,7 @@ public void Video_Join()
Assert.AreEqual(expectedDuration.Hours, result.Duration.Hours);
Assert.AreEqual(expectedDuration.Minutes, result.Duration.Minutes);
Assert.AreEqual(expectedDuration.Seconds, result.Duration.Seconds);
- Assert.AreEqual(input.PrimaryVideoStream.Height, result.PrimaryVideoStream.Height);
+ Assert.AreEqual(input.PrimaryVideoStream!.Height, result.PrimaryVideoStream!.Height);
Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width);
}
@@ -493,7 +460,7 @@ public void Video_Join_Image_Sequence()
Assert.IsTrue(success);
var result = FFProbe.Analyse(outputFile);
Assert.AreEqual(3, result.Duration.Seconds);
- Assert.AreEqual(imageSet.First().Width, result.PrimaryVideoStream.Width);
+ Assert.AreEqual(imageSet.First().Width, result.PrimaryVideoStream!.Width);
Assert.AreEqual(imageSet.First().Height, result.PrimaryVideoStream.Height);
}
@@ -502,7 +469,7 @@ public void Video_With_Only_Audio_Should_Extract_Metadata()
{
var video = FFProbe.Analyse(TestResources.Mp4WithoutVideo);
Assert.AreEqual(null, video.PrimaryVideoStream);
- Assert.AreEqual("aac", video.PrimaryAudioStream.CodecName);
+ Assert.AreEqual("aac", video.PrimaryAudioStream!.CodecName);
Assert.AreEqual(10, video.Duration.TotalSeconds, 0.5);
}
@@ -557,7 +524,7 @@ public void Video_OutputsData()
var outputFile = new TemporaryFile("out.mp4");
var dataReceived = false;
- FFMpegOptions.Configure(opt => opt.Encoding = Encoding.UTF8);
+ GlobalFFOptions.Configure(opt => opt.Encoding = Encoding.UTF8);
var success = FFMpegArguments
.FromFileInput(TestResources.Mp4Video)
.WithGlobalOptions(options => options
@@ -588,7 +555,7 @@ public void Video_TranscodeInMemory()
resStream.Position = 0;
var vi = FFProbe.Analyse(resStream);
- Assert.AreEqual(vi.PrimaryVideoStream.Width, 128);
+ Assert.AreEqual(vi.PrimaryVideoStream!.Width, 128);
Assert.AreEqual(vi.PrimaryVideoStream.Height, 128);
}
@@ -596,24 +563,114 @@ public void Video_TranscodeInMemory()
public async Task Video_Cancel_Async()
{
var outputFile = new TemporaryFile("out.mp4");
-
+
var task = FFMpegArguments
- .FromFileInput(TestResources.Mp4Video)
+ .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args
+ .WithCustomArgument("-re")
+ .ForceFormat("lavfi"))
.OutputToFile(outputFile, false, opt => opt
- .Resize(new Size(1000, 1000))
.WithAudioCodec(AudioCodec.Aac)
.WithVideoCodec(VideoCodec.LibX264)
- .WithConstantRateFactor(14)
- .WithSpeedPreset(Speed.VerySlow)
- .Loop(3))
+ .WithSpeedPreset(Speed.VeryFast))
.CancellableThrough(out var cancel)
.ProcessAsynchronously(false);
-
+
await Task.Delay(300);
cancel();
-
+
var result = await task;
+
Assert.IsFalse(result);
}
+
+ [TestMethod, Timeout(10000)]
+ public async Task Video_Cancel_Async_With_Timeout()
+ {
+ var outputFile = new TemporaryFile("out.mp4");
+
+ var task = FFMpegArguments
+ .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args
+ .WithCustomArgument("-re")
+ .ForceFormat("lavfi"))
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.Aac)
+ .WithVideoCodec(VideoCodec.LibX264)
+ .WithSpeedPreset(Speed.VeryFast))
+ .CancellableThrough(out var cancel, 10000)
+ .ProcessAsynchronously(false);
+
+ await Task.Delay(300);
+ cancel();
+
+ var result = await task;
+
+ var outputInfo = await FFProbe.AnalyseAsync(outputFile);
+
+ Assert.IsTrue(result);
+ Assert.IsNotNull(outputInfo);
+ Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width);
+ Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height);
+ Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName);
+ Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName);
+ }
+
+ [TestMethod, Timeout(10000)]
+ public async Task Video_Cancel_CancellationToken_Async()
+ {
+ var outputFile = new TemporaryFile("out.mp4");
+
+ var cts = new CancellationTokenSource();
+
+ var task = FFMpegArguments
+ .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args
+ .WithCustomArgument("-re")
+ .ForceFormat("lavfi"))
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.Aac)
+ .WithVideoCodec(VideoCodec.LibX264)
+ .WithSpeedPreset(Speed.VeryFast))
+ .CancellableThrough(cts.Token)
+ .ProcessAsynchronously(false);
+
+ await Task.Delay(300);
+ cts.Cancel();
+
+ var result = await task;
+
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod, Timeout(10000)]
+ public async Task Video_Cancel_CancellationToken_Async_With_Timeout()
+ {
+ var outputFile = new TemporaryFile("out.mp4");
+
+ var cts = new CancellationTokenSource();
+
+ var task = FFMpegArguments
+ .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args
+ .WithCustomArgument("-re")
+ .ForceFormat("lavfi"))
+ .OutputToFile(outputFile, false, opt => opt
+ .WithAudioCodec(AudioCodec.Aac)
+ .WithVideoCodec(VideoCodec.LibX264)
+ .WithSpeedPreset(Speed.VeryFast))
+ .CancellableThrough(cts.Token, 5000)
+ .ProcessAsynchronously(false);
+
+ await Task.Delay(300);
+ cts.Cancel();
+
+ var result = await task;
+
+ var outputInfo = await FFProbe.AnalyseAsync(outputFile);
+
+ Assert.IsTrue(result);
+ Assert.IsNotNull(outputInfo);
+ Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width);
+ Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height);
+ Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName);
+ Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName);
+ }
}
}
diff --git a/FFMpegCore.sln b/FFMpegCore.sln
index eab20fd..7a27980 100644
--- a/FFMpegCore.sln
+++ b/FFMpegCore.sln
@@ -1,12 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.28307.329
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31005.135
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore", "FFMpegCore\FFMpegCore.csproj", "{19DE2EC2-9955-4712-8096-C22EF6713E4F}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore", "FFMpegCore\FFMpegCore.csproj", "{19DE2EC2-9955-4712-8096-C22EF6713E4F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Test", "FFMpegCore.Test\FFMpegCore.Test.csproj", "{F20C8353-72D9-454B-9F16-3624DBAD2328}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Examples", "FFMpegCore.Examples\FFMpegCore.Examples.csproj", "{3125CF91-FFBD-4E4E-8930-247116AFE772}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
{F20C8353-72D9-454B-9F16-3624DBAD2328}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F20C8353-72D9-454B-9F16-3624DBAD2328}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F20C8353-72D9-454B-9F16-3624DBAD2328}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3125CF91-FFBD-4E4E-8930-247116AFE772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3125CF91-FFBD-4E4E-8930-247116AFE772}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/FFMpegCore/Assembly.cs b/FFMpegCore/Assembly.cs
new file mode 100644
index 0000000..0117671
--- /dev/null
+++ b/FFMpegCore/Assembly.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("FFMpegCore.Test")]
\ No newline at end of file
diff --git a/FFMpegCore/Extend/BitmapExtensions.cs b/FFMpegCore/Extend/BitmapExtensions.cs
index bf10336..e2f5505 100644
--- a/FFMpegCore/Extend/BitmapExtensions.cs
+++ b/FFMpegCore/Extend/BitmapExtensions.cs
@@ -6,7 +6,7 @@ namespace FFMpegCore.Extend
{
public static class BitmapExtensions
{
- public static bool AddAudio(this Bitmap poster, string audio, string output)
+ public static bool AddAudio(this Image poster, string audio, string output)
{
var destination = $"{Environment.TickCount}.png";
poster.Save(destination);
diff --git a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs
index e2f0737..678bdcb 100644
--- a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs
+++ b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs
@@ -1,6 +1,7 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
+using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -24,7 +25,7 @@ public BitmapVideoFrameWrapper(Bitmap bitmap)
Format = ConvertStreamFormat(bitmap.PixelFormat);
}
- public void Serialize(System.IO.Stream stream)
+ public void Serialize(Stream stream)
{
var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat);
@@ -40,7 +41,7 @@ public void Serialize(System.IO.Stream stream)
}
}
- public async Task SerializeAsync(System.IO.Stream stream, CancellationToken token)
+ public async Task SerializeAsync(Stream stream, CancellationToken token)
{
var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat);
@@ -67,6 +68,8 @@ private static string ConvertStreamFormat(PixelFormat fmt)
{
case PixelFormat.Format16bppGrayScale:
return "gray16le";
+ case PixelFormat.Format16bppRgb555:
+ return "bgr555le";
case PixelFormat.Format16bppRgb565:
return "bgr565le";
case PixelFormat.Format24bppRgb:
diff --git a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs
new file mode 100644
index 0000000..d67038b
--- /dev/null
+++ b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs
@@ -0,0 +1,27 @@
+using FFMpegCore.Pipes;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+public class PcmAudioSampleWrapper : IAudioSample
+{
+ //This could actually be short or int, but copies would be inefficient.
+ //Handling bytes lets the user decide on the conversion, and abstract the library
+ //from handling shorts, unsigned shorts, integers, unsigned integers and floats.
+ private readonly byte[] _sample;
+
+ public PcmAudioSampleWrapper(byte[] sample)
+ {
+ _sample = sample;
+ }
+
+ public void Serialize(Stream stream)
+ {
+ stream.Write(_sample, 0, _sample.Length);
+ }
+
+ public async Task SerializeAsync(Stream stream, CancellationToken token)
+ {
+ await stream.WriteAsync(_sample, 0, _sample.Length, token);
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs
index 5651802..c672c74 100644
--- a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs
@@ -18,7 +18,7 @@ public DemuxConcatArgument(IEnumerable values)
{
Values = values.Select(value => $"file '{value}'");
}
- private readonly string _tempFileName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid() + ".txt");
+ private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"concat_{Guid.NewGuid()}.txt");
public void Pre() => File.WriteAllLines(_tempFileName, Values);
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
diff --git a/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs
index d4eabb8..c148328 100644
--- a/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs
@@ -6,7 +6,7 @@ namespace FFMpegCore.Arguments
///
/// Drawtext video filter argument
///
- public class DrawTextArgument : IArgument
+ public class DrawTextArgument : IVideoFilterArgument
{
public readonly DrawTextOptions Options;
@@ -15,7 +15,8 @@ public DrawTextArgument(DrawTextOptions options)
Options = options;
}
- public string Text => $"-vf drawtext=\"{Options.TextInternal}\"";
+ public string Key { get; } = "drawtext";
+ public string Value => Options.TextInternal;
}
public class DrawTextOptions
diff --git a/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs
new file mode 100644
index 0000000..f276bbb
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs
@@ -0,0 +1,26 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FFMpegCore.Arguments
+{
+ ///
+ /// Represents an input device parameter
+ ///
+ public class InputDeviceArgument : IInputArgument
+ {
+ private readonly string Device;
+
+ public InputDeviceArgument(string device)
+ {
+ Device = device;
+ }
+
+ public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
+
+ public void Pre() { }
+
+ public void Post() { }
+
+ public string Text => $"-i {Device}";
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs
index 479fa90..199d324 100644
--- a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs
@@ -17,7 +17,7 @@ public InputPipeArgument(IPipeSource writer) : base(PipeDirection.Out)
Writer = writer;
}
- public override string Text => $"-y {Writer.GetStreamArguments()} -i \"{PipePath}\"";
+ public override string Text => $"{Writer.GetStreamArguments()} -i \"{PipePath}\"";
protected override async Task ProcessDataAsync(CancellationToken token)
{
diff --git a/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs
new file mode 100644
index 0000000..15cbef9
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs
@@ -0,0 +1,27 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FFMpegCore.Arguments
+{
+ ///
+ /// Represents outputting to url using supported protocols
+ /// See http://ffmpeg.org/ffmpeg-protocols.html
+ ///
+ public class OutputUrlArgument : IOutputArgument
+ {
+ public readonly string Url;
+
+ public OutputUrlArgument(string url)
+ {
+ Url = url;
+ }
+
+ public void Post() { }
+
+ public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
+
+ public void Pre() { }
+
+ public string Text => Url;
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs
index 428f21b..fcb944a 100644
--- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs
@@ -40,14 +40,17 @@ public async Task During(CancellationToken cancellationToken = default)
{
try
{
- await ProcessDataAsync(cancellationToken);
- Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}");
- Pipe?.Disconnect();
+ await ProcessDataAsync(cancellationToken);
}
catch (TaskCanceledException)
{
Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled");
}
+ finally
+ {
+ Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}");
+ Pipe?.Disconnect();
+ }
}
protected abstract Task ProcessDataAsync(CancellationToken token);
diff --git a/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs b/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs
index 40e98d0..6ed2b31 100644
--- a/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs
@@ -6,7 +6,7 @@ namespace FFMpegCore.Arguments
///
/// Represents scale parameter
///
- public class ScaleArgument : IArgument
+ public class ScaleArgument : IVideoFilterArgument
{
public readonly Size? Size;
public ScaleArgument(Size? size)
@@ -18,9 +18,10 @@ public ScaleArgument(int width, int height) : this(new Size(width, height)) { }
public ScaleArgument(VideoSize videosize)
{
- Size = videosize == VideoSize.Original ? new Size(-1, -1) : new Size(-1, (int)videosize);
+ Size = videosize == VideoSize.Original ? null : (Size?)new Size(-1, (int)videosize);
}
- public virtual string Text => Size.HasValue ? $"-vf scale={Size.Value.Width}:{Size.Value.Height}" : string.Empty;
+ public string Key { get; } = "scale";
+ public string Value => Size == null ? string.Empty : $"{Size.Value.Width}:{Size.Value.Height}";
}
}
diff --git a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs
index 1057b88..1b58890 100644
--- a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs
@@ -8,11 +8,28 @@ namespace FFMpegCore.Arguments
public class SeekArgument : IArgument
{
public readonly TimeSpan? SeekTo;
+
public SeekArgument(TimeSpan? seekTo)
{
SeekTo = seekTo;
}
-
- public string Text => !SeekTo.HasValue ? string.Empty : $"-ss {SeekTo.Value}";
+
+ public string Text {
+ get {
+ if(SeekTo.HasValue)
+ {
+ int 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;
+ }
+ }
+ }
}
}
diff --git a/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs b/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs
new file mode 100644
index 0000000..fff98f3
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs
@@ -0,0 +1,30 @@
+using FFMpegCore.Enums;
+using System;
+
+namespace FFMpegCore.Arguments
+{
+ public class SetMirroringArgument : IVideoFilterArgument
+ {
+ public SetMirroringArgument(Mirroring mirroring)
+ {
+ Mirroring = mirroring;
+ }
+
+ public Mirroring Mirroring { get; set; }
+
+ public string Key => string.Empty;
+
+ public string Value
+ {
+ get
+ {
+ return Mirroring switch
+ {
+ Mirroring.Horizontal => "hflip",
+ Mirroring.Vertical => "vflip",
+ _ => throw new ArgumentOutOfRangeException(nameof(Mirroring))
+ };
+ }
+ }
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs
index 2ccde92..924c0a0 100644
--- a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs
@@ -1,19 +1,20 @@
using System.Drawing;
-using FFMpegCore.Enums;
namespace FFMpegCore.Arguments
{
///
/// Represents size parameter
///
- public class SizeArgument : ScaleArgument
+ public class SizeArgument : IArgument
{
- public SizeArgument(Size? value) : base(value) { }
+ public readonly Size? Size;
+ public SizeArgument(Size? size)
+ {
+ Size = size;
+ }
- public SizeArgument(VideoSize videosize) : base(videosize) { }
+ public SizeArgument(int width, int height) : this(new Size(width, height)) { }
- public SizeArgument(int width, int height) : base(width, height) { }
-
- public override string Text => Size.HasValue ? $"-s {Size.Value.Width}x{Size.Value.Height}" : string.Empty;
+ public string Text => Size == null ? string.Empty : $"-s {Size.Value.Width}x{Size.Value.Height}";
}
}
diff --git a/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs
index acc26f4..bd15c47 100644
--- a/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs
@@ -9,7 +9,7 @@ namespace FFMpegCore.Arguments
/// 2 = 90CounterClockwise
/// 3 = 90Clockwise and Vertical Flip
///
- public class TransposeArgument : IArgument
+ public class TransposeArgument : IVideoFilterArgument
{
public readonly Transposition Transposition;
public TransposeArgument(Transposition transposition)
@@ -17,6 +17,7 @@ public TransposeArgument(Transposition transposition)
Transposition = transposition;
}
- public string Text => $"-vf \"transpose={(int)Transposition}\"";
+ public string Key { get; } = "transpose";
+ public string Value => ((int)Transposition).ToString();
}
}
\ No newline at end of file
diff --git a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs
new file mode 100644
index 0000000..fa4ae1e
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using FFMpegCore.Enums;
+using FFMpegCore.Exceptions;
+
+namespace FFMpegCore.Arguments
+{
+ public class VideoFiltersArgument : IArgument
+ {
+ public readonly VideoFilterOptions Options;
+
+ public VideoFiltersArgument(VideoFilterOptions options)
+ {
+ Options = options;
+ }
+
+ public string Text => GetText();
+
+ private string GetText()
+ {
+ if (!Options.Arguments.Any())
+ throw new FFMpegArgumentException("No video-filter arguments provided");
+
+ var arguments = Options.Arguments
+ .Where(arg => !string.IsNullOrEmpty(arg.Value))
+ .Select(arg =>
+ {
+ var escapedValue = arg.Value.Replace(",", "\\,");
+ return string.IsNullOrEmpty(arg.Key) ? escapedValue : $"{arg.Key}={escapedValue}";
+ });
+
+ return $"-vf \"{string.Join(", ", arguments)}\"";
+ }
+ }
+
+ public interface IVideoFilterArgument
+ {
+ public string Key { get; }
+ public string Value { get; }
+ }
+
+ public class VideoFilterOptions
+ {
+ public List Arguments { get; } = new List();
+
+ public VideoFilterOptions Scale(VideoSize videoSize) => WithArgument(new ScaleArgument(videoSize));
+ public VideoFilterOptions Scale(int width, int height) => WithArgument(new ScaleArgument(width, height));
+ public VideoFilterOptions Scale(Size size) => WithArgument(new ScaleArgument(size));
+ public VideoFilterOptions Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition));
+ public VideoFilterOptions Mirror(Mirroring mirroring) => WithArgument(new SetMirroringArgument(mirroring));
+ public VideoFilterOptions DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions));
+
+ private VideoFilterOptions WithArgument(IVideoFilterArgument argument)
+ {
+ Arguments.Add(argument);
+ return this;
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs
index 90da0d2..2da1572 100644
--- a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs
+++ b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs
@@ -15,8 +15,8 @@ public string Extension
{
get
{
- if (FFMpegOptions.Options.ExtensionOverrides.ContainsKey(Name))
- return FFMpegOptions.Options.ExtensionOverrides[Name];
+ if (GlobalFFOptions.Current.ExtensionOverrides.ContainsKey(Name))
+ return GlobalFFOptions.Current.ExtensionOverrides[Name];
return "." + Name;
}
}
diff --git a/FFMpegCore/FFMpeg/Enums/Mirroring.cs b/FFMpegCore/FFMpeg/Enums/Mirroring.cs
new file mode 100644
index 0000000..5768163
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Enums/Mirroring.cs
@@ -0,0 +1,8 @@
+namespace FFMpegCore.Enums
+{
+ public enum Mirroring
+ {
+ Vertical,
+ Horizontal
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs
index fc154ac..485cf20 100644
--- a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs
+++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs
@@ -4,7 +4,6 @@ namespace FFMpegCore.Exceptions
{
public enum FFMpegExceptionType
{
- Dependency,
Conversion,
File,
Operation,
@@ -13,16 +12,49 @@ public enum FFMpegExceptionType
public class FFMpegException : Exception
{
- public FFMpegException(FFMpegExceptionType type, string? message = null, Exception? innerException = null, string ffmpegErrorOutput = "", string ffmpegOutput = "")
+ public FFMpegException(FFMpegExceptionType type, string message, Exception? innerException = null, string ffMpegErrorOutput = "")
: base(message, innerException)
{
- FfmpegOutput = ffmpegOutput;
- FfmpegErrorOutput = ffmpegErrorOutput;
+ FFMpegErrorOutput = ffMpegErrorOutput;
Type = type;
}
-
+ public FFMpegException(FFMpegExceptionType type, string message, string ffMpegErrorOutput = "")
+ : base(message)
+ {
+ FFMpegErrorOutput = ffMpegErrorOutput;
+ Type = type;
+ }
+ public FFMpegException(FFMpegExceptionType type, string message)
+ : base(message)
+ {
+ FFMpegErrorOutput = string.Empty;
+ Type = type;
+ }
+
public FFMpegExceptionType Type { get; }
- public string FfmpegOutput { get; }
- public string FfmpegErrorOutput { get; }
+ public string FFMpegErrorOutput { get; }
+ }
+ public class FFOptionsException : Exception
+ {
+ public FFOptionsException(string message, Exception? innerException = null)
+ : base(message, innerException)
+ {
+ }
+ }
+
+ public class FFMpegArgumentException : Exception
+ {
+ public FFMpegArgumentException(string? message = null, Exception? innerException = null)
+ : base(message, innerException)
+ {
+ }
+ }
+
+ public class FFMpegStreamFormatException : FFMpegException
+ {
+ public FFMpegStreamFormatException(FFMpegExceptionType type, string message, Exception? innerException = null)
+ : base(type, message, innerException)
+ {
+ }
}
}
\ No newline at end of file
diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs
index cc611e3..42c344b 100644
--- a/FFMpegCore/FFMpeg/FFMpeg.cs
+++ b/FFMpegCore/FFMpeg/FFMpeg.cs
@@ -16,19 +16,19 @@ public static class FFMpeg
///
/// Saves a 'png' thumbnail from the input video to drive
///
- /// Source video analysis
+ /// Source video analysis
/// Output video file path
/// Seek position where the thumbnail should be taken.
/// Thumbnail size. If width or height equal 0, the other will be computed automatically.
- /// Index of video stream in input file. Default it is 0.
/// Bitmap with the requested snapshot.
- public static bool Snapshot(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0)
+ public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null)
{
if (Path.GetExtension(output) != FileExtension.Png)
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
- var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, streamIndex);
-
+ var source = FFProbe.Analyse(input);
+ var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime);
+
return arguments
.OutputToFile(output, true, outputOptions)
.ProcessSynchronously();
@@ -36,36 +36,37 @@ public static bool Snapshot(IMediaAnalysis source, string output, Size? size = n
///
/// Saves a 'png' thumbnail from the input video to drive
///
- /// Source video analysis
+ /// Source video analysis
/// Output video file path
/// Seek position where the thumbnail should be taken.
/// Thumbnail size. If width or height equal 0, the other will be computed automatically.
- /// Index of video stream in input file. Default it is 0.
/// Bitmap with the requested snapshot.
- public static Task SnapshotAsync(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0)
+ public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null)
{
if (Path.GetExtension(output) != FileExtension.Png)
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
- var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, streamIndex);
-
- return arguments
+ var source = await FFProbe.AnalyseAsync(input);
+ var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime);
+
+ return await arguments
.OutputToFile(output, true, outputOptions)
.ProcessAsynchronously();
}
+
///
/// Saves a 'png' thumbnail to an in-memory bitmap
///
- /// Source video file.
+ /// 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.
- /// Index of video stream in input file. Default it is 0.
/// Bitmap with the requested snapshot.
- public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0)
+ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null)
{
- var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, streamIndex);
+ var source = FFProbe.Analyse(input);
+ var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime);
using var ms = new MemoryStream();
-
+
arguments
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
.ForceFormat("rawvideo")))
@@ -78,16 +79,16 @@ public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan
///
/// Saves a 'png' thumbnail to an in-memory bitmap
///
- /// Source video file.
+ /// 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.
- /// Index of video stream in input file. Default it is 0.
/// Bitmap with the requested snapshot.
- public static async Task SnapshotAsync(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0)
+ public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null)
{
- var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, streamIndex);
+ var source = await FFProbe.AnalyseAsync(input);
+ var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime);
using var ms = new MemoryStream();
-
+
await arguments
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
.ForceFormat("rawvideo")))
@@ -97,25 +98,15 @@ await arguments
return new Bitmap(ms);
}
- private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int streamIndex = 0)
+ private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(string input, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null)
{
captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3);
size = PrepareSnapshotSize(source, size);
- // If user will know about numeration of streams (user passes index of necessary video stream)
- int index = source.VideoStreams.Where(videoStream => videoStream.Index == streamIndex).FirstOrDefault().Index;
-
- // User passes number of video stream
- // E.g: user can pass 0, but index of first video stream will be 1
- /*int index = 0;
- try { index = source.VideoStreams[streamIndex].Index; }
- catch { };*/
-
return (FFMpegArguments
- .FromFileInput(source, options => options
- .Seek(captureTime)),
+ .FromFileInput(input, false, options => options
+ .Seek(captureTime)),
options => options
- .SelectStream(index)
.WithVideoCodec(VideoCodec.Png)
.WithFrameOutputCount(1)
.Resize(size));
@@ -123,13 +114,13 @@ private static (FFMpegArguments, Action outputOptions) Bu
private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize)
{
- if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0))
+ if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0) || source.PrimaryVideoStream == null)
return null;
-
+
var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height);
if (source.PrimaryVideoStream.Rotation == 90 || source.PrimaryVideoStream.Rotation == 180)
currentSize = new Size(source.PrimaryVideoStream.Height, source.PrimaryVideoStream.Width);
-
+
if (wantedSize.Value.Width != currentSize.Width || wantedSize.Value.Height != currentSize.Height)
{
if (wantedSize.Value.Width <= 0 && wantedSize.Value.Height > 0)
@@ -160,7 +151,7 @@ private static (FFMpegArguments, Action outputOptions) Bu
/// Is encoding multithreaded.
/// Output video information.
public static bool Convert(
- IMediaAnalysis source,
+ string input,
string output,
ContainerFormat format,
Speed speed = Speed.SuperFast,
@@ -169,10 +160,11 @@ public static bool Convert(
bool multithreaded = false)
{
FFMpegHelper.ExtensionExceptionCheck(output, format.Extension);
+ var source = FFProbe.Analyse(input);
FFMpegHelper.ConversionSizeExceptionCheck(source);
- var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream.Height / (int)size;
- var outputSize = new Size((int)(source.PrimaryVideoStream.Width / scale), (int)(source.PrimaryVideoStream.Height / scale));
+ var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream!.Height / (int)size;
+ var outputSize = new Size((int)(source.PrimaryVideoStream!.Width / scale), (int)(source.PrimaryVideoStream.Height / scale));
if (outputSize.Width % 2 != 0)
outputSize.Width += 1;
@@ -180,41 +172,44 @@ public static bool Convert(
return format.Name switch
{
"mp4" => FFMpegArguments
- .FromFileInput(source)
+ .FromFileInput(input)
.OutputToFile(output, true, options => options
.UsingMultithreading(multithreaded)
.WithVideoCodec(VideoCodec.LibX264)
.WithVideoBitrate(2400)
- .Scale(outputSize)
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(outputSize))
.WithSpeedPreset(speed)
.WithAudioCodec(AudioCodec.Aac)
.WithAudioBitrate(audioQuality))
.ProcessSynchronously(),
"ogv" => FFMpegArguments
- .FromFileInput(source)
+ .FromFileInput(input)
.OutputToFile(output, true, options => options
.UsingMultithreading(multithreaded)
.WithVideoCodec(VideoCodec.LibTheora)
.WithVideoBitrate(2400)
- .Scale(outputSize)
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(outputSize))
.WithSpeedPreset(speed)
.WithAudioCodec(AudioCodec.LibVorbis)
.WithAudioBitrate(audioQuality))
.ProcessSynchronously(),
"mpegts" => FFMpegArguments
- .FromFileInput(source)
+ .FromFileInput(input)
.OutputToFile(output, true, options => options
.CopyChannel()
.WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB)
.ForceFormat(VideoType.Ts))
.ProcessSynchronously(),
"webm" => FFMpegArguments
- .FromFileInput(source)
+ .FromFileInput(input)
.OutputToFile(output, true, options => options
.UsingMultithreading(multithreaded)
.WithVideoCodec(VideoCodec.LibVpx)
.WithVideoBitrate(2400)
- .Scale(outputSize)
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(outputSize))
.WithSpeedPreset(speed)
.WithAudioCodec(AudioCodec.LibVorbis)
.WithAudioBitrate(audioQuality))
@@ -246,21 +241,22 @@ public static bool PosterWithAudio(string image, string audio, string output)
.UsingShortest())
.ProcessSynchronously();
}
-
+
///
/// Joins a list of video files.
///
/// Output video file.
/// List of vides that need to be joined together.
/// Output video information.
- public static bool Join(string output, params IMediaAnalysis[] videos)
+ public static bool Join(string output, params string[] videos)
{
- var temporaryVideoParts = videos.Select(video =>
+ var temporaryVideoParts = videos.Select(videoPath =>
{
+ var video = FFProbe.Analyse(videoPath);
FFMpegHelper.ConversionSizeExceptionCheck(video);
- var destinationPath = Path.Combine(FFMpegOptions.Options.TempDirectory, $"{Path.GetFileNameWithoutExtension(video.Path)}{FileExtension.Ts}");
- Directory.CreateDirectory(FFMpegOptions.Options.TempDirectory);
- Convert(video, destinationPath, VideoType.Ts);
+ var destinationPath = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}");
+ Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder);
+ Convert(videoPath, destinationPath, VideoType.Ts);
return destinationPath;
}).ToArray();
@@ -278,16 +274,6 @@ public static bool Join(string output, params IMediaAnalysis[] videos)
Cleanup(temporaryVideoParts);
}
}
- ///
- /// Joins a list of video files.
- ///
- /// Output video file.
- /// List of vides that need to be joined together.
- /// Output video information.
- public static bool Join(string output, params string[] videos)
- {
- return Join(output, videos.Select(videoPath => FFProbe.Analyse(videoPath)).ToArray());
- }
///
/// Converts an image sequence to a video.
@@ -298,7 +284,7 @@ public static bool Join(string output, params string[] videos)
/// Output video information.
public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images)
{
- var tempFolderName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid().ToString());
+ var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString());
var temporaryImageFiles = images.Select((image, index) =>
{
FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName));
@@ -337,7 +323,7 @@ public static bool SaveM3U8Stream(Uri uri, string output)
if (uri.Scheme != "http" && uri.Scheme != "https")
throw new ArgumentException($"Uri: {uri.AbsoluteUri}, does not point to a valid http(s) stream.");
-
+
return FFMpegArguments
.FromUrlInput(uri)
.OutputToFile(output)
@@ -354,10 +340,10 @@ public static bool Mute(string input, string output)
{
var source = FFProbe.Analyse(input);
FFMpegHelper.ConversionSizeExceptionCheck(source);
- FFMpegHelper.ExtensionExceptionCheck(output, source.Extension);
+ // FFMpegHelper.ExtensionExceptionCheck(output, source.Extension);
return FFMpegArguments
- .FromFileInput(source)
+ .FromFileInput(input)
.OutputToFile(output, true, options => options
.CopyChannel(Channel.Video)
.DisableChannel(Channel.Audio))
@@ -393,10 +379,10 @@ public static bool ReplaceAudio(string input, string inputAudio, string output,
{
var source = FFProbe.Analyse(input);
FFMpegHelper.ConversionSizeExceptionCheck(source);
- FFMpegHelper.ExtensionExceptionCheck(output, source.Extension);
+ // FFMpegHelper.ExtensionExceptionCheck(output, source.Format.);
return FFMpegArguments
- .FromFileInput(source)
+ .FromFileInput(input)
.AddFileInput(inputAudio)
.OutputToFile(output, true, options => options
.CopyChannel()
@@ -412,7 +398,7 @@ internal static IReadOnlyList GetPixelFormatsInternal()
FFMpegHelper.RootExceptionCheck();
var list = new List();
- using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), "-pix_fmts");
+ using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts");
instance.DataReceived += (e, args) =>
{
if (PixelFormat.TryParse(args.Data, out var format))
@@ -427,14 +413,14 @@ internal static IReadOnlyList GetPixelFormatsInternal()
public static IReadOnlyList GetPixelFormats()
{
- if (!FFMpegOptions.Options.UseCache)
+ if (!GlobalFFOptions.Current.UseCache)
return GetPixelFormatsInternal();
return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly();
}
public static bool TryGetPixelFormat(string name, out PixelFormat fmt)
{
- if (!FFMpegOptions.Options.UseCache)
+ if (!GlobalFFOptions.Current.UseCache)
{
fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return fmt != null;
@@ -457,11 +443,11 @@ private static void ParsePartOfCodecs(Dictionary codecs, string a
{
FFMpegHelper.RootExceptionCheck();
- using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), arguments);
+ using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), arguments);
instance.DataReceived += (e, args) =>
{
var codec = parser(args.Data);
- if (codec != null)
+ if(codec != null)
if (codecs.TryGetValue(codec.Name, out var parentCodec))
parentCodec.Merge(codec);
else
@@ -499,16 +485,16 @@ internal static Dictionary GetCodecsInternal()
public static IReadOnlyList GetCodecs()
{
- if (!FFMpegOptions.Options.UseCache)
+ if (!GlobalFFOptions.Current.UseCache)
return GetCodecsInternal().Values.ToList().AsReadOnly();
return FFMpegCache.Codecs.Values.ToList().AsReadOnly();
}
public static IReadOnlyList GetCodecs(CodecType type)
{
- if (!FFMpegOptions.Options.UseCache)
+ if (!GlobalFFOptions.Current.UseCache)
return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly();
- return FFMpegCache.Codecs.Values.Where(x => x.Type == type).ToList().AsReadOnly();
+ return FFMpegCache.Codecs.Values.Where(x=>x.Type == type).ToList().AsReadOnly();
}
public static IReadOnlyList GetVideoCodecs() => GetCodecs(CodecType.Video);
@@ -518,7 +504,7 @@ public static IReadOnlyList GetCodecs(CodecType type)
public static bool TryGetCodec(string name, out Codec codec)
{
- if (!FFMpegOptions.Options.UseCache)
+ if (!GlobalFFOptions.Current.UseCache)
{
codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return codec != null;
@@ -541,7 +527,7 @@ internal static IReadOnlyList GetContainersFormatsInternal()
FFMpegHelper.RootExceptionCheck();
var list = new List();
- using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), "-formats");
+ using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats");
instance.DataReceived += (e, args) =>
{
if (ContainerFormat.TryParse(args.Data, out var fmt))
@@ -556,14 +542,14 @@ internal static IReadOnlyList GetContainersFormatsInternal()
public static IReadOnlyList GetContainerFormats()
{
- if (!FFMpegOptions.Options.UseCache)
+ if (!GlobalFFOptions.Current.UseCache)
return GetContainersFormatsInternal();
return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly();
}
public static bool TryGetContainerFormat(string name, out ContainerFormat fmt)
{
- if (!FFMpegOptions.Options.UseCache)
+ if (!GlobalFFOptions.Current.UseCache)
{
fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return fmt != null;
diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs
index bb7f0ad..94b1cb2 100644
--- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs
+++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs
@@ -5,7 +5,7 @@
namespace FFMpegCore
{
- public class FFMpegArgumentOptions : FFMpegOptionsBase
+ public class FFMpegArgumentOptions : FFMpegArgumentsBase
{
internal FFMpegArgumentOptions() { }
@@ -15,14 +15,10 @@ internal FFMpegArgumentOptions() { }
public FFMpegArgumentOptions WithAudioBitrate(int bitrate) => WithArgument(new AudioBitrateArgument(bitrate));
public FFMpegArgumentOptions WithAudioSamplingRate(int samplingRate = 48000) => WithArgument(new AudioSamplingRateArgument(samplingRate));
public FFMpegArgumentOptions WithVariableBitrate(int vbr) => WithArgument(new VariableBitRateArgument(vbr));
-
- public FFMpegArgumentOptions Resize(VideoSize videoSize) => WithArgument(new SizeArgument(videoSize));
public FFMpegArgumentOptions Resize(int width, int height) => WithArgument(new SizeArgument(width, height));
public FFMpegArgumentOptions Resize(Size? size) => WithArgument(new SizeArgument(size));
- public FFMpegArgumentOptions Scale(VideoSize videoSize) => WithArgument(new ScaleArgument(videoSize));
- public FFMpegArgumentOptions Scale(int width, int height) => WithArgument(new ScaleArgument(width, height));
- public FFMpegArgumentOptions Scale(Size size) => WithArgument(new ScaleArgument(size));
+
public FFMpegArgumentOptions WithBitStreamFilter(Channel channel, Filter filter) => WithArgument(new BitStreamFilterArgument(channel, filter));
public FFMpegArgumentOptions WithConstantRateFactor(int crf) => WithArgument(new ConstantRateFactorArgument(crf));
@@ -40,6 +36,13 @@ internal FFMpegArgumentOptions() { }
public FFMpegArgumentOptions WithVideoCodec(Codec videoCodec) => WithArgument(new VideoCodecArgument(videoCodec));
public FFMpegArgumentOptions WithVideoCodec(string videoCodec) => WithArgument(new VideoCodecArgument(videoCodec));
public FFMpegArgumentOptions WithVideoBitrate(int bitrate) => WithArgument(new VideoBitrateArgument(bitrate));
+ public FFMpegArgumentOptions WithVideoFilters(Action videoFilterOptions)
+ {
+ var videoFilterOptionsObj = new VideoFilterOptions();
+ videoFilterOptions(videoFilterOptionsObj);
+ return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj));
+ }
+
public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate));
public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument());
public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed));
@@ -47,7 +50,6 @@ internal FFMpegArgumentOptions() { }
public FFMpegArgumentOptions WithCustomArgument(string argument) => WithArgument(new CustomArgument(argument));
public FFMpegArgumentOptions Seek(TimeSpan? seekTo) => WithArgument(new SeekArgument(seekTo));
- public FFMpegArgumentOptions Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition));
public FFMpegArgumentOptions Loop(int times) => WithArgument(new LoopArgument(times));
public FFMpegArgumentOptions OverwriteExisting() => WithArgument(new OverwriteArgument());
public FFMpegArgumentOptions SelectStream(int index) => WithArgument(new MapStreamArgument(index));
@@ -57,8 +59,6 @@ internal FFMpegArgumentOptions() { }
public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat));
public FFMpegArgumentOptions ForcePixelFormat(PixelFormat pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat));
- public FFMpegArgumentOptions DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions));
-
public FFMpegArgumentOptions WithArgument(IArgument argument)
{
Arguments.Add(argument);
diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs
index cfbe42a..060ffc3 100644
--- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs
+++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs
@@ -27,7 +27,7 @@ internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments)
public string Arguments => _ffMpegArguments.Text;
- private event EventHandler CancelEvent = null!;
+ private event EventHandler CancelEvent = null!;
public FFMpegArgumentProcessor NotifyOnProgress(Action onPercentageProgress, TimeSpan totalTimeSpan)
{
@@ -45,21 +45,30 @@ public FFMpegArgumentProcessor NotifyOnOutput(Action onOutput)
_onOutput = onOutput;
return this;
}
- public FFMpegArgumentProcessor CancellableThrough(out Action cancel)
+ public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0)
{
- cancel = () => CancelEvent?.Invoke(this, EventArgs.Empty);
+ cancel = () => CancelEvent?.Invoke(this, timeout);
return this;
}
- public bool ProcessSynchronously(bool throwOnError = true)
+ public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0)
{
- using var instance = PrepareInstance(out var cancellationTokenSource);
+ token.Register(() => CancelEvent?.Invoke(this, timeout));
+ return this;
+ }
+ public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
+ {
+ using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource);
var errorCode = -1;
- void OnCancelEvent(object sender, EventArgs args)
+ void OnCancelEvent(object sender, int timeout)
{
instance.SendInput("q");
- cancellationTokenSource.Cancel();
- instance.Started = false;
+
+ if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true))
+ {
+ cancellationTokenSource.Cancel();
+ instance.Started = false;
+ }
}
CancelEvent += OnCancelEvent;
instance.Exited += delegate { cancellationTokenSource.Cancel(); };
@@ -76,37 +85,30 @@ void OnCancelEvent(object sender, EventArgs args)
}
catch (Exception e)
{
- if (!HandleException(throwOnError, e, instance.ErrorData, instance.OutputData)) return false;
+ if (!HandleException(throwOnError, e, instance.ErrorData)) return false;
}
finally
{
CancelEvent -= OnCancelEvent;
}
- return HandleCompletion(throwOnError, errorCode, instance.ErrorData, instance.OutputData);
+ return HandleCompletion(throwOnError, errorCode, instance.ErrorData);
}
- private bool HandleCompletion(bool throwOnError, int errorCode, IReadOnlyList errorData, IReadOnlyList outputData)
+ public async Task ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{
- if (throwOnError && errorCode != 0)
- throw new FFMpegException(FFMpegExceptionType.Conversion, "FFMpeg exited with non-zero exitcode.", null, string.Join("\n", errorData), string.Join("\n", outputData));
-
- _onPercentageProgress?.Invoke(100.0);
- if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value);
-
- return errorCode == 0;
- }
-
- public async Task ProcessAsynchronously(bool throwOnError = true)
- {
- using var instance = PrepareInstance(out var cancellationTokenSource);
+ using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource);
var errorCode = -1;
- void OnCancelEvent(object sender, EventArgs args)
+ void OnCancelEvent(object sender, int timeout)
{
instance.SendInput("q");
- cancellationTokenSource.Cancel();
- instance.Started = false;
+
+ if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true))
+ {
+ cancellationTokenSource.Cancel();
+ instance.Started = false;
+ }
}
CancelEvent += OnCancelEvent;
@@ -122,26 +124,38 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t =>
}
catch (Exception e)
{
- if (!HandleException(throwOnError, e, instance.ErrorData, instance.OutputData)) return false;
+ if (!HandleException(throwOnError, e, instance.ErrorData)) return false;
}
finally
{
CancelEvent -= OnCancelEvent;
}
- return HandleCompletion(throwOnError, errorCode, instance.ErrorData, instance.OutputData);
+ return HandleCompletion(throwOnError, errorCode, instance.ErrorData);
}
- private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSource)
+ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData)
+ {
+ if (throwOnError && exitCode != 0)
+ throw new FFMpegException(FFMpegExceptionType.Process, $"ffmpeg exited with non-zero exit-code ({exitCode} - {string.Join("\n", errorData)})", null, string.Join("\n", errorData));
+
+ _onPercentageProgress?.Invoke(100.0);
+ if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value);
+
+ return exitCode == 0;
+ }
+
+ private Instance PrepareInstance(FFOptions ffMpegOptions,
+ out CancellationTokenSource cancellationTokenSource)
{
FFMpegHelper.RootExceptionCheck();
- FFMpegHelper.VerifyFFMpegExists();
+ FFMpegHelper.VerifyFFMpegExists(ffMpegOptions);
var startInfo = new ProcessStartInfo
{
- FileName = FFMpegOptions.Options.FFmpegBinary(),
+ FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions),
Arguments = _ffMpegArguments.Text,
- StandardOutputEncoding = FFMpegOptions.Options.Encoding,
- StandardErrorEncoding = FFMpegOptions.Options.Encoding,
+ StandardOutputEncoding = ffMpegOptions.Encoding,
+ StandardErrorEncoding = ffMpegOptions.Encoding,
};
var instance = new Instance(startInfo);
cancellationTokenSource = new CancellationTokenSource();
@@ -153,12 +167,12 @@ private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSo
}
- private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData, IReadOnlyList outputData)
+ private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData)
{
if (!throwOnError)
return false;
- throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, string.Join("\n", errorData), string.Join("\n", outputData));
+ throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, string.Join("\n", errorData));
}
private void OutputData(object sender, (DataType Type, string Data) msg)
diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs
index 44e20d2..847e68c 100644
--- a/FFMpegCore/FFMpeg/FFMpegArguments.cs
+++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs
@@ -9,26 +9,26 @@
namespace FFMpegCore
{
- public sealed class FFMpegArguments : FFMpegOptionsBase
+ public sealed class FFMpegArguments : FFMpegArgumentsBase
{
- private readonly FFMpegGlobalOptions _globalOptions = new FFMpegGlobalOptions();
+ private readonly FFMpegGlobalArguments _globalArguments = new FFMpegGlobalArguments();
private FFMpegArguments() { }
- public string Text => string.Join(" ", _globalOptions.Arguments.Concat(Arguments).Select(arg => arg.Text));
+ public string Text => string.Join(" ", _globalArguments.Arguments.Concat(Arguments).Select(arg => arg.Text));
public static FFMpegArguments FromConcatInput(IEnumerable filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments);
public static FFMpegArguments FromDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments);
public static FFMpegArguments FromFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(verifyExists, filePath), addArguments);
public static FFMpegArguments FromFileInput(FileInfo fileInfo, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(fileInfo.FullName, false), addArguments);
- public static FFMpegArguments FromFileInput(IMediaAnalysis mediaAnalysis, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(mediaAnalysis.Path, false), addArguments);
public static FFMpegArguments FromUrlInput(Uri uri, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments);
+ public static FFMpegArguments FromDeviceInput(string device, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputDeviceArgument(device), addArguments);
public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments);
- public FFMpegArguments WithGlobalOptions(Action configureOptions)
+ public FFMpegArguments WithGlobalOptions(Action configureOptions)
{
- configureOptions(_globalOptions);
+ configureOptions(_globalArguments);
return this;
}
@@ -36,7 +36,6 @@ public FFMpegArguments WithGlobalOptions(Action configureOp
public FFMpegArguments AddDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) => WithInput(new DemuxConcatArgument(filePaths), addArguments);
public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) => WithInput(new InputArgument(verifyExists, filePath), addArguments);
public FFMpegArguments AddFileInput(FileInfo fileInfo, Action? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments);
- public FFMpegArguments AddFileInput(IMediaAnalysis mediaAnalysis, Action? addArguments = null) => WithInput(new InputArgument(mediaAnalysis.Path, false), addArguments);
public FFMpegArguments AddUrlInput(Uri uri, Action? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments);
public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments);
@@ -50,7 +49,8 @@ private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments = null) => ToProcessor(new OutputArgument(file, overwrite), addArguments);
- public FFMpegArgumentProcessor OutputToFile(Uri uri, bool overwrite = true, Action? addArguments = null) => ToProcessor(new OutputArgument(uri.AbsolutePath, overwrite), addArguments);
+ public FFMpegArgumentProcessor OutputToUrl(string uri, Action? addArguments = null) => ToProcessor(new OutputUrlArgument(uri), addArguments);
+ public FFMpegArgumentProcessor OutputToUrl(Uri uri, Action? addArguments = null) => ToProcessor(new OutputUrlArgument(uri.ToString()), addArguments);
public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader, Action? addArguments = null) => ToProcessor(new OutputPipeArgument(reader), addArguments);
private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action? addArguments)
diff --git a/FFMpegCore/FFMpeg/FFMpegOptionsBase.cs b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs
similarity index 79%
rename from FFMpegCore/FFMpeg/FFMpegOptionsBase.cs
rename to FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs
index 015e609..fc51ab1 100644
--- a/FFMpegCore/FFMpeg/FFMpegOptionsBase.cs
+++ b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs
@@ -3,7 +3,7 @@
namespace FFMpegCore
{
- public abstract class FFMpegOptionsBase
+ public abstract class FFMpegArgumentsBase
{
internal readonly List Arguments = new List();
}
diff --git a/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs
new file mode 100644
index 0000000..e7d6e24
--- /dev/null
+++ b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs
@@ -0,0 +1,18 @@
+using FFMpegCore.Arguments;
+
+namespace FFMpegCore
+{
+ public sealed class FFMpegGlobalArguments : FFMpegArgumentsBase
+ {
+ internal FFMpegGlobalArguments() { }
+
+ public FFMpegGlobalArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel));
+
+ private FFMpegGlobalArguments WithOption(IArgument argument)
+ {
+ Arguments.Add(argument);
+ return this;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs b/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs
deleted file mode 100644
index 00dc66f..0000000
--- a/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using FFMpegCore.Arguments;
-
-namespace FFMpegCore
-{
- public sealed class FFMpegGlobalOptions : FFMpegOptionsBase
- {
- internal FFMpegGlobalOptions() { }
-
- public FFMpegGlobalOptions WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel));
-
- private FFMpegGlobalOptions WithOption(IArgument argument)
- {
- Arguments.Add(argument);
- return this;
- }
-
- }
-}
\ No newline at end of file
diff --git a/FFMpegCore/FFMpeg/FFMpegOptions.cs b/FFMpegCore/FFMpeg/FFMpegOptions.cs
deleted file mode 100644
index a7d29b4..0000000
--- a/FFMpegCore/FFMpeg/FFMpegOptions.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Text.Json;
-
-namespace FFMpegCore
-{
- public class FFMpegOptions
- {
- private static readonly string ConfigFile = "ffmpeg.config.json";
- private static readonly string DefaultRoot = "";
- private static readonly string DefaultTemp = Path.GetTempPath();
- private static readonly Dictionary DefaultExtensionsOverrides = new Dictionary
- {
- { "mpegts", ".ts" },
- };
-
- public static FFMpegOptions Options { get; private set; } = new FFMpegOptions();
-
- public static void Configure(Action optionsAction)
- {
- optionsAction?.Invoke(Options);
- }
-
- public static void Configure(FFMpegOptions options)
- {
- Options = options ?? throw new ArgumentNullException(nameof(options));
- }
-
- static FFMpegOptions()
- {
- if (File.Exists(ConfigFile))
- {
- Options = JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))!;
- foreach (var pair in DefaultExtensionsOverrides)
- if (!Options.ExtensionOverrides.ContainsKey(pair.Key)) Options.ExtensionOverrides.Add(pair.Key, pair.Value);
- }
- }
-
- public string RootDirectory { get; set; } = DefaultRoot;
- public string TempDirectory { get; set; } = DefaultTemp;
- public bool UseCache { get; set; } = true;
- public Encoding Encoding { get; set; } = Encoding.Default;
-
- public string FFmpegBinary() => FFBinary("FFMpeg");
-
- public string FFProbeBinary() => FFBinary("FFProbe");
-
- public Dictionary ExtensionOverrides { get; private set; } = new Dictionary();
-
- private static string FFBinary(string name)
- {
- var ffName = name.ToLowerInvariant();
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- ffName += ".exe";
-
- var target = Environment.Is64BitProcess ? "x64" : "x86";
- if (Directory.Exists(Path.Combine(Options.RootDirectory, target)))
- ffName = Path.Combine(target, ffName);
-
- return Path.Combine(Options.RootDirectory, ffName);
- }
- }
-}
diff --git a/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs b/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs
new file mode 100644
index 0000000..c7dea65
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs
@@ -0,0 +1,16 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FFMpegCore.Pipes
+{
+ ///
+ /// Interface for Audio sample
+ ///
+ public interface IAudioSample
+ {
+ void Serialize(Stream stream);
+
+ Task SerializeAsync(Stream stream, CancellationToken token);
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs
index 875407e..e5f2bf4 100644
--- a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs
+++ b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs
@@ -1,11 +1,12 @@
-using System.Threading;
+using System.IO;
+using System.Threading;
using System.Threading.Tasks;
namespace FFMpegCore.Pipes
{
public interface IPipeSink
{
- Task ReadAsync(System.IO.Stream inputStream, CancellationToken cancellationToken);
+ Task ReadAsync(Stream inputStream, CancellationToken cancellationToken);
string GetFormat();
}
}
diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs
index cdd5139..c250421 100644
--- a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs
+++ b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs
@@ -1,4 +1,5 @@
-using System.Threading;
+using System.IO;
+using System.Threading;
using System.Threading.Tasks;
namespace FFMpegCore.Pipes
@@ -9,6 +10,6 @@ namespace FFMpegCore.Pipes
public interface IPipeSource
{
string GetStreamArguments();
- Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken);
+ Task WriteAsync(Stream outputStream, CancellationToken cancellationToken);
}
}
diff --git a/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs
index 094040b..dd583d9 100644
--- a/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs
+++ b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs
@@ -1,4 +1,5 @@
-using System.Threading;
+using System.IO;
+using System.Threading;
using System.Threading.Tasks;
namespace FFMpegCore.Pipes
@@ -12,7 +13,7 @@ public interface IVideoFrame
int Height { get; }
string Format { get; }
- void Serialize(System.IO.Stream pipe);
- Task SerializeAsync(System.IO.Stream pipe, CancellationToken token);
+ void Serialize(Stream pipe);
+ Task SerializeAsync(Stream pipe, CancellationToken token);
}
}
diff --git a/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs
new file mode 100644
index 0000000..8797694
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FFMpegCore.Pipes
+{
+ ///
+ /// Implementation of for a raw audio stream that is gathered from .
+ /// It is the user's responbility to make sure the enumerated samples match the configuration provided to this pipe.
+ ///
+ public class RawAudioPipeSource : IPipeSource
+ {
+ private readonly IEnumerator _sampleEnumerator;
+
+ public string Format { get; set; } = "s16le";
+ public uint SampleRate { get; set; } = 8000;
+ public uint Channels { get; set; } = 1;
+
+ public RawAudioPipeSource(IEnumerator sampleEnumerator)
+ {
+ _sampleEnumerator = sampleEnumerator;
+ }
+
+ public RawAudioPipeSource(IEnumerable sampleEnumerator)
+ : this(sampleEnumerator.GetEnumerator()) { }
+
+ public string GetStreamArguments()
+ {
+ return $"-f {Format} -ar {SampleRate} -ac {Channels}";
+ }
+
+ public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken)
+ {
+ if (_sampleEnumerator.Current != null)
+ {
+ await _sampleEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false);
+ }
+
+ while (_sampleEnumerator.MoveNext())
+ {
+ await _sampleEnumerator.Current!.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs
index f61bb7c..0e3ab61 100644
--- a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs
+++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using FFMpegCore.Exceptions;
@@ -14,7 +16,7 @@ public class RawVideoPipeSource : IPipeSource
public string StreamFormat { get; private set; } = null!;
public int Width { get; private set; }
public int Height { get; private set; }
- public int FrameRate { get; set; } = 25;
+ public double FrameRate { get; set; } = 25;
private bool _formatInitialized;
private readonly IEnumerator _framesEnumerator;
@@ -42,10 +44,10 @@ public string GetStreamArguments()
_formatInitialized = true;
}
- return $"-f rawvideo -r {FrameRate} -pix_fmt {StreamFormat} -s {Width}x{Height}";
+ return $"-f rawvideo -r {FrameRate.ToString(CultureInfo.InvariantCulture)} -pix_fmt {StreamFormat} -s {Width}x{Height}";
}
- public async Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken)
+ public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken)
{
if (_framesEnumerator.Current != null)
{
@@ -63,7 +65,7 @@ public async Task WriteAsync(System.IO.Stream outputStream, CancellationToken ca
private void CheckFrameAndThrow(IVideoFrame frame)
{
if (frame.Width != Width || frame.Height != Height || frame.Format != StreamFormat)
- throw new FFMpegException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" +
+ throw new FFMpegStreamFormatException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" +
$"Frame format: {frame.Width}x{frame.Height} pix_fmt: {frame.Format}\r\n" +
$"Stream format: {Width}x{Height} pix_fmt: {StreamFormat}");
}
diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs
index cd13f40..addc14e 100644
--- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs
+++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs
@@ -20,7 +20,7 @@ public StreamPipeSink(Stream destination)
Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken);
}
- public Task ReadAsync(System.IO.Stream inputStream, CancellationToken cancellationToken)
+ public Task ReadAsync(Stream inputStream, CancellationToken cancellationToken)
=> Writer(inputStream, cancellationToken);
public string GetFormat() => Format;
diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs
index 404029f..99bc081 100644
--- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs
+++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs
@@ -1,4 +1,5 @@
-using System.Threading;
+using System.IO;
+using System.Threading;
using System.Threading.Tasks;
namespace FFMpegCore.Pipes
@@ -8,17 +9,17 @@ namespace FFMpegCore.Pipes
///
public class StreamPipeSource : IPipeSource
{
- public System.IO.Stream Source { get; }
+ public Stream Source { get; }
public int BlockSize { get; } = 4096;
public string StreamFormat { get; } = string.Empty;
- public StreamPipeSource(System.IO.Stream source)
+ public StreamPipeSource(Stream source)
{
Source = source;
}
public string GetStreamArguments() => StreamFormat;
- public Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) => Source.CopyToAsync(outputStream, BlockSize, cancellationToken);
+ public Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) => Source.CopyToAsync(outputStream, BlockSize, cancellationToken);
}
}
diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj
index cb5d906..dd96a3d 100644
--- a/FFMpegCore/FFMpegCore.csproj
+++ b/FFMpegCore/FFMpegCore.csproj
@@ -5,16 +5,17 @@
https://github.com/rosenbjerg/FFMpegCore
https://github.com/rosenbjerg/FFMpegCore
- A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your C# applications
+ A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications
3.0.0.0
3.0.0.0
3.0.0.0
- - return null from FFProbe.Analyse* when no media format was detected
-- Expose tags as string dictionary on IMediaAnalysis (thanks hey-red)
+ - Cancellation token support (thanks patagonaa)
+- Support for setting stdout and stderr encoding for ffprobe (thanks CepheiSigma)
+- Improved ffprobe exceptions
8
- 3.4.0
+ 4.4.0
MIT
- Malte Rosenbjerg, Vlad Jerca
+ Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev
ffmpeg ffprobe convert video audio mediafile resize analyze muxing
GitHub
true
@@ -30,7 +31,7 @@
-
+
diff --git a/FFMpegCore/FFMpegCore.csproj.DotSettings b/FFMpegCore/FFMpegCore.csproj.DotSettings
index 69be7ec..7a8d17a 100644
--- a/FFMpegCore/FFMpegCore.csproj.DotSettings
+++ b/FFMpegCore/FFMpegCore.csproj.DotSettings
@@ -1,2 +1,3 @@
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs
new file mode 100644
index 0000000..1f7e497
--- /dev/null
+++ b/FFMpegCore/FFOptions.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace FFMpegCore
+{
+ public class FFOptions
+ {
+ ///
+ /// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH
+ ///
+ public string BinaryFolder { get; set; } = string.Empty;
+
+ ///
+ /// Folder used for temporary files necessary for static methods on FFMpeg class
+ ///
+ public string TemporaryFilesFolder { get; set; } = Path.GetTempPath();
+
+ ///
+ /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes
+ ///
+ public Encoding Encoding { get; set; } = Encoding.Default;
+
+ ///
+ ///
+ ///
+ public Dictionary ExtensionOverrides { get; set; } = new Dictionary
+ {
+ { "mpegts", ".ts" },
+ };
+
+ ///
+ /// Whether to cache calls to get ffmpeg codec, pixel- and container-formats
+ ///
+ public bool UseCache { get; set; } = true;
+ }
+}
\ No newline at end of file
diff --git a/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs b/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs
new file mode 100644
index 0000000..3495193
--- /dev/null
+++ b/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace FFMpegCore.Exceptions
+{
+ public class FFProbeException : Exception
+ {
+ public FFProbeException(string message, Exception? inner = null) : base(message, inner)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs b/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs
new file mode 100644
index 0000000..5ab6b93
--- /dev/null
+++ b/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+
+namespace FFMpegCore.Exceptions
+{
+ public class FFProbeProcessException : FFProbeException
+ {
+ public IReadOnlyCollection ProcessErrors { get; }
+
+ public FFProbeProcessException(string message, IReadOnlyCollection processErrors, Exception? inner = null) : base(message, inner)
+ {
+ ProcessErrors = processErrors;
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs b/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs
new file mode 100644
index 0000000..4141f5f
--- /dev/null
+++ b/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs
@@ -0,0 +1,9 @@
+namespace FFMpegCore.Exceptions
+{
+ public class FormatNullException : FFProbeException
+ {
+ public FormatNullException() : base("Format not specified")
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs
index 8bb68db..7d043a6 100644
--- a/FFMpegCore/FFProbe/FFProbe.cs
+++ b/FFMpegCore/FFProbe/FFProbe.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
@@ -12,26 +13,32 @@ namespace FFMpegCore
{
public static class FFProbe
{
- public static IMediaAnalysis? Analyse(string filePath, int outputCapacity = int.MaxValue)
+ public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{
if (!File.Exists(filePath))
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
- using var instance = PrepareInstance(filePath, outputCapacity);
- instance.BlockUntilFinished();
- return ParseOutput(filePath, instance);
+ using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
+ var exitCode = instance.BlockUntilFinished();
+ if (exitCode != 0)
+ throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
+
+ return ParseOutput(instance);
}
- public static IMediaAnalysis? Analyse(Uri uri, int outputCapacity = int.MaxValue)
+ public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{
- using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity);
- instance.BlockUntilFinished();
- return ParseOutput(uri.AbsoluteUri, instance);
+ using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
+ var exitCode = instance.BlockUntilFinished();
+ if (exitCode != 0)
+ throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
+
+ return ParseOutput(instance);
}
- public static IMediaAnalysis? Analyse(Stream stream, int outputCapacity = int.MaxValue)
+ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{
var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource);
- using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity);
+ using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
pipeArgument.Pre();
var task = instance.FinishedRunning();
@@ -46,36 +53,36 @@ public static class FFProbe
}
var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult();
if (exitCode != 0)
- throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}", null, string.Join("\n", instance.ErrorData), string.Join("\n", instance.OutputData));
+ throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
- return ParseOutput(pipeArgument.PipePath, instance);
+ return ParseOutput(instance);
}
- public static async Task AnalyseAsync(string filePath, int outputCapacity = int.MaxValue)
+ public static async Task AnalyseAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{
if (!File.Exists(filePath))
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
- using var instance = PrepareInstance(filePath, outputCapacity);
- await instance.FinishedRunning();
- return ParseOutput(filePath, instance);
+ using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
+ await instance.FinishedRunning().ConfigureAwait(false);
+ return ParseOutput(instance);
}
- public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue)
+ public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{
- using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity);
- await instance.FinishedRunning();
- return ParseOutput(uri.AbsoluteUri, instance);
+ using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
+ await instance.FinishedRunning().ConfigureAwait(false);
+ return ParseOutput(instance);
}
- public static async Task AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue)
+ public static async Task AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{
var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource);
- using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity);
+ using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
pipeArgument.Pre();
var task = instance.FinishedRunning();
try
{
- await pipeArgument.During();
+ await pipeArgument.During().ConfigureAwait(false);
}
catch(IOException)
{
@@ -84,31 +91,39 @@ public static class FFProbe
{
pipeArgument.Post();
}
- var exitCode = await task;
+ var exitCode = await task.ConfigureAwait(false);
if (exitCode != 0)
- throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}", null, string.Join("\n", instance.ErrorData), string.Join("\n", instance.OutputData));
+ throw new FFProbeProcessException($"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", instance.ErrorData);
pipeArgument.Post();
- return ParseOutput(pipeArgument.PipePath, instance);
+ return ParseOutput(instance);
}
- private static IMediaAnalysis? ParseOutput(string filePath, Instance instance)
+ private static IMediaAnalysis ParseOutput(Instance instance)
{
var json = string.Join(string.Empty, instance.OutputData);
var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
- })!;
- if (ffprobeAnalysis?.Format == null) return null;
- return new MediaAnalysis(filePath, ffprobeAnalysis);
+ });
+
+ if (ffprobeAnalysis?.Format == null)
+ throw new FormatNullException();
+
+ return new MediaAnalysis(ffprobeAnalysis);
}
- private static Instance PrepareInstance(string filePath, int outputCapacity)
+ private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions)
{
FFProbeHelper.RootExceptionCheck();
- FFProbeHelper.VerifyFFProbeExists();
+ FFProbeHelper.VerifyFFProbeExists(ffOptions);
var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"";
- var instance = new Instance(FFMpegOptions.Options.FFProbeBinary(), arguments) {DataBufferCapacity = outputCapacity};
+ var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments)
+ {
+ StandardOutputEncoding = ffOptions.Encoding,
+ StandardErrorEncoding = ffOptions.Encoding
+ };
+ var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity };
return instance;
}
}
diff --git a/FFMpegCore/FFProbe/IMediaAnalysis.cs b/FFMpegCore/FFProbe/IMediaAnalysis.cs
index 660d776..4e67d4f 100644
--- a/FFMpegCore/FFProbe/IMediaAnalysis.cs
+++ b/FFMpegCore/FFProbe/IMediaAnalysis.cs
@@ -5,12 +5,10 @@ namespace FFMpegCore
{
public interface IMediaAnalysis
{
- string Path { get; }
- string Extension { get; }
TimeSpan Duration { get; }
MediaFormat Format { get; }
- AudioStream PrimaryAudioStream { get; }
- VideoStream PrimaryVideoStream { get; }
+ AudioStream? PrimaryAudioStream { get; }
+ VideoStream? PrimaryVideoStream { get; }
List VideoStreams { get; }
List AudioStreams { get; }
}
diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs
index 5a43aa2..2602f86 100644
--- a/FFMpegCore/FFProbe/MediaAnalysis.cs
+++ b/FFMpegCore/FFProbe/MediaAnalysis.cs
@@ -7,23 +7,18 @@ namespace FFMpegCore
{
internal class MediaAnalysis : IMediaAnalysis
{
- private static readonly Regex DurationRegex = new Regex("^(\\d{1,2}:\\d{1,2}:\\d{1,2}(.\\d{1,7})?)", RegexOptions.Compiled);
-
- internal MediaAnalysis(string path, FFProbeAnalysis analysis)
+ internal MediaAnalysis(FFProbeAnalysis analysis)
{
Format = ParseFormat(analysis.Format);
VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList();
AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList();
- PrimaryVideoStream = VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault();
- PrimaryAudioStream = AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault();
- Path = path;
}
private MediaFormat ParseFormat(Format analysisFormat)
{
return new MediaFormat
{
- Duration = TimeSpan.Parse(analysisFormat.Duration ?? "0"),
+ Duration = MediaAnalysisUtils.ParseDuration(analysisFormat.Duration),
FormatName = analysisFormat.FormatName,
FormatLongName = analysisFormat.FormatLongName,
StreamCount = analysisFormat.NbStreams,
@@ -33,9 +28,6 @@ private MediaFormat ParseFormat(Format analysisFormat)
};
}
- public string Path { get; }
- public string Extension => System.IO.Path.GetExtension(Path);
-
public TimeSpan Duration => new[]
{
Format.Duration,
@@ -44,9 +36,9 @@ private MediaFormat ParseFormat(Format analysisFormat)
}.Max();
public MediaFormat Format { get; }
- public AudioStream PrimaryAudioStream { get; }
+ public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault();
- public VideoStream PrimaryVideoStream { get; }
+ public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault();
public List VideoStreams { get; }
public List AudioStreams { get; }
@@ -56,14 +48,14 @@ private VideoStream ParseVideoStream(FFProbeStream stream)
return new VideoStream
{
Index = stream.Index,
- AvgFrameRate = DivideRatio(ParseRatioDouble(stream.AvgFrameRate, '/')),
- BitRate = !string.IsNullOrEmpty(stream.BitRate) ? ParseIntInvariant(stream.BitRate) : default,
- BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? ParseIntInvariant(stream.BitsPerRawSample) : default,
+ AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')),
+ BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default,
+ BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default,
CodecName = stream.CodecName,
CodecLongName = stream.CodecLongName,
- DisplayAspectRatio = ParseRatioInt(stream.DisplayAspectRatio, ':'),
- Duration = ParseDuration(stream),
- FrameRate = DivideRatio(ParseRatioDouble(stream.FrameRate, '/')),
+ DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'),
+ Duration = MediaAnalysisUtils.ParseDuration(stream),
+ FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')),
Height = stream.Height ?? 0,
Width = stream.Width ?? 0,
Profile = stream.Profile,
@@ -74,57 +66,89 @@ private VideoStream ParseVideoStream(FFProbeStream stream)
};
}
- private static TimeSpan ParseDuration(FFProbeStream ffProbeStream)
- {
- return !string.IsNullOrEmpty(ffProbeStream.Duration)
- ? TimeSpan.Parse(ffProbeStream.Duration)
- : TimeSpan.Parse(TrimTimeSpan(ffProbeStream.GetDuration()) ?? "0");
- }
-
- private static string? TrimTimeSpan(string? durationTag)
- {
- var durationMatch = DurationRegex.Match(durationTag ?? "");
- return durationMatch.Success ? durationMatch.Groups[1].Value : null;
- }
-
private AudioStream ParseAudioStream(FFProbeStream stream)
{
return new AudioStream
{
Index = stream.Index,
- BitRate = !string.IsNullOrEmpty(stream.BitRate) ? ParseIntInvariant(stream.BitRate) : default,
+ BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default,
CodecName = stream.CodecName,
CodecLongName = stream.CodecLongName,
Channels = stream.Channels ?? default,
ChannelLayout = stream.ChannelLayout,
- Duration = ParseDuration(stream),
- SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? ParseIntInvariant(stream.SampleRate) : default,
+ Duration = MediaAnalysisUtils.ParseDuration(stream),
+ SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default,
Profile = stream.Profile,
Language = stream.GetLanguage(),
Tags = stream.Tags,
};
}
- private static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2;
- private static (int, int) ParseRatioInt(string input, char separator)
+ }
+
+ public static class MediaAnalysisUtils
+ {
+ private static readonly Regex DurationRegex = new Regex(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled);
+
+ public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2;
+
+ public static (int, int) ParseRatioInt(string input, char separator)
{
if (string.IsNullOrEmpty(input)) return (0, 0);
var ratio = input.Split(separator);
return (ParseIntInvariant(ratio[0]), ParseIntInvariant(ratio[1]));
}
- private static (double, double) ParseRatioDouble(string input, char separator)
+ public static (double, double) ParseRatioDouble(string input, char separator)
{
if (string.IsNullOrEmpty(input)) return (0, 0);
var ratio = input.Split(separator);
return (ratio.Length > 0 ? ParseDoubleInvariant(ratio[0]) : 0, ratio.Length > 1 ? ParseDoubleInvariant(ratio[1]) : 0);
}
- private static double ParseDoubleInvariant(string line) =>
+ public static double ParseDoubleInvariant(string line) =>
double.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
- private static int ParseIntInvariant(string line) =>
+ public static int ParseIntInvariant(string line) =>
int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
+
+
+ public static TimeSpan ParseDuration(string duration)
+ {
+ if (!string.IsNullOrEmpty(duration))
+ {
+ var match = DurationRegex.Match(duration);
+ if (match.Success)
+ {
+ // ffmpeg may provide < 3-digit number of milliseconds (omitting trailing zeros), which won't simply parse correctly
+ // e.g. 00:12:02.11 -> 12 minutes 2 seconds and 110 milliseconds
+ var millisecondsPart = match.Groups[4].Value;
+ if (millisecondsPart.Length < 3)
+ {
+ millisecondsPart = millisecondsPart.PadRight(3, '0');
+ }
+
+ var hours = int.Parse(match.Groups[1].Value);
+ var minutes = int.Parse(match.Groups[2].Value);
+ var seconds = int.Parse(match.Groups[3].Value);
+ var milliseconds = int.Parse(millisecondsPart);
+ return new TimeSpan(0, hours, minutes, seconds, milliseconds);
+ }
+ else
+ {
+ return TimeSpan.Zero;
+ }
+ }
+ else
+ {
+ return TimeSpan.Zero;
+ }
+ }
+
+ public static TimeSpan ParseDuration(FFProbeStream ffProbeStream)
+ {
+ return ParseDuration(ffProbeStream.Duration);
+ }
}
}
\ No newline at end of file
diff --git a/FFMpegCore/GlobalFFOptions.cs b/FFMpegCore/GlobalFFOptions.cs
new file mode 100644
index 0000000..358787a
--- /dev/null
+++ b/FFMpegCore/GlobalFFOptions.cs
@@ -0,0 +1,52 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+
+namespace FFMpegCore
+{
+ public static class GlobalFFOptions
+ {
+ private static readonly string ConfigFile = "ffmpeg.config.json";
+
+ public static FFOptions Current { get; private set; }
+ static GlobalFFOptions()
+ {
+ if (File.Exists(ConfigFile))
+ {
+ Current = JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))!;
+ }
+ else
+ {
+ Current = new FFOptions();
+ }
+ }
+
+ public static void Configure(Action optionsAction)
+ {
+ optionsAction?.Invoke(Current);
+ }
+ public static void Configure(FFOptions ffOptions)
+ {
+ Current = ffOptions ?? throw new ArgumentNullException(nameof(ffOptions));
+ }
+
+
+ public static string GetFFMpegBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFMpeg", ffOptions ?? Current);
+
+ public static string GetFFProbeBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFProbe", ffOptions ?? Current);
+
+ private static string GetFFBinaryPath(string name, FFOptions ffOptions)
+ {
+ var ffName = name.ToLowerInvariant();
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ ffName += ".exe";
+
+ var target = Environment.Is64BitProcess ? "x64" : "x86";
+ if (Directory.Exists(Path.Combine(ffOptions.BinaryFolder, target)))
+ ffName = Path.Combine(target, ffName);
+
+ return Path.Combine(ffOptions.BinaryFolder, ffName);
+ }
+ }
+}
diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs
index f2a214e..12e52c3 100644
--- a/FFMpegCore/Helpers/FFMpegHelper.cs
+++ b/FFMpegCore/Helpers/FFMpegHelper.cs
@@ -11,21 +11,15 @@ public static class FFMpegHelper
private static bool _ffmpegVerified;
public static void ConversionSizeExceptionCheck(Image image)
- {
- ConversionSizeExceptionCheck(image.Size);
- }
+ => ConversionSizeExceptionCheck(image.Size.Width, image.Size.Height);
public static void ConversionSizeExceptionCheck(IMediaAnalysis info)
- {
- ConversionSizeExceptionCheck(new Size(info.PrimaryVideoStream.Width, info.PrimaryVideoStream.Height));
- }
+ => ConversionSizeExceptionCheck(info.PrimaryVideoStream!.Width, info.PrimaryVideoStream.Height);
- private static void ConversionSizeExceptionCheck(Size size)
+ private static void ConversionSizeExceptionCheck(int width, int height)
{
- if (size.Height % 2 != 0 || size.Width % 2 != 0 )
- {
+ if (height % 2 != 0 || width % 2 != 0 )
throw new ArgumentException("FFMpeg yuv420p encoding requires the width and height to be a multiple of 2!");
- }
}
public static void ExtensionExceptionCheck(string filename, string extension)
@@ -37,17 +31,17 @@ public static void ExtensionExceptionCheck(string filename, string extension)
public static void RootExceptionCheck()
{
- if (FFMpegOptions.Options.RootDirectory == null)
- throw new FFMpegException(FFMpegExceptionType.Dependency,
- "FFMpeg root is not configured in app config. Missing key 'ffmpegRoot'.");
+ if (GlobalFFOptions.Current.BinaryFolder == null)
+ throw new FFOptionsException("FFMpeg root is not configured in app config. Missing key 'BinaryFolder'.");
}
- public static void VerifyFFMpegExists()
+ public static void VerifyFFMpegExists(FFOptions ffMpegOptions)
{
if (_ffmpegVerified) return;
- var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFmpegBinary(), "-version");
+ var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version");
_ffmpegVerified = exitCode == 0;
- if (!_ffmpegVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system");
+ if (!_ffmpegVerified)
+ throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system");
}
}
}
diff --git a/FFMpegCore/Helpers/FFProbeHelper.cs b/FFMpegCore/Helpers/FFProbeHelper.cs
index 1e833e0..4989542 100644
--- a/FFMpegCore/Helpers/FFProbeHelper.cs
+++ b/FFMpegCore/Helpers/FFProbeHelper.cs
@@ -20,18 +20,17 @@ public static int Gcd(int first, int second)
public static void RootExceptionCheck()
{
- if (FFMpegOptions.Options.RootDirectory == null)
- throw new FFMpegException(FFMpegExceptionType.Dependency,
- "FFProbe root is not configured in app config. Missing key 'ffmpegRoot'.");
-
+ if (GlobalFFOptions.Current.BinaryFolder == null)
+ throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'.");
}
- public static void VerifyFFProbeExists()
+ public static void VerifyFFProbeExists(FFOptions ffMpegOptions)
{
if (_ffprobeVerified) return;
- var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFProbeBinary(), "-version");
+ var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version");
_ffprobeVerified = exitCode == 0;
- if (!_ffprobeVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system");
+ if (!_ffprobeVerified)
+ throw new FFProbeException("ffprobe was not found on your system");
}
}
}
diff --git a/README.md b/README.md
index 6a9fc35..9dce345 100644
--- a/README.md
+++ b/README.md
@@ -22,11 +22,11 @@ A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and
FFProbe is used to gather media information:
```csharp
-var mediaInfo = FFProbe.Analyse(inputFile);
+var mediaInfo = FFProbe.Analyse(inputPath);
```
or
```csharp
-var mediaInfo = await FFProbe.AnalyseAsync(inputFile);
+var mediaInfo = await FFProbe.AnalyseAsync(inputPath);
```
@@ -43,20 +43,19 @@ FFMpegArguments
.WithConstantRateFactor(21)
.WithAudioCodec(AudioCodec.Aac)
.WithVariableBitrate(4)
- .WithFastStart()
- .Scale(VideoSize.Hd))
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(VideoSize.Hd))
+ .WithFastStart())
.ProcessSynchronously();
```
Easily capture screens from your videos:
```csharp
-var mediaFileAnalysis = FFProbe.Analyse(inputPath);
-
// process the snapshot in-memory and use the Bitmap directly
-var bitmap = FFMpeg.Snapshot(mediaFileAnalysis, new Size(200, 400), TimeSpan.FromMinutes(1));
+var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
// or persists the image on the drive
-FFMpeg.Snapshot(mediaFileAnalysis, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1))
+FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
```
Convert to and/or from streams
@@ -89,25 +88,25 @@ FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1,
Mute videos:
```csharp
-FFMpeg.Mute(inputFilePath, outputFilePath);
+FFMpeg.Mute(inputPath, outputPath);
```
Save audio track from video:
```csharp
-FFMpeg.ExtractAudio(inputVideoFilePath, outputAudioFilePath);
+FFMpeg.ExtractAudio(inputPath, outputPath);
```
Add or replace audio track on video:
```csharp
-FFMpeg.ReplaceAudio(inputVideoFilePath, inputAudioFilePath, outputVideoFilePath);
+FFMpeg.ReplaceAudio(inputPath, inputAudioPath, outputPath);
```
Add poster image to audio file (good for youtube videos):
```csharp
-FFMpeg.PosterWithAudio(inputImageFilePath, inputAudioFilePath, outputVideoFilePath);
+FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath);
// or
-var image = Image.FromFile(inputImageFile);
-image.AddAudio(inputAudioFilePath, outputVideoFilePath);
+var image = Image.FromFile(inputImagePath);
+image.AddAudio(inputAudioPath, outputPath);
```
Other available arguments could be found in `FFMpegCore.Arguments` namespace.
@@ -135,10 +134,11 @@ var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumera
{
FrameRate = 30 //set source frame rate
};
-FFMpegArguments
- .FromPipeInput(videoFramesSource, )
- .OutputToFile("temporary.mp4", false, )
- .ProcessSynchronously();
+await FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputPath, false, options => options
+ .WithVideoCodec(VideoCodec.LibVpx))
+ .ProcessAsynchronously();
```
if you want to use `System.Drawing.Bitmap` as `IVideoFrame`, there is a `BitmapVideoFrameWrapper` wrapper class.
@@ -179,13 +179,19 @@ If these folders are not defined, it will try to find the binaries in `/root/(ff
#### Option 1
-The default value (`\\FFMPEG\\bin`) can be overwritten via the `FFMpegOptions` class:
+The default value of an empty string (expecting ffmpeg to be found through PATH) can be overwritten via the `FFOptions` class:
```c#
-public Startup()
-{
- FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "./bin", TempDirectory = "/tmp" });
-}
+// setting global options
+GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
+// or
+GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin");
+
+// or individual, per-run options
+await FFMpegArguments
+ .FromFileInput(inputPath)
+ .OutputToFile(outputPath)
+ .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
```
#### Option 2
@@ -194,8 +200,8 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f
```json
{
- "RootDirectory": "./bin",
- "TempDirectory": "/tmp"
+ "BinaryFolder": "./bin",
+ "TemporaryFilesFolder": "/tmp"
}
```
@@ -217,6 +223,6 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f
### License
-Copyright © 2020
+Copyright © 2021
Released under [MIT license](https://github.com/rosenbjerg/FFMpegCore/blob/master/LICENSE)