Merge pull request #185 from rosenbjerg/master

v4.0.0

Former-commit-id: aed2dc0907
This commit is contained in:
Malte Rosenbjerg 2021-03-07 00:36:26 +01:00 committed by GitHub
commit d2155541f9
34 changed files with 776 additions and 673 deletions

View file

@ -8,7 +8,7 @@ namespace FFMpegCore.Test
[TestClass] [TestClass]
public class ArgumentBuilderTest 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] [TestMethod]
@ -21,28 +21,35 @@ public void Builder_BuildString_IO_1()
[TestMethod] [TestMethod]
public void Builder_BuildString_Scale() public void Builder_BuildString_Scale()
{ {
var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.Scale(VideoSize.Hd)).Arguments; var str = FFMpegArguments.FromFileInput("input.mp4")
Assert.AreEqual("-i \"input.mp4\" -vf scale=-1:720 \"output.mp4\" -y", str); .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] [TestMethod]
public void Builder_BuildString_AudioCodec() 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); Assert.AreEqual("-i \"input.mp4\" -c:a aac \"output.mp4\" -y", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_AudioBitrate() 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); Assert.AreEqual("-i \"input.mp4\" -b:a 128k \"output.mp4\" -y", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Quiet() 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); Assert.AreEqual("-hide_banner -loglevel error -i \"input.mp4\" \"output.mp4\"", str);
} }
@ -50,27 +57,32 @@ public void Builder_BuildString_Quiet()
[TestMethod] [TestMethod]
public void Builder_BuildString_AudioCodec_Fluent() 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); Assert.AreEqual("-i \"input.mp4\" -c:a aac -b:a 128k \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_BitStream() 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); Assert.AreEqual("-i \"input.mp4\" -bsf:a h264_mp4toannexb \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_HardwareAcceleration_Auto() 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); Assert.AreEqual("-i \"input.mp4\" -hwaccel \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_HardwareAcceleration_Specific() 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); Assert.AreEqual("-i \"input.mp4\" -hwaccel cuvid \"output.mp4\"", str);
} }
@ -84,140 +96,175 @@ public void Builder_BuildString_Concat()
[TestMethod] [TestMethod]
public void Builder_BuildString_Copy_Audio() 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); Assert.AreEqual("-i \"input.mp4\" -c:a copy \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Copy_Video() 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); Assert.AreEqual("-i \"input.mp4\" -c:v copy \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Copy_Both() 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); Assert.AreEqual("-i \"input.mp4\" -c copy \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_DisableChannel_Audio() 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); Assert.AreEqual("-i \"input.mp4\" -an \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_DisableChannel_Video() 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); Assert.AreEqual("-i \"input.mp4\" -vn \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_AudioSamplingRate_Default() 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); Assert.AreEqual("-i \"input.mp4\" -ar 48000 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_AudioSamplingRate() 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); Assert.AreEqual("-i \"input.mp4\" -ar 44000 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_VariableBitrate() 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); Assert.AreEqual("-i \"input.mp4\" -vbr 5 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Faststart() 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); Assert.AreEqual("-i \"input.mp4\" -movflags faststart \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Overwrite() 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); Assert.AreEqual("-i \"input.mp4\" -y \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_RemoveMetadata() 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); Assert.AreEqual("-i \"input.mp4\" -map_metadata -1 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Transpose() 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); Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2\" \"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] [TestMethod]
public void Builder_BuildString_ForceFormat() 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); Assert.AreEqual("-f mp4 -i \"input.mp4\" -f mp4 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_FrameOutputCount() 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); Assert.AreEqual("-i \"input.mp4\" -vframes 50 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_FrameRate() 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); Assert.AreEqual("-i \"input.mp4\" -r 50 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Loop() 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); Assert.AreEqual("-i \"input.mp4\" -loop 50 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Seek() 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; 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 -i \"input.mp4\" -ss 00:00:10 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Shortest() 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); Assert.AreEqual("-i \"input.mp4\" -shortest \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Size() 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); Assert.AreEqual("-i \"input.mp4\" -s 1920x1080 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Speed() 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); Assert.AreEqual("-i \"input.mp4\" -preset fast \"output.mp4\"", str);
} }
@ -227,18 +274,21 @@ public void Builder_BuildString_DrawtextFilter()
var str = FFMpegArguments var str = FFMpegArguments
.FromFileInput("input.mp4") .FromFileInput("input.mp4")
.OutputToFile("output.mp4", false, opt => opt .OutputToFile("output.mp4", false, opt => opt
.DrawText(DrawTextOptions .WithVideoFilters(filterOptions => filterOptions
.Create("Stack Overflow", "/path/to/font.ttf") .DrawText(DrawTextOptions
.WithParameter("fontcolor", "white") .Create("Stack Overflow", "/path/to/font.ttf")
.WithParameter("fontsize", "24") .WithParameter("fontcolor", "white")
.WithParameter("box", "1") .WithParameter("fontsize", "24")
.WithParameter("boxcolor", "black@0.5") .WithParameter("box", "1")
.WithParameter("boxborderw", "5") .WithParameter("boxcolor", "black@0.5")
.WithParameter("x", "(w-text_w)/2") .WithParameter("boxborderw", "5")
.WithParameter("y", "(h-text_h)/2"))) .WithParameter("x", "(w-text_w)/2")
.WithParameter("y", "(h-text_h)/2"))))
.Arguments; .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] [TestMethod]
@ -247,45 +297,53 @@ public void Builder_BuildString_DrawtextFilter_Alt()
var str = FFMpegArguments var str = FFMpegArguments
.FromFileInput("input.mp4") .FromFileInput("input.mp4")
.OutputToFile("output.mp4", false, opt => opt .OutputToFile("output.mp4", false, opt => opt
.DrawText(DrawTextOptions .WithVideoFilters(filterOptions => filterOptions
.Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24")))) .DrawText(DrawTextOptions
.Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24")))))
.Arguments; .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] [TestMethod]
public void Builder_BuildString_StartNumber() 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); Assert.AreEqual("-i \"input.mp4\" -start_number 50 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Threads_1() 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); Assert.AreEqual("-i \"input.mp4\" -threads 50 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Threads_2() 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); Assert.AreEqual($"-i \"input.mp4\" -threads {Environment.ProcessorCount} \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Codec() 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); Assert.AreEqual("-i \"input.mp4\" -c:v libx264 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Codec_Override() 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); Assert.AreEqual("-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\" -y", str);
} }
@ -293,17 +351,20 @@ public void Builder_BuildString_Codec_Override()
[TestMethod] [TestMethod]
public void Builder_BuildString_Duration() 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); Assert.AreEqual("-i \"input.mp4\" -t 00:00:20 \"output.mp4\"", str);
} }
[TestMethod] [TestMethod]
public void Builder_BuildString_Raw() 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); 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); Assert.AreEqual("-i \"input.mp4\" -acodec copy \"output.mp4\"", str);
} }
@ -311,7 +372,8 @@ public void Builder_BuildString_Raw()
[TestMethod] [TestMethod]
public void Builder_BuildString_ForcePixelFormat() 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); Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str);
} }
} }

View file

@ -10,39 +10,39 @@ public class FFMpegOptionsTest
[TestMethod] [TestMethod]
public void Options_Initialized() public void Options_Initialized()
{ {
Assert.IsNotNull(FFMpegOptions.Options); Assert.IsNotNull(GlobalFFOptions.Current);
} }
[TestMethod] [TestMethod]
public void Options_Defaults_Configured() public void Options_Defaults_Configured()
{ {
Assert.AreEqual(new FFMpegOptions().RootDirectory, $""); Assert.AreEqual(new FFOptions().BinaryFolder, $"");
} }
[TestMethod] [TestMethod]
public void Options_Loaded_From_File() public void Options_Loaded_From_File()
{ {
Assert.AreEqual( Assert.AreEqual(
FFMpegOptions.Options.RootDirectory, GlobalFFOptions.Current.BinaryFolder,
JsonConvert.DeserializeObject<FFMpegOptions>(File.ReadAllText("ffmpeg.config.json")).RootDirectory JsonConvert.DeserializeObject<FFOptions>(File.ReadAllText("ffmpeg.config.json")).BinaryFolder
); );
} }
[TestMethod] [TestMethod]
public void Options_Set_Programmatically() public void Options_Set_Programmatically()
{ {
var original = FFMpegOptions.Options; var original = GlobalFFOptions.Current;
try try
{ {
FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "Whatever" }); GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" });
Assert.AreEqual( Assert.AreEqual(
FFMpegOptions.Options.RootDirectory, GlobalFFOptions.Current.BinaryFolder,
"Whatever" "Whatever"
); );
} }
finally finally
{ {
FFMpegOptions.Configure(original); GlobalFFOptions.Configure(original);
} }
} }
} }

