diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae51d97..994a569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,18 @@ name: CI -on: [push, pull_request] + +on: + push: + branches-ignore: + - release + pull_request: + branches: + - master + - release + jobs: ci: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 7 steps: - uses: actions/checkout@v1 - name: Prepare FFMpeg @@ -12,7 +21,5 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.101 - - name: Build with dotnet - run: dotnet build - name: Test with dotnet - run: dotnet test + run: dotnet test --logger GitHubActions diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b20d192..5f7efcd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,16 +8,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - name: Prepare FFMpeg - run: sudo apt-get update && sudo apt-get install -y ffmpeg - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1 - - name: Build solution -c Release - run: dotnet build - - name: Run unit tests - run: dotnet test + - name: Build solution + run: dotnet build --output build - name: Publish NuGet package - run: NUPKG=`find . -type f -name FFMpegCore*.nupkg` && dotnet nuget push $NUPKG -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json + run: dotnet nuget push "build/*.nupkg" --skip-duplicate --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index ee954c5..4092ad6 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -2,7 +2,6 @@ using System; using FFMpegCore.Arguments; using FFMpegCore.Enums; -using FFMpegCore.Exceptions; namespace FFMpegCore.Test { @@ -15,217 +14,217 @@ public class ArgumentBuilderTest : BaseTest [TestMethod] public void Builder_BuildString_IO_1() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").OutputToFile("output.mp4").Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4").Arguments; Assert.AreEqual("-i \"input.mp4\" \"output.mp4\" -y", str); } [TestMethod] public void Builder_BuildString_Scale() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").Scale(VideoSize.Hd).OutputToFile("output.mp4").Arguments; + 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); } [TestMethod] public void Builder_BuildString_AudioCodec() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithAudioCodec(AudioCodec.Aac).OutputToFile("output.mp4").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.FromInputFiles(true, "input.mp4").WithAudioBitrate(AudioQuality.Normal).OutputToFile("output.mp4").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.FromInputFiles(true, "input.mp4").WithVerbosityLevel().OutputToFile("output.mp4", false).Arguments; - Assert.AreEqual("-i \"input.mp4\" -hide_banner -loglevel error \"output.mp4\"", str); + 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); } [TestMethod] public void Builder_BuildString_AudioCodec_Fluent() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithAudioCodec(AudioCodec.Aac).WithAudioBitrate(128).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").WithBitStreamFilter(Channel.Audio, Filter.H264_Mp4ToAnnexB).OutputToFile("output.mp4", false).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; + 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; + Assert.AreEqual("-i \"input.mp4\" -hwaccel cuvid \"output.mp4\"", str); + } [TestMethod] public void Builder_BuildString_Concat() { - var str = FFMpegArguments.FromConcatenation(_concatFiles).OutputToFile("output.mp4", false).Arguments; + var str = FFMpegArguments.FromConcatInput(_concatFiles).OutputToFile("output.mp4", false).Arguments; Assert.AreEqual("-i \"concat:1.mp4|2.mp4|3.mp4|4.mp4\" \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Copy_Audio() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").CopyChannel(Channel.Audio).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").CopyChannel(Channel.Video).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").CopyChannel().OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").DisableChannel(Channel.Audio).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").DisableChannel(Channel.Video).OutputToFile("output.mp4", false).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_DisableChannel_Both() - { - Assert.ThrowsException(() => FFMpegArguments.FromInputFiles(true, "input.mp4").DisableChannel(Channel.Both)); - } - [TestMethod] public void Builder_BuildString_AudioSamplingRate_Default() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithAudioSamplingRate().OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").WithAudioSamplingRate(44000).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").WithVariableBitrate(5).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").WithFastStart().OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").OverwriteExisting().OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").WithoutMetadata().OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").Transpose(Transposition.CounterClockwise90).OutputToFile("output.mp4", false).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Transpose(Transposition.CounterClockwise90)).Arguments; Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2\" \"output.mp4\"", str); } - [TestMethod] - public void Builder_BuildString_CpuSpeed() - { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithCpuSpeed(10).OutputToFile("output.mp4", false).Arguments; - Assert.AreEqual("-i \"input.mp4\" -quality good -cpu-used 10 -deadline realtime \"output.mp4\"", str); - } - [TestMethod] public void Builder_BuildString_ForceFormat() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").ForceFormat(VideoType.Mp4).OutputToFile("output.mp4", false).Arguments; - Assert.AreEqual("-i \"input.mp4\" -f mp4 \"output.mp4\"", str); + 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.FromInputFiles(true, "input.mp4").WithFrameOutputCount(50).OutputToFile("output.mp4", false).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); } [TestMethod] public void Builder_BuildString_VideoStreamNumber() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithVideoStream(1).OutputToFile("output.mp4", false).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.SelectStream(1)).Arguments; Assert.AreEqual("-i \"input.mp4\" -map 0:1 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_FrameRate() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithFramerate(50).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").Loop(50).OutputToFile("output.mp4", false).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); } [TestMethod] public void Builder_BuildString_Seek() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").Seek(TimeSpan.FromSeconds(10)).OutputToFile("output.mp4", false).Arguments; - Assert.AreEqual("-i \"input.mp4\" -ss 00:00:10 \"output.mp4\"", str); + 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); } [TestMethod] public void Builder_BuildString_Shortest() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").UsingShortest().OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").Resize(1920, 1080).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").WithSpeedPreset(Speed.Fast).OutputToFile("output.mp4", false).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); } @@ -233,17 +232,18 @@ public void Builder_BuildString_Speed() public void Builder_BuildString_DrawtextFilter() { var str = FFMpegArguments - .FromInputFiles(true, "input.mp4") - .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")) - .OutputToFile("output.mp4", false).Arguments; + .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"))) + .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); } @@ -252,10 +252,11 @@ public void Builder_BuildString_DrawtextFilter() public void Builder_BuildString_DrawtextFilter_Alt() { var str = FFMpegArguments - .FromInputFiles(true, "input.mp4") - .DrawText(DrawTextOptions - .Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24"))) - .OutputToFile("output.mp4", false).Arguments; + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .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); } @@ -263,35 +264,35 @@ public void Builder_BuildString_DrawtextFilter_Alt() [TestMethod] public void Builder_BuildString_StartNumber() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithStartNumber(50).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").UsingThreads(50).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").UsingMultithreading(true).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").WithVideoCodec(VideoCodec.LibX264).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").WithVideoCodec(VideoCodec.LibX264).ForcePixelFormat("yuv420p").OutputToFile("output.mp4").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); } @@ -299,17 +300,17 @@ public void Builder_BuildString_Codec_Override() [TestMethod] public void Builder_BuildString_Duration() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").WithDuration(TimeSpan.FromSeconds(20)).OutputToFile("output.mp4", false).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.FromInputFiles(true, "input.mp4").WithCustomArgument(null).OutputToFile("output.mp4", false).Arguments; - Assert.AreEqual("-i \"input.mp4\" \"output.mp4\"", str); + 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.FromInputFiles(true, "input.mp4").WithCustomArgument("-acodec copy").OutputToFile("output.mp4", false).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); } @@ -317,7 +318,7 @@ public void Builder_BuildString_Raw() [TestMethod] public void Builder_BuildString_ForcePixelFormat() { - var str = FFMpegArguments.FromInputFiles(true, "input.mp4").ForcePixelFormat("yuv444p").OutputToFile("output.mp4", false).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/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 50ea8d5..971b098 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -15,6 +15,18 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + @@ -24,6 +36,7 @@ + diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index d0f5f7d..7fe928e 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -18,7 +18,7 @@ public void Probe_TooLongOutput() public void Probe_Success() { var info = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); - Assert.AreEqual(13, info.Duration.Seconds); + Assert.AreEqual(3, info.Duration.Seconds); Assert.AreEqual(".mp4", info.Extension); Assert.AreEqual(VideoLibrary.LocalVideo.FullName, info.Path); @@ -27,10 +27,10 @@ public void Probe_Success() Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName); Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName); Assert.AreEqual("LC", info.PrimaryAudioStream.Profile); - Assert.AreEqual(381988, info.PrimaryAudioStream.BitRate); + Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate); Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz); - Assert.AreEqual(862991, 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); @@ -44,11 +44,11 @@ public void Probe_Success() Assert.AreEqual("Main", info.PrimaryVideoStream.Profile); } - [TestMethod] + [TestMethod, Timeout(10000)] public async Task Probe_Async_Success() { var info = await FFProbe.AnalyseAsync(VideoLibrary.LocalVideo.FullName); - Assert.AreEqual(13, info.Duration.Seconds); + Assert.AreEqual(3, info.Duration.Seconds); } [TestMethod, Timeout(10000)] @@ -56,15 +56,15 @@ public void Probe_Success_FromStream() { using var stream = File.OpenRead(VideoLibrary.LocalVideoWebm.FullName); var info = FFProbe.Analyse(stream); - Assert.AreEqual(10, info.Duration.Seconds); + Assert.AreEqual(3, info.Duration.Seconds); } - [TestMethod] + [TestMethod, Timeout(10000)] public async Task Probe_Success_FromStream_Async() { await using var stream = File.OpenRead(VideoLibrary.LocalVideoWebm.FullName); var info = await FFProbe.AnalyseAsync(stream); - Assert.AreEqual(10, info.Duration.Seconds); + Assert.AreEqual(3, info.Duration.Seconds); } } } \ No newline at end of file diff --git a/FFMpegCore.Test/Resources/VideoLibrary.cs b/FFMpegCore.Test/Resources/VideoLibrary.cs index 2214ced..8bb0139 100644 --- a/FFMpegCore.Test/Resources/VideoLibrary.cs +++ b/FFMpegCore.Test/Resources/VideoLibrary.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using FFMpegCore.Enums; namespace FFMpegCore.Test.Resources @@ -15,10 +16,10 @@ public enum ImageType public static class VideoLibrary { - public static readonly FileInfo LocalVideo = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input.mp4"); - public static readonly FileInfo LocalVideoWebm = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input.webm"); - public static readonly FileInfo LocalVideoAudioOnly = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}audio_only.mp4"); - public static readonly FileInfo LocalVideoNoAudio = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}mute.mp4"); + public static readonly FileInfo LocalVideo = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input_3sec.mp4"); + public static readonly FileInfo LocalVideoWebm = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input_3sec.webm"); + public static readonly FileInfo LocalVideoAudioOnly = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input_audio_only_10sec.mp4"); + public static readonly FileInfo LocalVideoNoAudio = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input_video_only_3sec.mp4"); public static readonly FileInfo LocalAudio = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}audio.mp3"); public static readonly FileInfo LocalCover = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}cover.png"); public static readonly FileInfo ImageDirectory = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}images"); @@ -44,7 +45,7 @@ public static string OutputLocation(this FileInfo file, string type, string keyw string originalLocation = file.Directory.FullName, outputFile = file.Name.Replace(file.Extension, keyword + "." + type.ToLowerInvariant()); - return $"{originalLocation}{Path.DirectorySeparatorChar}{outputFile}"; + return $"{originalLocation}{Path.DirectorySeparatorChar}{Guid.NewGuid()}_{outputFile}"; } } } diff --git a/FFMpegCore.Test/Resources/audio_only.mp4 b/FFMpegCore.Test/Resources/audio_only.mp4 deleted file mode 100644 index 55aa483..0000000 Binary files a/FFMpegCore.Test/Resources/audio_only.mp4 and /dev/null differ diff --git a/FFMpegCore.Test/Resources/input.mp4 b/FFMpegCore.Test/Resources/input.mp4 deleted file mode 100644 index 73bbd71..0000000 Binary files a/FFMpegCore.Test/Resources/input.mp4 and /dev/null differ diff --git a/FFMpegCore.Test/Resources/input.webm b/FFMpegCore.Test/Resources/input.webm deleted file mode 100644 index 3faead4..0000000 Binary files a/FFMpegCore.Test/Resources/input.webm and /dev/null differ diff --git a/FFMpegCore.Test/Resources/input_3sec.mp4 b/FFMpegCore.Test/Resources/input_3sec.mp4 new file mode 100644 index 0000000..7b59bc7 Binary files /dev/null and b/FFMpegCore.Test/Resources/input_3sec.mp4 differ diff --git a/FFMpegCore.Test/Resources/input_3sec.webm b/FFMpegCore.Test/Resources/input_3sec.webm new file mode 100644 index 0000000..8f6790f Binary files /dev/null and b/FFMpegCore.Test/Resources/input_3sec.webm differ diff --git a/FFMpegCore.Test/Resources/input_audio_only_10sec.mp4 b/FFMpegCore.Test/Resources/input_audio_only_10sec.mp4 new file mode 100644 index 0000000..67243df Binary files /dev/null and b/FFMpegCore.Test/Resources/input_audio_only_10sec.mp4 differ diff --git a/FFMpegCore.Test/Resources/input_video_only_3sec.mp4 b/FFMpegCore.Test/Resources/input_video_only_3sec.mp4 new file mode 100644 index 0000000..7d13848 Binary files /dev/null and b/FFMpegCore.Test/Resources/input_video_only_3sec.mp4 differ diff --git a/FFMpegCore.Test/Resources/mute.mp4 b/FFMpegCore.Test/Resources/mute.mp4 deleted file mode 100644 index 095e8ba..0000000 Binary files a/FFMpegCore.Test/Resources/mute.mp4 and /dev/null differ diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index e51fdea..e4f750d 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -63,51 +63,51 @@ public bool Convert(ContainerFormat type, bool multithreaded = false, VideoSize } } - private void ConvertFromStreamPipe(ContainerFormat type, params IArgument[] inputArguments) + private void ConvertFromStreamPipe(ContainerFormat type, params IArgument[] arguments) { var output = Input.OutputLocation(type); try { var input = FFProbe.Analyse(VideoLibrary.LocalVideoWebm.FullName); - using (var inputStream = File.OpenRead(input.Path)) + using var inputStream = File.OpenRead(input.Path); + var processor = FFMpegArguments + .FromPipeInput(new StreamPipeSource(inputStream)) + .OutputToFile(output, false, opt => + { + foreach (var arg in arguments) + opt.WithArgument(arg); + }); + + var scaling = arguments.OfType().FirstOrDefault(); + + var success = processor.ProcessSynchronously(); + + var outputVideo = FFProbe.Analyse(output); + + Assert.IsTrue(success); + Assert.IsTrue(File.Exists(output)); + Assert.IsTrue(Math.Abs((outputVideo.Duration - input.Duration).TotalMilliseconds) < 1000.0 / input.PrimaryVideoStream.FrameRate); + + if (scaling?.Size == null) { - var pipeSource = new StreamPipeSource(inputStream); - var arguments = FFMpegArguments.FromPipe(pipeSource); - foreach (var arg in inputArguments) - arguments.WithArgument(arg); - var processor = arguments.OutputToFile(output); - - var scaling = arguments.Find(); - - var success = processor.ProcessSynchronously(); - - var outputVideo = FFProbe.Analyse(output); - - Assert.IsTrue(success); - Assert.IsTrue(File.Exists(output)); - 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, input.PrimaryVideoStream.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); } - else + + if (scaling.Size.Value.Height != -1) { - 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); + 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); } } finally @@ -117,17 +117,18 @@ private void ConvertFromStreamPipe(ContainerFormat type, params IArgument[] inpu } } - private void ConvertToStreamPipe(params IArgument[] inputArguments) + private void ConvertToStreamPipe(params IArgument[] arguments) { using var ms = new MemoryStream(); - var arguments = FFMpegArguments.FromInputFiles(VideoLibrary.LocalVideo); - foreach (var arg in inputArguments) - arguments.WithArgument(arg); + var processor = FFMpegArguments + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToPipe(new StreamPipeSink(ms), opt => + { + foreach (var arg in arguments) + opt.WithArgument(arg); + }); - var streamPipeDataReader = new StreamPipeSink(ms); - var processor = arguments.OutputToPipe(streamPipeDataReader); - - var scaling = arguments.Find(); + var scaling = arguments.OfType().FirstOrDefault(); processor.ProcessSynchronously(); @@ -159,7 +160,7 @@ private void ConvertToStreamPipe(params IArgument[] inputArguments) } } - public void Convert(ContainerFormat type, Action validationMethod, params IArgument[] inputArguments) + public void Convert(ContainerFormat type, Action validationMethod, params IArgument[] arguments) { var output = Input.OutputLocation(type); @@ -167,13 +168,15 @@ public void Convert(ContainerFormat type, Action validationMetho { var input = FFProbe.Analyse(Input.FullName); - var arguments = FFMpegArguments.FromInputFiles(VideoLibrary.LocalVideo.FullName); - foreach (var arg in inputArguments) - arguments.WithArgument(arg); + var processor = FFMpegArguments + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile(output, false, opt => + { + foreach (var arg in arguments) + opt.WithArgument(arg); + }); - var processor = arguments.OutputToFile(output); - - var scaling = arguments.Find(); + var scaling = arguments.OfType().FirstOrDefault(); processor.ProcessSynchronously(); var outputVideo = FFProbe.Analyse(output); @@ -214,19 +217,19 @@ public void Convert(ContainerFormat type, params IArgument[] inputArguments) Convert(type, null, inputArguments); } - public void ConvertFromPipe(ContainerFormat type, System.Drawing.Imaging.PixelFormat fmt, params IArgument[] inputArguments) + public void ConvertFromPipe(ContainerFormat type, System.Drawing.Imaging.PixelFormat fmt, params IArgument[] arguments) { var output = Input.OutputLocation(type); try { var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, fmt, 256, 256)); - var arguments = FFMpegArguments.FromPipe(videoFramesSource); - foreach (var arg in inputArguments) - arguments.WithArgument(arg); - var processor = arguments.OutputToFile(output); - - var scaling = arguments.Find(); + var processor = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, false, opt => + { + foreach (var arg in arguments) + opt.WithArgument(arg); + }); + var scaling = arguments.OfType().FirstOrDefault(); processor.ProcessSynchronously(); var outputVideo = FFProbe.Analyse(output); @@ -262,26 +265,26 @@ public void ConvertFromPipe(ContainerFormat type, System.Drawing.Imaging.PixelFo } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4() { Convert(VideoType.Mp4); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4_YUV444p() { Convert(VideoType.Mp4, (a) => Assert.IsTrue(a.VideoStreams.First().PixelFormat == "yuv444p"), new ForcePixelFormat("yuv444p")); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4_Args() { Convert(VideoType.Mp4, new VideoCodecArgument(VideoCodec.LibX264)); } - [DataTestMethod] + [DataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] // [DataRow(PixelFormat.Format48bppRgb)] @@ -290,13 +293,13 @@ public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat ConvertFromPipe(VideoType.Mp4, pixelFormat, new VideoCodecArgument(VideoCodec.LibX264)); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamPipe() { ConvertFromStreamPipe(VideoType.Mp4, new VideoCodecArgument(VideoCodec.LibX264)); } - // [TestMethod, Timeout(10000)] + [TestMethod, Timeout(10000)] public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure() { await Assert.ThrowsExceptionAsync(async () => @@ -304,61 +307,60 @@ await Assert.ThrowsExceptionAsync(async () => await using var ms = new MemoryStream(); var pipeSource = new StreamPipeSink(ms); await FFMpegArguments - .FromInputFiles(VideoLibrary.LocalVideo) - .ForceFormat("mkv") - .OutputToPipe(pipeSource) + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToPipe(pipeSource, opt => opt.ForceFormat("mkv")) .ProcessAsynchronously(); }); } - // [TestMethod, Timeout(10000)] + [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamOutputPipe_Failure() { Assert.ThrowsException(() => ConvertToStreamPipe(new ForceFormatArgument("mkv"))); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamOutputPipe_Async() { using var ms = new MemoryStream(); var pipeSource = new StreamPipeSink(ms); FFMpegArguments - .FromInputFiles(VideoLibrary.LocalVideo) - .WithVideoCodec(VideoCodec.LibX264) - .ForceFormat("matroska") - .OutputToPipe(pipeSource) + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToPipe(pipeSource, opt => opt + .WithVideoCodec(VideoCodec.LibX264) + .ForceFormat("matroska")) .ProcessAsynchronously() .WaitForResult(); } - [TestMethod] + [TestMethod, Timeout(10000)] public async Task TestDuplicateRun() { - FFMpegArguments.FromInputFiles(VideoLibrary.LocalVideo) - .OutputToFile("temporary.mp4", true) + FFMpegArguments.FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile("temporary.mp4") .ProcessSynchronously(); - await FFMpegArguments.FromInputFiles(VideoLibrary.LocalVideo) - .OutputToFile("temporary.mp4", true) + await FFMpegArguments.FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile("temporary.mp4") .ProcessAsynchronously(); File.Delete("temporary.mp4"); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamOutputPipe() { ConvertToStreamPipe(new VideoCodecArgument(VideoCodec.LibX264), new ForceFormatArgument("matroska")); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToTS() { Convert(VideoType.Ts); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToTS_Args() { Convert(VideoType.Ts, @@ -367,7 +369,7 @@ public void Video_ToTS_Args() new ForceFormatArgument(VideoType.MpegTs)); } - [DataTestMethod] + [DataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] // [DataRow(PixelFormat.Format48bppRgb)] @@ -376,19 +378,19 @@ public void Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) ConvertFromPipe(VideoType.Ts, pixelFormat, new ForceFormatArgument(VideoType.Ts)); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToOGV_Resize() { Convert(VideoType.Ogv, true, VideoSize.Ed); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToOGV_Resize_Args() { Convert(VideoType.Ogv, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora)); } - [DataTestMethod] + [DataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] // [DataRow(PixelFormat.Format48bppRgb)] @@ -397,19 +399,19 @@ public void Video_ToOGV_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixe ConvertFromPipe(VideoType.Ogv, pixelFormat, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora)); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4_Resize() { Convert(VideoType.Mp4, true, VideoSize.Ed); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4_Resize_Args() { Convert(VideoType.Mp4, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264)); } - [DataTestMethod] + [DataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] // [DataRow(PixelFormat.Format48bppRgb)] @@ -418,31 +420,31 @@ public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixe ConvertFromPipe(VideoType.Mp4, pixelFormat, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264)); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToOGV() { Convert(VideoType.Ogv); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4_MultiThread() { Convert(VideoType.Mp4, true); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToTS_MultiThread() { Convert(VideoType.Ts, true); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToOGV_MultiThread() { Convert(VideoType.Ogv, true); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_Snapshot_InMemory() { var output = Input.OutputLocation(ImageType.Png); @@ -463,7 +465,7 @@ public void Video_Snapshot_InMemory() } } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_Snapshot_PersistSnapshot() { var output = Input.OutputLocation(ImageType.Png); @@ -486,7 +488,7 @@ public void Video_Snapshot_PersistSnapshot() } } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_Join() { var output = Input.OutputLocation(VideoType.Mp4); @@ -520,7 +522,7 @@ public void Video_Join() } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_Join_Image_Sequence() { try @@ -558,17 +560,17 @@ public void Video_Join_Image_Sequence() } } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_With_Only_Audio_Should_Extract_Metadata() { var video = FFProbe.Analyse(VideoLibrary.LocalVideoAudioOnly.FullName); Assert.AreEqual(null, video.PrimaryVideoStream); Assert.AreEqual("aac", video.PrimaryAudioStream.CodecName); - Assert.AreEqual(79.5, video.Duration.TotalSeconds, 0.5); + Assert.AreEqual(10, video.Duration.TotalSeconds, 0.5); // Assert.AreEqual(1.25, video.Size); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_Duration() { var video = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); @@ -577,9 +579,8 @@ public void Video_Duration() try { FFMpegArguments - .FromInputFiles(VideoLibrary.LocalVideo) - .WithDuration(TimeSpan.FromSeconds(video.Duration.TotalSeconds - 5)) - .OutputToFile(output) + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile(output, false, opt => opt.WithDuration(TimeSpan.FromSeconds(video.Duration.TotalSeconds - 2))) .ProcessSynchronously(); Assert.IsTrue(File.Exists(output)); @@ -588,7 +589,7 @@ public void Video_Duration() Assert.AreEqual(video.Duration.Days, outputVideo.Duration.Days); Assert.AreEqual(video.Duration.Hours, outputVideo.Duration.Hours); Assert.AreEqual(video.Duration.Minutes, outputVideo.Duration.Minutes); - Assert.AreEqual(video.Duration.Seconds - 5, outputVideo.Duration.Seconds); + Assert.AreEqual(video.Duration.Seconds - 2, outputVideo.Duration.Seconds); } finally { @@ -597,7 +598,7 @@ public void Video_Duration() } } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_UpdatesProgress() { var output = Input.OutputLocation(VideoType.Mp4); @@ -613,9 +614,9 @@ public void Video_UpdatesProgress() try { var success = FFMpegArguments - .FromInputFiles(VideoLibrary.LocalVideo) - .WithDuration(TimeSpan.FromSeconds(8)) - .OutputToFile(output) + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile(output, false, opt => opt + .WithDuration(TimeSpan.FromSeconds(2))) .NotifyOnProgress(OnPercentageProgess, analysis.Duration) .NotifyOnProgress(OnTimeProgess) .ProcessSynchronously(); @@ -632,7 +633,7 @@ public void Video_UpdatesProgress() } } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_TranscodeInMemory() { using var resStream = new MemoryStream(); @@ -640,10 +641,10 @@ public void Video_TranscodeInMemory() var writer = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 128, 128)); FFMpegArguments - .FromPipe(writer) - .WithVideoCodec("vp9") - .ForceFormat("webm") - .OutputToPipe(reader) + .FromPipeInput(writer) + .OutputToPipe(reader, opt => opt + .WithVideoCodec("vp9") + .ForceFormat("webm")) .ProcessSynchronously(); resStream.Position = 0; @@ -652,26 +653,36 @@ public void Video_TranscodeInMemory() Assert.AreEqual(vi.PrimaryVideoStream.Height, 128); } - [TestMethod] + [TestMethod, Timeout(10000)] public async Task Video_Cancel_Async() { - await using var resStream = new MemoryStream(); - var reader = new StreamPipeSink(resStream); - var writer = new RawVideoPipeSource(BitmapSource.CreateBitmaps(512, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 128, 128)); - + var output = Input.OutputLocation(VideoType.Mp4); + var task = FFMpegArguments - .FromPipe(writer) - .WithVideoCodec("vp9") - .ForceFormat("webm") - .OutputToPipe(reader) + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile(output, false, opt => opt + .Resize(new Size(1000, 1000)) + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithConstantRateFactor(14) + .WithSpeedPreset(Speed.VerySlow) + .Loop(3)) .CancellableThrough(out var cancel) .ProcessAsynchronously(false); - - await Task.Delay(300); - cancel(); - var result = await task; - Assert.IsFalse(result); + try + { + await Task.Delay(300); + cancel(); + + var result = await task; + Assert.IsFalse(result); + } + finally + { + if (File.Exists(output)) + File.Delete(output); + } } } } diff --git a/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs index d43988f..9c6ffa2 100644 --- a/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs @@ -18,7 +18,7 @@ public ConcatArgument(IEnumerable values) } public void Pre() { } - public Task During(CancellationToken? cancellationToken = null) => Task.CompletedTask; + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; public void Post() { } public string Text => $"-i \"concat:{string.Join(@"|", Values)}\""; diff --git a/FFMpegCore/FFMpeg/Arguments/CpuSpeedArgument.cs b/FFMpegCore/FFMpeg/Arguments/CpuSpeedArgument.cs deleted file mode 100644 index 228262e..0000000 --- a/FFMpegCore/FFMpeg/Arguments/CpuSpeedArgument.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace FFMpegCore.Arguments -{ - /// - /// Represents cpu speed parameter - /// - public class CpuSpeedArgument : IArgument - { - public readonly int CpuSpeed; - public CpuSpeedArgument(int cpuSpeed) - { - CpuSpeed = cpuSpeed; - } - - public string Text => $"-quality good -cpu-used {CpuSpeed} -deadline realtime"; - } -} diff --git a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs index e915c77..5651802 100644 --- a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs @@ -21,7 +21,7 @@ public DemuxConcatArgument(IEnumerable values) private readonly string _tempFileName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid() + ".txt"); public void Pre() => File.WriteAllLines(_tempFileName, Values); - public Task During(CancellationToken? cancellationToken = null) => Task.CompletedTask; + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; public void Post() => File.Delete(_tempFileName); public string Text => $"-f concat -safe 0 -i \"{_tempFileName}\""; diff --git a/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs b/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs index 7614ae3..8402552 100644 --- a/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs +++ b/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs @@ -4,7 +4,7 @@ namespace FFMpegCore.Arguments { public class ForcePixelFormat : IArgument { - public string PixelFormat { get; private set; } + public string PixelFormat { get; } public string Text => $"-pix_fmt {PixelFormat}"; public ForcePixelFormat(string format) diff --git a/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs b/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs index e000e2e..7c921af 100644 --- a/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs @@ -12,6 +12,6 @@ public FrameRateArgument(double framerate) Framerate = framerate; } - public string Text => $"-r {Framerate}"; + public string Text => $"-r {Framerate.ToString(System.Globalization.CultureInfo.InvariantCulture)}"; } } diff --git a/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs b/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs new file mode 100644 index 0000000..da4b9ee --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs @@ -0,0 +1,18 @@ +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + public class HardwareAccelerationArgument : IArgument + { + public HardwareAccelerationDevice HardwareAccelerationDevice { get; } + + public HardwareAccelerationArgument(HardwareAccelerationDevice hardwareAccelerationDevice) + { + HardwareAccelerationDevice = hardwareAccelerationDevice; + } + + public string Text => HardwareAccelerationDevice != HardwareAccelerationDevice.Auto + ? $"-hwaccel {HardwareAccelerationDevice.ToString().ToLower()}" + : "-hwaccel"; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs index 6998d5c..99def82 100644 --- a/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs @@ -6,7 +6,7 @@ namespace FFMpegCore.Arguments public interface IInputOutputArgument : IArgument { void Pre(); - Task During(CancellationToken? cancellationToken = null); + Task During(CancellationToken cancellationToken = default); void Post(); } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/InputArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputArgument.cs index 0ff4bd8..68c34b4 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputArgument.cs @@ -1,6 +1,4 @@ -using System; -using System.IO; -using System.Linq; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -12,33 +10,25 @@ namespace FFMpegCore.Arguments public class InputArgument : IInputArgument { public readonly bool VerifyExists; - public readonly string[] FilePaths; + public readonly string FilePath; - public InputArgument(bool verifyExists, params string[] filePaths) + public InputArgument(bool verifyExists, string filePaths) { VerifyExists = verifyExists; - FilePaths = filePaths; + FilePath = filePaths; } - public InputArgument(params string[] filePaths) : this(true, filePaths) { } - public InputArgument(params FileInfo[] fileInfos) : this(false, fileInfos) { } - public InputArgument(params Uri[] uris) : this(false, uris) { } - public InputArgument(bool verifyExists, params FileInfo[] fileInfos) : this(verifyExists, fileInfos.Select(v => v.FullName).ToArray()) { } - public InputArgument(bool verifyExists, params Uri[] uris) : this(verifyExists, uris.Select(v => v.AbsoluteUri).ToArray()) { } + public InputArgument(string path, bool verifyExists) : this(verifyExists, path) { } public void Pre() { - if (!VerifyExists) return; - foreach (var filePath in FilePaths) - { - if (!File.Exists(filePath)) - throw new FileNotFoundException("Input file not found", filePath); - } + if (VerifyExists && !File.Exists(FilePath)) + throw new FileNotFoundException("Input file not found", FilePath); } - public Task During(CancellationToken? cancellationToken = null) => Task.CompletedTask; + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; public void Post() { } - public string Text => string.Join(" ", FilePaths.Select(v => $"-i \"{v}\"")); + public string Text => $"-i \"{FilePath}\""; } } diff --git a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs index 990995a..17d0372 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs @@ -19,7 +19,7 @@ public InputPipeArgument(IPipeSource writer) : base(PipeDirection.Out) public override string Text => $"-y {Writer.GetFormat()} -i \"{PipePath}\""; - public override async Task ProcessDataAsync(CancellationToken token) + protected override async Task ProcessDataAsync(CancellationToken token) { await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false); if (!Pipe.IsConnected) diff --git a/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs index 64469f8..c2aad38 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs @@ -25,11 +25,9 @@ public void Pre() if (!Overwrite && File.Exists(Path)) throw new FFMpegException(FFMpegExceptionType.File, "Output file already exists and overwrite is disabled"); } - public Task During(CancellationToken? cancellationToken = null) => Task.CompletedTask; + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; public void Post() { - if (!File.Exists(Path)) - throw new FFMpegException(FFMpegExceptionType.File, "Output file was not created"); } public OutputArgument(FileInfo value) : this(value.FullName) { } diff --git a/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs index f762752..ebf1e7f 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs @@ -16,7 +16,7 @@ public OutputPipeArgument(IPipeSink reader) : base(PipeDirection.In) public override string Text => $"\"{PipePath}\" -y"; - public override async Task ProcessDataAsync(CancellationToken token) + protected override async Task ProcessDataAsync(CancellationToken token) { await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false); if (!Pipe.IsConnected) diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index 77db5db..4a6113a 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -34,19 +34,19 @@ public void Post() Pipe = null!; } - public async Task During(CancellationToken? cancellationToken = null) + public async Task During(CancellationToken cancellationToken = default) { try { - await ProcessDataAsync(cancellationToken ?? CancellationToken.None).ConfigureAwait(false); + await ProcessDataAsync(cancellationToken); } catch (TaskCanceledException) { } - Post(); + Pipe.Disconnect(); } - public abstract Task ProcessDataAsync(CancellationToken token); + protected abstract Task ProcessDataAsync(CancellationToken token); public abstract string Text { get; } } } diff --git a/FFMpegCore/FFMpeg/Arguments/SeekedFileInputArgument.cs b/FFMpegCore/FFMpeg/Arguments/SeekedFileInputArgument.cs deleted file mode 100644 index 4a29116..0000000 --- a/FFMpegCore/FFMpeg/Arguments/SeekedFileInputArgument.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Arguments -{ - public class SeekedFileInputArgument : IInputArgument - { - public readonly (string FilePath, TimeSpan StartTime)[] SeekedFiles; - - public SeekedFileInputArgument((string file, TimeSpan startTime)[] seekedFiles) - { - SeekedFiles = seekedFiles; - } - - public void Pre() - { - foreach (var (seekedFile, _) in SeekedFiles) - { - if (!File.Exists(seekedFile)) - throw new FileNotFoundException("Input file not found", seekedFile); - } - } - public Task During(CancellationToken? cancellationToken = null) => Task.CompletedTask; - public void Post() { } - - public string Text => string.Join(" ", SeekedFiles.Select(seekedFile => $"-ss {seekedFile.StartTime} -i \"{seekedFile.FilePath}\"")); - } -} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs b/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs new file mode 100644 index 0000000..1d92f53 --- /dev/null +++ b/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs @@ -0,0 +1,14 @@ +namespace FFMpegCore.Enums +{ + public enum HardwareAccelerationDevice + { + Auto, + D3D11VA, + DXVA2, + QSV, + CUVID, + VDPAU, + VAAPI, + LibMFX + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 0f516e8..f252489 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -26,11 +26,11 @@ public static bool Snapshot(IMediaAnalysis source, string output, Size? size = n { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; - - var arguments = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); + + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); return arguments - .OutputToFile(output) + .OutputToFile(output, true, outputOptions) .ProcessSynchronously(); } /// @@ -46,11 +46,11 @@ public static Task SnapshotAsync(IMediaAnalysis source, string output, Siz { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; - - var arguments = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); + + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); return arguments - .OutputToFile(output) + .OutputToFile(output, true, outputOptions) .ProcessAsynchronously(); } /// @@ -62,13 +62,13 @@ public static Task SnapshotAsync(IMediaAnalysis source, string output, Siz /// Number 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 videoStreamNumber = 0) - { - var arguments = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); + { + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); using var ms = new MemoryStream(); arguments - .ForceFormat("rawvideo") - .OutputToPipe(new StreamPipeSink(ms)) + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) .ProcessSynchronously(); ms.Position = 0; @@ -84,29 +84,31 @@ public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan /// Bitmap with the requested snapshot. public static async Task SnapshotAsync(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) { - var arguments = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime, videoStreamNumber); using var ms = new MemoryStream(); await arguments - .ForceFormat("rawvideo") - .OutputToPipe(new StreamPipeSink(ms)) + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) .ProcessAsynchronously(); ms.Position = 0; return new Bitmap(ms); } - - private static FFMpegArguments BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) + + private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int videoStreamNumber = 0) { captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); size = PrepareSnapshotSize(source, size); - - return FFMpegArguments - .FromSeekedFiles((source.Path, captureTime ?? TimeSpan.Zero)) - .WithVideoStream(videoStreamNumber) - .WithVideoCodec(VideoCodec.Png) - .WithFrameOutputCount(1) - .Resize(size); + + return (FFMpegArguments + .FromFileInput(source, options => options + .Seek(captureTime)), + options => options + .SelectStream(videoStreamNumber) + .WithVideoCodec(VideoCodec.Png) + .WithFrameOutputCount(1) + .Resize(size)); } private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize) @@ -168,44 +170,44 @@ public static bool Convert( return format.Name switch { "mp4" => FFMpegArguments - .FromInputFiles(true, source.Path) - .UsingMultithreading(multithreaded) - .WithVideoCodec(VideoCodec.LibX264) - .WithVideoBitrate(2400) - .Scale(outputSize) - .WithSpeedPreset(speed) - .WithAudioCodec(AudioCodec.Aac) - .WithAudioBitrate(audioQuality) - .OutputToFile(output) + .FromFileInput(source) + .OutputToFile(output, true, options => options + .UsingMultithreading(multithreaded) + .WithVideoCodec(VideoCodec.LibX264) + .WithVideoBitrate(2400) + .Scale(outputSize) + .WithSpeedPreset(speed) + .WithAudioCodec(AudioCodec.Aac) + .WithAudioBitrate(audioQuality)) .ProcessSynchronously(), "ogv" => FFMpegArguments - .FromInputFiles(true, source.Path) - .UsingMultithreading(multithreaded) - .WithVideoCodec(VideoCodec.LibTheora) - .WithVideoBitrate(2400) - .Scale(outputSize) - .WithSpeedPreset(speed) - .WithAudioCodec(AudioCodec.LibVorbis) - .WithAudioBitrate(audioQuality) - .OutputToFile(output) + .FromFileInput(source) + .OutputToFile(output, true, options => options + .UsingMultithreading(multithreaded) + .WithVideoCodec(VideoCodec.LibTheora) + .WithVideoBitrate(2400) + .Scale(outputSize) + .WithSpeedPreset(speed) + .WithAudioCodec(AudioCodec.LibVorbis) + .WithAudioBitrate(audioQuality)) .ProcessSynchronously(), "mpegts" => FFMpegArguments - .FromInputFiles(true, source.Path) - .CopyChannel() - .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) - .ForceFormat(VideoType.Ts) - .OutputToFile(output) + .FromFileInput(source) + .OutputToFile(output, true, options => options + .CopyChannel() + .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) + .ForceFormat(VideoType.Ts)) .ProcessSynchronously(), "webm" => FFMpegArguments - .FromInputFiles(true, source.Path) - .UsingMultithreading(multithreaded) - .WithVideoCodec(VideoCodec.LibVpx) - .WithVideoBitrate(2400) - .Scale(outputSize) - .WithSpeedPreset(speed) - .WithAudioCodec(AudioCodec.LibVorbis) - .WithAudioBitrate(audioQuality) - .OutputToFile(output) + .FromFileInput(source) + .OutputToFile(output, true, options => options + .UsingMultithreading(multithreaded) + .WithVideoCodec(VideoCodec.LibVpx) + .WithVideoBitrate(2400) + .Scale(outputSize) + .WithSpeedPreset(speed) + .WithAudioCodec(AudioCodec.LibVorbis) + .WithAudioBitrate(audioQuality)) .ProcessSynchronously(), _ => throw new ArgumentOutOfRangeException(nameof(format)) }; @@ -224,13 +226,14 @@ public static bool PosterWithAudio(string image, string audio, string output) FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image)); return FFMpegArguments - .FromInputFiles(true, image, audio) - .Loop(1) - .WithVideoCodec(VideoCodec.LibX264) - .WithConstantRateFactor(21) - .WithAudioBitrate(AudioQuality.Normal) - .UsingShortest() - .OutputToFile(output) + .FromFileInput(image) + .AddFileInput(audio) + .OutputToFile(output, true, options => options + .Loop(1) + .WithVideoCodec(VideoCodec.LibX264) + .WithConstantRateFactor(21) + .WithAudioBitrate(AudioQuality.Normal) + .UsingShortest()) .ProcessSynchronously(); } @@ -254,10 +257,10 @@ public static bool Join(string output, params IMediaAnalysis[] videos) try { return FFMpegArguments - .FromConcatenation(temporaryVideoParts) - .CopyChannel() - .WithBitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc) - .OutputToFile(output) + .FromConcatInput(temporaryVideoParts) + .OutputToFile(output, true, options => options + .CopyChannel() + .WithBitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc)) .ProcessSynchronously(); } finally @@ -299,10 +302,10 @@ public static bool JoinImageSequence(string output, double frameRate = 30, param try { return FFMpegArguments - .FromInputFiles(false, Path.Combine(tempFolderName, "%09d.png")) - .Resize(firstImage.Width, firstImage.Height) - .WithFramerate(frameRate) - .OutputToFile(output) + .FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false) + .OutputToFile(output, true, options => options + .Resize(firstImage.Width, firstImage.Height) + .WithFramerate(frameRate)) .ProcessSynchronously(); } finally @@ -326,7 +329,7 @@ public static bool SaveM3U8Stream(Uri uri, string output) throw new ArgumentException($"Uri: {uri.AbsoluteUri}, does not point to a valid http(s) stream."); return FFMpegArguments - .FromInputFiles(false, uri) + .FromUrlInput(uri) .OutputToFile(output) .ProcessSynchronously(); } @@ -344,10 +347,10 @@ public static bool Mute(string input, string output) FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); return FFMpegArguments - .FromInputFiles(true, source.Path) - .CopyChannel(Channel.Video) - .DisableChannel(Channel.Audio) - .OutputToFile(output) + .FromFileInput(source) + .OutputToFile(output, true, options => options + .CopyChannel(Channel.Video) + .DisableChannel(Channel.Audio)) .ProcessSynchronously(); } @@ -362,9 +365,9 @@ public static bool ExtractAudio(string input, string output) FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp3); return FFMpegArguments - .FromInputFiles(true, input) - .DisableChannel(Channel.Video) - .OutputToFile(output) + .FromFileInput(input) + .OutputToFile(output, true, options => options + .DisableChannel(Channel.Video)) .ProcessSynchronously(); } @@ -383,26 +386,27 @@ public static bool ReplaceAudio(string input, string inputAudio, string output, FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); return FFMpegArguments - .FromInputFiles(true, source.Path, inputAudio) - .CopyChannel() - .WithAudioCodec(AudioCodec.Aac) - .WithAudioBitrate(AudioQuality.Good) - .UsingShortest(stopAtShortest) - .OutputToFile(output) + .FromFileInput(source) + .AddFileInput(inputAudio) + .OutputToFile(output, true, options => options + .CopyChannel() + .WithAudioCodec(AudioCodec.Aac) + .WithAudioBitrate(AudioQuality.Good) + .UsingShortest(stopAtShortest)) .ProcessSynchronously(); } #region PixelFormats - internal static IReadOnlyList GetPixelFormatsInternal() + internal static IReadOnlyList GetPixelFormatsInternal() { - FFMpegHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory); + FFMpegHelper.RootExceptionCheck(); - var list = new List(); + var list = new List(); using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), "-pix_fmts"); instance.DataReceived += (e, args) => { - if (Enums.PixelFormat.TryParse(args.Data, out var fmt)) - list.Add(fmt); + if (PixelFormat.TryParse(args.Data, out var format)) + list.Add(format); }; var exitCode = instance.BlockUntilFinished(); @@ -411,14 +415,14 @@ public static bool ReplaceAudio(string input, string inputAudio, string output, return list.AsReadOnly(); } - public static IReadOnlyList GetPixelFormats() + public static IReadOnlyList GetPixelFormats() { if (!FFMpegOptions.Options.UseCache) return GetPixelFormatsInternal(); return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly(); } - public static bool TryGetPixelFormat(string name, out Enums.PixelFormat fmt) + public static bool TryGetPixelFormat(string name, out PixelFormat fmt) { if (!FFMpegOptions.Options.UseCache) { @@ -429,7 +433,7 @@ public static bool TryGetPixelFormat(string name, out Enums.PixelFormat fmt) return FFMpegCache.PixelFormats.TryGetValue(name, out fmt); } - public static Enums.PixelFormat GetPixelFormat(string name) + public static PixelFormat GetPixelFormat(string name) { if (TryGetPixelFormat(name, out var fmt)) return fmt; @@ -438,9 +442,10 @@ public static Enums.PixelFormat GetPixelFormat(string name) #endregion #region Codecs - internal static void ParsePartOfCodecs(Dictionary codecs, string arguments, Func parser) + + private static void ParsePartOfCodecs(Dictionary codecs, string arguments, Func parser) { - FFMpegHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory); + FFMpegHelper.RootExceptionCheck(); using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), arguments); instance.DataReceived += (e, args) => @@ -523,7 +528,7 @@ public static Codec GetCodec(string name) #region ContainerFormats internal static IReadOnlyList GetContainersFormatsInternal() { - FFMpegHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory); + FFMpegHelper.RootExceptionCheck(); var list = new List(); using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), "-formats"); diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs new file mode 100644 index 0000000..bb7f0ad --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -0,0 +1,68 @@ +using System; +using System.Drawing; +using FFMpegCore.Arguments; +using FFMpegCore.Enums; + +namespace FFMpegCore +{ + public class FFMpegArgumentOptions : FFMpegOptionsBase + { + internal FFMpegArgumentOptions() { } + + public FFMpegArgumentOptions WithAudioCodec(Codec audioCodec) => WithArgument(new AudioCodecArgument(audioCodec)); + public FFMpegArgumentOptions WithAudioCodec(string audioCodec) => WithArgument(new AudioCodecArgument(audioCodec)); + public FFMpegArgumentOptions WithAudioBitrate(AudioQuality audioQuality) => WithArgument(new AudioBitrateArgument(audioQuality)); + 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)); + public FFMpegArgumentOptions CopyChannel(Channel channel = Channel.Both) => WithArgument(new CopyArgument(channel)); + public FFMpegArgumentOptions DisableChannel(Channel channel) => WithArgument(new DisableChannelArgument(channel)); + public FFMpegArgumentOptions WithDuration(TimeSpan? duration) => WithArgument(new DurationArgument(duration)); + public FFMpegArgumentOptions WithFastStart() => WithArgument(new FaststartArgument()); + public FFMpegArgumentOptions WithFrameOutputCount(int frames) => WithArgument(new FrameOutputCountArgument(frames)); + public FFMpegArgumentOptions WithHardwareAcceleration(HardwareAccelerationDevice hardwareAccelerationDevice = HardwareAccelerationDevice.Auto) => WithArgument(new HardwareAccelerationArgument(hardwareAccelerationDevice)); + + public FFMpegArgumentOptions UsingShortest(bool shortest = true) => WithArgument(new ShortestArgument(shortest)); + public FFMpegArgumentOptions UsingMultithreading(bool multithread) => WithArgument(new ThreadsArgument(multithread)); + public FFMpegArgumentOptions UsingThreads(int threads) => WithArgument(new ThreadsArgument(threads)); + + 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 WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate)); + public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument()); + public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed)); + public FFMpegArgumentOptions WithStartNumber(int startNumber) => WithArgument(new StartNumberArgument(startNumber)); + 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)); + + public FFMpegArgumentOptions ForceFormat(ContainerFormat format) => WithArgument(new ForceFormatArgument(format)); + public FFMpegArgumentOptions ForceFormat(string format) => WithArgument(new ForceFormatArgument(format)); + 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); + return this; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index e0262d4..92fa6fe 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -51,20 +51,22 @@ public bool ProcessSynchronously(bool throwOnError = true) void OnCancelEvent(object sender, EventArgs args) { - instance?.SendInput("q"); + instance.SendInput("q"); cancellationTokenSource.Cancel(); + instance.Started = false; } CancelEvent += OnCancelEvent; + instance.Exited += delegate { cancellationTokenSource.Cancel(); }; - _ffMpegArguments.Pre(); try { + _ffMpegArguments.Pre(); Task.WaitAll(instance.FinishedRunning().ContinueWith(t => { errorCode = t.Result; cancellationTokenSource.Cancel(); + _ffMpegArguments.Post(); }), _ffMpegArguments.During(cancellationTokenSource.Token)); - _ffMpegArguments.Post(); } catch (Exception e) { @@ -98,16 +100,18 @@ void OnCancelEvent(object sender, EventArgs args) { instance?.SendInput("q"); cancellationTokenSource.Cancel(); + instance.Started = false; } CancelEvent += OnCancelEvent; - _ffMpegArguments.Pre(); try { + _ffMpegArguments.Pre(); await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => { errorCode = t.Result; cancellationTokenSource.Cancel(); + _ffMpegArguments.Post(); }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); } catch (Exception e) @@ -117,7 +121,6 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => finally { CancelEvent -= OnCancelEvent; - _ffMpegArguments.Post(); } return HandleCompletion(throwOnError, errorCode, instance.ErrorData); @@ -125,9 +128,9 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSource) { - FFMpegHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory); + FFMpegHelper.RootExceptionCheck(); + FFMpegHelper.VerifyFFMpegExists(); var instance = new Instance(FFMpegOptions.Options.FFmpegBinary(), _ffMpegArguments.Text); - instance.DataReceived += OutputData; cancellationTokenSource = new CancellationTokenSource(); if (_onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null)) @@ -135,6 +138,7 @@ private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSo return instance; } + private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData) { diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index db129ee..44e20d2 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -1,127 +1,81 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using FFMpegCore.Arguments; -using FFMpegCore.Enums; using FFMpegCore.Pipes; namespace FFMpegCore { - public class FFMpegArguments + public sealed class FFMpegArguments : FFMpegOptionsBase { - private readonly IInputArgument _inputArgument; - private IOutputArgument _outputArgument = null!; - private readonly List _arguments; + private readonly FFMpegGlobalOptions _globalOptions = new FFMpegGlobalOptions(); - private FFMpegArguments(IInputArgument inputArgument) + private FFMpegArguments() { } + + public string Text => string.Join(" ", _globalOptions.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 FromPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments); + + + public FFMpegArguments WithGlobalOptions(Action configureOptions) { - _inputArgument = inputArgument; - _arguments = new List { inputArgument }; - } - - public string Text => string.Join(" ", _arguments.Select(arg => arg.Text)); - - public static FFMpegArguments FromSeekedFiles(params (string file, TimeSpan startTime)[] seekedFiles) => new FFMpegArguments(new SeekedFileInputArgument(seekedFiles)); - public static FFMpegArguments FromInputFiles(params string[] files) => new FFMpegArguments(new InputArgument(true, files)); - public static FFMpegArguments FromInputFiles(bool verifyExists, params string[] files) => new FFMpegArguments(new InputArgument(verifyExists, files)); - public static FFMpegArguments FromInputFiles(params Uri[] uris) => new FFMpegArguments(new InputArgument(false, uris)); - public static FFMpegArguments FromInputFiles(bool verifyExists, params Uri[] uris) => new FFMpegArguments(new InputArgument(verifyExists, uris)); - public static FFMpegArguments FromInputFiles(params FileInfo[] files) => new FFMpegArguments(new InputArgument(false, files)); - public static FFMpegArguments FromInputFiles(bool verifyExists, params FileInfo[] files) => new FFMpegArguments(new InputArgument(verifyExists, files)); - public static FFMpegArguments FromConcatenation(params string[] files) => new FFMpegArguments(new ConcatArgument(files)); - public static FFMpegArguments FromDemuxConcatenation(params string[] files) => new FFMpegArguments(new DemuxConcatArgument(files)); - public static FFMpegArguments FromPipe(IPipeSource writer) => new FFMpegArguments(new InputPipeArgument(writer)); - - - public FFMpegArguments WithAudioCodec(Codec audioCodec) => WithArgument(new AudioCodecArgument(audioCodec)); - public FFMpegArguments WithAudioCodec(string audioCodec) => WithArgument(new AudioCodecArgument(audioCodec)); - public FFMpegArguments WithAudioBitrate(AudioQuality audioQuality) => WithArgument(new AudioBitrateArgument(audioQuality)); - public FFMpegArguments WithAudioBitrate(int bitrate) => WithArgument(new AudioBitrateArgument(bitrate)); - public FFMpegArguments WithAudioSamplingRate(int samplingRate = 48000) => WithArgument(new AudioSamplingRateArgument(samplingRate)); - public FFMpegArguments WithVariableBitrate(int vbr) => WithArgument(new VariableBitRateArgument(vbr)); - - public FFMpegArguments Resize(VideoSize videoSize) => WithArgument(new SizeArgument(videoSize)); - public FFMpegArguments Resize(int width, int height) => WithArgument(new SizeArgument(width, height)); - public FFMpegArguments Resize(Size? size) => WithArgument(new SizeArgument(size)); - - public FFMpegArguments Scale(VideoSize videoSize) => WithArgument(new ScaleArgument(videoSize)); - public FFMpegArguments Scale(int width, int height) => WithArgument(new ScaleArgument(width, height)); - public FFMpegArguments Scale(Size size) => WithArgument(new ScaleArgument(size)); - - public FFMpegArguments WithBitStreamFilter(Channel channel, Filter filter) => WithArgument(new BitStreamFilterArgument(channel, filter)); - public FFMpegArguments WithConstantRateFactor(int crf) => WithArgument(new ConstantRateFactorArgument(crf)); - public FFMpegArguments CopyChannel(Channel channel = Channel.Both) => WithArgument(new CopyArgument(channel)); - public FFMpegArguments DisableChannel(Channel channel) => WithArgument(new DisableChannelArgument(channel)); - public FFMpegArguments WithDuration(TimeSpan? duration) => WithArgument(new DurationArgument(duration)); - public FFMpegArguments WithFastStart() => WithArgument(new FaststartArgument()); - public FFMpegArguments WithFrameOutputCount(int frames) => WithArgument(new FrameOutputCountArgument(frames)); - public FFMpegArguments WithVideoStream(int videoStreamNumber) => WithArgument(new MapStreamArgument(videoStreamNumber)); - - public FFMpegArguments UsingShortest(bool shortest = true) => WithArgument(new ShortestArgument(shortest)); - public FFMpegArguments UsingMultithreading(bool multithread) => WithArgument(new ThreadsArgument(multithread)); - public FFMpegArguments UsingThreads(int threads) => WithArgument(new ThreadsArgument(threads)); - - public FFMpegArguments WithVideoCodec(Codec videoCodec) => WithArgument(new VideoCodecArgument(videoCodec)); - public FFMpegArguments WithVideoCodec(string videoCodec) => WithArgument(new VideoCodecArgument(videoCodec)); - public FFMpegArguments WithVideoBitrate(int bitrate) => WithArgument(new VideoBitrateArgument(bitrate)); - public FFMpegArguments WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate)); - public FFMpegArguments WithoutMetadata() => WithArgument(new RemoveMetadataArgument()); - public FFMpegArguments WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed)); - public FFMpegArguments WithStartNumber(int startNumber) => WithArgument(new StartNumberArgument(startNumber)); - public FFMpegArguments WithCpuSpeed(int cpuSpeed) => WithArgument(new CpuSpeedArgument(cpuSpeed)); - public FFMpegArguments WithCustomArgument(string argument) => WithArgument(new CustomArgument(argument)); - - public FFMpegArguments Seek(TimeSpan? seekTo) => WithArgument(new SeekArgument(seekTo)); - public FFMpegArguments Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition)); - public FFMpegArguments Loop(int times) => WithArgument(new LoopArgument(times)); - public FFMpegArguments OverwriteExisting() => WithArgument(new OverwriteArgument()); - public FFMpegArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithArgument(new VerbosityLevelArgument(verbosityLevel)); - - public FFMpegArguments ForceFormat(ContainerFormat format) => WithArgument(new ForceFormatArgument(format)); - public FFMpegArguments ForceFormat(string format) => WithArgument(new ForceFormatArgument(format)); - public FFMpegArguments ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); - public FFMpegArguments ForcePixelFormat(PixelFormat pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); - - public FFMpegArguments DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions)); - - public FFMpegArgumentProcessor OutputToFile(string file, bool overwrite = true) => ToProcessor(new OutputArgument(file, overwrite)); - public FFMpegArgumentProcessor OutputToFile(Uri uri, bool overwrite = true) => ToProcessor(new OutputArgument(uri.AbsolutePath, overwrite)); - public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader) => ToProcessor(new OutputPipeArgument(reader)); - - public FFMpegArguments WithArgument(IArgument argument) - { - _arguments.Add(argument); + configureOptions(_globalOptions); return this; } - private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument) + + public FFMpegArguments AddConcatInput(IEnumerable filePaths, Action? addArguments = null) => WithInput(new ConcatArgument(filePaths), addArguments); + 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); + + private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments) { - _arguments.Add(argument); - _outputArgument = argument; + var arguments = new FFMpegArgumentOptions(); + addArguments?.Invoke(arguments); + Arguments.AddRange(arguments.Arguments); + Arguments.Add(inputArgument); + return this; + } + + public FFMpegArgumentProcessor OutputToFile(string file, bool overwrite = true, 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 OutputToPipe(IPipeSink reader, Action? addArguments = null) => ToProcessor(new OutputPipeArgument(reader), addArguments); + + private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action? addArguments) + { + var args = new FFMpegArgumentOptions(); + addArguments?.Invoke(args); + Arguments.AddRange(args.Arguments); + Arguments.Add(argument); return new FFMpegArgumentProcessor(this); } internal void Pre() { - _inputArgument.Pre(); - _outputArgument.Pre(); + foreach (var argument in Arguments.OfType()) + argument.Pre(); } - internal async Task During(CancellationToken? cancellationToken = null) + internal async Task During(CancellationToken cancellationToken = default) { - await Task.WhenAll(_inputArgument.During(cancellationToken), _outputArgument.During(cancellationToken)).ConfigureAwait(false); + var inputOutputArguments = Arguments.OfType(); + await Task.WhenAll(inputOutputArguments.Select(io => io.During(cancellationToken))).ConfigureAwait(false); } internal void Post() { - _inputArgument.Post(); - _outputArgument.Post(); - } - - public TArgument Find() where TArgument : class, IArgument - { - return _arguments.OfType().FirstOrDefault(); + foreach (var argument in Arguments.OfType()) + argument.Post(); } } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs b/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs new file mode 100644 index 0000000..00dc66f --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs @@ -0,0 +1,18 @@ +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/FFMpegOptionsBase.cs b/FFMpegCore/FFMpeg/FFMpegOptionsBase.cs new file mode 100644 index 0000000..015e609 --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegOptionsBase.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using FFMpegCore.Arguments; + +namespace FFMpegCore +{ + public abstract class FFMpegOptionsBase + { + internal readonly List Arguments = new List(); + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs b/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs index 7e79f55..c680c3e 100644 --- a/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs +++ b/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs @@ -1,21 +1,18 @@ using System; -using System.IO; using System.Runtime.InteropServices; namespace FFMpegCore.Pipes { static class PipeHelpers { - static readonly string PipePrefix = Path.Combine(Path.GetTempPath(), "CoreFxPipe_"); - - public static string GetUnqiuePipeName() => "FFMpegCore_" + Guid.NewGuid(); + public static string GetUnqiuePipeName() => $"FFMpegCore_{Guid.NewGuid()}"; public static string GetPipePath(string pipeName) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return $@"\\.\pipe\{pipeName}"; else - return $"unix:{PipePrefix}{pipeName}"; // dotnet uses unix sockets on unix, for more see https://github.com/dotnet/runtime/issues/24390 + return $"unix:/tmp/CoreFxPipe_{pipeName}"; } } } diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index fb81a9c..057f605 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -5,15 +5,15 @@ https://github.com/rosenbjerg/FFMpegCore https://github.com/rosenbjerg/FFMpegCore - A great way to use FFMpeg encoding when writing video applications, client-side and server-side. It has wrapper methods that allow conversion to all web formats: MP4, OGV, TS and methods of capturing screens from the videos. - 1.0.12 - 1.1.0.0 - 1.1.0.0 - - Make Tags a dictionary for flexibility -- Handle rotated video frames in snapshot + A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your C# applications + 3.0.0.0 + 3.0.0.0 + 3.0.0.0 + - Fix hanging pipes on unix sockets +- Internal API cleanup 8 - 2.2.6 - Vlad Jerca, Malte Rosenbjerg + 3.1.0 + Malte Rosenbjerg, Vlad Jerca ffmpeg ffprobe convert video audio mediafile resize analyze muxing GitHub true diff --git a/FFMpegCore/FFMpegCore.csproj.DotSettings b/FFMpegCore/FFMpegCore.csproj.DotSettings deleted file mode 100644 index 7a8d17a..0000000 --- a/FFMpegCore/FFMpegCore.csproj.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ - - True - True \ No newline at end of file diff --git a/FFMpegCore/FFMpegCore.nuspec b/FFMpegCore/FFMpegCore.nuspec deleted file mode 100644 index 35e8526..0000000 --- a/FFMpegCore/FFMpegCore.nuspec +++ /dev/null @@ -1,18 +0,0 @@ - - - - $id$ - $version$ - $title$ - Vlad Jerca - Vlad Jerca - https://github.com/rosenbjerg/FFMpegCore - false - $description$ - - More information available @ https://github.com/rosenbjerg/FFMpegCore - - Copyright 2020 - ffmpeg ffprobe video audio media conversion analysis ffmpegcore mp4 ogv - - diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 131f465..f650371 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Text.Json; using System.Threading.Tasks; using FFMpegCore.Arguments; @@ -13,11 +14,20 @@ public static class FFProbe { public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue) { + 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); } - public static IMediaAnalysis Analyse(System.IO.Stream stream, int outputCapacity = int.MaxValue) + public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue) + { + using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); + instance.BlockUntilFinished(); + return ParseOutput(uri.AbsoluteUri, instance); + } + public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue) { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); @@ -42,11 +52,20 @@ public static IMediaAnalysis Analyse(System.IO.Stream stream, int outputCapacity } public static async Task AnalyseAsync(string filePath, int outputCapacity = int.MaxValue) { + 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); } - public static async Task AnalyseAsync(System.IO.Stream stream, int outputCapacity = int.MaxValue) + public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue) + { + using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); + await instance.FinishedRunning(); + return ParseOutput(uri.AbsoluteUri, instance); + } + public static async Task AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue) { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); @@ -85,10 +104,10 @@ private static IMediaAnalysis ParseOutput(string filePath, Instance instance) private static Instance PrepareInstance(string filePath, int outputCapacity) { - FFProbeHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory); - var ffprobe = FFMpegOptions.Options.FFProbeBinary(); + FFProbeHelper.RootExceptionCheck(); + FFProbeHelper.VerifyFFProbeExists(); var arguments = $"-print_format json -show_format -sexagesimal -show_streams \"{filePath}\""; - var instance = new Instance(ffprobe, arguments) {DataBufferCapacity = outputCapacity}; + var instance = new Instance(FFMpegOptions.Options.FFProbeBinary(), arguments) {DataBufferCapacity = outputCapacity}; return instance; } } diff --git a/FFMpegCore/FFProbe/IMediaAnalysis.cs b/FFMpegCore/FFProbe/IMediaAnalysis.cs index 04e2ae3..660d776 100644 --- a/FFMpegCore/FFProbe/IMediaAnalysis.cs +++ b/FFMpegCore/FFProbe/IMediaAnalysis.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace FFMpegCore { diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs index 8553573..f2a214e 100644 --- a/FFMpegCore/Helpers/FFMpegHelper.cs +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -2,11 +2,14 @@ using System.Drawing; using System.IO; using FFMpegCore.Exceptions; +using Instances; namespace FFMpegCore.Helpers { public static class FFMpegHelper { + private static bool _ffmpegVerified; + public static void ConversionSizeExceptionCheck(Image image) { ConversionSizeExceptionCheck(image.Size); @@ -32,11 +35,19 @@ public static void ExtensionExceptionCheck(string filename, string extension) $"Invalid output file. File extension should be '{extension}' required."); } - public static void RootExceptionCheck(string root) + public static void RootExceptionCheck() { - if (root == null) + if (FFMpegOptions.Options.RootDirectory == null) throw new FFMpegException(FFMpegExceptionType.Dependency, "FFMpeg root is not configured in app config. Missing key 'ffmpegRoot'."); } + + public static void VerifyFFMpegExists() + { + if (_ffmpegVerified) return; + var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFmpegBinary(), "-version"); + _ffmpegVerified = exitCode == 0; + 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 eed1b7a..1e833e0 100644 --- a/FFMpegCore/Helpers/FFProbeHelper.cs +++ b/FFMpegCore/Helpers/FFProbeHelper.cs @@ -1,9 +1,12 @@ using FFMpegCore.Exceptions; +using Instances; namespace FFMpegCore.Helpers { public class FFProbeHelper { + private static bool _ffprobeVerified; + public static int Gcd(int first, int second) { while (first != 0 && second != 0) @@ -15,12 +18,20 @@ public static int Gcd(int first, int second) return first == 0 ? second : first; } - public static void RootExceptionCheck(string root) + public static void RootExceptionCheck() { - if (root == null) + if (FFMpegOptions.Options.RootDirectory == null) throw new FFMpegException(FFMpegExceptionType.Dependency, "FFProbe root is not configured in app config. Missing key 'ffmpegRoot'."); - + + } + + public static void VerifyFFProbeExists() + { + if (_ffprobeVerified) return; + var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFProbeBinary(), "-version"); + _ffprobeVerified = exitCode == 0; + if (!_ffprobeVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system"); } } } diff --git a/README.md b/README.md index 29b547e..0012def 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # FFMpegCore -[![NuGet Badge](https://buildstats.info/nuget/FFMpegCore)](https://www.nuget.org/packages/FFMpegCore/) [![CI](https://github.com/rosenbjerg/FFMpegCore/workflows/CI/badge.svg)](https://github.com/rosenbjerg/FFMpegCore/actions?query=workflow%3ACI) +[![NuGet Badge](https://buildstats.info/nuget/FFMpegCore)](https://www.nuget.org/packages/FFMpegCore/) +[![GitHub issues](https://img.shields.io/github/issues/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/issues) +[![GitHub stars](https://img.shields.io/github/stars/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/stargazers) +[![GitHub](https://img.shields.io/github/license/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/blob/master/LICENSE) # Setup @@ -10,7 +13,7 @@ Install-Package FFMpegCore ``` -A great way to use FFMpeg encoding when writing video applications, client-side and server-side. It has wrapper methods that allow conversion to popular web formats, such as Mp4, WebM, Ogv, TS, and methods for capturing screenshots from videos, among other. +A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your C# applications. Support both synchronous and asynchronous use # API @@ -34,20 +37,20 @@ Easily build your FFMpeg arguments using the fluent argument builder: Convert input file to h264/aac scaled to 720p w/ faststart, for web playback ```csharp FFMpegArguments - .FromInputFiles(inputFilePath) - .WithVideoCodec(VideoCodec.LibX264) - .WithConstantRateFactor(21) - .WithAudioCodec(AudioCodec.Aac) - .WithVariableBitrate(4) - .WithFastStart() - .Scale(VideoSize.Hd) - .OutputToFile(output) - .ProcessSynchronously(), + .FromFileInput(inputPath) + .OutputToFile(outputPath, false, options => options + .WithVideoCodec(VideoCodec.LibX264) + .WithConstantRateFactor(21) + .WithAudioCodec(AudioCodec.Aac) + .WithVariableBitrate(4) + .WithFastStart() + .Scale(VideoSize.Hd)) + .ProcessSynchronously(); ``` Easily capture screens from your videos: ```csharp -var mediaFileAnalysis = FFProbe.Analyse(inputFilePath); +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)); @@ -59,10 +62,10 @@ FFMpeg.Snapshot(mediaFileAnalysis, outputPath, new Size(200, 400), TimeSpan.From Convert to and/or from streams ```csharp await FFMpegArguments - .FromPipe(new StreamPipeDataWriter(inputStream)) - .WithVideoCodec("vp9") - .ForceFormat("webm") - .OutputToPipe(new StreamPipeDataReader(outputStream)) + .FromPipeInput(new StreamPipeSource(inputStream)) + .OutputToPipe(new StreamPipeSink(outputStream), options => options + .WithVideoCodec("vp9") + .ForceFormat("webm")) .ProcessAsynchronously(); ``` @@ -133,9 +136,8 @@ var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumera FrameRate = 30 //set source frame rate }; FFMpegArguments - .FromPipe(videoFramesSource) - // ... other encoding arguments - .OutputToFile("temporary.mp4") + .FromPipeInput(videoFramesSource, ) + .OutputToFile("temporary.mp4", false, ) .ProcessSynchronously(); ``` @@ -210,6 +212,7 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f + ### License