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