View file

@ -1,4 +1,5 @@
using System.IO; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using FFMpegCore.Test.Resources; using FFMpegCore.Test.Resources;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -23,16 +24,21 @@ public async Task Audio_FromStream_Duration()
var streamAnalysis = await FFProbe.AnalyseAsync(inputStream); var streamAnalysis = await FFProbe.AnalyseAsync(inputStream);
Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration);
} }
[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] [TestMethod]
public void Probe_Success() public void Probe_Success()
{ {
var info = FFProbe.Analyse(TestResources.Mp4Video); var info = FFProbe.Analyse(TestResources.Mp4Video);
Assert.AreEqual(3, info.Duration.Seconds); 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(6, info.PrimaryAudioStream.Channels);
Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName); Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName);
Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName); Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName);
@ -40,7 +46,7 @@ public void Probe_Success()
Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate); Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate);
Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz); 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(16, info.PrimaryVideoStream.DisplayAspectRatio.Width);
Assert.AreEqual(9, info.PrimaryVideoStream.DisplayAspectRatio.Height); Assert.AreEqual(9, info.PrimaryVideoStream.DisplayAspectRatio.Height);
Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat); Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat);

View file

@ -1,10 +0,0 @@
using System.Threading.Tasks;
namespace FFMpegCore.Test
{
static class TasksExtensions
{
public static T WaitForResult<T>(this Task<T> task) =>
task.ConfigureAwait(false).GetAwaiter().GetResult();
}
}

View file

@ -18,239 +18,57 @@ namespace FFMpegCore.Test
[TestClass] [TestClass]
public class VideoTest 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}"); using var outputFile = new TemporaryFile($"out{VideoType.Ogv.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}");
var input = FFProbe.Analyse(TestResources.WebmVideo); var success = FFMpegArguments
using var inputStream = File.OpenRead(input.Path); .FromFileInput(TestResources.WebmVideo)
var processor = FFMpegArguments .OutputToFile(outputFile, false)
.FromPipeInput(new StreamPipeSource(inputStream)) .ProcessSynchronously();
.OutputToFile(outputFile, false, opt =>
{
foreach (var arg in arguments)
opt.WithArgument(arg);
});
var scaling = arguments.OfType<ScaleArgument>().FirstOrDefault();
var success = processor.ProcessSynchronously();
var outputVideo = FFProbe.Analyse(outputFile);
Assert.IsTrue(success); 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<ScaleArgument>().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<IMediaAnalysis> 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<ScaleArgument>().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<ScaleArgument>().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)] [TestMethod, Timeout(10000)]
public void Video_ToMP4() 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)] [TestMethod, Timeout(10000)]
public void Video_ToMP4_YUV444p() public void Video_ToMP4_YUV444p()
{ {
Convert(VideoType.Mp4, (a) => Assert.IsTrue(a.VideoStreams.First().PixelFormat == "yuv444p"), using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
new ForcePixelFormat("yuv444p"));
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)] [TestMethod, Timeout(10000)]
public void Video_ToMP4_Args() 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)] [DataTestMethod, Timeout(10000)]
@ -258,13 +76,29 @@ public void Video_ToMP4_Args()
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) 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)] [TestMethod, Timeout(10000)]
public void Video_ToMP4_Args_StreamPipe() 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)] [TestMethod, Timeout(10000)]
@ -276,7 +110,7 @@ await Assert.ThrowsExceptionAsync<FFMpegException>(async () =>
var pipeSource = new StreamPipeSink(ms); var pipeSource = new StreamPipeSink(ms);
await FFMpegArguments await FFMpegArguments
.FromFileInput(TestResources.Mp4Video) .FromFileInput(TestResources.Mp4Video)
.OutputToPipe(pipeSource, opt => opt.ForceFormat("mkv")) .OutputToPipe(pipeSource, opt => opt.ForceFormat("mp4"))
.ProcessAsynchronously(); .ProcessAsynchronously();
}); });
} }
@ -286,8 +120,9 @@ public void Video_StreamFile_OutputToMemoryStream()
var output = new MemoryStream(); var output = new MemoryStream();
FFMpegArguments FFMpegArguments
.FromPipeInput(new StreamPipeSource(File.OpenRead(TestResources.WebmVideo)), options => options.ForceFormat("webm")) .FromPipeInput(new StreamPipeSource(File.OpenRead(TestResources.WebmVideo)), opt => opt
.OutputToPipe(new StreamPipeSink(output), options => options .ForceFormat("webm"))
.OutputToPipe(new StreamPipeSink(output), opt => opt
.ForceFormat("mpegts")) .ForceFormat("mpegts"))
.ProcessSynchronously(); .ProcessSynchronously();
@ -299,32 +134,41 @@ public void Video_StreamFile_OutputToMemoryStream()
[TestMethod, Timeout(10000)] [TestMethod, Timeout(10000)]
public void Video_ToMP4_Args_StreamOutputPipe_Failure() public void Video_ToMP4_Args_StreamOutputPipe_Failure()
{ {
Assert.ThrowsException<FFMpegException>(() => ConvertToStreamPipe(new ForceFormatArgument("mkv"))); Assert.ThrowsException<FFMpegException>(() =>
{
using var ms = new MemoryStream();
FFMpegArguments
.FromFileInput(TestResources.Mp4Video)
.OutputToPipe(new StreamPipeSink(ms), opt => opt
.ForceFormat("mkv"))
.ProcessSynchronously();
});
} }
[TestMethod, Timeout(10000)] [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); var pipeSource = new StreamPipeSink(ms);
FFMpegArguments await FFMpegArguments
.FromFileInput(TestResources.Mp4Video) .FromFileInput(TestResources.Mp4Video)
.OutputToPipe(pipeSource, opt => opt .OutputToPipe(pipeSource, opt => opt
.WithVideoCodec(VideoCodec.LibX264) .WithVideoCodec(VideoCodec.LibX264)
.ForceFormat("matroska")) .ForceFormat("matroska"))
.ProcessAsynchronously() .ProcessAsynchronously();
.WaitForResult();
} }
[TestMethod, Timeout(10000)] [TestMethod, Timeout(10000)]
public async Task TestDuplicateRun() public async Task TestDuplicateRun()
{ {
FFMpegArguments.FromFileInput(TestResources.Mp4Video) FFMpegArguments
.FromFileInput(TestResources.Mp4Video)
.OutputToFile("temporary.mp4") .OutputToFile("temporary.mp4")
.ProcessSynchronously(); .ProcessSynchronously();
await FFMpegArguments.FromFileInput(TestResources.Mp4Video) await FFMpegArguments
.FromFileInput(TestResources.Mp4Video)
.OutputToFile("temporary.mp4") .OutputToFile("temporary.mp4")
.ProcessAsynchronously(); .ProcessAsynchronously();
@ -332,65 +176,115 @@ await FFMpegArguments.FromFileInput(TestResources.Mp4Video)
} }
[TestMethod, Timeout(10000)] [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)] [TestMethod, Timeout(10000)]
public void Video_ToTS() 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)] [TestMethod, Timeout(10000)]
public void Video_ToTS_Args() public void Video_ToTS_Args()
{ {
Convert(VideoType.Ts, using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}");
new CopyArgument(),
new BitStreamFilterArgument(Channel.Video, Filter.H264_Mp4ToAnnexB), var success = FFMpegArguments
new ForceFormatArgument(VideoType.MpegTs)); .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)] [DataTestMethod, Timeout(10000)]
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] [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)] [TestMethod, Timeout(10000)]
public void Video_ToOGV_Resize() public async Task Video_ToOGV_Resize()
{ {
Convert(VideoType.Ogv, true, VideoSize.Ed); using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}");
} var success = await FFMpegArguments
.FromFileInput(TestResources.Mp4Video)
[TestMethod, Timeout(10000)] .OutputToFile(outputFile, false, opt => opt
public void Video_ToOGV_Resize_Args() .Resize(200, 200)
{ .WithVideoCodec(VideoCodec.LibTheora))
Convert(VideoType.Ogv, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora)); .ProcessAsynchronously();
Assert.IsTrue(success);
} }
[DataTestMethod, Timeout(10000)] [DataTestMethod, Timeout(10000)]
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
// [DataRow(PixelFormat.Format48bppRgb)] // [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)] [TestMethod, Timeout(10000)]
public void Video_ToMP4_Resize() public void Scale_Mp4_Multithreaded()
{ {
Convert(VideoType.Mp4, true, VideoSize.Ed); using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
}
var success = FFMpegArguments
[TestMethod, Timeout(10000)] .FromFileInput(TestResources.Mp4Video)
public void Video_ToMP4_Resize_Args() .OutputToFile(outputFile, false, opt => opt
{ .UsingMultithreading(true)
Convert(VideoType.Mp4, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264)); .WithVideoCodec(VideoCodec.LibX264))
.ProcessSynchronously();
Assert.IsTrue(success);
} }
[DataTestMethod, Timeout(10000)] [DataTestMethod, Timeout(10000)]
@ -399,40 +293,24 @@ public void Video_ToMP4_Resize_Args()
// [DataRow(PixelFormat.Format48bppRgb)] // [DataRow(PixelFormat.Format48bppRgb)]
public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
{ {
ConvertFromPipe(VideoType.Mp4, pixelFormat, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264)); using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
} var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256));
[TestMethod, Timeout(10000)] var success = FFMpegArguments
public void Video_ToOGV() .FromPipeInput(videoFramesSource)
{ .OutputToFile(outputFile, false, opt => opt
Convert(VideoType.Ogv); .WithVideoCodec(VideoCodec.LibX264))
} .ProcessSynchronously();
Assert.IsTrue(success);
[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);
} }
[TestMethod, Timeout(10000)] [TestMethod, Timeout(10000)]
public void Video_Snapshot_InMemory() public void Video_Snapshot_InMemory()
{ {
var input = FFProbe.Analyse(TestResources.Mp4Video); 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(input.PrimaryVideoStream.Height, bitmap.Height);
Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png);
} }
@ -443,10 +321,10 @@ public void Video_Snapshot_PersistSnapshot()
var outputPath = new TemporaryFile("out.png"); var outputPath = new TemporaryFile("out.png");
var input = FFProbe.Analyse(TestResources.Mp4Video); var input = FFProbe.Analyse(TestResources.Mp4Video);
FFMpeg.Snapshot(input, outputPath); FFMpeg.Snapshot(TestResources.Mp4Video, outputPath);
using var bitmap = Image.FromFile(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(input.PrimaryVideoStream.Height, bitmap.Height);
Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png);
} }
@ -469,7 +347,7 @@ public void Video_Join()
Assert.AreEqual(expectedDuration.Hours, result.Duration.Hours); Assert.AreEqual(expectedDuration.Hours, result.Duration.Hours);
Assert.AreEqual(expectedDuration.Minutes, result.Duration.Minutes); Assert.AreEqual(expectedDuration.Minutes, result.Duration.Minutes);
Assert.AreEqual(expectedDuration.Seconds, result.Duration.Seconds); 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); Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width);
} }
@ -493,7 +371,7 @@ public void Video_Join_Image_Sequence()
Assert.IsTrue(success); Assert.IsTrue(success);
var result = FFProbe.Analyse(outputFile); var result = FFProbe.Analyse(outputFile);
Assert.AreEqual(3, result.Duration.Seconds); 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); Assert.AreEqual(imageSet.First().Height, result.PrimaryVideoStream.Height);
} }
@ -502,7 +380,7 @@ public void Video_With_Only_Audio_Should_Extract_Metadata()
{ {
var video = FFProbe.Analyse(TestResources.Mp4WithoutVideo); var video = FFProbe.Analyse(TestResources.Mp4WithoutVideo);
Assert.AreEqual(null, video.PrimaryVideoStream); 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); Assert.AreEqual(10, video.Duration.TotalSeconds, 0.5);
} }
@ -557,7 +435,7 @@ public void Video_OutputsData()
var outputFile = new TemporaryFile("out.mp4"); var outputFile = new TemporaryFile("out.mp4");
var dataReceived = false; var dataReceived = false;
FFMpegOptions.Configure(opt => opt.Encoding = Encoding.UTF8); GlobalFFOptions.Configure(opt => opt.Encoding = Encoding.UTF8);
var success = FFMpegArguments var success = FFMpegArguments
.FromFileInput(TestResources.Mp4Video) .FromFileInput(TestResources.Mp4Video)
.WithGlobalOptions(options => options .WithGlobalOptions(options => options
@ -588,7 +466,7 @@ public void Video_TranscodeInMemory()
resStream.Position = 0; resStream.Position = 0;
var vi = FFProbe.Analyse(resStream); var vi = FFProbe.Analyse(resStream);
Assert.AreEqual(vi.PrimaryVideoStream.Width, 128); Assert.AreEqual(vi.PrimaryVideoStream!.Width, 128);
Assert.AreEqual(vi.PrimaryVideoStream.Height, 128); Assert.AreEqual(vi.PrimaryVideoStream.Height, 128);
} }
@ -596,24 +474,55 @@ public void Video_TranscodeInMemory()
public async Task Video_Cancel_Async() public async Task Video_Cancel_Async()
{ {
var outputFile = new TemporaryFile("out.mp4"); var outputFile = new TemporaryFile("out.mp4");
var task = FFMpegArguments 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 .OutputToFile(outputFile, false, opt => opt
.Resize(new Size(1000, 1000))
.WithAudioCodec(AudioCodec.Aac) .WithAudioCodec(AudioCodec.Aac)
.WithVideoCodec(VideoCodec.LibX264) .WithVideoCodec(VideoCodec.LibX264)
.WithConstantRateFactor(14) .WithSpeedPreset(Speed.VeryFast))
.WithSpeedPreset(Speed.VerySlow)
.Loop(3))
.CancellableThrough(out var cancel) .CancellableThrough(out var cancel)
.ProcessAsynchronously(false); .ProcessAsynchronously(false);
await Task.Delay(300); await Task.Delay(300);
cancel(); cancel();
var result = await task; var result = await task;
Assert.IsFalse(result); 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);
}
} }
} }

View file

@ -18,7 +18,7 @@ public DemuxConcatArgument(IEnumerable<string> values)
{ {
Values = values.Select(value => $"file '{value}'"); 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 void Pre() => File.WriteAllLines(_tempFileName, Values);
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;

View file

@ -6,7 +6,7 @@ namespace FFMpegCore.Arguments
/// <summary> /// <summary>
/// Drawtext video filter argument /// Drawtext video filter argument
/// </summary> /// </summary>
public class DrawTextArgument : IArgument public class DrawTextArgument : IVideoFilterArgument
{ {
public readonly DrawTextOptions Options; public readonly DrawTextOptions Options;
@ -15,7 +15,8 @@ public DrawTextArgument(DrawTextOptions options)
Options = options; Options = options;
} }
public string Text => $"-vf drawtext=\"{Options.TextInternal}\""; public string Key { get; } = "drawtext";
public string Value => Options.TextInternal;
} }
public class DrawTextOptions public class DrawTextOptions

View file

@ -0,0 +1,26 @@
using System.Threading;
using System.Threading.Tasks;
namespace FFMpegCore.Arguments
{
/// <summary>
/// Represents an input device parameter
/// </summary>
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}";
}
}

View file

@ -6,7 +6,7 @@ namespace FFMpegCore.Arguments
/// <summary> /// <summary>
/// Represents scale parameter /// Represents scale parameter
/// </summary> /// </summary>
public class ScaleArgument : IArgument public class ScaleArgument : IVideoFilterArgument
{ {
public readonly Size? Size; public readonly Size? Size;
public ScaleArgument(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) 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}";
} }
} }

View file

@ -6,14 +6,16 @@ namespace FFMpegCore.Arguments
/// <summary> /// <summary>
/// Represents size parameter /// Represents size parameter
/// </summary> /// </summary>
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 string Text => Size == null ? string.Empty : $"-s {Size.Value.Width}x{Size.Value.Height}";
public override string Text => Size.HasValue ? $"-s {Size.Value.Width}x{Size.Value.Height}" : string.Empty;
} }
} }

View file

@ -9,7 +9,7 @@ namespace FFMpegCore.Arguments
/// 2 = 90CounterClockwise /// 2 = 90CounterClockwise
/// 3 = 90Clockwise and Vertical Flip /// 3 = 90Clockwise and Vertical Flip
/// </summary> /// </summary>
public class TransposeArgument : IArgument public class TransposeArgument : IVideoFilterArgument
{ {
public readonly Transposition Transposition; public readonly Transposition Transposition;
public TransposeArgument(Transposition transposition) public TransposeArgument(Transposition transposition)
@ -17,6 +17,7 @@ public TransposeArgument(Transposition transposition)
Transposition = transposition; Transposition = transposition;
} }
public string Text => $"-vf \"transpose={(int)Transposition}\""; public string Key { get; } = "transpose";
public string Value => ((int)Transposition).ToString();
} }
} }

View file

@ -0,0 +1,51 @@
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();
public string GetText()
{
if (!Options.Arguments.Any())
throw new FFMpegArgumentException("No video-filter arguments provided");
return $"-vf \"{string.Join(", ", Options.Arguments.Where(arg => !string.IsNullOrEmpty(arg.Value)).Select(arg => $"{arg.Key}={arg.Value.Replace(",", "\\,")}"))}\"";
}
}
public interface IVideoFilterArgument
{
public string Key { get; }
public string Value { get; }
}
public class VideoFilterOptions
{
public List<IVideoFilterArgument> Arguments { get; } = new List<IVideoFilterArgument>();
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 DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions));
private VideoFilterOptions WithArgument(IVideoFilterArgument argument)
{
Arguments.Add(argument);
return this;
}
}
}

View file

@ -15,8 +15,8 @@ public string Extension
{ {
get get
{ {
if (FFMpegOptions.Options.ExtensionOverrides.ContainsKey(Name)) if (GlobalFFOptions.Current.ExtensionOverrides.ContainsKey(Name))
return FFMpegOptions.Options.ExtensionOverrides[Name]; return GlobalFFOptions.Current.ExtensionOverrides[Name];
return "." + Name; return "." + Name;
} }
} }

View file

@ -4,7 +4,6 @@ namespace FFMpegCore.Exceptions
{ {
public enum FFMpegExceptionType public enum FFMpegExceptionType
{ {
Dependency,
Conversion, Conversion,
File, File,
Operation, Operation,
@ -13,16 +12,41 @@ public enum FFMpegExceptionType
public class FFMpegException : Exception 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) : base(message, innerException)
{ {
FfmpegOutput = ffmpegOutput; FFMpegErrorOutput = ffMpegErrorOutput;
FfmpegErrorOutput = ffmpegErrorOutput;
Type = type; 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 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)
{
}
} }
} }

View file

@ -16,17 +16,18 @@ public static class FFMpeg
/// <summary> /// <summary>
/// Saves a 'png' thumbnail from the input video to drive /// Saves a 'png' thumbnail from the input video to drive
/// </summary> /// </summary>
/// <param name="source">Source video analysis</param> /// <param name="input">Source video analysis</param>
/// <param name="output">Output video file path</param> /// <param name="output">Output video file path</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param> /// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param> /// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <returns>Bitmap with the requested snapshot.</returns> /// <returns>Bitmap with the requested snapshot.</returns>
public static bool Snapshot(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null) public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null)
{ {
if (Path.GetExtension(output) != FileExtension.Png) if (Path.GetExtension(output) != FileExtension.Png)
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); var source = FFProbe.Analyse(input);
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime);
return arguments return arguments
.OutputToFile(output, true, outputOptions) .OutputToFile(output, true, outputOptions)
@ -35,32 +36,35 @@ public static bool Snapshot(IMediaAnalysis source, string output, Size? size = n
/// <summary> /// <summary>
/// Saves a 'png' thumbnail from the input video to drive /// Saves a 'png' thumbnail from the input video to drive
/// </summary> /// </summary>
/// <param name="source">Source video analysis</param> /// <param name="input">Source video analysis</param>
/// <param name="output">Output video file path</param> /// <param name="output">Output video file path</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param> /// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param> /// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <returns>Bitmap with the requested snapshot.</returns> /// <returns>Bitmap with the requested snapshot.</returns>
public static Task<bool> SnapshotAsync(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null) public static async Task<bool> SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null)
{ {
if (Path.GetExtension(output) != FileExtension.Png) if (Path.GetExtension(output) != FileExtension.Png)
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); var source = await FFProbe.AnalyseAsync(input);
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime);
return arguments return await arguments
.OutputToFile(output, true, outputOptions) .OutputToFile(output, true, outputOptions)
.ProcessAsynchronously(); .ProcessAsynchronously();
} }
/// <summary> /// <summary>
/// Saves a 'png' thumbnail to an in-memory bitmap /// Saves a 'png' thumbnail to an in-memory bitmap
/// </summary> /// </summary>
/// <param name="source">Source video file.</param> /// <param name="input">Source video file.</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param> /// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param> /// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <returns>Bitmap with the requested snapshot.</returns> /// <returns>Bitmap with the requested snapshot.</returns>
public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null)
{ {
var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); var source = FFProbe.Analyse(input);
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime);
using var ms = new MemoryStream(); using var ms = new MemoryStream();
arguments arguments
@ -75,13 +79,14 @@ public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan
/// <summary> /// <summary>
/// Saves a 'png' thumbnail to an in-memory bitmap /// Saves a 'png' thumbnail to an in-memory bitmap
/// </summary> /// </summary>
/// <param name="source">Source video file.</param> /// <param name="input">Source video file.</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param> /// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param> /// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <returns>Bitmap with the requested snapshot.</returns> /// <returns>Bitmap with the requested snapshot.</returns>
public static async Task<Bitmap> SnapshotAsync(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) public static async Task<Bitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null)
{ {
var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); var source = await FFProbe.AnalyseAsync(input);
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime);
using var ms = new MemoryStream(); using var ms = new MemoryStream();
await arguments await arguments
@ -93,13 +98,13 @@ await arguments
return new Bitmap(ms); return new Bitmap(ms);
} }
private static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) private static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) BuildSnapshotArguments(string input, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null)
{ {
captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3);
size = PrepareSnapshotSize(source, size); size = PrepareSnapshotSize(source, size);
return (FFMpegArguments return (FFMpegArguments
.FromFileInput(source, options => options .FromFileInput(input, false, options => options
.Seek(captureTime)), .Seek(captureTime)),
options => options options => options
.WithVideoCodec(VideoCodec.Png) .WithVideoCodec(VideoCodec.Png)
@ -109,7 +114,7 @@ private static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) Bu
private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize) 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; return null;
var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height); var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height);
@ -146,7 +151,7 @@ private static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) Bu
/// <param name="multithreaded">Is encoding multithreaded.</param> /// <param name="multithreaded">Is encoding multithreaded.</param>
/// <returns>Output video information.</returns> /// <returns>Output video information.</returns>
public static bool Convert( public static bool Convert(
IMediaAnalysis source, string input,
string output, string output,
ContainerFormat format, ContainerFormat format,
Speed speed = Speed.SuperFast, Speed speed = Speed.SuperFast,
@ -155,6 +160,7 @@ public static bool Convert(
bool multithreaded = false) bool multithreaded = false)
{ {
FFMpegHelper.ExtensionExceptionCheck(output, format.Extension); FFMpegHelper.ExtensionExceptionCheck(output, format.Extension);
var source = FFProbe.Analyse(input);
FFMpegHelper.ConversionSizeExceptionCheck(source); FFMpegHelper.ConversionSizeExceptionCheck(source);
var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream.Height / (int)size; var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream.Height / (int)size;
@ -166,41 +172,44 @@ public static bool Convert(
return format.Name switch return format.Name switch
{ {
"mp4" => FFMpegArguments "mp4" => FFMpegArguments
.FromFileInput(source) .FromFileInput(input)
.OutputToFile(output, true, options => options .OutputToFile(output, true, options => options
.UsingMultithreading(multithreaded) .UsingMultithreading(multithreaded)
.WithVideoCodec(VideoCodec.LibX264) .WithVideoCodec(VideoCodec.LibX264)
.WithVideoBitrate(2400) .WithVideoBitrate(2400)
.Scale(outputSize) .WithVideoFilters(filterOptions => filterOptions
.Scale(outputSize))
.WithSpeedPreset(speed) .WithSpeedPreset(speed)
.WithAudioCodec(AudioCodec.Aac) .WithAudioCodec(AudioCodec.Aac)
.WithAudioBitrate(audioQuality)) .WithAudioBitrate(audioQuality))
.ProcessSynchronously(), .ProcessSynchronously(),
"ogv" => FFMpegArguments "ogv" => FFMpegArguments
.FromFileInput(source) .FromFileInput(input)
.OutputToFile(output, true, options => options .OutputToFile(output, true, options => options
.UsingMultithreading(multithreaded) .UsingMultithreading(multithreaded)
.WithVideoCodec(VideoCodec.LibTheora) .WithVideoCodec(VideoCodec.LibTheora)
.WithVideoBitrate(2400) .WithVideoBitrate(2400)
.Scale(outputSize) .WithVideoFilters(filterOptions => filterOptions
.Scale(outputSize))
.WithSpeedPreset(speed) .WithSpeedPreset(speed)
.WithAudioCodec(AudioCodec.LibVorbis) .WithAudioCodec(AudioCodec.LibVorbis)
.WithAudioBitrate(audioQuality)) .WithAudioBitrate(audioQuality))
.ProcessSynchronously(), .ProcessSynchronously(),
"mpegts" => FFMpegArguments "mpegts" => FFMpegArguments
.FromFileInput(source) .FromFileInput(input)
.OutputToFile(output, true, options => options .OutputToFile(output, true, options => options
.CopyChannel() .CopyChannel()
.WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB)
.ForceFormat(VideoType.Ts)) .ForceFormat(VideoType.Ts))
.ProcessSynchronously(), .ProcessSynchronously(),
"webm" => FFMpegArguments "webm" => FFMpegArguments
.FromFileInput(source) .FromFileInput(input)
.OutputToFile(output, true, options => options .OutputToFile(output, true, options => options
.UsingMultithreading(multithreaded) .UsingMultithreading(multithreaded)
.WithVideoCodec(VideoCodec.LibVpx) .WithVideoCodec(VideoCodec.LibVpx)
.WithVideoBitrate(2400) .WithVideoBitrate(2400)
.Scale(outputSize) .WithVideoFilters(filterOptions => filterOptions
.Scale(outputSize))
.WithSpeedPreset(speed) .WithSpeedPreset(speed)
.WithAudioCodec(AudioCodec.LibVorbis) .WithAudioCodec(AudioCodec.LibVorbis)
.WithAudioBitrate(audioQuality)) .WithAudioBitrate(audioQuality))
@ -232,21 +241,22 @@ public static bool PosterWithAudio(string image, string audio, string output)
.UsingShortest()) .UsingShortest())
.ProcessSynchronously(); .ProcessSynchronously();
} }
/// <summary> /// <summary>
/// Joins a list of video files. /// Joins a list of video files.
/// </summary> /// </summary>
/// <param name="output">Output video file.</param> /// <param name="output">Output video file.</param>
/// <param name="videos">List of vides that need to be joined together.</param> /// <param name="videos">List of vides that need to be joined together.</param>
/// <returns>Output video information.</returns> /// <returns>Output video information.</returns>
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); FFMpegHelper.ConversionSizeExceptionCheck(video);
var destinationPath = Path.Combine(FFMpegOptions.Options.TempDirectory, $"{Path.GetFileNameWithoutExtension(video.Path)}{FileExtension.Ts}"); var destinationPath = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}");
Directory.CreateDirectory(FFMpegOptions.Options.TempDirectory); Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder);
Convert(video, destinationPath, VideoType.Ts); Convert(videoPath, destinationPath, VideoType.Ts);
return destinationPath; return destinationPath;
}).ToArray(); }).ToArray();
@ -264,16 +274,6 @@ public static bool Join(string output, params IMediaAnalysis[] videos)
Cleanup(temporaryVideoParts); Cleanup(temporaryVideoParts);
} }
} }
/// <summary>
/// Joins a list of video files.
/// </summary>
/// <param name="output">Output video file.</param>
/// <param name="videos">List of vides that need to be joined together.</param>
/// <returns>Output video information.</returns>
public static bool Join(string output, params string[] videos)
{
return Join(output, videos.Select(videoPath => FFProbe.Analyse(videoPath)).ToArray());
}
/// <summary> /// <summary>
/// Converts an image sequence to a video. /// Converts an image sequence to a video.
@ -284,7 +284,7 @@ public static bool Join(string output, params string[] videos)
/// <returns>Output video information.</returns> /// <returns>Output video information.</returns>
public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images) 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) => var temporaryImageFiles = images.Select((image, index) =>
{ {
FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName)); FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName));
@ -340,10 +340,10 @@ public static bool Mute(string input, string output)
{ {
var source = FFProbe.Analyse(input); var source = FFProbe.Analyse(input);
FFMpegHelper.ConversionSizeExceptionCheck(source); FFMpegHelper.ConversionSizeExceptionCheck(source);
FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); // FFMpegHelper.ExtensionExceptionCheck(output, source.Extension);
return FFMpegArguments return FFMpegArguments
.FromFileInput(source) .FromFileInput(input)
.OutputToFile(output, true, options => options .OutputToFile(output, true, options => options
.CopyChannel(Channel.Video) .CopyChannel(Channel.Video)
.DisableChannel(Channel.Audio)) .DisableChannel(Channel.Audio))
@ -379,10 +379,10 @@ public static bool ReplaceAudio(string input, string inputAudio, string output,
{ {
var source = FFProbe.Analyse(input); var source = FFProbe.Analyse(input);
FFMpegHelper.ConversionSizeExceptionCheck(source); FFMpegHelper.ConversionSizeExceptionCheck(source);
FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); // FFMpegHelper.ExtensionExceptionCheck(output, source.Format.);
return FFMpegArguments return FFMpegArguments
.FromFileInput(source) .FromFileInput(input)
.AddFileInput(inputAudio) .AddFileInput(inputAudio)
.OutputToFile(output, true, options => options .OutputToFile(output, true, options => options
.CopyChannel() .CopyChannel()
@ -398,7 +398,7 @@ internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal()
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
var list = new List<PixelFormat>(); var list = new List<PixelFormat>();
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) => instance.DataReceived += (e, args) =>
{ {
if (PixelFormat.TryParse(args.Data, out var format)) if (PixelFormat.TryParse(args.Data, out var format))
@ -413,14 +413,14 @@ internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal()
public static IReadOnlyList<PixelFormat> GetPixelFormats() public static IReadOnlyList<PixelFormat> GetPixelFormats()
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
return GetPixelFormatsInternal(); return GetPixelFormatsInternal();
return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly(); return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly();
} }
public static bool TryGetPixelFormat(string name, out PixelFormat fmt) 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()); fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return fmt != null; return fmt != null;
@ -443,7 +443,7 @@ private static void ParsePartOfCodecs(Dictionary<string, Codec> codecs, string a
{ {
FFMpegHelper.RootExceptionCheck(); 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) => instance.DataReceived += (e, args) =>
{ {
var codec = parser(args.Data); var codec = parser(args.Data);
@ -485,14 +485,14 @@ internal static Dictionary<string, Codec> GetCodecsInternal()
public static IReadOnlyList<Codec> GetCodecs() public static IReadOnlyList<Codec> GetCodecs()
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
return GetCodecsInternal().Values.ToList().AsReadOnly(); return GetCodecsInternal().Values.ToList().AsReadOnly();
return FFMpegCache.Codecs.Values.ToList().AsReadOnly(); return FFMpegCache.Codecs.Values.ToList().AsReadOnly();
} }
public static IReadOnlyList<Codec> GetCodecs(CodecType type) public static IReadOnlyList<Codec> GetCodecs(CodecType type)
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly(); 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();
} }
@ -504,7 +504,7 @@ public static IReadOnlyList<Codec> GetCodecs(CodecType type)
public static bool TryGetCodec(string name, out Codec codec) 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()); codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return codec != null; return codec != null;
@ -527,7 +527,7 @@ internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
var list = new List<ContainerFormat>(); var list = new List<ContainerFormat>();
using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), "-formats"); using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats");
instance.DataReceived += (e, args) => instance.DataReceived += (e, args) =>
{ {
if (ContainerFormat.TryParse(args.Data, out var fmt)) if (ContainerFormat.TryParse(args.Data, out var fmt))
@ -542,14 +542,14 @@ internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
public static IReadOnlyList<ContainerFormat> GetContainerFormats() public static IReadOnlyList<ContainerFormat> GetContainerFormats()
{ {
if (!FFMpegOptions.Options.UseCache) if (!GlobalFFOptions.Current.UseCache)
return GetContainersFormatsInternal(); return GetContainersFormatsInternal();
return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly(); return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly();
} }
public static bool TryGetContainerFormat(string name, out ContainerFormat fmt) 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()); fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
return fmt != null; return fmt != null;

View file

@ -5,7 +5,7 @@
namespace FFMpegCore namespace FFMpegCore
{ {
public class FFMpegArgumentOptions : FFMpegOptionsBase public class FFMpegArgumentOptions : FFMpegArgumentsBase
{ {
internal FFMpegArgumentOptions() { } internal FFMpegArgumentOptions() { }
@ -15,14 +15,10 @@ internal FFMpegArgumentOptions() { }
public FFMpegArgumentOptions WithAudioBitrate(int bitrate) => WithArgument(new AudioBitrateArgument(bitrate)); public FFMpegArgumentOptions WithAudioBitrate(int bitrate) => WithArgument(new AudioBitrateArgument(bitrate));
public FFMpegArgumentOptions WithAudioSamplingRate(int samplingRate = 48000) => WithArgument(new AudioSamplingRateArgument(samplingRate)); public FFMpegArgumentOptions WithAudioSamplingRate(int samplingRate = 48000) => WithArgument(new AudioSamplingRateArgument(samplingRate));
public FFMpegArgumentOptions WithVariableBitrate(int vbr) => WithArgument(new VariableBitRateArgument(vbr)); 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(int width, int height) => WithArgument(new SizeArgument(width, height));
public FFMpegArgumentOptions Resize(Size? size) => WithArgument(new SizeArgument(size)); 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 WithBitStreamFilter(Channel channel, Filter filter) => WithArgument(new BitStreamFilterArgument(channel, filter));
public FFMpegArgumentOptions WithConstantRateFactor(int crf) => WithArgument(new ConstantRateFactorArgument(crf)); 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(Codec videoCodec) => WithArgument(new VideoCodecArgument(videoCodec));
public FFMpegArgumentOptions WithVideoCodec(string 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 WithVideoBitrate(int bitrate) => WithArgument(new VideoBitrateArgument(bitrate));
public FFMpegArgumentOptions WithVideoFilters(Action<VideoFilterOptions> videoFilterOptions)
{
var videoFilterOptionsObj = new VideoFilterOptions();
videoFilterOptions(videoFilterOptionsObj);
return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj));
}
public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate)); public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate));
public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument()); public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument());
public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed)); 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 WithCustomArgument(string argument) => WithArgument(new CustomArgument(argument));
public FFMpegArgumentOptions Seek(TimeSpan? seekTo) => WithArgument(new SeekArgument(seekTo)); 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 Loop(int times) => WithArgument(new LoopArgument(times));
public FFMpegArgumentOptions OverwriteExisting() => WithArgument(new OverwriteArgument()); public FFMpegArgumentOptions OverwriteExisting() => WithArgument(new OverwriteArgument());
@ -56,8 +58,6 @@ internal FFMpegArgumentOptions() { }
public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat));
public FFMpegArgumentOptions ForcePixelFormat(PixelFormat 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) public FFMpegArgumentOptions WithArgument(IArgument argument)
{ {
Arguments.Add(argument); Arguments.Add(argument);

View file

@ -27,7 +27,7 @@ internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments)
public string Arguments => _ffMpegArguments.Text; public string Arguments => _ffMpegArguments.Text;
private event EventHandler CancelEvent = null!; private event EventHandler<int> CancelEvent = null!;
public FFMpegArgumentProcessor NotifyOnProgress(Action<double> onPercentageProgress, TimeSpan totalTimeSpan) public FFMpegArgumentProcessor NotifyOnProgress(Action<double> onPercentageProgress, TimeSpan totalTimeSpan)
{ {
@ -45,21 +45,25 @@ public FFMpegArgumentProcessor NotifyOnOutput(Action<string, DataType> onOutput)
_onOutput = onOutput; _onOutput = onOutput;
return this; 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; return this;
} }
public bool ProcessSynchronously(bool throwOnError = true) public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{ {
using var instance = PrepareInstance(out var cancellationTokenSource); using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource);
var errorCode = -1; var errorCode = -1;
void OnCancelEvent(object sender, EventArgs args) void OnCancelEvent(object sender, int timeout)
{ {
instance.SendInput("q"); instance.SendInput("q");
cancellationTokenSource.Cancel();
instance.Started = false; if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true))
{
cancellationTokenSource.Cancel();
instance.Started = false;
}
} }
CancelEvent += OnCancelEvent; CancelEvent += OnCancelEvent;
instance.Exited += delegate { cancellationTokenSource.Cancel(); }; instance.Exited += delegate { cancellationTokenSource.Cancel(); };
@ -76,37 +80,30 @@ void OnCancelEvent(object sender, EventArgs args)
} }
catch (Exception e) catch (Exception e)
{ {
if (!HandleException(throwOnError, e, instance.ErrorData, instance.OutputData)) return false; if (!HandleException(throwOnError, e, instance.ErrorData)) return false;
} }
finally finally
{ {
CancelEvent -= OnCancelEvent; CancelEvent -= OnCancelEvent;
} }
return HandleCompletion(throwOnError, errorCode, instance.ErrorData, instance.OutputData); return HandleCompletion(throwOnError, errorCode, instance.ErrorData);
} }
private bool HandleCompletion(bool throwOnError, int errorCode, IReadOnlyList<string> errorData, IReadOnlyList<string> outputData) public async Task<bool> ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{ {
if (throwOnError && errorCode != 0) using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource);
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<bool> ProcessAsynchronously(bool throwOnError = true)
{
using var instance = PrepareInstance(out var cancellationTokenSource);
var errorCode = -1; var errorCode = -1;
void OnCancelEvent(object sender, EventArgs args) void OnCancelEvent(object sender, int timeout)
{ {
instance.SendInput("q"); instance.SendInput("q");
cancellationTokenSource.Cancel();
instance.Started = false; if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true))
{
cancellationTokenSource.Cancel();
instance.Started = false;
}
} }
CancelEvent += OnCancelEvent; CancelEvent += OnCancelEvent;
@ -122,26 +119,38 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t =>
} }
catch (Exception e) catch (Exception e)
{ {
if (!HandleException(throwOnError, e, instance.ErrorData, instance.OutputData)) return false; if (!HandleException(throwOnError, e, instance.ErrorData)) return false;
} }
finally finally
{ {
CancelEvent -= OnCancelEvent; 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<string> 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.RootExceptionCheck();
FFMpegHelper.VerifyFFMpegExists(); FFMpegHelper.VerifyFFMpegExists(ffMpegOptions);
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
FileName = FFMpegOptions.Options.FFmpegBinary(), FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions),
Arguments = _ffMpegArguments.Text, Arguments = _ffMpegArguments.Text,
StandardOutputEncoding = FFMpegOptions.Options.Encoding, StandardOutputEncoding = ffMpegOptions.Encoding,
StandardErrorEncoding = FFMpegOptions.Options.Encoding, StandardErrorEncoding = ffMpegOptions.Encoding,
}; };
var instance = new Instance(startInfo); var instance = new Instance(startInfo);
cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource = new CancellationTokenSource();
@ -153,12 +162,12 @@ private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSo
} }
private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList<string> errorData, IReadOnlyList<string> outputData) private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList<string> errorData)
{ {
if (!throwOnError) if (!throwOnError)
return false; 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) private void OutputData(object sender, (DataType Type, string Data) msg)

View file

@ -9,26 +9,26 @@
namespace FFMpegCore 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() { } 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<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments); public static FFMpegArguments FromConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments);
public static FFMpegArguments FromDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments); public static FFMpegArguments FromDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments);
public static FFMpegArguments FromFileInput(string filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(verifyExists, filePath), addArguments); public static FFMpegArguments FromFileInput(string filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(verifyExists, filePath), addArguments);
public static FFMpegArguments FromFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(fileInfo.FullName, false), addArguments); public static FFMpegArguments FromFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(fileInfo.FullName, false), addArguments);
public static FFMpegArguments FromFileInput(IMediaAnalysis mediaAnalysis, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(mediaAnalysis.Path, false), addArguments);
public static FFMpegArguments FromUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); public static FFMpegArguments FromUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments);
public static FFMpegArguments FromDeviceInput(string device, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputDeviceArgument(device), addArguments);
public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments); public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments);
public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalOptions> configureOptions) public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalArguments> configureOptions)
{ {
configureOptions(_globalOptions); configureOptions(_globalArguments);
return this; return this;
} }
@ -36,7 +36,6 @@ public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalOptions> configureOp
public FFMpegArguments AddDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new DemuxConcatArgument(filePaths), addArguments); public FFMpegArguments AddDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new DemuxConcatArgument(filePaths), addArguments);
public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(verifyExists, filePath), addArguments); public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(verifyExists, filePath), addArguments);
public FFMpegArguments AddFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments); public FFMpegArguments AddFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments);
public FFMpegArguments AddFileInput(IMediaAnalysis mediaAnalysis, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(mediaAnalysis.Path, false), addArguments);
public FFMpegArguments AddUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); public FFMpegArguments AddUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments);
public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments); public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments);

View file

@ -3,7 +3,7 @@
namespace FFMpegCore namespace FFMpegCore
{ {
public abstract class FFMpegOptionsBase public abstract class FFMpegArgumentsBase
{ {
internal readonly List<IArgument> Arguments = new List<IArgument>(); internal readonly List<IArgument> Arguments = new List<IArgument>();
} }

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<string, string> DefaultExtensionsOverrides = new Dictionary<string, string>
{
{ "mpegts", ".ts" },
};
public static FFMpegOptions Options { get; private set; } = new FFMpegOptions();
public static void Configure(Action<FFMpegOptions> 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<FFMpegOptions>(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<string, string> ExtensionOverrides { get; private set; } = new Dictionary<string, string>();
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);
}
}
}

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FFMpegCore.Exceptions; using FFMpegCore.Exceptions;
@ -14,7 +15,7 @@ public class RawVideoPipeSource : IPipeSource
public string StreamFormat { get; private set; } = null!; public string StreamFormat { get; private set; } = null!;
public int Width { get; private set; } public int Width { get; private set; }
public int Height { 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 bool _formatInitialized;
private readonly IEnumerator<IVideoFrame> _framesEnumerator; private readonly IEnumerator<IVideoFrame> _framesEnumerator;
@ -42,7 +43,7 @@ public string GetStreamArguments()
_formatInitialized = true; _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(System.IO.Stream outputStream, CancellationToken cancellationToken)

View file

@ -9,10 +9,12 @@
<Version>3.0.0.0</Version> <Version>3.0.0.0</Version>
<AssemblyVersion>3.0.0.0</AssemblyVersion> <AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion> <FileVersion>3.0.0.0</FileVersion>
<PackageReleaseNotes>- return null from FFProbe.Analyse* when no media format was detected <PackageReleaseNotes>- Video filter args refactored to support multiple arguments
- Expose tags as string dictionary on IMediaAnalysis (thanks hey-red)</PackageReleaseNotes> - Cancel improved with timeout (thanks TFleury)
- Basic support for webcam/mic input through InputDeviceArgument (thanks TFleury)
- Other fixes and improvements</PackageReleaseNotes>
<LangVersion>8</LangVersion> <LangVersion>8</LangVersion>
<PackageVersion>3.4.0</PackageVersion> <PackageVersion>4.0.0</PackageVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Authors>Malte Rosenbjerg, Vlad Jerca</Authors> <Authors>Malte Rosenbjerg, Vlad Jerca</Authors>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags> <PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>

View file

@ -1,2 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffmpeg/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffmpeg/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffprobe/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

37
FFMpegCore/FFOptions.cs Normal file
View file

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace FFMpegCore
{
public class FFOptions
{
/// <summary>
/// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH
/// </summary>
public string BinaryFolder { get; set; } = string.Empty;
/// <summary>
/// Folder used for temporary files necessary for static methods on FFMpeg class
/// </summary>
public string TemporaryFilesFolder { get; set; } = Path.GetTempPath();
/// <summary>
/// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes
/// </summary>
public Encoding Encoding { get; set; } = Encoding.Default;
/// <summary>
///
/// </summary>
public Dictionary<string, string> ExtensionOverrides { get; set; } = new Dictionary<string, string>
{
{ "mpegts", ".ts" },
};
/// <summary>
/// Whether to cache calls to get ffmpeg codec, pixel- and container-formats
/// </summary>
public bool UseCache { get; set; } = true;
}
}

View file

@ -12,26 +12,32 @@ namespace FFMpegCore
{ {
public static class FFProbe 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)) if (!File.Exists(filePath))
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PrepareInstance(filePath, outputCapacity); using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
instance.BlockUntilFinished(); var exitCode = instance.BlockUntilFinished();
return ParseOutput(filePath, instance); 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); using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
instance.BlockUntilFinished(); var exitCode = instance.BlockUntilFinished();
return ParseOutput(uri.AbsoluteUri, instance); 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 streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource); var pipeArgument = new InputPipeArgument(streamPipeSource);
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity); using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
pipeArgument.Pre(); pipeArgument.Pre();
var task = instance.FinishedRunning(); var task = instance.FinishedRunning();
@ -46,36 +52,36 @@ public static class FFProbe
} }
var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult(); var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult();
if (exitCode != 0) 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<IMediaAnalysis?> AnalyseAsync(string filePath, int outputCapacity = int.MaxValue) public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
if (!File.Exists(filePath)) if (!File.Exists(filePath))
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PrepareInstance(filePath, outputCapacity); using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
await instance.FinishedRunning(); await instance.FinishedRunning().ConfigureAwait(false);
return ParseOutput(filePath, instance); return ParseOutput(instance);
} }
public static async Task<IMediaAnalysis?> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue) public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
await instance.FinishedRunning(); await instance.FinishedRunning().ConfigureAwait(false);
return ParseOutput(uri.AbsoluteUri, instance); return ParseOutput(instance);
} }
public static async Task<IMediaAnalysis?> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue) public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
{ {
var streamPipeSource = new StreamPipeSource(stream); var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource); var pipeArgument = new InputPipeArgument(streamPipeSource);
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity); using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
pipeArgument.Pre(); pipeArgument.Pre();
var task = instance.FinishedRunning(); var task = instance.FinishedRunning();
try try
{ {
await pipeArgument.During(); await pipeArgument.During().ConfigureAwait(false);
} }
catch(IOException) catch(IOException)
{ {
@ -84,31 +90,34 @@ public static class FFProbe
{ {
pipeArgument.Post(); pipeArgument.Post();
} }
var exitCode = await task; var exitCode = await task.ConfigureAwait(false);
if (exitCode != 0) 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 process returned exit status {exitCode}", null, string.Join("\n", instance.ErrorData));
pipeArgument.Post(); 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 json = string.Join(string.Empty, instance.OutputData);
var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbeAnalysis>(json, new JsonSerializerOptions var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbeAnalysis>(json, new JsonSerializerOptions
{ {
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true
})!; });
if (ffprobeAnalysis?.Format == null) return null;
return new MediaAnalysis(filePath, ffprobeAnalysis); if (ffprobeAnalysis?.Format == null)
throw new Exception();
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.RootExceptionCheck();
FFProbeHelper.VerifyFFProbeExists(); FFProbeHelper.VerifyFFProbeExists(ffOptions);
var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\""; var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"";
var instance = new Instance(FFMpegOptions.Options.FFProbeBinary(), arguments) {DataBufferCapacity = outputCapacity}; var instance = new Instance(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) {DataBufferCapacity = outputCapacity};
return instance; return instance;
} }
} }

View file

@ -5,12 +5,10 @@ namespace FFMpegCore
{ {
public interface IMediaAnalysis public interface IMediaAnalysis
{ {
string Path { get; }
string Extension { get; }
TimeSpan Duration { get; } TimeSpan Duration { get; }
MediaFormat Format { get; } MediaFormat Format { get; }
AudioStream PrimaryAudioStream { get; } AudioStream? PrimaryAudioStream { get; }
VideoStream PrimaryVideoStream { get; } VideoStream? PrimaryVideoStream { get; }
List<VideoStream> VideoStreams { get; } List<VideoStream> VideoStreams { get; }
List<AudioStream> AudioStreams { get; } List<AudioStream> AudioStreams { get; }
} }

View file

@ -9,14 +9,11 @@ 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); 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); Format = ParseFormat(analysis.Format);
VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList(); VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList();
AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).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) private MediaFormat ParseFormat(Format analysisFormat)
@ -33,9 +30,6 @@ private MediaFormat ParseFormat(Format analysisFormat)
}; };
} }
public string Path { get; }
public string Extension => System.IO.Path.GetExtension(Path);
public TimeSpan Duration => new[] public TimeSpan Duration => new[]
{ {
Format.Duration, Format.Duration,
@ -44,9 +38,9 @@ private MediaFormat ParseFormat(Format analysisFormat)
}.Max(); }.Max();
public MediaFormat Format { get; } 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<VideoStream> VideoStreams { get; } public List<VideoStream> VideoStreams { get; }
public List<AudioStream> AudioStreams { get; } public List<AudioStream> AudioStreams { get; }

View file

@ -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<FFOptions>(File.ReadAllText(ConfigFile))!;
}
else
{
Current = new FFOptions();
}
}
public static void Configure(Action<FFOptions> 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);
}
}
}

View file

@ -11,21 +11,15 @@ public static class FFMpegHelper
private static bool _ffmpegVerified; private static bool _ffmpegVerified;
public static void ConversionSizeExceptionCheck(Image image) public static void ConversionSizeExceptionCheck(Image image)
{ => ConversionSizeExceptionCheck(image.Size.Width, image.Size.Height);
ConversionSizeExceptionCheck(image.Size);
}
public static void ConversionSizeExceptionCheck(IMediaAnalysis info) public static void ConversionSizeExceptionCheck(IMediaAnalysis info)
{ => ConversionSizeExceptionCheck(info.PrimaryVideoStream!.Width, info.PrimaryVideoStream.Height);
ConversionSizeExceptionCheck(new Size(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!"); 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) public static void ExtensionExceptionCheck(string filename, string extension)
@ -37,17 +31,17 @@ public static void ExtensionExceptionCheck(string filename, string extension)
public static void RootExceptionCheck() public static void RootExceptionCheck()
{ {
if (FFMpegOptions.Options.RootDirectory == null) if (GlobalFFOptions.Current.BinaryFolder == null)
throw new FFMpegException(FFMpegExceptionType.Dependency, throw new FFOptionsException("FFMpeg root is not configured in app config. Missing key 'BinaryFolder'.");
"FFMpeg root is not configured in app config. Missing key 'ffmpegRoot'.");
} }
public static void VerifyFFMpegExists() public static void VerifyFFMpegExists(FFOptions ffMpegOptions)
{ {
if (_ffmpegVerified) return; if (_ffmpegVerified) return;
var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFmpegBinary(), "-version"); var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version");
_ffmpegVerified = exitCode == 0; _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");
} }
} }
} }

View file

@ -20,18 +20,17 @@ public static int Gcd(int first, int second)
public static void RootExceptionCheck() public static void RootExceptionCheck()
{ {
if (FFMpegOptions.Options.RootDirectory == null) if (GlobalFFOptions.Current.BinaryFolder == null)
throw new FFMpegException(FFMpegExceptionType.Dependency, throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'.");
"FFProbe root is not configured in app config. Missing key 'ffmpegRoot'.");
} }
public static void VerifyFFProbeExists() public static void VerifyFFProbeExists(FFOptions ffMpegOptions)
{ {
if (_ffprobeVerified) return; if (_ffprobeVerified) return;
var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFProbeBinary(), "-version"); var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version");
_ffprobeVerified = exitCode == 0; _ffprobeVerified = exitCode == 0;
if (!_ffprobeVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system"); if (!_ffprobeVerified)
throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system");
} }
} }
} }

View file

@ -213,6 +213,7 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f
<a href="https://github.com/tugrulelmas"><img src="https://avatars3.githubusercontent.com/u/3829187?v=4" title="tugrulelmas" width="80" height="80"></a> <a href="https://github.com/tugrulelmas"><img src="https://avatars3.githubusercontent.com/u/3829187?v=4" title="tugrulelmas" width="80" height="80"></a>
<a href="https://github.com/rosenbjerg"><img src="https://avatars3.githubusercontent.com/u/11181960?v=4" title="rosenbjerg" width="80" height="80"></a> <a href="https://github.com/rosenbjerg"><img src="https://avatars3.githubusercontent.com/u/11181960?v=4" title="rosenbjerg" width="80" height="80"></a>
<a href="https://github.com/WeihanLi"><img src="https://avatars3.githubusercontent.com/u/7604648?v=4" title="weihanli" width="80" height="80"></a> <a href="https://github.com/WeihanLi"><img src="https://avatars3.githubusercontent.com/u/7604648?v=4" title="weihanli" width="80" height="80"></a>
<a href="https://github.com/tiesont"><img src="https://avatars3.githubusercontent.com/u/420293?v=4" title="tiesont" width="80" height="80"></a>
### License ### License