diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 824e3bf..bf07a86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,9 @@ name: CI -on: push +on: [push, pull_request] jobs: ci: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v1 - name: Prepare FFMpeg @@ -11,7 +12,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 d93af1d..b20d192 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: NuGet release on: - pull_request: + push: branches: - release jobs: @@ -9,18 +9,15 @@ jobs: steps: - uses: actions/checkout@v1 - name: Prepare FFMpeg - if: github.event.pull_request.merged == false 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.101 + dotnet-version: 3.1 - name: Build solution -c Release run: dotnet build - name: Run unit tests - if: github.event.pull_request.merged == false run: dotnet test - name: Publish NuGet package - if: github.event.pull_request.merged == true run: NUPKG=`find . -type f -name FFMpegCore*.nupkg` && dotnet nuget push $NUPKG -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json diff --git a/.nuget/nuget.exe b/.nuget/nuget.exe deleted file mode 100644 index ccb2979..0000000 Binary files a/.nuget/nuget.exe and /dev/null differ diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 9dd1b9a..f4dbfe9 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -1,225 +1,318 @@ -using FFMpegCore.FFMPEG.Argument; -using FFMpegCore.FFMPEG.Enums; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -using System.Collections.Generic; +using FFMpegCore.Arguments; +using FFMpegCore.Enums; namespace FFMpegCore.Test { [TestClass] public class ArgumentBuilderTest : BaseTest { - List concatFiles = new List - { "1.mp4", "2.mp4", "3.mp4", "4.mp4"}; - - FFArgumentBuilder builder; - - public ArgumentBuilderTest() : base() - { - builder = new FFArgumentBuilder(); - } - - private string GetArgumentsString(params Argument[] args) - { - var container = new ArgumentContainer(); - container.Add(new InputArgument("input.mp4")); - foreach (var a in args) - { - container.Add(a); - } - container.Add(new OutputArgument("output.mp4")); - - return builder.BuildArguments(container); - } + private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4"}; [TestMethod] public void Builder_BuildString_IO_1() { - var str = GetArgumentsString(); - - Assert.AreEqual(str, "-i \"input.mp4\" \"output.mp4\""); + 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 = GetArgumentsString(new ScaleArgument(VideoSize.Hd)); - - Assert.AreEqual(str, "-i \"input.mp4\" -vf scale=-1:720 \"output.mp4\""); + 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 = GetArgumentsString(new AudioCodecArgument(AudioCodec.Aac, AudioQuality.Normal)); - Assert.AreEqual(str, "-i \"input.mp4\" -c:a aac -b:a 128k \"output.mp4\""); + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.WithAudioCodec(AudioCodec.Aac)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -c:a aac \"output.mp4\" -y", str); + } + + [TestMethod] + public void Builder_BuildString_AudioBitrate() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.WithAudioBitrate(AudioQuality.Normal)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -b:a 128k \"output.mp4\" -y", str); + } + + [TestMethod] + public void Builder_BuildString_Quiet() + { + var str = FFMpegArguments.FromFileInput("input.mp4").WithGlobalOptions(opt => opt.WithVerbosityLevel()).OutputToFile("output.mp4", false).Arguments; + Assert.AreEqual("-hide_banner -loglevel error -i \"input.mp4\" \"output.mp4\"", str); + } + + + [TestMethod] + public void Builder_BuildString_AudioCodec_Fluent() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithAudioCodec(AudioCodec.Aac).WithAudioBitrate(128)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -c:a aac -b:a 128k \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_BitStream() { - var str = GetArgumentsString(new BitStreamFilterArgument(Channel.Audio, Filter.H264_Mp4ToAnnexB)); - - Assert.AreEqual(str, "-i \"input.mp4\" -bsf:a h264_mp4toannexb \"output.mp4\""); + 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 container = new ArgumentContainer(); - - container.Add(new ConcatArgument(concatFiles)); - container.Add(new OutputArgument("output.mp4")); - - var str = builder.BuildArguments(container); - - Assert.AreEqual(str, "-i \"concat:1.mp4|2.mp4|3.mp4|4.mp4\" \"output.mp4\""); + 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 = GetArgumentsString(new CopyArgument(Channel.Audio)); - - Assert.AreEqual(str, "-i \"input.mp4\" -c:a copy \"output.mp4\""); + 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 = GetArgumentsString(new CopyArgument(Channel.Video)); - - Assert.AreEqual(str, "-i \"input.mp4\" -c:v copy \"output.mp4\""); + 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 = GetArgumentsString(new CopyArgument(Channel.Both)); - - Assert.AreEqual(str, "-i \"input.mp4\" -c copy \"output.mp4\""); + 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_CpuSpeed() + public void Builder_BuildString_DisableChannel_Audio() { - var str = GetArgumentsString(new CpuSpeedArgument(10)); + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Audio)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -an \"output.mp4\"", str); + } - Assert.AreEqual(str, "-i \"input.mp4\" -quality good -cpu-used 10 -deadline realtime \"output.mp4\""); + [TestMethod] + public void Builder_BuildString_DisableChannel_Video() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Video)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -vn \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_AudioSamplingRate_Default() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -ar 48000 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_AudioSamplingRate() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate(44000)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -ar 44000 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_VariableBitrate() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithVariableBitrate(5)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -vbr 5 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Faststart() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithFastStart()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -movflags faststart \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Overwrite() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.OverwriteExisting()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -y \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_RemoveMetadata() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithoutMetadata()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -map_metadata -1 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Transpose() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Transpose(Transposition.CounterClockwise90)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2\" \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_ForceFormat() { - var str = GetArgumentsString(new ForceFormatArgument(VideoCodec.LibX264)); - - Assert.AreEqual(str, "-i \"input.mp4\" -f libx264 \"output.mp4\""); + 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 = GetArgumentsString(new FrameOutputCountArgument(50)); - - Assert.AreEqual(str, "-i \"input.mp4\" -vframes 50 \"output.mp4\""); + 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_FrameRate() { - var str = GetArgumentsString(new FrameRateArgument(50)); - - Assert.AreEqual(str, "-i \"input.mp4\" -r 50 \"output.mp4\""); + 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 = GetArgumentsString(new LoopArgument(50)); - - Assert.AreEqual(str, "-i \"input.mp4\" -loop 50 \"output.mp4\""); + 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 = GetArgumentsString(new SeekArgument(TimeSpan.FromSeconds(10))); - - Assert.AreEqual(str, "-i \"input.mp4\" -ss 00:00:10 \"output.mp4\""); + 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 = GetArgumentsString(new ShortestArgument(true)); - - Assert.AreEqual(str, "-i \"input.mp4\" -shortest \"output.mp4\""); + 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 = GetArgumentsString(new SizeArgument(1920, 1080)); - - Assert.AreEqual(str, "-i \"input.mp4\" -s 1920x1080 \"output.mp4\""); + 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 = GetArgumentsString(new SpeedArgument(Speed.Fast)); - - Assert.AreEqual(str, "-i \"input.mp4\" -preset fast \"output.mp4\""); + 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); } + [TestMethod] + public void Builder_BuildString_DrawtextFilter() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .DrawText(DrawTextOptions + .Create("Stack Overflow", "/path/to/font.ttf") + .WithParameter("fontcolor", "white") + .WithParameter("fontsize", "24") + .WithParameter("box", "1") + .WithParameter("boxcolor", "black@0.5") + .WithParameter("boxborderw", "5") + .WithParameter("x", "(w-text_w)/2") + .WithParameter("y", "(h-text_h)/2"))) + .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); + } + + [TestMethod] + public void Builder_BuildString_DrawtextFilter_Alt() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .DrawText(DrawTextOptions + .Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24")))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -vf drawtext=\"text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24\" \"output.mp4\"", str); + } + [TestMethod] public void Builder_BuildString_StartNumber() { - var str = GetArgumentsString(new StartNumberArgument(50)); - - Assert.AreEqual(str, "-i \"input.mp4\" -start_number 50 \"output.mp4\""); + 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 = GetArgumentsString(new ThreadsArgument(50)); - - Assert.AreEqual(str, "-i \"input.mp4\" -threads 50 \"output.mp4\""); + 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 = GetArgumentsString(new ThreadsArgument(true)); - - Assert.AreEqual(str, $"-i \"input.mp4\" -threads {Environment.ProcessorCount} \"output.mp4\""); + 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 = GetArgumentsString(new VideoCodecArgument(VideoCodec.LibX264)); - - Assert.AreEqual(str, "-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\""); + 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 = GetArgumentsString(new VideoCodecArgument(VideoCodec.LibX264), new OverrideArgument()); - - Assert.AreEqual(str, "-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p -y \"output.mp4\""); + 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); } + [TestMethod] - public void Builder_BuildString_Duration() { - var str = GetArgumentsString(new DurationArgument(TimeSpan.FromSeconds(20))); + public void Builder_BuildString_Duration() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithDuration(TimeSpan.FromSeconds(20))).Arguments; + Assert.AreEqual("-i \"input.mp4\" -t 00:00:20 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Raw() + { + var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.WithCustomArgument(null!)).OutputToFile("output.mp4", false, opt => opt.WithCustomArgument(null!)).Arguments; + Assert.AreEqual(" -i \"input.mp4\" \"output.mp4\"", str); - Assert.AreEqual(str, "-i \"input.mp4\" -t 00:00:20 \"output.mp4\""); + 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); + } + + + [TestMethod] + public void Builder_BuildString_ForcePixelFormat() + { + 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); } } -} +} \ No newline at end of file diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index 0594404..552ca24 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -1,4 +1,5 @@ -using FFMpegCore.Enums; +using System; +using FFMpegCore.Enums; using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.IO; @@ -15,14 +16,12 @@ public void Audio_Remove() try { - Encoder.Mute(VideoInfo.FromFileInfo(Input), output); - - Assert.IsTrue(File.Exists(output.FullName)); + FFMpeg.Mute(Input.FullName, output); + Assert.IsTrue(File.Exists(output)); } finally { - if (File.Exists(output.FullName)) - output.Delete(); + if (File.Exists(output)) File.Delete(output); } } @@ -33,14 +32,12 @@ public void Audio_Save() try { - Encoder.ExtractAudio(VideoInfo.FromFileInfo(Input), output); - - Assert.IsTrue(File.Exists(output.FullName)); + FFMpeg.ExtractAudio(Input.FullName, output); + Assert.IsTrue(File.Exists(output)); } finally { - if (File.Exists(output.FullName)) - output.Delete(); + if (File.Exists(output)) File.Delete(output); } } @@ -50,16 +47,17 @@ public void Audio_Add() var output = Input.OutputLocation(VideoType.Mp4); try { - var input = VideoInfo.FromFileInfo(VideoLibrary.LocalVideoNoAudio); - Encoder.ReplaceAudio(input, VideoLibrary.LocalAudio, output); - - Assert.AreEqual(input.Duration, VideoInfo.FromFileInfo(output).Duration); - Assert.IsTrue(File.Exists(output.FullName)); + var success = FFMpeg.ReplaceAudio(VideoLibrary.LocalVideoNoAudio.FullName, VideoLibrary.LocalAudio.FullName, output); + Assert.IsTrue(success); + var audioAnalysis = FFProbe.Analyse(VideoLibrary.LocalVideoNoAudio.FullName); + var videoAnalysis = FFProbe.Analyse(VideoLibrary.LocalAudio.FullName); + var outputAnalysis = FFProbe.Analyse(output); + Assert.AreEqual(Math.Max(videoAnalysis.Duration.TotalSeconds, audioAnalysis.Duration.TotalSeconds), outputAnalysis.Duration.TotalSeconds, 0.15); + Assert.IsTrue(File.Exists(output)); } finally { - if (File.Exists(output.FullName)) - output.Delete(); + if (File.Exists(output)) File.Delete(output); } } @@ -70,14 +68,14 @@ public void Image_AddAudio() try { - var result = Encoder.PosterWithAudio(new FileInfo(VideoLibrary.LocalCover.FullName), VideoLibrary.LocalAudio, output); - Assert.IsTrue(result.Duration.TotalSeconds > 0); - Assert.IsTrue(result.Exists); + FFMpeg.PosterWithAudio(VideoLibrary.LocalCover.FullName, VideoLibrary.LocalAudio.FullName, output); + var analysis = FFProbe.Analyse(VideoLibrary.LocalAudio.FullName); + Assert.IsTrue(analysis.Duration.TotalSeconds > 0); + Assert.IsTrue(File.Exists(output)); } finally { - if (File.Exists(output.FullName)) - output.Delete(); + if (File.Exists(output)) File.Delete(output); } } } diff --git a/FFMpegCore.Test/BaseTest.cs b/FFMpegCore.Test/BaseTest.cs index 4e0eab3..38a5bcc 100644 --- a/FFMpegCore.Test/BaseTest.cs +++ b/FFMpegCore.Test/BaseTest.cs @@ -1,17 +1,14 @@ -using FFMpegCore.FFMPEG; -using FFMpegCore.Test.Resources; +using FFMpegCore.Test.Resources; using System.IO; namespace FFMpegCore.Test { public class BaseTest { - protected FFMpeg Encoder; protected FileInfo Input; public BaseTest() { - Encoder = new FFMpeg(); Input = VideoLibrary.LocalVideo; } } diff --git a/FFMpegCore.Test/BitmapSources.cs b/FFMpegCore.Test/BitmapSources.cs new file mode 100644 index 0000000..c3e8d40 --- /dev/null +++ b/FFMpegCore.Test/BitmapSources.cs @@ -0,0 +1,220 @@ +using FFMpegCore.Extend; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Numerics; +using FFMpegCore.Pipes; + +namespace FFMpegCore.Test +{ + static class BitmapSource + { + public static IEnumerable CreateBitmaps(int count, PixelFormat fmt, int w, int h) + { + for (int i = 0; i < count; i++) + { + using (var frame = CreateVideoFrame(i, fmt, w, h, 0.025f, 0.025f * w * 0.03f)) + { + yield return frame; + } + } + } + + private static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset) + { + var bitmap = new Bitmap(w, h, fmt); + + offset = offset * index; + + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + var xf = x / (float)w; + var yf = y / (float)h; + var nx = x * scaleNoise + offset; + var ny = y * scaleNoise + offset; + + var value = (int)((Perlin.Noise(nx, ny) + 1.0f) / 2.0f * 255); + + var color = Color.FromArgb((int)(value * xf), (int)(value * yf), value); + + bitmap.SetPixel(x, y, color); + } + + return new BitmapVideoFrameWrapper(bitmap); + } + + // + // Perlin noise generator for Unity + // Keijiro Takahashi, 2013, 2015 + // https://github.com/keijiro/PerlinNoise + // + // Based on the original implementation by Ken Perlin + // http://mrl.nyu.edu/~perlin/noise/ + // + static class Perlin + { + #region Noise functions + + public static float Noise(float x) + { + var X = (int)MathF.Floor(x) & 0xff; + x -= MathF.Floor(x); + var u = Fade(x); + return Lerp(u, Grad(perm[X], x), Grad(perm[X + 1], x - 1)) * 2; + } + + public static float Noise(float x, float y) + { + var X = (int)MathF.Floor(x) & 0xff; + var Y = (int)MathF.Floor(y) & 0xff; + x -= MathF.Floor(x); + y -= MathF.Floor(y); + var u = Fade(x); + var v = Fade(y); + var A = (perm[X] + Y) & 0xff; + var B = (perm[X + 1] + Y) & 0xff; + return Lerp(v, Lerp(u, Grad(perm[A], x, y), Grad(perm[B], x - 1, y)), + Lerp(u, Grad(perm[A + 1], x, y - 1), Grad(perm[B + 1], x - 1, y - 1))); + } + + public static float Noise(Vector2 coord) + { + return Noise(coord.X, coord.Y); + } + + public static float Noise(float x, float y, float z) + { + var X = (int)MathF.Floor(x) & 0xff; + var Y = (int)MathF.Floor(y) & 0xff; + var Z = (int)MathF.Floor(z) & 0xff; + x -= MathF.Floor(x); + y -= MathF.Floor(y); + z -= MathF.Floor(z); + var u = Fade(x); + var v = Fade(y); + var w = Fade(z); + var A = (perm[X] + Y) & 0xff; + var B = (perm[X + 1] + Y) & 0xff; + var AA = (perm[A] + Z) & 0xff; + var BA = (perm[B] + Z) & 0xff; + var AB = (perm[A + 1] + Z) & 0xff; + var BB = (perm[B + 1] + Z) & 0xff; + return Lerp(w, Lerp(v, Lerp(u, Grad(perm[AA], x, y, z), Grad(perm[BA], x - 1, y, z)), + Lerp(u, Grad(perm[AB], x, y - 1, z), Grad(perm[BB], x - 1, y - 1, z))), + Lerp(v, Lerp(u, Grad(perm[AA + 1], x, y, z - 1), Grad(perm[BA + 1], x - 1, y, z - 1)), + Lerp(u, Grad(perm[AB + 1], x, y - 1, z - 1), Grad(perm[BB + 1], x - 1, y - 1, z - 1)))); + } + + public static float Noise(Vector3 coord) + { + return Noise(coord.X, coord.Y, coord.Z); + } + + #endregion + + #region fBm functions + + public static float Fbm(float x, int octave) + { + var f = 0.0f; + var w = 0.5f; + for (var i = 0; i < octave; i++) + { + f += w * Noise(x); + x *= 2.0f; + w *= 0.5f; + } + return f; + } + + public static float Fbm(Vector2 coord, int octave) + { + var f = 0.0f; + var w = 0.5f; + for (var i = 0; i < octave; i++) + { + f += w * Noise(coord); + coord *= 2.0f; + w *= 0.5f; + } + return f; + } + + public static float Fbm(float x, float y, int octave) + { + return Fbm(new Vector2(x, y), octave); + } + + public static float Fbm(Vector3 coord, int octave) + { + var f = 0.0f; + var w = 0.5f; + for (var i = 0; i < octave; i++) + { + f += w * Noise(coord); + coord *= 2.0f; + w *= 0.5f; + } + return f; + } + + public static float Fbm(float x, float y, float z, int octave) + { + return Fbm(new Vector3(x, y, z), octave); + } + + #endregion + + #region Private functions + + static float Fade(float t) + { + return t * t * t * (t * (t * 6 - 15) + 10); + } + + static float Lerp(float t, float a, float b) + { + return a + t * (b - a); + } + + static float Grad(int hash, float x) + { + return (hash & 1) == 0 ? x : -x; + } + + static float Grad(int hash, float x, float y) + { + return ((hash & 1) == 0 ? x : -x) + ((hash & 2) == 0 ? y : -y); + } + + static float Grad(int hash, float x, float y, float z) + { + var h = hash & 15; + var u = h < 8 ? x : y; + var v = h < 4 ? y : (h == 12 || h == 14 ? x : z); + return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); + } + + static int[] perm = { + 151,160,137,91,90,15, + 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, + 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, + 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, + 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, + 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, + 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, + 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, + 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, + 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, + 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, + 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180, + 151 + }; + + #endregion + } + } +} diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 8e749dd..971b098 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -4,22 +4,42 @@ netcoreapp3.1 false + + disable + + default + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + - Always + PreserveNewest - - - + + + + diff --git a/FFMpegCore.Test/FFMpegOptions.cs b/FFMpegCore.Test/FFMpegOptionsTests.cs similarity index 80% rename from FFMpegCore.Test/FFMpegOptions.cs rename to FFMpegCore.Test/FFMpegOptionsTests.cs index efdd6e3..d175644 100644 --- a/FFMpegCore.Test/FFMpegOptions.cs +++ b/FFMpegCore.Test/FFMpegOptionsTests.cs @@ -1,5 +1,4 @@ -using FFMpegCore.FFMPEG; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using System.IO; @@ -17,7 +16,7 @@ public void Options_Initialized() [TestMethod] public void Options_Defaults_Configured() { - Assert.AreEqual(new FFMpegOptions().RootDirectory, $".{Path.DirectorySeparatorChar}FFMPEG{Path.DirectorySeparatorChar}bin"); + Assert.AreEqual(new FFMpegOptions().RootDirectory, $""); } [TestMethod] @@ -25,12 +24,12 @@ public void Options_Loaded_From_File() { Assert.AreEqual( FFMpegOptions.Options.RootDirectory, - JsonConvert.DeserializeObject(File.ReadAllText($".{Path.DirectorySeparatorChar}ffmpeg.config.json")).RootDirectory + JsonConvert.DeserializeObject(File.ReadAllText("ffmpeg.config.json")).RootDirectory ); } [TestMethod] - public void Options_Overrided() + public void Options_Set_Programmatically() { var original = FFMpegOptions.Options; try diff --git a/FFMpegCore.Test/FFMpegTest.cs b/FFMpegCore.Test/FFMpegTest.cs deleted file mode 100644 index ac1abf1..0000000 --- a/FFMpegCore.Test/FFMpegTest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FFMpegCore.FFMPEG; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace FFMpegCore.Test -{ - [TestClass] - public class FFMpegTest - { - [TestMethod] - public void CTOR_Default() - { - var encoder = new FFMpeg(); - var probe = new FFProbe(); - - Assert.IsNotNull(encoder); - Assert.IsNotNull(probe); - } - } -} diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs new file mode 100644 index 0000000..7fe928e --- /dev/null +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -0,0 +1,70 @@ +using System.IO; +using System.Threading.Tasks; +using FFMpegCore.Test.Resources; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace FFMpegCore.Test +{ + [TestClass] + public class FFProbeTests + { + [TestMethod] + public void Probe_TooLongOutput() + { + Assert.ThrowsException(() => FFProbe.Analyse(VideoLibrary.LocalVideo.FullName, 5)); + } + + [TestMethod] + public void Probe_Success() + { + var info = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); + Assert.AreEqual(3, info.Duration.Seconds); + Assert.AreEqual(".mp4", info.Extension); + Assert.AreEqual(VideoLibrary.LocalVideo.FullName, info.Path); + + Assert.AreEqual("5.1", info.PrimaryAudioStream.ChannelLayout); + Assert.AreEqual(6, info.PrimaryAudioStream.Channels); + Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName); + Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName); + Assert.AreEqual("LC", info.PrimaryAudioStream.Profile); + Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate); + Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz); + + 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); + Assert.AreEqual(1280, info.PrimaryVideoStream.Width); + Assert.AreEqual(720, info.PrimaryVideoStream.Height); + Assert.AreEqual(25, info.PrimaryVideoStream.AvgFrameRate); + Assert.AreEqual(25, info.PrimaryVideoStream.FrameRate); + Assert.AreEqual("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", info.PrimaryVideoStream.CodecLongName); + Assert.AreEqual("h264", info.PrimaryVideoStream.CodecName); + Assert.AreEqual(8, info.PrimaryVideoStream.BitsPerRawSample); + Assert.AreEqual("Main", info.PrimaryVideoStream.Profile); + } + + [TestMethod, Timeout(10000)] + public async Task Probe_Async_Success() + { + var info = await FFProbe.AnalyseAsync(VideoLibrary.LocalVideo.FullName); + Assert.AreEqual(3, info.Duration.Seconds); + } + + [TestMethod, Timeout(10000)] + public void Probe_Success_FromStream() + { + using var stream = File.OpenRead(VideoLibrary.LocalVideoWebm.FullName); + var info = FFProbe.Analyse(stream); + Assert.AreEqual(3, info.Duration.Seconds); + } + + [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(3, info.Duration.Seconds); + } + } +} \ No newline at end of file diff --git a/FFMpegCore.Test/PixelFormatTests.cs b/FFMpegCore.Test/PixelFormatTests.cs new file mode 100644 index 0000000..2c22fc5 --- /dev/null +++ b/FFMpegCore.Test/PixelFormatTests.cs @@ -0,0 +1,41 @@ +using FFMpegCore.Exceptions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace FFMpegCore.Test +{ + [TestClass] + public class PixelFormatTests + { + [TestMethod] + public void PixelFormats_Enumerate() + { + var formats = FFMpeg.GetPixelFormats(); + Assert.IsTrue(formats.Count > 0); + } + + [TestMethod] + public void PixelFormats_TryGetExisting() + { + Assert.IsTrue(FFMpeg.TryGetPixelFormat("yuv420p", out _)); + } + + [TestMethod] + public void PixelFormats_TryGetNotExisting() + { + Assert.IsFalse(FFMpeg.TryGetPixelFormat("yuv420pppUnknown", out _)); + } + + [TestMethod] + public void PixelFormats_GetExisting() + { + var fmt = FFMpeg.GetPixelFormat("yuv420p"); + Assert.IsTrue(fmt.Components == 3 && fmt.BitsPerPixel == 12); + } + + [TestMethod] + public void PixelFormats_GetNotExisting() + { + Assert.ThrowsException(() => FFMpeg.GetPixelFormat("yuv420pppUnknown")); + } + } +} diff --git a/FFMpegCore.Test/Resources/VideoLibrary.cs b/FFMpegCore.Test/Resources/VideoLibrary.cs index 90280f8..8bb0139 100644 --- a/FFMpegCore.Test/Resources/VideoLibrary.cs +++ b/FFMpegCore.Test/Resources/VideoLibrary.cs @@ -16,35 +16,36 @@ 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 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"); public static readonly FileInfo ImageJoinOutput = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}images{Path.DirectorySeparatorChar}output.mp4"); - public static FileInfo OutputLocation(this FileInfo file, VideoType type) + public static string OutputLocation(this FileInfo file, ContainerFormat type) { - return OutputLocation(file, type, "_converted"); + return OutputLocation(file, type.Extension, "_converted"); } - public static FileInfo OutputLocation(this FileInfo file, AudioType type) + public static string OutputLocation(this FileInfo file, AudioType type) { - return OutputLocation(file, type, "_audio"); + return OutputLocation(file, type.ToString(), "_audio"); } - public static FileInfo OutputLocation(this FileInfo file, ImageType type) + public static string OutputLocation(this FileInfo file, ImageType type) { - return OutputLocation(file, type, "_screenshot"); + return OutputLocation(file, type.ToString(), "_screenshot"); } - public static FileInfo OutputLocation(this FileInfo file, Enum type, string keyword) + public static string OutputLocation(this FileInfo file, string type, string keyword) { string originalLocation = file.Directory.FullName, - outputFile = file.Name.Replace(file.Extension, keyword + "." + type.ToString().ToLower()); + outputFile = file.Name.Replace(file.Extension, keyword + "." + type.ToLowerInvariant()); - return new FileInfo($"{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_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/TasksExtensions.cs b/FFMpegCore.Test/TasksExtensions.cs new file mode 100644 index 0000000..c9549ca --- /dev/null +++ b/FFMpegCore.Test/TasksExtensions.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace FFMpegCore.Test +{ + static class TasksExtensions + { + public static T WaitForResult(this Task task) => + task.ConfigureAwait(false).GetAwaiter().GetResult(); + } +} diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 819fb3a..bc44f20 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -1,284 +1,528 @@ using FFMpegCore.Enums; -using FFMpegCore.FFMPEG.Argument; -using FFMpegCore.FFMPEG.Enums; using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; +using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; +using System.Threading.Tasks; +using FFMpegCore.Arguments; +using FFMpegCore.Exceptions; +using FFMpegCore.Pipes; namespace FFMpegCore.Test { [TestClass] public class VideoTest : BaseTest { - public bool Convert(VideoType type, bool multithreaded = false, VideoSize size = VideoSize.Original) + public bool Convert(ContainerFormat type, bool multithreaded = false, VideoSize size = VideoSize.Original) { var output = Input.OutputLocation(type); try { - var input = VideoInfo.FromFileInfo(Input); + var input = FFProbe.Analyse(Input.FullName); + FFMpeg.Convert(input, output, type, size: size, multithreaded: multithreaded); + var outputVideo = FFProbe.Analyse(output); - Encoder.Convert(input, output, type, size: size, multithreaded: multithreaded); - - var outputVideo = new VideoInfo(output.FullName); - - Assert.IsTrue(File.Exists(output.FullName)); - Assert.AreEqual(outputVideo.Duration, input.Duration); + Assert.IsTrue(File.Exists(output)); + Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1); if (size == VideoSize.Original) { - Assert.AreEqual(outputVideo.Width, input.Width); - Assert.AreEqual(outputVideo.Height, input.Height); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); } else { - Assert.AreNotEqual(outputVideo.Width, input.Width); - Assert.AreNotEqual(outputVideo.Height, input.Height); - Assert.AreEqual(outputVideo.Height, (int)size); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, (int)size); } - return File.Exists(output.FullName) && + return File.Exists(output) && outputVideo.Duration == input.Duration && ( ( size == VideoSize.Original && - outputVideo.Width == input.Width && - outputVideo.Height == input.Height + outputVideo.PrimaryVideoStream.Width == input.PrimaryVideoStream.Width && + outputVideo.PrimaryVideoStream.Height == input.PrimaryVideoStream.Height ) || ( size != VideoSize.Original && - outputVideo.Width != input.Width && - outputVideo.Height != input.Height && - outputVideo.Height == (int)size + outputVideo.PrimaryVideoStream.Width != input.PrimaryVideoStream.Width && + outputVideo.PrimaryVideoStream.Height != input.PrimaryVideoStream.Height && + outputVideo.PrimaryVideoStream.Height == (int)size ) ); } finally { - if (File.Exists(output.FullName)) - File.Delete(output.FullName); + if (File.Exists(output)) + File.Delete(output); } } - public void Convert(VideoType type, ArgumentContainer container) + private void ConvertFromStreamPipe(ContainerFormat type, params IArgument[] arguments) { var output = Input.OutputLocation(type); try { - var input = VideoInfo.FromFileInfo(Input); + var input = FFProbe.Analyse(VideoLibrary.LocalVideoWebm.FullName); + 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 arguments = new ArgumentContainer(); - arguments.Add(new InputArgument(input)); - foreach (var arg in container) + 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) { - arguments.Add(arg.Value); - } - arguments.Add(new OutputArgument(output)); - - var scaling = container.Find(); - - Encoder.Convert(arguments); - - var outputVideo = new VideoInfo(output.FullName); - - Assert.IsTrue(File.Exists(output.FullName)); - Assert.AreEqual(outputVideo.Duration, input.Duration); - - if (scaling == null) - { - Assert.AreEqual(outputVideo.Width, input.Width); - Assert.AreEqual(outputVideo.Height, input.Height); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); } else { - if (scaling.Value.Width != -1) + if (scaling.Size.Value.Width != -1) { - Assert.AreEqual(outputVideo.Width, scaling.Value.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); } - if (scaling.Value.Height != -1) + if (scaling.Size.Value.Height != -1) { - Assert.AreEqual(outputVideo.Height, scaling.Value.Height); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); } - Assert.AreNotEqual(outputVideo.Width, input.Width); - Assert.AreNotEqual(outputVideo.Height, input.Height); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); } } finally { - if (File.Exists(output.FullName)) - File.Delete(output.FullName); + if (File.Exists(output)) + File.Delete(output); } } - [TestMethod] + private void ConvertToStreamPipe(params IArgument[] arguments) + { + using var ms = new MemoryStream(); + var processor = FFMpegArguments + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToPipe(new StreamPipeSink(ms), opt => + { + foreach (var arg in arguments) + opt.WithArgument(arg); + }); + + var scaling = arguments.OfType().FirstOrDefault(); + + processor.ProcessSynchronously(); + + ms.Position = 0; + var outputVideo = FFProbe.Analyse(ms); + + var input = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); + // Assert.IsTrue(Math.Abs((outputVideo.Duration - input.Duration).TotalMilliseconds) < 1000.0 / input.PrimaryVideoStream.FrameRate); + + if (scaling?.Size == null) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); + } + else + { + if (scaling.Size.Value.Width != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); + } + + if (scaling.Size.Value.Height != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); + } + + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); + } + } + + public void Convert(ContainerFormat type, Action validationMethod, params IArgument[] arguments) + { + var output = Input.OutputLocation(type); + + try + { + var input = FFProbe.Analyse(Input.FullName); + + var processor = FFMpegArguments + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile(output, false, opt => + { + foreach (var arg in arguments) + opt.WithArgument(arg); + }); + + var scaling = arguments.OfType().FirstOrDefault(); + processor.ProcessSynchronously(); + + var outputVideo = FFProbe.Analyse(output); + + Assert.IsTrue(File.Exists(output)); + Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1); + validationMethod?.Invoke(outputVideo); + if (scaling?.Size == null) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); + } + else + { + if (scaling.Size.Value.Width != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); + } + + if (scaling.Size.Value.Height != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); + } + + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); + } + } + finally + { + if (File.Exists(output)) + File.Delete(output); + } + } + + public void Convert(ContainerFormat type, params IArgument[] inputArguments) + { + Convert(type, null, inputArguments); + } + + public void ConvertFromPipe(ContainerFormat type, System.Drawing.Imaging.PixelFormat fmt, params IArgument[] arguments) + { + var output = Input.OutputLocation(type); + + try + { + var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, fmt, 256, 256)); + 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); + + Assert.IsTrue(File.Exists(output)); + + if (scaling?.Size == null) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height); + } + else + { + if (scaling.Size.Value.Width != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); + } + + if (scaling.Size.Value.Height != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); + } + + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height); + } + } + finally + { + if (File.Exists(output)) + File.Delete(output); + } + + } + + [TestMethod, Timeout(10000)] public void Video_ToMP4() { Convert(VideoType.Mp4); } - [TestMethod] - public void Video_ToMP4_Args() + [TestMethod, Timeout(10000)] + public void Video_ToMP4_YUV444p() { - var container = new ArgumentContainer(); - container.Add(new VideoCodecArgument(VideoCodec.LibX264)); - Convert(VideoType.Mp4, container); + 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, Timeout(10000)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] + // [DataRow(PixelFormat.Format48bppRgb)] + public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) + { + ConvertFromPipe(VideoType.Mp4, pixelFormat, new VideoCodecArgument(VideoCodec.LibX264)); + } + + [TestMethod, Timeout(10000)] + public void Video_ToMP4_Args_StreamPipe() + { + ConvertFromStreamPipe(VideoType.Mp4, new VideoCodecArgument(VideoCodec.LibX264)); + } + + [TestMethod, Timeout(10000)] + public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure() + { + await Assert.ThrowsExceptionAsync(async () => + { + await using var ms = new MemoryStream(); + var pipeSource = new StreamPipeSink(ms); + await FFMpegArguments + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToPipe(pipeSource, opt => opt.ForceFormat("mkv")) + .ProcessAsynchronously(); + }); + } + + [TestMethod, Timeout(10000)] + public void Video_ToMP4_Args_StreamOutputPipe_Failure() + { + Assert.ThrowsException(() => ConvertToStreamPipe(new ForceFormatArgument("mkv"))); + } + + + [TestMethod, Timeout(10000)] + public void Video_ToMP4_Args_StreamOutputPipe_Async() + { + using var ms = new MemoryStream(); + var pipeSource = new StreamPipeSink(ms); + FFMpegArguments + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToPipe(pipeSource, opt => opt + .WithVideoCodec(VideoCodec.LibX264) + .ForceFormat("matroska")) + .ProcessAsynchronously() + .WaitForResult(); + } + + [TestMethod, Timeout(10000)] + public async Task TestDuplicateRun() + { + FFMpegArguments.FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile("temporary.mp4") + .ProcessSynchronously(); + + await FFMpegArguments.FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile("temporary.mp4") + .ProcessAsynchronously(); + + File.Delete("temporary.mp4"); + } + + [TestMethod, Timeout(10000)] + public void Video_ToMP4_Args_StreamOutputPipe() + { + ConvertToStreamPipe(new VideoCodecArgument(VideoCodec.LibX264), new ForceFormatArgument("matroska")); + } + + [TestMethod, Timeout(10000)] public void Video_ToTS() { Convert(VideoType.Ts); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToTS_Args() { - var container = new ArgumentContainer(); - container.Add(new CopyArgument()); - container.Add(new BitStreamFilterArgument(Channel.Video, Filter.H264_Mp4ToAnnexB)); - container.Add(new ForceFormatArgument(VideoCodec.MpegTs)); - Convert(VideoType.Ts, container); + Convert(VideoType.Ts, + new CopyArgument(), + new BitStreamFilterArgument(Channel.Video, Filter.H264_Mp4ToAnnexB), + new ForceFormatArgument(VideoType.MpegTs)); } + [DataTestMethod, Timeout(10000)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] + // [DataRow(PixelFormat.Format48bppRgb)] + 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() { - var container = new ArgumentContainer(); - container.Add(new ScaleArgument(VideoSize.Ed)); - container.Add(new VideoCodecArgument(VideoCodec.LibTheora)); - Convert(VideoType.Ogv, container); + Convert(VideoType.Ogv, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora)); } - [TestMethod] + [DataTestMethod, Timeout(10000)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] + // [DataRow(PixelFormat.Format48bppRgb)] + public void Video_ToOGV_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) + { + ConvertFromPipe(VideoType.Ogv, pixelFormat, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora)); + } + + [TestMethod, Timeout(10000)] public void Video_ToMP4_Resize() { Convert(VideoType.Mp4, true, VideoSize.Ed); } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_ToMP4_Resize_Args() { - var container = new ArgumentContainer(); - container.Add(new ScaleArgument(VideoSize.Ld)); - container.Add(new VideoCodecArgument(VideoCodec.LibX264)); - Convert(VideoType.Mp4, container); + Convert(VideoType.Mp4, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264)); } - [TestMethod] + [DataTestMethod, Timeout(10000)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] + // [DataRow(PixelFormat.Format48bppRgb)] + public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) + { + ConvertFromPipe(VideoType.Mp4, pixelFormat, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264)); + } + + [TestMethod, Timeout(10000)] public void Video_ToOGV() { Convert(VideoType.Ogv); } - [TestMethod] + [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] - public void Video_Snapshot() + [TestMethod, Timeout(10000)] + public void Video_Snapshot_InMemory() { var output = Input.OutputLocation(ImageType.Png); try { - var input = VideoInfo.FromFileInfo(Input); + var input = FFProbe.Analyse(Input.FullName); - using (var bitmap = Encoder.Snapshot(input, output)) - { - Assert.AreEqual(input.Width, bitmap.Width); - Assert.AreEqual(input.Height, bitmap.Height); - Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); - } + using var bitmap = FFMpeg.Snapshot(input); + Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); + Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); } finally { - if (File.Exists(output.FullName)) - File.Delete(output.FullName); + if (File.Exists(output)) + File.Delete(output); } } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_Snapshot_PersistSnapshot() { var output = Input.OutputLocation(ImageType.Png); try { - var input = VideoInfo.FromFileInfo(Input); + var input = FFProbe.Analyse(Input.FullName); - using (var bitmap = Encoder.Snapshot(input, output, persistSnapshotOnFileSystem: true)) - { - Assert.AreEqual(input.Width, bitmap.Width); - Assert.AreEqual(input.Height, bitmap.Height); - Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); - Assert.IsTrue(File.Exists(output.FullName)); - } + FFMpeg.Snapshot(input, output); + + var bitmap = Image.FromFile(output); + Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); + Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); + bitmap.Dispose(); } finally { - if (File.Exists(output.FullName)) - File.Delete(output.FullName); + if (File.Exists(output)) + File.Delete(output); } } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_Join() { var output = Input.OutputLocation(VideoType.Mp4); - var newInput = Input.OutputLocation(VideoType.Mp4, "duplicate"); + var newInput = Input.OutputLocation(VideoType.Mp4.Name, "duplicate"); try { - var input = VideoInfo.FromFileInfo(Input); - File.Copy(input.FullName, newInput.FullName); - var input2 = VideoInfo.FromFileInfo(newInput); + var input = FFProbe.Analyse(Input.FullName); + File.Copy(Input.FullName, newInput); - var result = Encoder.Join(output, input, input2); + var success = FFMpeg.Join(output, Input.FullName, newInput); + Assert.IsTrue(success); - Assert.IsTrue(File.Exists(output.FullName)); - TimeSpan expectedDuration = input.Duration * 2; + Assert.IsTrue(File.Exists(output)); + var expectedDuration = input.Duration * 2; + var result = FFProbe.Analyse(output); Assert.AreEqual(expectedDuration.Days, result.Duration.Days); Assert.AreEqual(expectedDuration.Hours, result.Duration.Hours); Assert.AreEqual(expectedDuration.Minutes, result.Duration.Minutes); Assert.AreEqual(expectedDuration.Seconds, result.Duration.Seconds); - Assert.AreEqual(input.Height, result.Height); - Assert.AreEqual(input.Width, result.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, result.PrimaryVideoStream.Height); + Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width); } finally { - if (File.Exists(output.FullName)) - File.Delete(output.FullName); + if (File.Exists(output)) + File.Delete(output); - if (File.Exists(newInput.FullName)) - File.Delete(newInput.FullName); + if (File.Exists(newInput)) + File.Delete(newInput); } + } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_Join_Image_Sequence() { try @@ -289,20 +533,22 @@ public void Video_Join_Image_Sequence() .ToList() .ForEach(file => { - for (int i = 0; i < 15; i++) + for (var i = 0; i < 15; i++) { imageSet.Add(new ImageInfo(file)); } }); - var result = Encoder.JoinImageSequence(VideoLibrary.ImageJoinOutput, images: imageSet.ToArray()); + var success = FFMpeg.JoinImageSequence(VideoLibrary.ImageJoinOutput.FullName, images: imageSet.ToArray()); + Assert.IsTrue(success); + var result = FFProbe.Analyse(VideoLibrary.ImageJoinOutput.FullName); VideoLibrary.ImageJoinOutput.Refresh(); Assert.IsTrue(VideoLibrary.ImageJoinOutput.Exists); Assert.AreEqual(3, result.Duration.Seconds); - Assert.AreEqual(imageSet.First().Width, result.Width); - Assert.AreEqual(imageSet.First().Height, result.Height); + Assert.AreEqual(imageSet.First().Width, result.PrimaryVideoStream.Width); + Assert.AreEqual(imageSet.First().Height, result.PrimaryVideoStream.Height); } finally { @@ -314,40 +560,119 @@ public void Video_Join_Image_Sequence() } } - [TestMethod] + [TestMethod, Timeout(10000)] public void Video_With_Only_Audio_Should_Extract_Metadata() { - var video = VideoInfo.FromFileInfo(VideoLibrary.LocalVideoAudioOnly); - Assert.AreEqual("none", video.VideoFormat); - Assert.AreEqual("aac", video.AudioFormat); - Assert.AreEqual(79.5, video.Duration.TotalSeconds, 0.5); - Assert.AreEqual(1.25, video.Size); + var video = FFProbe.Analyse(VideoLibrary.LocalVideoAudioOnly.FullName); + Assert.AreEqual(null, video.PrimaryVideoStream); + Assert.AreEqual("aac", video.PrimaryAudioStream.CodecName); + Assert.AreEqual(10, video.Duration.TotalSeconds, 0.5); + // Assert.AreEqual(1.25, video.Size); } - - [TestMethod] - public void Video_Duration() { - var video = VideoInfo.FromFileInfo(VideoLibrary.LocalVideo); + + [TestMethod, Timeout(10000)] + public void Video_Duration() + { + var video = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); var output = Input.OutputLocation(VideoType.Mp4); - var arguments = new ArgumentContainer(); - arguments.Add(new InputArgument(VideoLibrary.LocalVideo)); - arguments.Add(new DurationArgument(TimeSpan.FromSeconds(video.Duration.TotalSeconds - 5))); - arguments.Add(new OutputArgument(output)); + try + { + FFMpegArguments + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile(output, false, opt => opt.WithDuration(TimeSpan.FromSeconds(video.Duration.TotalSeconds - 2))) + .ProcessSynchronously(); - try { - Encoder.Convert(arguments); - - Assert.IsTrue(File.Exists(output.FullName)); - var outputVideo = new VideoInfo(output.FullName); + Assert.IsTrue(File.Exists(output)); + var outputVideo = FFProbe.Analyse(output); 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); - } finally { - if (File.Exists(output.FullName)) - output.Delete(); + Assert.AreEqual(video.Duration.Seconds - 2, outputVideo.Duration.Seconds); } + finally + { + if (File.Exists(output)) + File.Delete(output); + } + } + + [TestMethod, Timeout(10000)] + public void Video_UpdatesProgress() + { + var output = Input.OutputLocation(VideoType.Mp4); + + var percentageDone = 0.0; + var timeDone = TimeSpan.Zero; + void OnPercentageProgess(double percentage) => percentageDone = percentage; + void OnTimeProgess(TimeSpan time) => timeDone = time; + + var analysis = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); + + + try + { + var success = FFMpegArguments + .FromFileInput(VideoLibrary.LocalVideo) + .OutputToFile(output, false, opt => opt + .WithDuration(TimeSpan.FromSeconds(2))) + .NotifyOnProgress(OnPercentageProgess, analysis.Duration) + .NotifyOnProgress(OnTimeProgess) + .ProcessSynchronously(); + + Assert.IsTrue(success); + Assert.IsTrue(File.Exists(output)); + Assert.AreNotEqual(0.0, percentageDone); + Assert.AreNotEqual(TimeSpan.Zero, timeDone); + } + finally + { + if (File.Exists(output)) + File.Delete(output); + } + } + + [TestMethod, Timeout(10000)] + public void Video_TranscodeInMemory() + { + using var resStream = new MemoryStream(); + var reader = new StreamPipeSink(resStream); + var writer = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 128, 128)); + + FFMpegArguments + .FromPipeInput(writer) + .OutputToPipe(reader, opt => opt + .WithVideoCodec("vp9") + .ForceFormat("webm")) + .ProcessSynchronously(); + + resStream.Position = 0; + var vi = FFProbe.Analyse(resStream); + Assert.AreEqual(vi.PrimaryVideoStream.Width, 128); + Assert.AreEqual(vi.PrimaryVideoStream.Height, 128); + } + + [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 task = FFMpegArguments + .FromPipeInput(writer) + .OutputToPipe(reader, opt => opt + .WithVideoCodec("vp9") + .ForceFormat("webm")) + .CancellableThrough(out var cancel) + .ProcessAsynchronously(false); + + await Task.Delay(300); + cancel(); + + var result = await task; + Assert.IsFalse(result); } } } diff --git a/FFMpegCore.Test/ffmpeg.config.json b/FFMpegCore.Test/ffmpeg.config.json index d80b26e..b9c9a56 100644 --- a/FFMpegCore.Test/ffmpeg.config.json +++ b/FFMpegCore.Test/ffmpeg.config.json @@ -1,3 +1,3 @@ { - "RootDirectory": "/usr/bin" + "RootDirectory": "" } \ No newline at end of file diff --git a/FFMpegCore/Enums/FileExtension.cs b/FFMpegCore/Enums/FileExtension.cs deleted file mode 100644 index 212e3db..0000000 --- a/FFMpegCore/Enums/FileExtension.cs +++ /dev/null @@ -1,38 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; -using System; - -namespace FFMpegCore.Enums -{ - public static class FileExtension - { - public static string ForType(VideoType type) - { - switch (type) - { - case VideoType.Mp4: return Mp4; - case VideoType.Ogv: return Ogv; - case VideoType.Ts: return Ts; - case VideoType.WebM: return WebM; - default: throw new Exception("The extension for this video type is not defined."); - } - } - public static string ForCodec(VideoCodec type) - { - switch (type) - { - case VideoCodec.LibX264: return Mp4; - case VideoCodec.LibVpx: return WebM; - case VideoCodec.LibTheora: return Ogv; - case VideoCodec.MpegTs: return Ts; - case VideoCodec.Png: return Png; - default: throw new Exception("The extension for this video type is not defined."); - } - } - public static readonly string Mp4 = ".mp4"; - public static readonly string Mp3 = ".mp3"; - public static readonly string Ts = ".ts"; - public static readonly string Ogv = ".ogv"; - public static readonly string Png = ".png"; - public static readonly string WebM = ".webm"; - } -} diff --git a/FFMpegCore/Enums/VideoType.cs b/FFMpegCore/Enums/VideoType.cs deleted file mode 100644 index 582330d..0000000 --- a/FFMpegCore/Enums/VideoType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace FFMpegCore.Enums -{ - public enum VideoType - { - Mp4, - Ogv, - Ts, - WebM - } -} \ No newline at end of file diff --git a/FFMpegCore/Extend/BitmapExtensions.cs b/FFMpegCore/Extend/BitmapExtensions.cs index b784be6..bf10336 100644 --- a/FFMpegCore/Extend/BitmapExtensions.cs +++ b/FFMpegCore/Extend/BitmapExtensions.cs @@ -1,26 +1,22 @@ using System; using System.Drawing; using System.IO; -using FFMpegCore.FFMPEG; namespace FFMpegCore.Extend { public static class BitmapExtensions { - public static VideoInfo AddAudio(this Bitmap poster, FileInfo audio, FileInfo output) + public static bool AddAudio(this Bitmap poster, string audio, string output) { var destination = $"{Environment.TickCount}.png"; - poster.Save(destination); - - var tempFile = new FileInfo(destination); try { - return new FFMpeg().PosterWithAudio(tempFile, audio, output); + return FFMpeg.PosterWithAudio(destination, audio, output); } finally { - tempFile.Delete(); + if (File.Exists(destination)) File.Delete(destination); } } } diff --git a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs new file mode 100644 index 0000000..e2f0737 --- /dev/null +++ b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs @@ -0,0 +1,88 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using FFMpegCore.Pipes; + +namespace FFMpegCore.Extend +{ + public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable + { + public int Width => Source.Width; + + public int Height => Source.Height; + + public string Format { get; private set; } + + public Bitmap Source { get; private set; } + + public BitmapVideoFrameWrapper(Bitmap bitmap) + { + Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap)); + Format = ConvertStreamFormat(bitmap.PixelFormat); + } + + public void Serialize(System.IO.Stream stream) + { + var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); + + try + { + var buffer = new byte[data.Stride * data.Height]; + Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); + stream.Write(buffer, 0, buffer.Length); + } + finally + { + Source.UnlockBits(data); + } + } + + public async Task SerializeAsync(System.IO.Stream stream, CancellationToken token) + { + var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); + + try + { + var buffer = new byte[data.Stride * data.Height]; + Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); + await stream.WriteAsync(buffer, 0, buffer.Length, token); + } + finally + { + Source.UnlockBits(data); + } + } + + public void Dispose() + { + Source.Dispose(); + } + + private static string ConvertStreamFormat(PixelFormat fmt) + { + switch (fmt) + { + case PixelFormat.Format16bppGrayScale: + return "gray16le"; + case PixelFormat.Format16bppRgb565: + return "bgr565le"; + case PixelFormat.Format24bppRgb: + return "bgr24"; + case PixelFormat.Format32bppArgb: + return "bgra"; + case PixelFormat.Format32bppPArgb: + //This is not really same as argb32 + return "argb"; + case PixelFormat.Format32bppRgb: + return "rgba"; + case PixelFormat.Format48bppRgb: + return "rgb48le"; + default: + throw new NotSupportedException($"Not supported pixel format {fmt}"); + } + } + } +} diff --git a/FFMpegCore/Extend/UriExtensions.cs b/FFMpegCore/Extend/UriExtensions.cs index 0d7629e..ebe92c0 100644 --- a/FFMpegCore/Extend/UriExtensions.cs +++ b/FFMpegCore/Extend/UriExtensions.cs @@ -1,14 +1,12 @@ using System; -using System.IO; -using FFMpegCore.FFMPEG; namespace FFMpegCore.Extend { public static class UriExtensions { - public static VideoInfo SaveStream(this Uri uri, FileInfo output) + public static bool SaveStream(this Uri uri, string output) { - return new FFMpeg().SaveM3U8Stream(uri, output); + return FFMpeg.SaveM3U8Stream(uri, output); } } } \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Argument/Argument.cs b/FFMpegCore/FFMPEG/Argument/Argument.cs deleted file mode 100644 index f911ea3..0000000 --- a/FFMpegCore/FFMPEG/Argument/Argument.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Abstract class implements basic functionality of ffmpeg arguments - /// - public abstract class Argument - { - /// - /// String representation of the argument - /// - /// String representation of the argument - public abstract string GetStringValue(); - - public override string ToString() - { - return GetStringValue(); - } - } - - /// - /// Abstract class implements basic functionality of ffmpeg arguments with one value property - /// - public abstract class Argument : Argument - { - private T _value; - - /// - /// Value type of - /// - public T Value { get => _value; set { CheckValue(value); _value = value; } } - - public Argument() { } - - public Argument(T value) - { - Value = value; - } - - protected virtual void CheckValue(T value) - { - - } - } - - /// - /// Abstract class implements basic functionality of ffmpeg arguments with two values properties - /// - public abstract class Argument : Argument - { - - private T1 _first; - private T2 _second; - - /// - /// First value type of - /// - public T1 First { get => _first; set { CheckFirst(_first); _first = value; } } - - /// - /// Second value type of - /// - public T2 Second { get => _second; set { CheckSecond(_second); _second = value; } } - - public Argument() { } - - public Argument(T1 first, T2 second) - { - First = first; - Second = second; - } - - protected virtual void CheckFirst(T1 value) - { - - } - - protected virtual void CheckSecond(T2 value) - { - - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/ArgumentContainer.cs b/FFMpegCore/FFMPEG/Argument/ArgumentContainer.cs deleted file mode 100644 index 25f6bde..0000000 --- a/FFMpegCore/FFMPEG/Argument/ArgumentContainer.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Container of arguments represented parameters of FFMPEG process - /// - public class ArgumentContainer : IDictionary - { - IDictionary _args; - - public ArgumentContainer(params Argument[] arguments) - { - _args = new Dictionary(); - - foreach(var argument in arguments) - { - this.Add(argument); - } - } - - public Argument this[Type key] { get => _args[key]; set => _args[key] = value; } - - public ICollection Keys => _args.Keys; - - public ICollection Values => _args.Values; - - public int Count => _args.Count; - - public bool IsReadOnly => _args.IsReadOnly; - - /// - /// This method is not supported, left for interface support - /// - /// - /// - [Obsolete] - public void Add(Type key, Argument value) - { - throw new InvalidOperationException("Not supported operation"); - } - - /// - /// This method is not supported, left for interface support - /// - /// - /// - [Obsolete] - public void Add(KeyValuePair item) - { - this.Add(item.Value); - } - - /// - /// Clears collection of arguments - /// - public void Clear() - { - _args.Clear(); - } - - /// - /// Returns if contains item - /// - /// Searching item - /// Returns if contains item - public bool Contains(KeyValuePair item) - { - return _args.Contains(item); - } - - /// - /// Adds argument to collection - /// - /// Argument that should be added to collection - public void Add(params Argument[] values) - { - foreach(var value in values) - { - _args.Add(value.GetType(), value); - } - } - - /// - /// Checks if container contains output and input parameters - /// - /// - public bool ContainsInputOutput() - { - return ((ContainsKey(typeof(InputArgument)) && !ContainsKey(typeof(ConcatArgument))) || - (!ContainsKey(typeof(InputArgument)) && ContainsKey(typeof(ConcatArgument)))) - && ContainsKey(typeof(OutputArgument)); - } - - /// - /// Checks if contains argument of type - /// - /// Type of argument is seraching - /// - public bool ContainsKey(Type key) - { - return _args.ContainsKey(key); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - _args.CopyTo(array, arrayIndex); - } - - public IEnumerator> GetEnumerator() - { - return _args.GetEnumerator(); - } - - public bool Remove(Type key) - { - return _args.Remove(key); - } - - public bool Remove(KeyValuePair item) - { - return _args.Remove(item); - } - - public bool TryGetValue(Type key, out Argument value) - { - return _args.TryGetValue(key, out value); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _args.GetEnumerator(); - } - - /// - /// Shortcut for finding arguments inside collection - /// - /// Type of argument - /// - public T Find() where T : Argument - { - if (ContainsKey(typeof(T))) - return (T)_args[typeof(T)]; - return null; - } - /// - /// Shortcut for checking if contains arguments inside collection - /// - /// Type of argument - /// - public bool Contains() where T : Argument - { - if (ContainsKey(typeof(T))) - return true; - return false; - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/ArgumentStringifier.cs b/FFMpegCore/FFMPEG/Argument/ArgumentStringifier.cs deleted file mode 100644 index ee11c03..0000000 --- a/FFMpegCore/FFMPEG/Argument/ArgumentStringifier.cs +++ /dev/null @@ -1,179 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; -using System; -using System.Collections.Generic; -using System.Drawing; - -namespace FFMpegCore.FFMPEG.Argument -{ - internal static class ArgumentStringifier - { - internal static string Speed(Speed speed) - { - return $"-preset {speed.ToString().ToLower()} "; - } - - internal static string Speed(int cpu) - { - return $"-quality good -cpu-used {cpu} -deadline realtime "; - } - - internal static string Audio(AudioCodec codec, int bitrate) - { - return Audio(codec) + Audio(bitrate); - } - - internal static string Audio(AudioCodec codec) - { - return $"-c:a {codec.ToString().ToLower()} "; - } - - internal static string Audio(AudioQuality bitrate) - { - return Audio((int)bitrate); - } - - internal static string Audio(int bitrate) - { - return $"-b:a {bitrate}k "; - } - - internal static string Video(VideoCodec codec, int bitrate = 0) - { - var video = $"-c:v {codec.ToString().ToLower()} -pix_fmt yuv420p "; - - if (bitrate > 0) - { - video += $"-b:v {bitrate}k "; - } - - return video; - } - - internal static string Threads(bool multiThread) - { - var threadCount = multiThread - ? Environment.ProcessorCount - : 1; - - return Threads(threadCount); - } - - internal static string Threads(int threads) - { - return $"-threads {threads} "; - } - - internal static string Disable(Channel type) - { - switch (type) - { - case Channel.Video: - return "-vn "; - case Channel.Audio: - return "-an "; - default: - return string.Empty; - } - } - - internal static string Output(string output) - { - return $"\"{output}\""; - } - - internal static string Input(string template) - { - return $"-i \"{template}\" "; - } - - internal static string Scale(VideoSize size, int width =-1) - { - return size == VideoSize.Original ? string.Empty : Scale(width, (int)size); - } - - internal static string Scale(int width, int height) - { - return $"-vf scale={width}:{height} "; - } - - internal static string Size(Size? size) - { - if (!size.HasValue) return string.Empty; - - var formatedSize = $"{size.Value.Width}x{size.Value.Height}"; - - return $"-s {formatedSize} "; - } - - internal static string ForceFormat(VideoCodec codec) - { - return $"-f {codec.ToString().ToLower()} "; - } - - internal static string BitStreamFilter(Channel type, Filter filter) - { - switch (type) - { - case Channel.Audio: - return $"-bsf:a {filter.ToString().ToLower()} "; - case Channel.Video: - return $"-bsf:v {filter.ToString().ToLower()} "; - default: - return string.Empty; - } - } - - internal static string Copy(Channel type = Channel.Both) - { - switch (type) - { - case Channel.Audio: - return "-c:a copy "; - case Channel.Video: - return "-c:v copy "; - default: - return "-c copy "; - } - } - - internal static string Seek(TimeSpan? seek) - { - return !seek.HasValue ? string.Empty : $"-ss {seek} "; - } - - internal static string FrameOutputCount(int number) - { - return $"-vframes {number} "; - } - - internal static string Loop(int count) - { - return $"-loop {count} "; - } - - internal static string FinalizeAtShortestInput(bool applicable) - { - return applicable ? "-shortest " : string.Empty; - } - - internal static string InputConcat(IEnumerable paths) - { - return $"-i \"concat:{string.Join(@"|", paths)}\" "; - } - - internal static string FrameRate(double frameRate) - { - return $"-r {frameRate} "; - } - - internal static string StartNumber(int v) - { - return $"-start_number {v} "; - } - - internal static string Duration(TimeSpan? duration) - { - return !duration.HasValue ? string.Empty : $"-t {duration} "; - } - } -} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/AudioCodecArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/AudioCodecArgument.cs deleted file mode 100644 index 66195f9..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/AudioCodecArgument.cs +++ /dev/null @@ -1,42 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents parameter of audio codec and it's quality - /// - public class AudioCodecArgument : Argument - { - /// - /// Bitrate of audio channel - /// - public int Bitrate { get; protected set; } = (int)AudioQuality.Normal; - - public AudioCodecArgument() - { - } - - public AudioCodecArgument(AudioCodec value) : base(value) - { - } - - public AudioCodecArgument(AudioCodec value, AudioQuality bitrate) : base(value) - { - Bitrate = (int)bitrate; - } - - public AudioCodecArgument(AudioCodec value, int bitrate) : base(value) - { - Bitrate = bitrate; - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Audio(Value, Bitrate); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/BitStreamFilterArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/BitStreamFilterArgument.cs deleted file mode 100644 index 553072b..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/BitStreamFilterArgument.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents parameter of bitstream filter - /// - public class BitStreamFilterArgument : Argument - { - public BitStreamFilterArgument() - { - } - - public BitStreamFilterArgument(Channel first, Filter second) : base(first, second) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.BitStreamFilter(First, Second); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/ConcatArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/ConcatArgument.cs deleted file mode 100644 index cbdbb51..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/ConcatArgument.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -namespace FFMpegCore.FFMPEG.Argument -{ - - /// - /// Represents parameter of concat argument - /// Used for creating video from multiple images or videos - /// - public class ConcatArgument : Argument>, IEnumerable - { - public ConcatArgument() - { - Value = new List(); - } - - public ConcatArgument(IEnumerable value) : base(value) - { - } - - public IEnumerator GetEnumerator() - { - return Value.GetEnumerator(); - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.InputConcat(Value); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/CopyArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/CopyArgument.cs deleted file mode 100644 index cd94e69..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/CopyArgument.cs +++ /dev/null @@ -1,29 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents parameter of copy parameter - /// Defines if channel (audio, video or both) should be copied to output file - /// - public class CopyArgument : Argument - { - public CopyArgument() - { - Value = Channel.Both; - } - - public CopyArgument(Channel value = Channel.Both) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Copy(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/CpuSpeedArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/CpuSpeedArgument.cs deleted file mode 100644 index 53c4014..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/CpuSpeedArgument.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents cpu speed parameter - /// - public class CpuSpeedArgument : Argument - { - public CpuSpeedArgument() - { - } - - public CpuSpeedArgument(int value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Speed(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/DisableChannelArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/DisableChannelArgument.cs deleted file mode 100644 index 258d1ad..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/DisableChannelArgument.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents cpu speed parameter - /// - public class DisableChannelArgument : Argument - { - public DisableChannelArgument() - { - } - - public DisableChannelArgument(Channel value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Disable(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/DurationArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/DurationArgument.cs deleted file mode 100644 index 0e764db..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/DurationArgument.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents duration parameter - /// - public class DurationArgument : Argument - { - public DurationArgument() - { - } - - public DurationArgument(TimeSpan? value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Duration(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/ForceFormatArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/ForceFormatArgument.cs deleted file mode 100644 index b9c3a4d..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/ForceFormatArgument.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents force format parameter - /// - public class ForceFormatArgument : Argument - { - public ForceFormatArgument() - { - } - - public ForceFormatArgument(VideoCodec value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.ForceFormat(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/FrameOutputCountArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/FrameOutputCountArgument.cs deleted file mode 100644 index 3facb87..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/FrameOutputCountArgument.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents frame output count parameter - /// - public class FrameOutputCountArgument : Argument - { - public FrameOutputCountArgument() - { - } - - public FrameOutputCountArgument(int value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.FrameOutputCount(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/FrameRateArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/FrameRateArgument.cs deleted file mode 100644 index f6e0f01..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/FrameRateArgument.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents frame rate parameter - /// - public class FrameRateArgument : Argument - { - public FrameRateArgument() - { - } - - public FrameRateArgument(double value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.FrameRate(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/InputArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/InputArgument.cs deleted file mode 100644 index e4a370f..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/InputArgument.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.IO; -using System.Linq; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents input parameter - /// - public class InputArgument : Argument - { - public InputArgument() - { - } - - public InputArgument(params string[] values) : base(values) - { - } - - public InputArgument(params VideoInfo[] values) : base(values.Select(v => v.FullName).ToArray()) - { - } - - public InputArgument(params FileInfo[] values) : base(values.Select(v => v.FullName).ToArray()) - { - } - - public InputArgument(params Uri[] values) : base(values.Select(v => v.AbsoluteUri).ToArray()) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return string.Join(" ", Value.Select(v => ArgumentStringifier.Input(v))); - } - public VideoInfo[] GetAsVideoInfo() - { - return Value.Select(v => new VideoInfo(v)).ToArray(); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/LoopArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/LoopArgument.cs deleted file mode 100644 index b09b2dd..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/LoopArgument.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents loop parameter - /// - public class LoopArgument : Argument - { - public LoopArgument() - { - } - - public LoopArgument(int value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Loop(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/OutputArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/OutputArgument.cs deleted file mode 100644 index 5049b0b..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/OutputArgument.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.IO; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents output parameter - /// - public class OutputArgument : Argument - { - public OutputArgument() - { - } - - public OutputArgument(string value) : base(value) - { - } - - public OutputArgument(VideoInfo value) : base(value.FullName) - { - } - - public OutputArgument(FileInfo value) : base(value.FullName) - { - } - - public OutputArgument(Uri value) : base(value.AbsolutePath) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Output(Value); - } - - public FileInfo GetAsFileInfo() - { - return new FileInfo(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/OverrideArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/OverrideArgument.cs deleted file mode 100644 index ec5ad73..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/OverrideArgument.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents override parameter - /// If output file should be overrided if exists - /// - public class OverrideArgument : Argument - { - public OverrideArgument() - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return "-y"; - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/ScaleArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/ScaleArgument.cs deleted file mode 100644 index a0b4712..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/ScaleArgument.cs +++ /dev/null @@ -1,37 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; -using System.Drawing; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents scale parameter - /// - public class ScaleArgument : Argument - { - public ScaleArgument() - { - } - - public ScaleArgument(Size value) : base(value) - { - } - - public ScaleArgument(int width, int heignt) : base(new Size(width, heignt)) - { - } - - public ScaleArgument(VideoSize videosize) - { - Value = videosize == VideoSize.Original ? new Size(-1, -1) : new Size(-1, (int)videosize); - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Scale(Value.Width, Value.Height); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/SeekArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/SeekArgument.cs deleted file mode 100644 index edc0a6e..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/SeekArgument.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents seek parameter - /// - public class SeekArgument : Argument - { - public SeekArgument() - { - } - - public SeekArgument(TimeSpan? value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Seek(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/ShortestArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/ShortestArgument.cs deleted file mode 100644 index c25b0ad..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/ShortestArgument.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents shortest parameter - /// - public class ShortestArgument : Argument - { - public ShortestArgument() - { - } - - public ShortestArgument(bool value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.FinalizeAtShortestInput(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/SizeArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/SizeArgument.cs deleted file mode 100644 index f70dc7d..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/SizeArgument.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Drawing; -using FFMpegCore.FFMPEG.Enums; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents size parameter - /// - public class SizeArgument : ScaleArgument - { - public SizeArgument() - { - } - - public SizeArgument(Size? value) : base(value ?? new Size()) - { - } - - public SizeArgument(VideoSize videosize) : base(videosize) - { - } - - public SizeArgument(int width, int heignt) : base(width, heignt) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Size(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/SpeedArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/SpeedArgument.cs deleted file mode 100644 index 6d8f5cd..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/SpeedArgument.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents speed parameter - /// - public class SpeedArgument : Argument - { - public SpeedArgument() - { - } - - public SpeedArgument(Speed value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Speed(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/StartNumberArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/StartNumberArgument.cs deleted file mode 100644 index 7a8201d..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/StartNumberArgument.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents start number parameter - /// - public class StartNumberArgument : Argument - { - public StartNumberArgument() - { - } - - public StartNumberArgument(int value) : base(value) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.StartNumber(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/ThreadsArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/ThreadsArgument.cs deleted file mode 100644 index 5c95a4a..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/ThreadsArgument.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents threads parameter - /// Number of threads used for video encoding - /// - public class ThreadsArgument : Argument - { - public ThreadsArgument() - { - } - - public ThreadsArgument(int value) : base(value) - { - } - - public ThreadsArgument(bool isMultiThreaded) : - base(isMultiThreaded - ? Environment.ProcessorCount - : 1) - { - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Threads(Value); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/VideoCodecArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/VideoCodecArgument.cs deleted file mode 100644 index eaf5d9b..0000000 --- a/FFMpegCore/FFMPEG/Argument/Atoms/VideoCodecArgument.cs +++ /dev/null @@ -1,34 +0,0 @@ -using FFMpegCore.FFMPEG.Enums; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Represents video codec parameter - /// - public class VideoCodecArgument : Argument - { - public int Bitrate { get; protected set; } = 0; - - public VideoCodecArgument() - { - } - - public VideoCodecArgument(VideoCodec value) : base(value) - { - } - - public VideoCodecArgument(VideoCodec value, int bitrate) : base(value) - { - Bitrate = bitrate; - } - - /// - /// String representation of the argument - /// - /// String representation of the argument - public override string GetStringValue() - { - return ArgumentStringifier.Video(Value, Bitrate); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/FFArgumentBuilder.cs b/FFMpegCore/FFMPEG/Argument/FFArgumentBuilder.cs deleted file mode 100644 index 693021f..0000000 --- a/FFMpegCore/FFMPEG/Argument/FFArgumentBuilder.cs +++ /dev/null @@ -1,47 +0,0 @@ -using FFMpegCore.Enums; -using FFMpegCore.Helpers; -using System; -using System.IO; -using System.Linq; - -namespace FFMpegCore.FFMPEG.Argument -{ - /// - /// Builds parameters string from that would be passed to ffmpeg process - /// - public class FFArgumentBuilder : IArgumentBuilder - { - /// - /// Builds parameters string from that would be passed to ffmpeg process - /// - /// Container of arguments - /// Parameters string - public string BuildArguments(ArgumentContainer container) - { - if (!container.ContainsInputOutput()) - throw new ArgumentException("No input or output parameter found", nameof(container)); - - - return string.Join(" ", container.Select(argument => argument.Value.GetStringValue().Trim())); - } - - private void CheckExtensionOfOutputExtension(ArgumentContainer container, FileInfo output) - { - if(container.ContainsKey(typeof(VideoCodecArgument))) - { - var codec = (VideoCodecArgument)container[typeof(VideoCodecArgument)]; - FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.ForCodec(codec.Value)); - } - } - - private Argument GetInput(ArgumentContainer container) - { - if (container.ContainsKey(typeof(InputArgument))) - return container[typeof(InputArgument)]; - else if (container.ContainsKey(typeof(ConcatArgument))) - return container[typeof(ConcatArgument)]; - else - throw new ArgumentException("No inputs found"); - } - } -} diff --git a/FFMpegCore/FFMPEG/Argument/IArgumentBuilder.cs b/FFMpegCore/FFMPEG/Argument/IArgumentBuilder.cs deleted file mode 100644 index 0248c36..0000000 --- a/FFMpegCore/FFMPEG/Argument/IArgumentBuilder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FFMpegCore.FFMPEG.Argument -{ - public interface IArgumentBuilder - { - string BuildArguments(ArgumentContainer container); - } -} diff --git a/FFMpegCore/FFMPEG/Enums/Codec.cs b/FFMpegCore/FFMPEG/Enums/Codec.cs deleted file mode 100644 index b0aaf60..0000000 --- a/FFMpegCore/FFMPEG/Enums/Codec.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace FFMpegCore.FFMPEG.Enums -{ - public enum VideoCodec - { - LibX264, - LibVpx, - LibTheora, - Png, - MpegTs - } - - public enum AudioCodec - { - Aac, - LibVorbis - } - - public enum Filter - { - H264_Mp4ToAnnexB, - Aac_AdtstoAsc - } - - public enum Channel - { - Audio, - Video, - Both - } -} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Exceptions/FFMpegException.cs b/FFMpegCore/FFMPEG/Exceptions/FFMpegException.cs deleted file mode 100644 index ca927d0..0000000 --- a/FFMpegCore/FFMPEG/Exceptions/FFMpegException.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Text; - -namespace FFMpegCore.FFMPEG.Exceptions -{ - public enum FFMpegExceptionType - { - Dependency, - Conversion, - File, - Operation, - Process - } - - public class FFMpegException : Exception - { - public FFMpegException(FFMpegExceptionType type): this(type, null, null) { } - - public FFMpegException(FFMpegExceptionType type, StringBuilder sb): this(type, sb.ToString(), null) { } - - public FFMpegException(FFMpegExceptionType type, string message): this(type, message, null) { } - - public FFMpegException(FFMpegExceptionType type, string message, FFMpegException innerException) - : base(message, innerException) - { - Type = type; - } - - public FFMpegExceptionType Type { get; set; } - } -} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/FFMpeg.cs b/FFMpegCore/FFMPEG/FFMpeg.cs deleted file mode 100644 index 783530b..0000000 --- a/FFMpegCore/FFMPEG/FFMpeg.cs +++ /dev/null @@ -1,543 +0,0 @@ -using FFMpegCore.Enums; -using FFMpegCore.FFMPEG.Argument; -using FFMpegCore.FFMPEG.Enums; -using FFMpegCore.FFMPEG.Exceptions; -using FFMpegCore.Helpers; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.Drawing.Imaging; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Instances; - -namespace FFMpegCore.FFMPEG -{ - public delegate void ConversionHandler(double percentage); - - public class FFMpeg - { - IArgumentBuilder ArgumentBuilder { get; set; } - - /// - /// Intializes the FFMPEG encoder. - /// - public FFMpeg() : base() - { - FFMpegHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory); - - _ffmpegPath = FFMpegOptions.Options.FFmpegBinary; - - ArgumentBuilder = new FFArgumentBuilder(); - } - - /// - /// Returns the percentage of the current conversion progress. - /// - public event ConversionHandler OnProgress; - - /// - /// Saves a 'png' thumbnail from the input video. - /// - /// Source video file. - /// Output video file - /// Seek position where the thumbnail should be taken. - /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// By default, it deletes the created image on disk. If set to true, it won't delete the image - /// Bitmap with the requested snapshot. - public Bitmap Snapshot(VideoInfo source, FileInfo output, Size? size = null, TimeSpan? captureTime = null, - bool persistSnapshotOnFileSystem = false) - { - if (captureTime == null) - captureTime = TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); - - if (output.Extension.ToLower() != FileExtension.Png) - output = new FileInfo(output.FullName.Replace(output.Extension, FileExtension.Png)); - - if (size == null || (size.Value.Height == 0 && size.Value.Width == 0)) - { - size = new Size(source.Width, source.Height); - } - - if (size.Value.Width != size.Value.Height) - { - if (size.Value.Width == 0) - { - var ratio = source.Width / (double) size.Value.Width; - - size = new Size((int) (source.Width * ratio), (int) (source.Height * ratio)); - } - - if (size.Value.Height == 0) - { - var ratio = source.Height / (double) size.Value.Height; - - size = new Size((int) (source.Width * ratio), (int) (source.Height * ratio)); - } - } - - FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); - var container = new ArgumentContainer( - new InputArgument(source), - new VideoCodecArgument(VideoCodec.Png), - new FrameOutputCountArgument(1), - new SeekArgument(captureTime), - new SizeArgument(size), - new OutputArgument(output) - ); - - if (!RunProcess(container, output)) - { - throw new OperationCanceledException("Could not take snapshot!"); - } - - output.Refresh(); - - Bitmap result; - using (var bmp = (Bitmap) Image.FromFile(output.FullName)) - { - using var ms = new MemoryStream(); - bmp.Save(ms, ImageFormat.Png); - result = new Bitmap(ms); - } - - if (output.Exists && !persistSnapshotOnFileSystem) - { - output.Delete(); - } - - return result; - } - - /// - /// Convert a video do a different format. - /// - /// Input video source. - /// Output information. - /// Target conversion video type. - /// Conversion target speed/quality (faster speed = lower quality). - /// Video size. - /// Conversion target audio quality. - /// Is encoding multithreaded. - /// Output video information. - public VideoInfo Convert( - VideoInfo source, - FileInfo output, - VideoType type = VideoType.Mp4, - Speed speed = Speed.SuperFast, - VideoSize size = VideoSize.Original, - AudioQuality audioQuality = AudioQuality.Normal, - bool multithreaded = false) - { - FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); - FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.ForType(type)); - FFMpegHelper.ConversionSizeExceptionCheck(source); - - _totalTime = source.Duration; - - var scale = VideoSize.Original == size ? 1 : (double) source.Height / (int) size; - - var outputSize = new Size( - (int) (source.Width / scale), - (int) (source.Height / scale) - ); - - if (outputSize.Width % 2 != 0) - { - outputSize.Width += 1; - } - - var container = new ArgumentContainer(); - - switch (type) - { - case VideoType.Mp4: - container.Add( - new InputArgument(source), - new ThreadsArgument(multithreaded), - new ScaleArgument(outputSize), - new VideoCodecArgument(VideoCodec.LibX264, 2400), - new SpeedArgument(speed), - new AudioCodecArgument(AudioCodec.Aac, audioQuality), - new OutputArgument(output) - ); - break; - case VideoType.Ogv: - container.Add( - new InputArgument(source), - new ThreadsArgument(multithreaded), - new ScaleArgument(outputSize), - new VideoCodecArgument(VideoCodec.LibTheora, 2400), - new SpeedArgument(speed), - new AudioCodecArgument(AudioCodec.LibVorbis, audioQuality), - new OutputArgument(output) - ); - break; - case VideoType.Ts: - container.Add( - new InputArgument(source), - new CopyArgument(), - new BitStreamFilterArgument(Channel.Video, Filter.H264_Mp4ToAnnexB), - new ForceFormatArgument(VideoCodec.MpegTs), - new OutputArgument(output) - ); - break; - } - - if (!RunProcess(container, output)) - { - throw new FFMpegException(FFMpegExceptionType.Conversion, - $"The video could not be converted to {Enum.GetName(typeof(VideoType), type)}"); - } - - _totalTime = TimeSpan.MinValue; - - return new VideoInfo(output); - } - - /// - /// Adds a poster image to an audio file. - /// - /// Source image file. - /// Source audio file. - /// Output video file. - /// - public VideoInfo PosterWithAudio(FileInfo image, FileInfo audio, FileInfo output) - { - FFMpegHelper.InputsExistExceptionCheck(image, audio); - FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); - FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName)); - - var container = new ArgumentContainer( - new LoopArgument(1), - new InputArgument(image.FullName, audio.FullName), - new VideoCodecArgument(VideoCodec.LibX264, 2400), - new AudioCodecArgument(AudioCodec.Aac, AudioQuality.Normal), - new ShortestArgument(true), - new OutputArgument(output) - ); - - if (!RunProcess(container, output)) - { - throw new FFMpegException(FFMpegExceptionType.Operation, - "An error occured while adding the audio file to the image."); - } - - return new VideoInfo(output); - } - - /// - /// Joins a list of video files. - /// - /// Output video file. - /// List of vides that need to be joined together. - /// Output video information. - public VideoInfo Join(FileInfo output, params VideoInfo[] videos) - { - FFMpegHelper.OutputExistsExceptionCheck(output); - FFMpegHelper.InputsExistExceptionCheck(videos.Select(video => video.ToFileInfo()).ToArray()); - - var temporaryVideoParts = videos.Select(video => - { - FFMpegHelper.ConversionSizeExceptionCheck(video); - var destinationPath = video.FullName.Replace(video.Extension, FileExtension.Ts); - Convert( - video, - new FileInfo(destinationPath), - VideoType.Ts - ); - return destinationPath; - }).ToList(); - - var container = new ArgumentContainer( - new ConcatArgument(temporaryVideoParts), - new CopyArgument(), - new BitStreamFilterArgument(Channel.Audio, Filter.Aac_AdtstoAsc), - new OutputArgument(output) - ); - - try - { - if (!RunProcess(container, output)) - { - throw new FFMpegException(FFMpegExceptionType.Operation, - "Could not join the provided video files."); - } - - return new VideoInfo(output); - } - finally - { - Cleanup(temporaryVideoParts); - } - } - - /// - /// Converts an image sequence to a video. - /// - /// Output video file. - /// FPS - /// Image sequence collection - /// Output video information. - public VideoInfo JoinImageSequence(FileInfo output, double frameRate = 30, params ImageInfo[] images) - { - var temporaryImageFiles = images.Select((image, index) => - { - FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName)); - var destinationPath = - image.FullName.Replace(image.Name, $"{index.ToString().PadLeft(9, '0')}{image.Extension}"); - File.Copy(image.FullName, destinationPath); - - return destinationPath; - }).ToList(); - - var firstImage = images.First(); - - var container = new ArgumentContainer( - new FrameRateArgument(frameRate), - new SizeArgument(firstImage.Width, firstImage.Height), - new StartNumberArgument(0), - new InputArgument($"{firstImage.Directory}{Path.DirectorySeparatorChar}%09d.png"), - new FrameOutputCountArgument(images.Length), - new VideoCodecArgument(VideoCodec.LibX264), - new OutputArgument(output) - ); - - try - { - if (!RunProcess(container, output)) - { - throw new FFMpegException(FFMpegExceptionType.Operation, - "Could not join the provided image sequence."); - } - - return new VideoInfo(output); - } - finally - { - Cleanup(temporaryImageFiles); - } - } - - /// - /// Records M3U8 streams to the specified output. - /// - /// URI to pointing towards stream. - /// Output file - /// Success state. - public VideoInfo SaveM3U8Stream(Uri uri, FileInfo output) - { - FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); - - if (uri.Scheme == "http" || uri.Scheme == "https") - { - var container = new ArgumentContainer( - new InputArgument(uri), - new OutputArgument(output) - ); - - if (!RunProcess(container, output)) - { - throw new FFMpegException(FFMpegExceptionType.Operation, - $"Saving the ${uri.AbsoluteUri} stream failed."); - } - - return new VideoInfo(output); - } - - throw new ArgumentException($"Uri: {uri.AbsoluteUri}, does not point to a valid http(s) stream."); - } - - /// - /// Strips a video file of audio. - /// - /// Source video file. - /// Output video file. - /// - public VideoInfo Mute(VideoInfo source, FileInfo output) - { - FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); - FFMpegHelper.ConversionSizeExceptionCheck(source); - FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); - - var container = new ArgumentContainer( - new InputArgument(source), - new CopyArgument(), - new DisableChannelArgument(Channel.Audio), - new OutputArgument(output) - ); - - if (!RunProcess(container, output)) - { - throw new FFMpegException(FFMpegExceptionType.Operation, "Could not mute the requested video."); - } - - return new VideoInfo(output); - } - - /// - /// Saves audio from a specific video file to disk. - /// - /// Source video file. - /// Output audio file. - /// Success state. - public FileInfo ExtractAudio(VideoInfo source, FileInfo output) - { - FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); - FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp3); - - var container = new ArgumentContainer( - new InputArgument(source), - new DisableChannelArgument(Channel.Video), - new OutputArgument(output) - ); - - if (!RunProcess(container, output)) - { - throw new FFMpegException(FFMpegExceptionType.Operation, - "Could not extract the audio from the requested video."); - } - - output.Refresh(); - - return output; - } - - /// - /// Adds audio to a video file. - /// - /// Source video file. - /// Source audio file. - /// Output video file. - /// Indicates if the encoding should stop at the shortest input file. - /// Success state - public VideoInfo ReplaceAudio(VideoInfo source, FileInfo audio, FileInfo output, bool stopAtShortest = false) - { - FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); - FFMpegHelper.InputsExistExceptionCheck(audio); - FFMpegHelper.ConversionSizeExceptionCheck(source); - FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); - - var container = new ArgumentContainer( - new InputArgument(source.FullName, audio.FullName), - new CopyArgument(), - new AudioCodecArgument(AudioCodec.Aac, AudioQuality.Hd), - new ShortestArgument(stopAtShortest), - new OutputArgument(output) - ); - - if (!RunProcess(container, output)) - { - throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio."); - } - - return new VideoInfo(output); - } - - public VideoInfo Convert(ArgumentContainer arguments) - { - var output = ((OutputArgument) arguments[typeof(OutputArgument)]).GetAsFileInfo(); - var sources = ((InputArgument) arguments[typeof(InputArgument)]).GetAsVideoInfo(); - - // Sum duration of all sources - _totalTime = TimeSpan.Zero; - foreach (var source in sources) - _totalTime += source.Duration; - - if (!RunProcess(arguments, output)) - { - throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio."); - } - - _totalTime = TimeSpan.MinValue; - - return new VideoInfo(output); - } - - /// - /// Returns true if the associated process is still alive/running. - /// - public bool IsWorking => _instance.Started; - - /// - /// Stops any current job that FFMpeg is running. - /// - public void Stop() - { - if (IsWorking) - { - _instance.SendInput("q").Wait(); - } - } - - #region Private Members & Methods - - private readonly string _ffmpegPath; - private TimeSpan _totalTime; - - private volatile StringBuilder _errorOutput = new StringBuilder(); - - private bool RunProcess(ArgumentContainer container, FileInfo output) - { - _instance?.Dispose(); - var arguments = ArgumentBuilder.BuildArguments(container); - - _instance = new Instance(_ffmpegPath, arguments); - _instance.DataReceived += OutputData; - var exitCode = _instance.BlockUntilFinished(); - - if (!File.Exists(output.FullName) || new FileInfo(output.FullName).Length == 0) - { - throw new FFMpegException(FFMpegExceptionType.Process, _errorOutput); - } - - return exitCode == 0; - } - - private void Cleanup(IEnumerable pathList) - { - foreach (var path in pathList) - { - if (File.Exists(path)) - { - File.Delete(path); - } - } - } - - private static readonly Regex ProgressRegex = new Regex(@"\w\w:\w\w:\w\w", RegexOptions.Compiled); - private Instance _instance; - - private void OutputData(object sender, (DataType Type, string Data) msg) - { - var (type, data) = msg; - - if (data == null) return; - if (type == DataType.Error) - { - _errorOutput.AppendLine(data); - return; - } - -#if DEBUG - Trace.WriteLine(data); -#endif - - if (OnProgress == null) return; - if (!data.Contains("frame")) return; - - var match = ProgressRegex.Match(data); - if (!match.Success) return; - - var processed = TimeSpan.Parse(match.Value, CultureInfo.InvariantCulture); - var percentage = Math.Round(processed.TotalSeconds / _totalTime.TotalSeconds * 100, 2); - OnProgress(percentage); - } - - #endregion - } -} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/FFMpegOptions.cs b/FFMpegCore/FFMPEG/FFMpegOptions.cs deleted file mode 100644 index 23f7559..0000000 --- a/FFMpegCore/FFMPEG/FFMpegOptions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using FFMpegCore.FFMPEG.Exceptions; -using Newtonsoft.Json; -using System; -using System.IO; -using System.Runtime.InteropServices; -using Instances; - -namespace FFMpegCore.FFMPEG -{ - public class FFMpegOptions - { - private static readonly string ConfigFile = Path.Combine(".", "ffmpeg.config.json"); - private static readonly string DefaultRoot = Path.Combine(".", "FFMPEG", "bin"); - - public static FFMpegOptions Options { get; private set; } = new FFMpegOptions(); - - public static void Configure(FFMpegOptions options) - { - Options = options; - } - - static FFMpegOptions() - { - if (File.Exists(ConfigFile)) - { - Options = JsonConvert.DeserializeObject(File.ReadAllText(ConfigFile)); - } - } - - public string RootDirectory { get; set; } = DefaultRoot; - - public string FFmpegBinary => FFBinary("FFMpeg"); - - public string FFProbeBinary => FFBinary("FFProbe"); - - private static string FFBinary(string name) - { - var ffName = name.ToLowerInvariant(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - ffName += ".exe"; - - var target = Environment.Is64BitProcess ? "x64" : "x86"; - if (Directory.Exists(Path.Combine(Options.RootDirectory, target))) - { - ffName = Path.Combine(target, ffName); - } - - return Path.Combine(Options.RootDirectory, ffName); - } - } -} diff --git a/FFMpegCore/FFMPEG/FFMpegStreamMetadata.cs b/FFMpegCore/FFMPEG/FFMpegStreamMetadata.cs deleted file mode 100644 index 3a85b7a..0000000 --- a/FFMpegCore/FFMPEG/FFMpegStreamMetadata.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace FFMpegCore.FFMPEG -{ - internal class Stream - { - [JsonProperty("index")] - internal int Index { get; set; } - - [JsonProperty("codec_name")] - internal string CodecName { get; set; } - - [JsonProperty("bit_rate")] - internal string BitRate { get; set; } - - [JsonProperty("profile")] - internal string Profile { get; set; } - - [JsonProperty("codec_type")] - internal string CodecType { get; set; } - - [JsonProperty("width")] - internal int Width { get; set; } - - [JsonProperty("height")] - internal int Height { get; set; } - - [JsonProperty("duration")] - internal string Duration { get; set; } - - [JsonProperty("r_frame_rate")] - internal string FrameRate { get; set; } - - [JsonProperty("tags")] - internal Tags Tags { get; set; } - } - - internal class Tags - { - [JsonProperty("DURATION")] - internal string Duration { get; set; } - } - - internal class FFMpegStreamMetadata - { - [JsonProperty("streams")] - internal List Streams { get; set; } - } -} diff --git a/FFMpegCore/FFMPEG/FFProbe.cs b/FFMpegCore/FFMPEG/FFProbe.cs deleted file mode 100644 index 1329b3c..0000000 --- a/FFMpegCore/FFMPEG/FFProbe.cs +++ /dev/null @@ -1,135 +0,0 @@ -using FFMpegCore.FFMPEG.Exceptions; -using FFMpegCore.Helpers; -using Newtonsoft.Json; -using System; -using System.Globalization; -using System.Threading.Tasks; -using Instances; - -namespace FFMpegCore.FFMPEG -{ - public sealed class FFProbe - { - static readonly double BITS_TO_MB = 1024 * 1024 * 8; - private readonly string _ffprobePath; - - public FFProbe(): base() - { - FFProbeHelper.RootExceptionCheck(FFMpegOptions.Options.RootDirectory); - _ffprobePath = FFMpegOptions.Options.FFProbeBinary; - } - - /// - /// Probes the targeted video file and retrieves all available details. - /// - /// Source video file. - /// A video info object containing all details necessary. - public VideoInfo ParseVideoInfo(string source) - { - return ParseVideoInfo(new VideoInfo(source)); - } - /// - /// Probes the targeted video file asynchronously and retrieves all available details. - /// - /// Source video file. - /// A task for the video info object containing all details necessary. - public Task ParseVideoInfoAsync(string source) - { - return ParseVideoInfoAsync(new VideoInfo(source)); - } - - /// - /// Probes the targeted video file and retrieves all available details. - /// - /// Source video file. - /// A video info object containing all details necessary. - public VideoInfo ParseVideoInfo(VideoInfo info) - { - var instance = new Instance(_ffprobePath, BuildFFProbeArguments(info)); - instance.BlockUntilFinished(); - var output = string.Join("", instance.OutputData); - return ParseVideoInfoInternal(info, output); - } - /// - /// Probes the targeted video file asynchronously and retrieves all available details. - /// - /// Source video file. - /// A video info object containing all details necessary. - public async Task ParseVideoInfoAsync(VideoInfo info) - { - var instance = new Instance(_ffprobePath, BuildFFProbeArguments(info)); - await instance.FinishedRunning(); - var output = string.Join("", instance.OutputData); - return ParseVideoInfoInternal(info, output); - } - - private static string BuildFFProbeArguments(VideoInfo info) => - $"-v quiet -print_format json -show_streams \"{info.FullName}\""; - - private VideoInfo ParseVideoInfoInternal(VideoInfo info, string probeOutput) - { - var metadata = JsonConvert.DeserializeObject(probeOutput); - - if (metadata.Streams == null || metadata.Streams.Count == 0) - { - throw new FFMpegException(FFMpegExceptionType.File, $"No video or audio streams could be detected. Source: ${info.FullName}"); - } - - var video = metadata.Streams.Find(s => s.CodecType == "video"); - var audio = metadata.Streams.Find(s => s.CodecType == "audio"); - - double videoSize = 0d; - double audioSize = 0d; - - string sDuration = (video ?? audio).Duration; - TimeSpan duration = TimeSpan.Zero; - if (sDuration != null) - { - duration = TimeSpan.FromSeconds(double.TryParse(sDuration, NumberStyles.Any, CultureInfo.InvariantCulture, out var output) ? output : 0); - } - else - { - sDuration = (video ?? audio).Tags.Duration; - if (sDuration != null) - TimeSpan.TryParse(sDuration.Remove(sDuration.LastIndexOf('.') + 8), CultureInfo.InvariantCulture, out duration); // TimeSpan fractions only allow up to 7 digits - } - info.Duration = duration; - - if (video != null) - { - var bitRate = Convert.ToDouble(video.BitRate, CultureInfo.InvariantCulture); - var fr = video.FrameRate.Split('/'); - var commonDenominator = FFProbeHelper.Gcd(video.Width, video.Height); - - videoSize = bitRate * duration.TotalSeconds / BITS_TO_MB; - - info.VideoFormat = video.CodecName; - info.Width = video.Width; - info.Height = video.Height; - info.FrameRate = Math.Round( - Convert.ToDouble(fr[0], CultureInfo.InvariantCulture) / - Convert.ToDouble(fr[1], CultureInfo.InvariantCulture), - 3); - info.Ratio = video.Width / commonDenominator + ":" + video.Height / commonDenominator; - } else - { - info.VideoFormat = "none"; - } - - if (audio != null) - { - var bitRate = Convert.ToDouble(audio.BitRate, CultureInfo.InvariantCulture); - info.AudioFormat = audio.CodecName; - audioSize = bitRate * duration.TotalSeconds / BITS_TO_MB; - } else - { - info.AudioFormat = "none"; - - } - - info.Size = Math.Round(videoSize + audioSize, 2); - - return info; - } - } -} diff --git a/FFMpegCore/FFMPEG/ProbeInfo.cs b/FFMpegCore/FFMPEG/ProbeInfo.cs deleted file mode 100644 index e69de29..0000000 diff --git a/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs new file mode 100644 index 0000000..9c7e813 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs @@ -0,0 +1,20 @@ +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents parameter of audio codec and it's quality + /// + public class AudioBitrateArgument : IArgument + { + public readonly int Bitrate; + public AudioBitrateArgument(AudioQuality value) : this((int)value) { } + public AudioBitrateArgument(int bitrate) + { + Bitrate = bitrate; + } + + + public string Text => $"-b:a {Bitrate}k"; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs new file mode 100644 index 0000000..273bb02 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs @@ -0,0 +1,28 @@ +using FFMpegCore.Enums; +using FFMpegCore.Exceptions; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents parameter of audio codec and it's quality + /// + public class AudioCodecArgument : IArgument + { + public readonly string AudioCodec; + + public AudioCodecArgument(Codec audioCodec) + { + if (audioCodec.Type != CodecType.Audio) + throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{audioCodec.Name}\" is not an audio codec"); + + AudioCodec = audioCodec.Name; + } + + public AudioCodecArgument(string audioCodec) + { + AudioCodec = audioCodec; + } + + public string Text => $"-c:a {AudioCodec.ToString().ToLowerInvariant()}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/AudioSamplingRateArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioSamplingRateArgument.cs new file mode 100644 index 0000000..6f1365d --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/AudioSamplingRateArgument.cs @@ -0,0 +1,16 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Audio sampling rate argument. Defaults to 48000 (Hz) + /// + public class AudioSamplingRateArgument : IArgument + { + public readonly int SamplingRate; + public AudioSamplingRateArgument(int samplingRate = 48000) + { + SamplingRate = samplingRate; + } + + public string Text => $"-ar {SamplingRate}"; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/BitStreamFilterArgument.cs b/FFMpegCore/FFMpeg/Arguments/BitStreamFilterArgument.cs new file mode 100644 index 0000000..e5a4b35 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/BitStreamFilterArgument.cs @@ -0,0 +1,26 @@ +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents parameter of bitstream filter + /// + public class BitStreamFilterArgument : IArgument + { + public readonly Channel Channel; + public readonly Filter Filter; + + public BitStreamFilterArgument(Channel channel, Filter filter) + { + Channel = channel; + Filter = filter; + } + + public string Text => Channel switch + { + Channel.Audio => $"-bsf:a {Filter.ToString().ToLowerInvariant()}", + Channel.Video => $"-bsf:v {Filter.ToString().ToLowerInvariant()}", + _ => string.Empty + }; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs new file mode 100644 index 0000000..9c6ffa2 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Arguments +{ + + /// + /// Represents parameter of concat argument + /// Used for creating video from multiple images or videos + /// + public class ConcatArgument : IInputArgument + { + public readonly IEnumerable Values; + public ConcatArgument(IEnumerable values) + { + Values = values; + } + + public void Pre() { } + 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/ConstantRateFactorArgument.cs b/FFMpegCore/FFMpeg/Arguments/ConstantRateFactorArgument.cs new file mode 100644 index 0000000..c02cfa3 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/ConstantRateFactorArgument.cs @@ -0,0 +1,24 @@ +using System; + +namespace FFMpegCore.Arguments +{ + /// + /// Constant Rate Factor (CRF) argument + /// + public class ConstantRateFactorArgument : IArgument + { + public readonly int Crf; + + public ConstantRateFactorArgument(int crf) + { + if (crf < 0 || crf > 63) + { + throw new ArgumentException("Argument is outside range (0 - 63)", nameof(crf)); + } + + Crf = crf; + } + + public string Text => $"-crf {Crf}"; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs b/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs new file mode 100644 index 0000000..91419d5 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs @@ -0,0 +1,24 @@ +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents parameter of copy parameter + /// Defines if channel (audio, video or both) should be copied to output file + /// + public class CopyArgument : IArgument + { + public readonly Channel Channel; + public CopyArgument(Channel channel = Channel.Both) + { + Channel = channel; + } + + public string Text => Channel switch + { + Channel.Audio => "-c:a copy", + Channel.Video => "-c:v copy", + _ => "-c copy" + }; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs b/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs new file mode 100644 index 0000000..8eedb12 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs @@ -0,0 +1,14 @@ +namespace FFMpegCore.Arguments +{ + public class CustomArgument : IArgument + { + public readonly string Argument; + + public CustomArgument(string argument) + { + Argument = argument; + } + + public string Text => Argument ?? string.Empty; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs new file mode 100644 index 0000000..5651802 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents parameter of concat argument + /// Used for creating video from multiple images or videos + /// + public class DemuxConcatArgument : IInputArgument + { + public readonly IEnumerable Values; + public DemuxConcatArgument(IEnumerable values) + { + Values = values.Select(value => $"file '{value}'"); + } + private readonly string _tempFileName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid() + ".txt"); + + public void Pre() => File.WriteAllLines(_tempFileName, Values); + 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/DisableChannelArgument.cs b/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs new file mode 100644 index 0000000..d683775 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs @@ -0,0 +1,27 @@ +using FFMpegCore.Enums; +using FFMpegCore.Exceptions; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents cpu speed parameter + /// + public class DisableChannelArgument : IArgument + { + public readonly Channel Channel; + + public DisableChannelArgument(Channel channel) + { + if (channel == Channel.Both) + throw new FFMpegException(FFMpegExceptionType.Conversion, "Cannot disable both channels"); + Channel = channel; + } + + public string Text => Channel switch + { + Channel.Video => "-vn", + Channel.Audio => "-an", + _ => string.Empty + }; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs new file mode 100644 index 0000000..d4eabb8 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FFMpegCore.Arguments +{ + /// + /// Drawtext video filter argument + /// + public class DrawTextArgument : IArgument + { + public readonly DrawTextOptions Options; + + public DrawTextArgument(DrawTextOptions options) + { + Options = options; + } + + public string Text => $"-vf drawtext=\"{Options.TextInternal}\""; + } + + public class DrawTextOptions + { + public readonly string Text; + public readonly string Font; + public readonly List<(string key, string value)> Parameters; + + public static DrawTextOptions Create(string text, string font) + { + return new DrawTextOptions(text, font, new List<(string, string)>()); + } + public static DrawTextOptions Create(string text, string font, params (string key, string value)[] parameters) + { + return new DrawTextOptions(text, font, parameters); + } + + internal string TextInternal => string.Join(":", new[] {("text", Text), ("fontfile", Font)}.Concat(Parameters).Select(FormatArgumentPair)); + + private static string FormatArgumentPair((string key, string value) pair) + { + return $"{pair.key}={EncloseIfContainsSpace(pair.value)}"; + } + + private static string EncloseIfContainsSpace(string input) + { + return input.Contains(" ") ? $"'{input}'" : input; + } + + private DrawTextOptions(string text, string font, IEnumerable<(string, string)> parameters) + { + Text = text; + Font = font; + Parameters = parameters.ToList(); + } + + public DrawTextOptions WithParameter(string key, string value) + { + Parameters.Add((key, value)); + return this; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs b/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs new file mode 100644 index 0000000..e47b966 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs @@ -0,0 +1,18 @@ +using System; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents duration parameter + /// + public class DurationArgument : IArgument + { + public readonly TimeSpan? Duration; + public DurationArgument(TimeSpan? duration) + { + Duration = duration; + } + + public string Text => !Duration.HasValue ? string.Empty : $"-t {Duration.Value}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs b/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs new file mode 100644 index 0000000..54cdd6f --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs @@ -0,0 +1,10 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Faststart argument - for moving moov atom to the start of file + /// + public class FaststartArgument : IArgument + { + public string Text => "-movflags faststart"; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/ForceFormatArgument.cs b/FFMpegCore/FFMpeg/Arguments/ForceFormatArgument.cs new file mode 100644 index 0000000..9524698 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/ForceFormatArgument.cs @@ -0,0 +1,23 @@ +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents force format parameter + /// + public class ForceFormatArgument : IArgument + { + private readonly string _format; + public ForceFormatArgument(string format) + { + _format = format; + } + + public ForceFormatArgument(ContainerFormat format) + { + _format = format.Name; + } + + public string Text => $"-f {_format}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs b/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs new file mode 100644 index 0000000..8402552 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs @@ -0,0 +1,17 @@ +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + public class ForcePixelFormat : IArgument + { + public string PixelFormat { get; } + public string Text => $"-pix_fmt {PixelFormat}"; + + public ForcePixelFormat(string format) + { + PixelFormat = format; + } + + public ForcePixelFormat(PixelFormat format) : this(format.Name) { } + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/FrameOutputCountArgument.cs b/FFMpegCore/FFMpeg/Arguments/FrameOutputCountArgument.cs new file mode 100644 index 0000000..08bc56b --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/FrameOutputCountArgument.cs @@ -0,0 +1,16 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Represents frame output count parameter + /// + public class FrameOutputCountArgument : IArgument + { + public readonly int Frames; + public FrameOutputCountArgument(int frames) + { + Frames = frames; + } + + public string Text => $"-vframes {Frames}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs b/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs new file mode 100644 index 0000000..7c921af --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs @@ -0,0 +1,17 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Represents frame rate parameter + /// + public class FrameRateArgument : IArgument + { + public readonly double Framerate; + + public FrameRateArgument(double framerate) + { + Framerate = 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/IArgument.cs b/FFMpegCore/FFMpeg/Arguments/IArgument.cs new file mode 100644 index 0000000..2a6c11a --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/IArgument.cs @@ -0,0 +1,10 @@ +namespace FFMpegCore.Arguments +{ + public interface IArgument + { + /// + /// The textual representation of the argument + /// + string Text { get; } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs new file mode 100644 index 0000000..81a1cbe --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs @@ -0,0 +1,6 @@ +namespace FFMpegCore.Arguments +{ + public interface IInputArgument : IInputOutputArgument + { + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs new file mode 100644 index 0000000..99def82 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Arguments +{ + public interface IInputOutputArgument : IArgument + { + void Pre(); + Task During(CancellationToken cancellationToken = default); + void Post(); + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs new file mode 100644 index 0000000..09ccc83 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs @@ -0,0 +1,6 @@ +namespace FFMpegCore.Arguments +{ + public interface IOutputArgument : IInputOutputArgument + { + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/InputArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputArgument.cs new file mode 100644 index 0000000..68c34b4 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/InputArgument.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents input parameter + /// + public class InputArgument : IInputArgument + { + public readonly bool VerifyExists; + public readonly string FilePath; + + public InputArgument(bool verifyExists, string filePaths) + { + VerifyExists = verifyExists; + FilePath = filePaths; + } + + public InputArgument(string path, bool verifyExists) : this(verifyExists, path) { } + + public void Pre() + { + if (VerifyExists && !File.Exists(FilePath)) + throw new FileNotFoundException("Input file not found", FilePath); + } + + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; + public void Post() { } + + public string Text => $"-i \"{FilePath}\""; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs new file mode 100644 index 0000000..17d0372 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs @@ -0,0 +1,30 @@ +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; +using FFMpegCore.Pipes; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents input parameter for a named pipe + /// + public class InputPipeArgument : PipeArgument, IInputArgument + { + public readonly IPipeSource Writer; + + public InputPipeArgument(IPipeSource writer) : base(PipeDirection.Out) + { + Writer = writer; + } + + public override string Text => $"-y {Writer.GetFormat()} -i \"{PipePath}\""; + + protected override async Task ProcessDataAsync(CancellationToken token) + { + await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false); + if (!Pipe.IsConnected) + throw new TaskCanceledException(); + await Writer.CopyAsync(Pipe, token).ConfigureAwait(false); + } + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/LoopArgument.cs b/FFMpegCore/FFMpeg/Arguments/LoopArgument.cs new file mode 100644 index 0000000..26adc3e --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/LoopArgument.cs @@ -0,0 +1,16 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Represents loop parameter + /// + public class LoopArgument : IArgument + { + public readonly int Times; + public LoopArgument(int times) + { + Times = times; + } + + public string Text => $"-loop {Times}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs new file mode 100644 index 0000000..c2aad38 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FFMpegCore.Exceptions; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents output parameter + /// + public class OutputArgument : IOutputArgument + { + public readonly string Path; + public readonly bool Overwrite; + + public OutputArgument(string path, bool overwrite = true) + { + Path = path; + Overwrite = overwrite; + } + + 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 = default) => Task.CompletedTask; + public void Post() + { + } + + public OutputArgument(FileInfo value) : this(value.FullName) { } + + public OutputArgument(Uri value) : this(value.AbsolutePath) { } + + public string Text => $"\"{Path}\"{(Overwrite ? " -y" : string.Empty)}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs new file mode 100644 index 0000000..ebf1e7f --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs @@ -0,0 +1,27 @@ +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; +using FFMpegCore.Pipes; + +namespace FFMpegCore.Arguments +{ + public class OutputPipeArgument : PipeArgument, IOutputArgument + { + public readonly IPipeSink Reader; + + public OutputPipeArgument(IPipeSink reader) : base(PipeDirection.In) + { + Reader = reader; + } + + public override string Text => $"\"{PipePath}\" -y"; + + protected override async Task ProcessDataAsync(CancellationToken token) + { + await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false); + if (!Pipe.IsConnected) + throw new TaskCanceledException(); + await Reader.CopyAsync(Pipe, token).ConfigureAwait(false); + } + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/OverwriteArgument.cs b/FFMpegCore/FFMpeg/Arguments/OverwriteArgument.cs new file mode 100644 index 0000000..3a633af --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/OverwriteArgument.cs @@ -0,0 +1,11 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Represents overwrite parameter + /// If output file should be overwritten if exists + /// + public class OverwriteArgument : IArgument + { + public string Text => "-y"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs new file mode 100644 index 0000000..4a6113a --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -0,0 +1,52 @@ +using System; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; +using FFMpegCore.Pipes; + +namespace FFMpegCore.Arguments +{ + public abstract class PipeArgument + { + private string PipeName { get; } + public string PipePath => PipeHelpers.GetPipePath(PipeName); + + protected NamedPipeServerStream Pipe { get; private set; } = null!; + private readonly PipeDirection _direction; + + protected PipeArgument(PipeDirection direction) + { + PipeName = PipeHelpers.GetUnqiuePipeName(); + _direction = direction; + } + + public void Pre() + { + if (Pipe != null) + throw new InvalidOperationException("Pipe already has been opened"); + + Pipe = new NamedPipeServerStream(PipeName, _direction, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + } + + public void Post() + { + Pipe?.Dispose(); + Pipe = null!; + } + + public async Task During(CancellationToken cancellationToken = default) + { + try + { + await ProcessDataAsync(cancellationToken); + } + catch (TaskCanceledException) + { + } + Pipe.Disconnect(); + } + + protected abstract Task ProcessDataAsync(CancellationToken token); + public abstract string Text { get; } + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs b/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs new file mode 100644 index 0000000..29cdac6 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs @@ -0,0 +1,10 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Remove metadata argument + /// + public class RemoveMetadataArgument : IArgument + { + public string Text => "-map_metadata -1"; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs b/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs new file mode 100644 index 0000000..40e98d0 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs @@ -0,0 +1,26 @@ +using System.Drawing; +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents scale parameter + /// + public class ScaleArgument : IArgument + { + public readonly Size? Size; + public ScaleArgument(Size? size) + { + Size = size; + } + + public ScaleArgument(int width, int height) : this(new Size(width, height)) { } + + public ScaleArgument(VideoSize videosize) + { + Size = videosize == VideoSize.Original ? new Size(-1, -1) : new Size(-1, (int)videosize); + } + + public virtual string Text => Size.HasValue ? $"-vf scale={Size.Value.Width}:{Size.Value.Height}" : string.Empty; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs new file mode 100644 index 0000000..1057b88 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs @@ -0,0 +1,18 @@ +using System; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents seek parameter + /// + public class SeekArgument : IArgument + { + public readonly TimeSpan? SeekTo; + public SeekArgument(TimeSpan? seekTo) + { + SeekTo = seekTo; + } + + public string Text => !SeekTo.HasValue ? string.Empty : $"-ss {SeekTo.Value}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/ShortestArgument.cs b/FFMpegCore/FFMpeg/Arguments/ShortestArgument.cs new file mode 100644 index 0000000..d85813e --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/ShortestArgument.cs @@ -0,0 +1,17 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Represents shortest parameter + /// + public class ShortestArgument : IArgument + { + public readonly bool Shortest; + + public ShortestArgument(bool shortest) + { + Shortest = shortest; + } + + public string Text => Shortest ? "-shortest" : string.Empty; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs new file mode 100644 index 0000000..2ccde92 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs @@ -0,0 +1,19 @@ +using System.Drawing; +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents size parameter + /// + public class SizeArgument : ScaleArgument + { + public SizeArgument(Size? value) : base(value) { } + + public SizeArgument(VideoSize videosize) : base(videosize) { } + + public SizeArgument(int width, int height) : base(width, height) { } + + public override string Text => Size.HasValue ? $"-s {Size.Value.Width}x{Size.Value.Height}" : string.Empty; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/SpeedPresetArgument.cs b/FFMpegCore/FFMpeg/Arguments/SpeedPresetArgument.cs new file mode 100644 index 0000000..6046c3c --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SpeedPresetArgument.cs @@ -0,0 +1,19 @@ +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents speed parameter + /// + public class SpeedPresetArgument : IArgument + { + public readonly Speed Speed; + + public SpeedPresetArgument(Speed speed) + { + Speed = speed; + } + + public string Text => $"-preset {Speed.ToString().ToLowerInvariant()}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs b/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs new file mode 100644 index 0000000..f7c09da --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs @@ -0,0 +1,17 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Represents start number parameter + /// + public class StartNumberArgument : IArgument + { + public readonly int StartNumber; + + public StartNumberArgument(int startNumber) + { + StartNumber = startNumber; + } + + public string Text => $"-start_number {StartNumber}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs b/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs new file mode 100644 index 0000000..6fd94e6 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs @@ -0,0 +1,21 @@ +using System; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents threads parameter + /// Number of threads used for video encoding + /// + public class ThreadsArgument : IArgument + { + public readonly int Threads; + public ThreadsArgument(int threads) + { + Threads = threads; + } + + public ThreadsArgument(bool isMultiThreaded) : this(isMultiThreaded ? Environment.ProcessorCount : 1) { } + + public string Text => $"-threads {Threads}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs new file mode 100644 index 0000000..acc26f4 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs @@ -0,0 +1,22 @@ +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments +{ + /// + /// Transpose argument. + /// 0 = 90CounterCLockwise and Vertical Flip (default) + /// 1 = 90Clockwise + /// 2 = 90CounterClockwise + /// 3 = 90Clockwise and Vertical Flip + /// + public class TransposeArgument : IArgument + { + public readonly Transposition Transposition; + public TransposeArgument(Transposition transposition) + { + Transposition = transposition; + } + + public string Text => $"-vf \"transpose={(int)Transposition}\""; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs b/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs new file mode 100644 index 0000000..b656ec4 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs @@ -0,0 +1,24 @@ +using System; + +namespace FFMpegCore.Arguments +{ + /// + /// Variable Bitrate Argument (VBR) argument + /// + public class VariableBitRateArgument : IArgument + { + public readonly int Vbr; + + public VariableBitRateArgument(int vbr) + { + if (vbr < 0 || vbr > 5) + { + throw new ArgumentException("Argument is outside range (0 - 5)", nameof(vbr)); + } + + Vbr = vbr; + } + + public string Text => $"-vbr {Vbr}"; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs b/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs new file mode 100644 index 0000000..f128aeb --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs @@ -0,0 +1,25 @@ +namespace FFMpegCore.Arguments +{ + public class VerbosityLevelArgument : IArgument + { + private readonly VerbosityLevel _verbosityLevel; + + public VerbosityLevelArgument(VerbosityLevel verbosityLevel) + { + _verbosityLevel = verbosityLevel; + } + public string Text => $"{((int)_verbosityLevel < 32 ? "-hide_banner " : "")}-loglevel {_verbosityLevel.ToString().ToLowerInvariant()}"; + } + + public enum VerbosityLevel + { + Quiet = -8, + Fatal = 8, + Error = 16, + Warning = 24, + Info = 32, + Verbose = 40, + Debug = 48, + Trace = 56 + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs new file mode 100644 index 0000000..ea5e641 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs @@ -0,0 +1,17 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Represents video bitrate parameter + /// + public class VideoBitrateArgument : IArgument + { + public readonly int Bitrate; + + public VideoBitrateArgument(int bitrate) + { + Bitrate = bitrate; + } + + public string Text => $"-b:v {Bitrate}k"; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs new file mode 100644 index 0000000..9386822 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs @@ -0,0 +1,28 @@ +using FFMpegCore.Enums; +using FFMpegCore.Exceptions; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents video codec parameter + /// + public class VideoCodecArgument : IArgument + { + public readonly string Codec; + + public VideoCodecArgument(string codec) + { + Codec = codec; + } + + public VideoCodecArgument(Codec value) + { + if (value.Type != CodecType.Video) + throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{value.Name}\" is not a video codec"); + + Codec = value.Name; + } + + public string Text => $"-c:v {Codec}"; + } +} diff --git a/FFMpegCore/FFMPEG/Enums/AudioQuality.cs b/FFMpegCore/FFMpeg/Enums/AudioQuality.cs similarity index 50% rename from FFMpegCore/FFMPEG/Enums/AudioQuality.cs rename to FFMpegCore/FFMpeg/Enums/AudioQuality.cs index d2b9465..60ba0eb 100644 --- a/FFMpegCore/FFMPEG/Enums/AudioQuality.cs +++ b/FFMpegCore/FFMpeg/Enums/AudioQuality.cs @@ -1,10 +1,12 @@ -namespace FFMpegCore.FFMPEG.Enums +namespace FFMpegCore.Enums { public enum AudioQuality { Ultra = 384, - Hd = 192, + VeryHigh = 256, + Good = 192, Normal = 128, + BelowNormal = 96, Low = 64 } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Enums/Codec.cs b/FFMpegCore/FFMpeg/Enums/Codec.cs new file mode 100644 index 0000000..8ac8456 --- /dev/null +++ b/FFMpegCore/FFMpeg/Enums/Codec.cs @@ -0,0 +1,152 @@ +using FFMpegCore.Exceptions; +using System; +using System.Text.RegularExpressions; + +namespace FFMpegCore.Enums +{ + public enum FeatureStatus + { + Unknown, + NotSupported, + Supported, + } + + public class Codec + { + private static readonly Regex _codecsFormatRegex = new Regex(@"([D\.])([E\.])([VASD\.])([I\.])([L\.])([S\.])\s+([a-z0-9_-]+)\s+(.+)"); + private static readonly Regex _decodersEncodersFormatRegex = new Regex(@"([VASD\.])([F\.])([S\.])([X\.])([B\.])([D\.])\s+([a-z0-9_-]+)\s+(.+)"); + + public class FeatureLevel + { + public bool IsExperimental { get; internal set; } + public FeatureStatus SupportsFrameLevelMultithreading { get; internal set; } + public FeatureStatus SupportsSliceLevelMultithreading { get; internal set; } + public FeatureStatus SupportsDrawHorizBand { get; internal set; } + public FeatureStatus SupportsDirectRendering { get; internal set; } + + internal void Merge(FeatureLevel other) + { + IsExperimental |= other.IsExperimental; + SupportsFrameLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsFrameLevelMultithreading, (int)other.SupportsFrameLevelMultithreading); + SupportsSliceLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsSliceLevelMultithreading, (int)other.SupportsSliceLevelMultithreading); + SupportsDrawHorizBand = (FeatureStatus)Math.Max((int)SupportsDrawHorizBand, (int)other.SupportsDrawHorizBand); + SupportsDirectRendering = (FeatureStatus)Math.Max((int)SupportsDirectRendering, (int)other.SupportsDirectRendering); + } + } + + public string Name { get; private set; } + public CodecType Type { get; private set; } + public bool DecodingSupported { get; private set; } + public bool EncodingSupported { get; private set; } + public bool IsIntraFrameOnly { get; private set; } + public bool IsLossy { get; private set; } + public bool IsLossless { get; private set; } + public string Description { get; private set; } = null!; + + public FeatureLevel EncoderFeatureLevel { get; private set; } + public FeatureLevel DecoderFeatureLevel { get; private set; } + + internal Codec(string name, CodecType type) + { + EncoderFeatureLevel = new FeatureLevel(); + DecoderFeatureLevel = new FeatureLevel(); + Name = name; + Type = type; + } + + internal static bool TryParseFromCodecs(string line, out Codec codec) + { + var match = _codecsFormatRegex.Match(line); + if (!match.Success) + { + codec = null!; + return false; + } + + var name = match.Groups[7].Value; + var type = match.Groups[3].Value switch + { + "V" => CodecType.Video, + "A" => CodecType.Audio, + "D" => CodecType.Data, + "S" => CodecType.Subtitle, + _ => CodecType.Unknown + }; + + if(type == CodecType.Unknown) + { + codec = null!; + return false; + } + + codec = new Codec(name, type); + + codec.DecodingSupported = match.Groups[1].Value != "."; + codec.EncodingSupported = match.Groups[2].Value != "."; + codec.IsIntraFrameOnly = match.Groups[4].Value != "."; + codec.IsLossy = match.Groups[5].Value != "."; + codec.IsLossless = match.Groups[6].Value != "."; + codec.Description = match.Groups[8].Value; + + return true; + } + internal static bool TryParseFromEncodersDecoders(string line, out Codec codec, bool isEncoder) + { + var match = _decodersEncodersFormatRegex.Match(line); + if (!match.Success) + { + codec = null!; + return false; + } + + var name = match.Groups[7].Value; + var type = match.Groups[1].Value switch + { + "V" => CodecType.Video, + "A" => CodecType.Audio, + "D" => CodecType.Data, + "S" => CodecType.Subtitle, + _ => CodecType.Unknown + }; + + if (type == CodecType.Unknown) + { + codec = null!; + return false; + } + + codec = new Codec(name, type); + + var featureLevel = isEncoder ? codec.EncoderFeatureLevel : codec.DecoderFeatureLevel; + + codec.DecodingSupported = !isEncoder; + codec.EncodingSupported = isEncoder; + featureLevel.SupportsFrameLevelMultithreading = match.Groups[2].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; + featureLevel.SupportsSliceLevelMultithreading = match.Groups[3].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; + featureLevel.IsExperimental = match.Groups[4].Value != "."; + featureLevel.SupportsDrawHorizBand = match.Groups[5].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; + featureLevel.SupportsDirectRendering = match.Groups[6].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; + codec.Description = match.Groups[8].Value; + + return true; + } + internal void Merge(Codec other) + { + if (Name != other.Name) + throw new FFMpegException(FFMpegExceptionType.Operation, "different codecs enable to merge"); + + Type |= other.Type; + DecodingSupported |= other.DecodingSupported; + EncodingSupported |= other.EncodingSupported; + IsIntraFrameOnly |= other.IsIntraFrameOnly; + IsLossy |= other.IsLossy; + IsLossless |= other.IsLossless; + + EncoderFeatureLevel.Merge(other.EncoderFeatureLevel); + DecoderFeatureLevel.Merge(other.DecoderFeatureLevel); + + if (Description != other.Description) + Description += "\r\n" + other.Description; + } + } +} diff --git a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs new file mode 100644 index 0000000..8c046ac --- /dev/null +++ b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs @@ -0,0 +1,44 @@ +using System.Text.RegularExpressions; + +namespace FFMpegCore.Enums +{ + public class ContainerFormat + { + private static readonly Regex _formatRegex = new Regex(@"([D ])([E ])\s+([a-z0-9_]+)\s+(.+)"); + + public string Name { get; private set; } + public bool DemuxingSupported { get; private set; } + public bool MuxingSupported { get; private set; } + public string Description { get; private set; } = null!; + public string Extension + { + get + { + if (FFMpegOptions.Options.ExtensionOverrides.ContainsKey(Name)) + return FFMpegOptions.Options.ExtensionOverrides[Name]; + return "." + Name; + } + } + + internal ContainerFormat(string name) + { + Name = name; + } + + internal static bool TryParse(string line, out ContainerFormat fmt) + { + var match = _formatRegex.Match(line); + if (!match.Success) + { + fmt = null!; + return false; + } + + fmt = new ContainerFormat(match.Groups[3].Value); + fmt.DemuxingSupported = match.Groups[1].Value == " "; + fmt.MuxingSupported = match.Groups[2].Value == " "; + fmt.Description = match.Groups[4].Value; + return true; + } + } +} diff --git a/FFMpegCore/FFMpeg/Enums/Enums.cs b/FFMpegCore/FFMpeg/Enums/Enums.cs new file mode 100644 index 0000000..31a5f1e --- /dev/null +++ b/FFMpegCore/FFMpeg/Enums/Enums.cs @@ -0,0 +1,54 @@ +namespace FFMpegCore.Enums +{ + public enum CodecType + { + Unknown = 0, + Video = 1 << 1, + Audio = 1 << 2, + Subtitle = 1 << 3, + Data = 1 << 4, + } + + public static class VideoCodec + { + public static Codec LibX264 => FFMpeg.GetCodec("libx264"); + public static Codec LibVpx => FFMpeg.GetCodec("libvpx"); + public static Codec LibTheora => FFMpeg.GetCodec("libtheora"); + public static Codec Png => FFMpeg.GetCodec("png"); + public static Codec MpegTs => FFMpeg.GetCodec("mpegts"); + } + + public static class AudioCodec + { + public static Codec Aac => FFMpeg.GetCodec("aac"); + public static Codec LibVorbis => FFMpeg.GetCodec("libvorbis"); + public static Codec LibFdk_Aac => FFMpeg.GetCodec("libfdk_aac"); + public static Codec Ac3 => FFMpeg.GetCodec("ac3"); + public static Codec Eac3 => FFMpeg.GetCodec("eac3"); + public static Codec LibMp3Lame => FFMpeg.GetCodec("libmp3lame"); + } + + public static class VideoType + { + public static ContainerFormat MpegTs => FFMpeg.GetContainerFormat("mpegts"); + public static ContainerFormat Ts => FFMpeg.GetContainerFormat("mpegts"); + public static ContainerFormat Mp4 => FFMpeg.GetContainerFormat("mp4"); + public static ContainerFormat Mov => FFMpeg.GetContainerFormat("mov"); + public static ContainerFormat Avi => FFMpeg.GetContainerFormat("avi"); + public static ContainerFormat Ogv => FFMpeg.GetContainerFormat("ogv"); + public static ContainerFormat WebM => FFMpeg.GetContainerFormat("webm"); + } + + public enum Filter + { + H264_Mp4ToAnnexB, + Aac_AdtstoAsc + } + + public enum Channel + { + Audio, + Video, + Both + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Enums/FileExtension.cs b/FFMpegCore/FFMpeg/Enums/FileExtension.cs new file mode 100644 index 0000000..d45faf6 --- /dev/null +++ b/FFMpegCore/FFMpeg/Enums/FileExtension.cs @@ -0,0 +1,26 @@ +using System; + +namespace FFMpegCore.Enums +{ + public static class FileExtension + { + public static string Extension(this Codec type) + { + return type.Name switch + { + "libx264" => Mp4, + "libxvpx" => WebM, + "libxtheora" => Ogv, + "mpegts" => Ts, + "png" => Png, + _ => throw new Exception("The extension for this video type is not defined.") + }; + } + public static readonly string Mp4 = VideoType.Mp4.Extension; + public static readonly string Ts = VideoType.MpegTs.Extension; + public static readonly string Ogv = VideoType.Ogv.Extension; + public static readonly string WebM = VideoType.WebM.Extension; + public static readonly string Png = ".png"; + public static readonly string Mp3 = ".mp3"; + } +} 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/Enums/PixelFormat.cs b/FFMpegCore/FFMpeg/Enums/PixelFormat.cs new file mode 100644 index 0000000..9808e43 --- /dev/null +++ b/FFMpegCore/FFMpeg/Enums/PixelFormat.cs @@ -0,0 +1,53 @@ +using System.Text.RegularExpressions; + +namespace FFMpegCore.Enums +{ + public class PixelFormat + { + private static readonly Regex _formatRegex = new Regex(@"([I\.])([O\.])([H\.])([P\.])([B\.])\s+(\S+)\s+([0-9]+)\s+([0-9]+)"); + + public bool InputConversionSupported { get; private set; } + public bool OutputConversionSupported { get; private set; } + public bool HardwareAccelerationSupported { get; private set; } + public bool IsPaletted { get; private set; } + public bool IsBitstream { get; private set; } + public string Name { get; private set; } + public int Components { get; private set; } + public int BitsPerPixel { get; private set; } + + public bool CanConvertTo(PixelFormat other) + { + return InputConversionSupported && other.OutputConversionSupported; + } + + internal PixelFormat(string name) + { + Name = name; + } + + internal static bool TryParse(string line, out PixelFormat fmt) + { + var match = _formatRegex.Match(line); + if (!match.Success) + { + fmt = null!; + return false; + } + + fmt = new PixelFormat(match.Groups[6].Value); + fmt.InputConversionSupported = match.Groups[1].Value != "."; + fmt.OutputConversionSupported = match.Groups[2].Value != "."; + fmt.HardwareAccelerationSupported = match.Groups[3].Value != "."; + fmt.IsPaletted = match.Groups[4].Value != "."; + fmt.IsBitstream = match.Groups[5].Value != "."; + if (!int.TryParse(match.Groups[7].Value, out var nbComponents)) + return false; + fmt.Components = nbComponents; + if (!int.TryParse(match.Groups[8].Value, out var bpp)) + return false; + fmt.BitsPerPixel = bpp; + + return true; + } + } +} diff --git a/FFMpegCore/FFMPEG/Enums/Speed.cs b/FFMpegCore/FFMpeg/Enums/Speed.cs similarity index 83% rename from FFMpegCore/FFMPEG/Enums/Speed.cs rename to FFMpegCore/FFMpeg/Enums/Speed.cs index 089ed9c..52272f0 100644 --- a/FFMpegCore/FFMPEG/Enums/Speed.cs +++ b/FFMpegCore/FFMpeg/Enums/Speed.cs @@ -1,4 +1,4 @@ -namespace FFMpegCore.FFMPEG.Enums +namespace FFMpegCore.Enums { public enum Speed { diff --git a/FFMpegCore/FFMpeg/Enums/Transposition.cs b/FFMpegCore/FFMpeg/Enums/Transposition.cs new file mode 100644 index 0000000..bacfccc --- /dev/null +++ b/FFMpegCore/FFMpeg/Enums/Transposition.cs @@ -0,0 +1,10 @@ +namespace FFMpegCore.Enums +{ + public enum Transposition + { + CounterClockwise90VerticalFlip = 0, + Clockwise90 = 1, + CounterClockwise90 = 2, + Clockwise90VerticalFlip = 3 + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Enums/VideoSize.cs b/FFMpegCore/FFMpeg/Enums/VideoSize.cs similarity index 68% rename from FFMpegCore/FFMPEG/Enums/VideoSize.cs rename to FFMpegCore/FFMpeg/Enums/VideoSize.cs index 396d349..d774b95 100644 --- a/FFMpegCore/FFMPEG/Enums/VideoSize.cs +++ b/FFMpegCore/FFMpeg/Enums/VideoSize.cs @@ -1,11 +1,11 @@ -namespace FFMpegCore.FFMPEG.Enums +namespace FFMpegCore.Enums { public enum VideoSize { - Hd = 720, FullHd = 1080, + Hd = 720, Ed = 480, Ld = 360, - Original + Original = -1 } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs new file mode 100644 index 0000000..6bd608d --- /dev/null +++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs @@ -0,0 +1,27 @@ +using System; + +namespace FFMpegCore.Exceptions +{ + public enum FFMpegExceptionType + { + Dependency, + Conversion, + File, + Operation, + Process + } + + public class FFMpegException : Exception + { + + public FFMpegException(FFMpegExceptionType type, string? message = null, Exception? innerException = null, string ffMpegErrorOutput = "") + : base(message, innerException) + { + FFMpegErrorOutput = ffMpegErrorOutput; + Type = type; + } + + public FFMpegExceptionType Type { get; } + public string FFMpegErrorOutput { get; } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs new file mode 100644 index 0000000..602f03a --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -0,0 +1,577 @@ +using FFMpegCore.Enums; +using FFMpegCore.Exceptions; +using FFMpegCore.Helpers; +using FFMpegCore.Pipes; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace FFMpegCore +{ + public static class FFMpeg + { + /// + /// Saves a 'png' thumbnail from the input video to drive + /// + /// Source video analysis + /// Output video file path + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Bitmap with the requested snapshot. + public static bool Snapshot(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null) + { + if (Path.GetExtension(output) != FileExtension.Png) + output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; + + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); + + return arguments + .OutputToFile(output, true, outputOptions) + .ProcessSynchronously(); + } + /// + /// Saves a 'png' thumbnail from the input video to drive + /// + /// Source video analysis + /// Output video file path + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Bitmap with the requested snapshot. + public static Task SnapshotAsync(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null) + { + if (Path.GetExtension(output) != FileExtension.Png) + output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; + + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); + + return arguments + .OutputToFile(output, true, outputOptions) + .ProcessAsynchronously(); + } + /// + /// Saves a 'png' thumbnail to an in-memory bitmap + /// + /// Source video file. + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Bitmap with the requested snapshot. + public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + { + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); + using var ms = new MemoryStream(); + + arguments + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) + .ProcessSynchronously(); + + ms.Position = 0; + return new Bitmap(ms); + } + /// + /// Saves a 'png' thumbnail to an in-memory bitmap + /// + /// Source video file. + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Bitmap with the requested snapshot. + public static async Task SnapshotAsync(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + { + var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); + using var ms = new MemoryStream(); + + await arguments + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) + .ProcessAsynchronously(); + + ms.Position = 0; + return new Bitmap(ms); + } + + private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + { + captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); + size = PrepareSnapshotSize(source, size); + + return (FFMpegArguments + .FromFileInput(source, options => options + .Seek(captureTime)), + options => options + .WithVideoCodec(VideoCodec.Png) + .WithFrameOutputCount(1) + .Resize(size)); + } + + private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize) + { + if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0)) + return null; + + var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height); + if (source.PrimaryVideoStream.Rotation == 90 || source.PrimaryVideoStream.Rotation == 180) + currentSize = new Size(source.PrimaryVideoStream.Height, source.PrimaryVideoStream.Width); + + if (wantedSize.Value.Width != currentSize.Width || wantedSize.Value.Height != currentSize.Height) + { + if (wantedSize.Value.Width <= 0 && wantedSize.Value.Height > 0) + { + var ratio = (double)wantedSize.Value.Height / currentSize.Height; + return new Size((int)(currentSize.Width * ratio), (int)(currentSize.Height * ratio)); + } + if (wantedSize.Value.Height <= 0 && wantedSize.Value.Width > 0) + { + var ratio = (double)wantedSize.Value.Width / currentSize.Width; + return new Size((int)(currentSize.Width * ratio), (int)(currentSize.Height * ratio)); + } + return wantedSize; + } + + return null; + } + + /// + /// Convert a video do a different format. + /// + /// Input video source. + /// Output information. + /// Target conversion video type. + /// Conversion target speed/quality (faster speed = lower quality). + /// Video size. + /// Conversion target audio quality. + /// Is encoding multithreaded. + /// Output video information. + public static bool Convert( + IMediaAnalysis source, + string output, + ContainerFormat format, + Speed speed = Speed.SuperFast, + VideoSize size = VideoSize.Original, + AudioQuality audioQuality = AudioQuality.Normal, + bool multithreaded = false) + { + FFMpegHelper.ExtensionExceptionCheck(output, format.Extension); + FFMpegHelper.ConversionSizeExceptionCheck(source); + + var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream.Height / (int)size; + var outputSize = new Size((int)(source.PrimaryVideoStream.Width / scale), (int)(source.PrimaryVideoStream.Height / scale)); + + if (outputSize.Width % 2 != 0) + outputSize.Width += 1; + + return format.Name switch + { + "mp4" => FFMpegArguments + .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 + .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 + .FromFileInput(source) + .OutputToFile(output, true, options => options + .CopyChannel() + .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) + .ForceFormat(VideoType.Ts)) + .ProcessSynchronously(), + "webm" => FFMpegArguments + .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)) + }; + } + + /// + /// Adds a poster image to an audio file. + /// + /// Source image file. + /// Source audio file. + /// Output video file. + /// + public static bool PosterWithAudio(string image, string audio, string output) + { + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); + FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image)); + + return FFMpegArguments + .FromFileInput(image) + .AddFileInput(audio) + .OutputToFile(output, true, options => options + .Loop(1) + .WithVideoCodec(VideoCodec.LibX264) + .WithConstantRateFactor(21) + .WithAudioBitrate(AudioQuality.Normal) + .UsingShortest()) + .ProcessSynchronously(); + } + + /// + /// Joins a list of video files. + /// + /// Output video file. + /// List of vides that need to be joined together. + /// Output video information. + public static bool Join(string output, params IMediaAnalysis[] videos) + { + var temporaryVideoParts = videos.Select(video => + { + FFMpegHelper.ConversionSizeExceptionCheck(video); + var destinationPath = Path.Combine(FFMpegOptions.Options.TempDirectory, $"{Path.GetFileNameWithoutExtension(video.Path)}{FileExtension.Ts}"); + Directory.CreateDirectory(FFMpegOptions.Options.TempDirectory); + Convert(video, destinationPath, VideoType.Ts); + return destinationPath; + }).ToArray(); + + try + { + return FFMpegArguments + .FromConcatInput(temporaryVideoParts) + .OutputToFile(output, true, options => options + .CopyChannel() + .WithBitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc)) + .ProcessSynchronously(); + } + finally + { + Cleanup(temporaryVideoParts); + } + } + /// + /// Joins a list of video files. + /// + /// Output video file. + /// List of vides that need to be joined together. + /// Output video information. + public static bool Join(string output, params string[] videos) + { + return Join(output, videos.Select(videoPath => FFProbe.Analyse(videoPath)).ToArray()); + } + + /// + /// Converts an image sequence to a video. + /// + /// Output video file. + /// FPS + /// Image sequence collection + /// Output video information. + public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images) + { + var tempFolderName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid().ToString()); + var temporaryImageFiles = images.Select((image, index) => + { + FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName)); + var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{image.Extension}"); + Directory.CreateDirectory(tempFolderName); + File.Copy(image.FullName, destinationPath); + return destinationPath; + }).ToArray(); + + var firstImage = images.First(); + try + { + return FFMpegArguments + .FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false) + .OutputToFile(output, true, options => options + .Resize(firstImage.Width, firstImage.Height) + .WithFramerate(frameRate)) + .ProcessSynchronously(); + } + finally + { + Cleanup(temporaryImageFiles); + Directory.Delete(tempFolderName); + } + } + + /// + /// Records M3U8 streams to the specified output. + /// + /// URI to pointing towards stream. + /// Output file + /// Success state. + public static bool SaveM3U8Stream(Uri uri, string output) + { + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); + + if (uri.Scheme != "http" && uri.Scheme != "https") + throw new ArgumentException($"Uri: {uri.AbsoluteUri}, does not point to a valid http(s) stream."); + + return FFMpegArguments + .FromUrlInput(uri) + .OutputToFile(output) + .ProcessSynchronously(); + } + + /// + /// Strips a video file of audio. + /// + /// Input video file. + /// Output video file. + /// + public static bool Mute(string input, string output) + { + var source = FFProbe.Analyse(input); + FFMpegHelper.ConversionSizeExceptionCheck(source); + FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); + + return FFMpegArguments + .FromFileInput(source) + .OutputToFile(output, true, options => options + .CopyChannel(Channel.Video) + .DisableChannel(Channel.Audio)) + .ProcessSynchronously(); + } + + /// + /// Saves audio from a specific video file to disk. + /// + /// Source video file. + /// Output audio file. + /// Success state. + public static bool ExtractAudio(string input, string output) + { + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp3); + + return FFMpegArguments + .FromFileInput(input) + .OutputToFile(output, true, options => options + .DisableChannel(Channel.Video)) + .ProcessSynchronously(); + } + + /// + /// Adds audio to a video file. + /// + /// Source video file. + /// Source audio file. + /// Output video file. + /// Indicates if the encoding should stop at the shortest input file. + /// Success state + public static bool ReplaceAudio(string input, string inputAudio, string output, bool stopAtShortest = false) + { + var source = FFProbe.Analyse(input); + FFMpegHelper.ConversionSizeExceptionCheck(source); + FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); + + return FFMpegArguments + .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() + { + FFMpegHelper.RootExceptionCheck(); + + var list = new List(); + using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), "-pix_fmts"); + instance.DataReceived += (e, args) => + { + if (PixelFormat.TryParse(args.Data, out var format)) + list.Add(format); + }; + + var exitCode = instance.BlockUntilFinished(); + if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData)); + + return list.AsReadOnly(); + } + + public static IReadOnlyList GetPixelFormats() + { + if (!FFMpegOptions.Options.UseCache) + return GetPixelFormatsInternal(); + return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly(); + } + + public static bool TryGetPixelFormat(string name, out PixelFormat fmt) + { + if (!FFMpegOptions.Options.UseCache) + { + fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); + return fmt != null; + } + else + return FFMpegCache.PixelFormats.TryGetValue(name, out fmt); + } + + public static PixelFormat GetPixelFormat(string name) + { + if (TryGetPixelFormat(name, out var fmt)) + return fmt; + throw new FFMpegException(FFMpegExceptionType.Operation, $"Pixel format \"{name}\" not supported"); + } + #endregion + + #region Codecs + + private static void ParsePartOfCodecs(Dictionary codecs, string arguments, Func parser) + { + FFMpegHelper.RootExceptionCheck(); + + using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), arguments); + instance.DataReceived += (e, args) => + { + var codec = parser(args.Data); + if(codec != null) + if (codecs.TryGetValue(codec.Name, out var parentCodec)) + parentCodec.Merge(codec); + else + codecs.Add(codec.Name, codec); + }; + + var exitCode = instance.BlockUntilFinished(); + if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData)); + } + + internal static Dictionary GetCodecsInternal() + { + var res = new Dictionary(); + ParsePartOfCodecs(res, "-codecs", (s) => + { + if (Codec.TryParseFromCodecs(s, out var codec)) + return codec; + return null; + }); + ParsePartOfCodecs(res, "-encoders", (s) => + { + if (Codec.TryParseFromEncodersDecoders(s, out var codec, true)) + return codec; + return null; + }); + ParsePartOfCodecs(res, "-decoders", (s) => + { + if (Codec.TryParseFromEncodersDecoders(s, out var codec, false)) + return codec; + return null; + }); + + return res; + } + + public static IReadOnlyList GetCodecs() + { + if (!FFMpegOptions.Options.UseCache) + return GetCodecsInternal().Values.ToList().AsReadOnly(); + return FFMpegCache.Codecs.Values.ToList().AsReadOnly(); + } + + public static IReadOnlyList GetCodecs(CodecType type) + { + if (!FFMpegOptions.Options.UseCache) + return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly(); + return FFMpegCache.Codecs.Values.Where(x=>x.Type == type).ToList().AsReadOnly(); + } + + public static IReadOnlyList GetVideoCodecs() => GetCodecs(CodecType.Video); + public static IReadOnlyList GetAudioCodecs() => GetCodecs(CodecType.Audio); + public static IReadOnlyList GetSubtitleCodecs() => GetCodecs(CodecType.Subtitle); + public static IReadOnlyList GetDataCodecs() => GetCodecs(CodecType.Data); + + public static bool TryGetCodec(string name, out Codec codec) + { + if (!FFMpegOptions.Options.UseCache) + { + codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); + return codec != null; + } + else + return FFMpegCache.Codecs.TryGetValue(name, out codec); + } + + public static Codec GetCodec(string name) + { + if (TryGetCodec(name, out var codec) && codec != null) + return codec; + throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{name}\" not supported"); + } + #endregion + + #region ContainerFormats + internal static IReadOnlyList GetContainersFormatsInternal() + { + FFMpegHelper.RootExceptionCheck(); + + var list = new List(); + using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), "-formats"); + instance.DataReceived += (e, args) => + { + if (ContainerFormat.TryParse(args.Data, out var fmt)) + list.Add(fmt); + }; + + var exitCode = instance.BlockUntilFinished(); + if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData)); + + return list.AsReadOnly(); + } + + public static IReadOnlyList GetContainerFormats() + { + if (!FFMpegOptions.Options.UseCache) + return GetContainersFormatsInternal(); + return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly(); + } + + public static bool TryGetContainerFormat(string name, out ContainerFormat fmt) + { + if (!FFMpegOptions.Options.UseCache) + { + fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); + return fmt != null; + } + else + return FFMpegCache.ContainerFormats.TryGetValue(name, out fmt); + } + + public static ContainerFormat GetContainerFormat(string name) + { + if (TryGetContainerFormat(name, out var fmt)) + return fmt; + throw new FFMpegException(FFMpegExceptionType.Operation, $"Container format \"{name}\" not supported"); + } + #endregion + + private static void Cleanup(IEnumerable pathList) + { + foreach (var path in pathList) + { + if (File.Exists(path)) + File.Delete(path); + } + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs new file mode 100644 index 0000000..e41f8ee --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -0,0 +1,67 @@ +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 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 new file mode 100644 index 0000000..2307787 --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using FFMpegCore.Exceptions; +using FFMpegCore.Helpers; +using Instances; + +namespace FFMpegCore +{ + public class FFMpegArgumentProcessor + { + private static readonly Regex ProgressRegex = new Regex(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled); + private readonly FFMpegArguments _ffMpegArguments; + private Action? _onPercentageProgress; + private Action? _onTimeProgress; + private TimeSpan? _totalTimespan; + + internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) + { + _ffMpegArguments = ffMpegArguments; + } + + public string Arguments => _ffMpegArguments.Text; + + private event EventHandler CancelEvent = null!; + + public FFMpegArgumentProcessor NotifyOnProgress(Action onPercentageProgress, TimeSpan totalTimeSpan) + { + _totalTimespan = totalTimeSpan; + _onPercentageProgress = onPercentageProgress; + return this; + } + public FFMpegArgumentProcessor NotifyOnProgress(Action onTimeProgress) + { + _onTimeProgress = onTimeProgress; + return this; + } + public FFMpegArgumentProcessor CancellableThrough(out Action cancel) + { + cancel = () => CancelEvent?.Invoke(this, EventArgs.Empty); + return this; + } + public bool ProcessSynchronously(bool throwOnError = true) + { + using var instance = PrepareInstance(out var cancellationTokenSource); + var errorCode = -1; + + void OnCancelEvent(object sender, EventArgs args) + { + instance?.SendInput("q"); + cancellationTokenSource.Cancel(); + } + CancelEvent += OnCancelEvent; + instance.Exited += delegate { cancellationTokenSource.Cancel(); }; + + _ffMpegArguments.Pre(); + try + { + Task.WaitAll(instance.FinishedRunning().ContinueWith(t => + { + errorCode = t.Result; + _ffMpegArguments.Post(); + }), _ffMpegArguments.During(cancellationTokenSource.Token)); + } + catch (Exception e) + { + if (!HandleException(throwOnError, e, instance.ErrorData)) return false; + } + finally + { + CancelEvent -= OnCancelEvent; + } + + return HandleCompletion(throwOnError, errorCode, instance.ErrorData); + } + + private bool HandleCompletion(bool throwOnError, int errorCode, IReadOnlyList errorData) + { + if (throwOnError && errorCode != 0) + throw new FFMpegException(FFMpegExceptionType.Conversion, string.Join("\n", errorData)); + + _onPercentageProgress?.Invoke(100.0); + if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value); + + return errorCode == 0; + } + + public async Task ProcessAsynchronously(bool throwOnError = true) + { + using var instance = PrepareInstance(out var cancellationTokenSource); + var errorCode = -1; + + void OnCancelEvent(object sender, EventArgs args) + { + instance?.SendInput("q"); + cancellationTokenSource.Cancel(); + } + CancelEvent += OnCancelEvent; + + _ffMpegArguments.Pre(); + try + { + await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => + { + errorCode = t.Result; + _ffMpegArguments.Post(); + }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); + } + catch (Exception e) + { + if (!HandleException(throwOnError, e, instance.ErrorData)) return false; + } + finally + { + CancelEvent -= OnCancelEvent; + } + + return HandleCompletion(throwOnError, errorCode, instance.ErrorData); + } + + private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSource) + { + FFMpegHelper.RootExceptionCheck(); + FFMpegHelper.VerifyFFMpegExists(); + var instance = new Instance(FFMpegOptions.Options.FFmpegBinary(), _ffMpegArguments.Text); + cancellationTokenSource = new CancellationTokenSource(); + + if (_onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null)) + instance.DataReceived += OutputData; + + return instance; + } + + + private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData) + { + if (!throwOnError) + return false; + + throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, + string.Join("\n", errorData)); + } + + private void OutputData(object sender, (DataType Type, string Data) msg) + { + var match = ProgressRegex.Match(msg.Data); + Debug.WriteLine(msg.Data); + if (!match.Success) return; + + var processed = TimeSpan.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + _onTimeProgress?.Invoke(processed); + + if (_onPercentageProgress == null || _totalTimespan == null) return; + var percentage = Math.Round(processed.TotalSeconds / _totalTimespan.Value.TotalSeconds * 100, 2); + _onPercentageProgress(percentage); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs new file mode 100644 index 0000000..44e20d2 --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FFMpegCore.Arguments; +using FFMpegCore.Pipes; + +namespace FFMpegCore +{ + public sealed class FFMpegArguments : FFMpegOptionsBase + { + private readonly FFMpegGlobalOptions _globalOptions = new FFMpegGlobalOptions(); + + 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) + { + configureOptions(_globalOptions); + return this; + } + + 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) + { + 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() + { + foreach (var argument in Arguments.OfType()) + argument.Pre(); + } + internal async Task During(CancellationToken cancellationToken = default) + { + var inputOutputArguments = Arguments.OfType(); + await Task.WhenAll(inputOutputArguments.Select(io => io.During(cancellationToken))).ConfigureAwait(false); + } + internal void Post() + { + foreach (var argument in Arguments.OfType()) + argument.Post(); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegCache.cs b/FFMpegCore/FFMpeg/FFMpegCache.cs new file mode 100644 index 0000000..0847202 --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegCache.cs @@ -0,0 +1,54 @@ +using FFMpegCore.Enums; +using System.Collections.Generic; +using System.Linq; + +namespace FFMpegCore +{ + static class FFMpegCache + { + private static readonly object _syncObject = new object(); + private static Dictionary? _pixelFormats; + private static Dictionary? _codecs; + private static Dictionary? _containers; + + public static IReadOnlyDictionary PixelFormats + { + get + { + if (_pixelFormats == null) //First check not thread safe + lock (_syncObject) + if (_pixelFormats == null)//Second check thread safe + _pixelFormats = FFMpeg.GetPixelFormatsInternal().ToDictionary(x => x.Name); + + return _pixelFormats; + } + + } + public static IReadOnlyDictionary Codecs + { + get + { + if (_codecs == null) //First check not thread safe + lock (_syncObject) + if (_codecs == null)//Second check thread safe + _codecs = FFMpeg.GetCodecsInternal(); + + return _codecs; + } + + } + public static IReadOnlyDictionary ContainerFormats + { + get + { + if (_containers == null) //First check not thread safe + lock (_syncObject) + if (_containers == null)//Second check thread safe + _containers = FFMpeg.GetContainersFormatsInternal().ToDictionary(x => x.Name); + + return _containers; + } + + } + } +} 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/FFMpegOptions.cs b/FFMpegCore/FFMpeg/FFMpegOptions.cs new file mode 100644 index 0000000..8a98a0a --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegOptions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.Json; + +namespace FFMpegCore +{ + public class FFMpegOptions + { + private static readonly string ConfigFile = "ffmpeg.config.json"; + private static readonly string DefaultRoot = ""; + private static readonly string DefaultTemp = Path.GetTempPath(); + private static readonly Dictionary DefaultExtensionsOverrides = new Dictionary + { + { "mpegts", ".ts" }, + }; + + public static FFMpegOptions Options { get; private set; } = new FFMpegOptions(); + + public static void Configure(Action optionsAction) + { + optionsAction?.Invoke(Options); + } + + public static void Configure(FFMpegOptions options) + { + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + static FFMpegOptions() + { + if (File.Exists(ConfigFile)) + { + Options = JsonSerializer.Deserialize(File.ReadAllText(ConfigFile)); + foreach (var pair in DefaultExtensionsOverrides) + if (!Options.ExtensionOverrides.ContainsKey(pair.Key)) Options.ExtensionOverrides.Add(pair.Key, pair.Value); + } + } + + public string RootDirectory { get; set; } = DefaultRoot; + public string TempDirectory { get; set; } = DefaultTemp; + + public string FFmpegBinary() => FFBinary("FFMpeg"); + + public string FFProbeBinary() => FFBinary("FFProbe"); + + public Dictionary ExtensionOverrides { get; private set; } = new Dictionary(); + + public bool UseCache { get; set; } = true; + + private static string FFBinary(string name) + { + var ffName = name.ToLowerInvariant(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + ffName += ".exe"; + + var target = Environment.Is64BitProcess ? "x64" : "x86"; + if (Directory.Exists(Path.Combine(Options.RootDirectory, target))) + ffName = Path.Combine(target, ffName); + + return Path.Combine(Options.RootDirectory, ffName); + } + } +} diff --git a/FFMpegCore/FFMpeg/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/IPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs new file mode 100644 index 0000000..8010d87 --- /dev/null +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Pipes +{ + public interface IPipeSink + { + Task CopyAsync(System.IO.Stream inputStream, CancellationToken cancellationToken); + string GetFormat(); + } +} diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs new file mode 100644 index 0000000..35766d0 --- /dev/null +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Pipes +{ + /// + /// Interface for ffmpeg pipe source data IO + /// + public interface IPipeSource + { + string GetFormat(); + Task CopyAsync(System.IO.Stream outputStream, CancellationToken cancellationToken); + } +} diff --git a/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs new file mode 100644 index 0000000..094040b --- /dev/null +++ b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Pipes +{ + /// + /// Interface for Video frame + /// + public interface IVideoFrame + { + int Width { get; } + int Height { get; } + string Format { get; } + + void Serialize(System.IO.Stream pipe); + Task SerializeAsync(System.IO.Stream pipe, CancellationToken token); + } +} diff --git a/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs b/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs new file mode 100644 index 0000000..c680c3e --- /dev/null +++ b/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; + +namespace FFMpegCore.Pipes +{ + static class PipeHelpers + { + public static string GetUnqiuePipeName() => $"FFMpegCore_{Guid.NewGuid()}"; + + public static string GetPipePath(string pipeName) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return $@"\\.\pipe\{pipeName}"; + else + return $"unix:/tmp/CoreFxPipe_{pipeName}"; + } + } +} diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs new file mode 100644 index 0000000..8739a40 --- /dev/null +++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FFMpegCore.Exceptions; + +namespace FFMpegCore.Pipes +{ + /// + /// Implementation of for a raw video stream that is gathered from + /// + public class RawVideoPipeSource : IPipeSource + { + public string StreamFormat { get; private set; } = null!; + public int Width { get; private set; } + public int Height { get; private set; } + public int FrameRate { get; set; } = 25; + private bool _formatInitialized; + private readonly IEnumerator _framesEnumerator; + + public RawVideoPipeSource(IEnumerator framesEnumerator) + { + _framesEnumerator = framesEnumerator; + } + + public RawVideoPipeSource(IEnumerable framesEnumerator) : this(framesEnumerator.GetEnumerator()) { } + + public string GetFormat() + { + if (!_formatInitialized) + { + //see input format references https://lists.ffmpeg.org/pipermail/ffmpeg-user/2012-July/007742.html + if (_framesEnumerator.Current == null) + { + if (!_framesEnumerator.MoveNext()) + throw new InvalidOperationException("Enumerator is empty, unable to get frame"); + } + StreamFormat = _framesEnumerator.Current!.Format; + Width = _framesEnumerator.Current!.Width; + Height = _framesEnumerator.Current!.Height; + + _formatInitialized = true; + } + + return $"-f rawvideo -r {FrameRate} -pix_fmt {StreamFormat} -s {Width}x{Height}"; + } + + public async Task CopyAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) + { + if (_framesEnumerator.Current != null) + { + CheckFrameAndThrow(_framesEnumerator.Current); + await _framesEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); + } + + while (_framesEnumerator.MoveNext()) + { + CheckFrameAndThrow(_framesEnumerator.Current!); + await _framesEnumerator.Current!.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); + } + } + + private void CheckFrameAndThrow(IVideoFrame frame) + { + if (frame.Width != Width || frame.Height != Height || frame.Format != StreamFormat) + throw new FFMpegException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" + + $"Frame format: {frame.Width}x{frame.Height} pix_fmt: {frame.Format}\r\n" + + $"Stream format: {Width}x{Height} pix_fmt: {StreamFormat}"); + } + } +} diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs new file mode 100644 index 0000000..ca2246f --- /dev/null +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Pipes +{ + public class StreamPipeSink : IPipeSink + { + public System.IO.Stream Destination { get; } + public int BlockSize { get; set; } = 4096; + public string Format { get; set; } = string.Empty; + + public StreamPipeSink(System.IO.Stream destination) + { + Destination = destination; + } + + public Task CopyAsync(System.IO.Stream inputStream, CancellationToken cancellationToken) => + inputStream.CopyToAsync(Destination, BlockSize, cancellationToken); + + public string GetFormat() => Format; + } +} diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs new file mode 100644 index 0000000..db41eb7 --- /dev/null +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Pipes +{ + /// + /// Implementation of used for stream redirection + /// + public class StreamPipeSource : IPipeSource + { + public System.IO.Stream Source { get; } + public int BlockSize { get; } = 4096; + public string StreamFormat { get; } = string.Empty; + + public StreamPipeSource(System.IO.Stream source) + { + Source = source; + } + + public Task CopyAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) => Source.CopyToAsync(outputStream, BlockSize, cancellationToken); + + public string GetFormat() => StreamFormat; + } +} diff --git a/FFMpegCore/FFMPEG/bin/presets/ffprobe.xsd b/FFMpegCore/FFMpeg/bin/presets/ffprobe.xsd similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/ffprobe.xsd rename to FFMpegCore/FFMpeg/bin/presets/ffprobe.xsd diff --git a/FFMpegCore/FFMPEG/bin/presets/libvpx-1080p.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libvpx-1080p.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libvpx-1080p.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libvpx-1080p.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libvpx-1080p50_60.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libvpx-1080p50_60.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libvpx-1080p50_60.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libvpx-1080p50_60.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libvpx-360p.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libvpx-360p.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libvpx-360p.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libvpx-360p.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libvpx-720p.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libvpx-720p.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libvpx-720p.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libvpx-720p.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libvpx-720p50_60.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libvpx-720p50_60.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libvpx-720p50_60.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libvpx-720p50_60.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libvpx-ultrafast.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libvpx-ultrafast.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libvpx-ultrafast.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libvpx-ultrafast.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-baseline.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-baseline.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-baseline.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-baseline.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-fast.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-fast.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-fast.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-fast.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-fast_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-fast_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-fast_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-fast_firstpass.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-faster.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-faster.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-faster.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-faster.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-faster_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-faster_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-faster_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-faster_firstpass.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-ipod320.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-ipod320.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-ipod320.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-ipod320.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-ipod640.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-ipod640.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-ipod640.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-ipod640.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-lossless_fast.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-lossless_fast.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-lossless_fast.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-lossless_fast.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-lossless_max.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-lossless_max.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-lossless_max.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-lossless_max.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-lossless_medium.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-lossless_medium.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-lossless_medium.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-lossless_medium.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-lossless_slow.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-lossless_slow.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-lossless_slow.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-lossless_slow.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-lossless_slower.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-lossless_slower.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-lossless_slower.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-lossless_slower.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-lossless_ultrafast.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-lossless_ultrafast.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-lossless_ultrafast.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-lossless_ultrafast.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-main.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-main.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-main.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-main.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-medium.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-medium.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-medium.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-medium.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-medium_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-medium_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-medium_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-medium_firstpass.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-placebo.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-placebo.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-placebo.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-placebo.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-placebo_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-placebo_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-placebo_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-placebo_firstpass.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-slow.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-slow.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-slow.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-slow.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-slow_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-slow_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-slow_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-slow_firstpass.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-slower.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-slower.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-slower.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-slower.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-slower_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-slower_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-slower_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-slower_firstpass.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-superfast.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-superfast.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-superfast.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-superfast.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-superfast_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-superfast_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-superfast_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-superfast_firstpass.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-ultrafast.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-ultrafast.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-ultrafast.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-ultrafast.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-ultrafast_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-ultrafast_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-ultrafast_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-ultrafast_firstpass.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-veryfast.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-veryfast.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-veryfast.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-veryfast.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-veryfast_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-veryfast_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-veryfast_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-veryfast_firstpass.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-veryslow.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-veryslow.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-veryslow.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-veryslow.ffpreset diff --git a/FFMpegCore/FFMPEG/bin/presets/libx264-veryslow_firstpass.ffpreset b/FFMpegCore/FFMpeg/bin/presets/libx264-veryslow_firstpass.ffpreset similarity index 100% rename from FFMpegCore/FFMPEG/bin/presets/libx264-veryslow_firstpass.ffpreset rename to FFMpegCore/FFMpeg/bin/presets/libx264-veryslow_firstpass.ffpreset diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 80a3db5..057f605 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -1,143 +1,36 @@  - netstandard2.0 en 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.1.0 - 1.1.0.0 - 1.1.0.0 - Minor fixes and refactoring + + 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 - 1.1.0 - Vlad Jerca, Malte Rosenbjerg + 3.1.0 + Malte Rosenbjerg, Vlad Jerca ffmpeg ffprobe convert video audio mediafile resize analyze muxing GitHub true + enable + netstandard2.0 - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - + Always - - - - - + + + diff --git a/FFMpegCore/FFMpegCore.nuspec b/FFMpegCore/FFMpegCore.nuspec deleted file mode 100644 index 5270043..0000000 --- a/FFMpegCore/FFMpegCore.nuspec +++ /dev/null @@ -1,23 +0,0 @@ - - - - $id$ - $version$ - $title$ - Vlad Jerca - Vlad Jerca - https://github.com/vladjerca/FFMpegCore - false - $description$ - - More information available @ https://github.com/vladjerca/FFMpegCore/blob/master/README.md - - Copyright 2020 - ffmpeg video conversion FFMpegCore mp4 ogv net.core core net - - - - - - - diff --git a/FFMpegCore/FFProbe/AudioStream.cs b/FFMpegCore/FFProbe/AudioStream.cs new file mode 100644 index 0000000..d6f4b33 --- /dev/null +++ b/FFMpegCore/FFProbe/AudioStream.cs @@ -0,0 +1,10 @@ +namespace FFMpegCore +{ + public class AudioStream : MediaStream + { + public int Channels { get; internal set; } + public string ChannelLayout { get; internal set; } = null!; + public int SampleRateHz { get; internal set; } + public string Profile { get; internal set; } = null!; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs new file mode 100644 index 0000000..f650371 --- /dev/null +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using FFMpegCore.Arguments; +using FFMpegCore.Exceptions; +using FFMpegCore.Helpers; +using FFMpegCore.Pipes; +using Instances; + +namespace FFMpegCore +{ + 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(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); + using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity); + pipeArgument.Pre(); + + var task = instance.FinishedRunning(); + try + { + pipeArgument.During().ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (IOException) { } + finally + { + pipeArgument.Post(); + } + var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult(); + if (exitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}: {string.Join("\n", instance.OutputData)} {string.Join("\n", instance.ErrorData)}"); + + return ParseOutput(pipeArgument.PipePath, instance); + } + 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(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); + using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity); + pipeArgument.Pre(); + + var task = instance.FinishedRunning(); + try + { + await pipeArgument.During(); + } + catch(IOException) + { + } + finally + { + pipeArgument.Post(); + } + var exitCode = await task; + if (exitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}: {string.Join("\n", instance.OutputData)} {string.Join("\n", instance.ErrorData)}"); + + pipeArgument.Post(); + return ParseOutput(pipeArgument.PipePath, instance); + } + + private static IMediaAnalysis ParseOutput(string filePath, Instance instance) + { + var json = string.Join(string.Empty, instance.OutputData); + var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + return new MediaAnalysis(filePath, ffprobeAnalysis); + } + + private static Instance PrepareInstance(string filePath, int outputCapacity) + { + FFProbeHelper.RootExceptionCheck(); + FFProbeHelper.VerifyFFProbeExists(); + var arguments = $"-print_format json -show_format -sexagesimal -show_streams \"{filePath}\""; + var instance = new Instance(FFMpegOptions.Options.FFProbeBinary(), arguments) {DataBufferCapacity = outputCapacity}; + return instance; + } + } +} diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs new file mode 100644 index 0000000..a0f2d41 --- /dev/null +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FFMpegCore +{ + public class FFProbeAnalysis + { + [JsonPropertyName("streams")] + public List Streams { get; set; } = null!; + + [JsonPropertyName("format")] + public Format Format { get; set; } = null!; + } + + public class FFProbeStream : ITagsContainer + { + [JsonPropertyName("index")] + public int Index { get; set; } + + [JsonPropertyName("avg_frame_rate")] + public string AvgFrameRate { get; set; } = null!; + + [JsonPropertyName("bits_per_raw_sample")] + public string BitsPerRawSample { get; set; } = null!; + + [JsonPropertyName("bit_rate")] + public string BitRate { get; set; } = null!; + + [JsonPropertyName("channels")] + public int? Channels { get; set; } + + [JsonPropertyName("channel_layout")] + public string ChannelLayout { get; set; } = null!; + + [JsonPropertyName("codec_type")] + public string CodecType { get; set; } = null!; + + [JsonPropertyName("codec_name")] + public string CodecName { get; set; } = null!; + + [JsonPropertyName("codec_long_name")] + public string CodecLongName { get; set; } = null!; + + [JsonPropertyName("display_aspect_ratio")] + public string DisplayAspectRatio { get; set; } = null!; + + [JsonPropertyName("duration")] + public string Duration { get; set; } = null!; + + [JsonPropertyName("profile")] + public string Profile { get; set; } = null!; + + [JsonPropertyName("width")] + public int? Width { get; set; } + + [JsonPropertyName("height")] + public int? Height { get; set; } + + [JsonPropertyName("r_frame_rate")] + public string FrameRate { get; set; } = null!; + + [JsonPropertyName("pix_fmt")] + public string PixelFormat { get; set; } = null!; + + [JsonPropertyName("sample_rate")] + public string SampleRate { get; set; } = null!; + + [JsonPropertyName("tags")] + public Dictionary Tags { get; set; } = null!; + } + public class Format : ITagsContainer + { + [JsonPropertyName("filename")] + public string Filename { get; set; } = null!; + + [JsonPropertyName("nb_streams")] + public int NbStreams { get; set; } + + [JsonPropertyName("nb_programs")] + public int NbPrograms { get; set; } + + [JsonPropertyName("format_name")] + public string FormatName { get; set; } = null!; + + [JsonPropertyName("format_long_name")] + public string FormatLongName { get; set; } = null!; + + [JsonPropertyName("start_time")] + public string StartTime { get; set; } = null!; + + [JsonPropertyName("duration")] + public string Duration { get; set; } = null!; + + [JsonPropertyName("size")] + public string Size { get; set; } = null!; + + [JsonPropertyName("bit_rate")] + public string BitRate { get; set; } = null!; + + [JsonPropertyName("probe_score")] + public int ProbeScore { get; set; } + + [JsonPropertyName("tags")] + public Dictionary Tags { get; set; } = null!; + } + + public interface ITagsContainer + { + Dictionary Tags { get; set; } + } + public static class TagExtensions + { + private static string? TryGetTagValue(ITagsContainer tagsContainer, string key) + { + if (tagsContainer.Tags != null && tagsContainer.Tags.TryGetValue(key, out var tagValue)) + return tagValue; + return null; + } + + public static string? GetLanguage(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "language"); + public static string? GetCreationTime(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "creation_time "); + public static string? GetRotate(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "rotate"); + public static string? GetDuration(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "duration"); + + + } +} diff --git a/FFMpegCore/FFProbe/IMediaAnalysis.cs b/FFMpegCore/FFProbe/IMediaAnalysis.cs new file mode 100644 index 0000000..660d776 --- /dev/null +++ b/FFMpegCore/FFProbe/IMediaAnalysis.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace FFMpegCore +{ + public interface IMediaAnalysis + { + string Path { get; } + string Extension { get; } + TimeSpan Duration { get; } + MediaFormat Format { get; } + AudioStream PrimaryAudioStream { get; } + VideoStream PrimaryVideoStream { get; } + List VideoStreams { get; } + List AudioStreams { get; } + } +} diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs new file mode 100644 index 0000000..4717a6f --- /dev/null +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace FFMpegCore +{ + internal class MediaAnalysis : IMediaAnalysis + { + private static readonly Regex DurationRegex = new Regex("^(\\d{1,2}:\\d{1,2}:\\d{1,2}(.\\d{1,7})?)", RegexOptions.Compiled); + internal MediaAnalysis(string path, FFProbeAnalysis analysis) + { + Format = ParseFormat(analysis.Format); + VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList(); + AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList(); + PrimaryVideoStream = VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault(); + PrimaryAudioStream = AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault(); + Path = path; + } + + private MediaFormat ParseFormat(Format analysisFormat) + { + return new MediaFormat + { + Duration = TimeSpan.Parse(analysisFormat.Duration ?? "0"), + FormatName = analysisFormat.FormatName, + FormatLongName = analysisFormat.FormatLongName, + StreamCount = analysisFormat.NbStreams, + ProbeScore = analysisFormat.ProbeScore, + BitRate = long.Parse(analysisFormat.BitRate ?? "0") + }; + } + + public string Path { get; } + public string Extension => System.IO.Path.GetExtension(Path); + + public TimeSpan Duration => new [] + { + Format.Duration, + PrimaryVideoStream?.Duration ?? TimeSpan.Zero, + PrimaryAudioStream?.Duration ?? TimeSpan.Zero + }.Max(); + + public MediaFormat Format { get; } + public AudioStream PrimaryAudioStream { get; } + + public VideoStream PrimaryVideoStream { get; } + + public List VideoStreams { get; } + public List AudioStreams { get; } + + private VideoStream ParseVideoStream(FFProbeStream stream) + { + return new VideoStream + { + Index = stream.Index, + AvgFrameRate = DivideRatio(ParseRatioDouble(stream.AvgFrameRate, '/')), + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? ParseIntInvariant(stream.BitRate) : default, + BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? ParseIntInvariant(stream.BitsPerRawSample) : default, + CodecName = stream.CodecName, + CodecLongName = stream.CodecLongName, + DisplayAspectRatio = ParseRatioInt(stream.DisplayAspectRatio, ':'), + Duration = ParseDuration(stream), + FrameRate = DivideRatio(ParseRatioDouble(stream.FrameRate, '/')), + Height = stream.Height ?? 0, + Width = stream.Width ?? 0, + Profile = stream.Profile, + PixelFormat = stream.PixelFormat, + Rotation = (int)float.Parse(stream.GetRotate() ?? "0"), + Language = stream.GetLanguage() + }; + } + + private static TimeSpan ParseDuration(FFProbeStream ffProbeStream) + { + return !string.IsNullOrEmpty(ffProbeStream.Duration) + ? TimeSpan.Parse(ffProbeStream.Duration) + : TimeSpan.Parse(TrimTimeSpan(ffProbeStream.GetDuration()) ?? "0"); + } + private static string? TrimTimeSpan(string? durationTag) + { + var durationMatch = DurationRegex.Match(durationTag ?? ""); + return durationMatch.Success ? durationMatch.Groups[1].Value : null; + } + + private AudioStream ParseAudioStream(FFProbeStream stream) + { + return new AudioStream + { + Index = stream.Index, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? ParseIntInvariant(stream.BitRate) : default, + CodecName = stream.CodecName, + CodecLongName = stream.CodecLongName, + Channels = stream.Channels ?? default, + ChannelLayout = stream.ChannelLayout, + Duration = ParseDuration(stream), + SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? ParseIntInvariant(stream.SampleRate) : default, + Profile = stream.Profile, + Language = stream.GetLanguage() + }; + } + + private static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2; + private static (int, int) ParseRatioInt(string input, char separator) + { + if (string.IsNullOrEmpty(input)) return (0, 0); + var ratio = input.Split(separator); + return (ParseIntInvariant(ratio[0]), ParseIntInvariant(ratio[1])); + } + private static (double, double) ParseRatioDouble(string input, char separator) + { + if (string.IsNullOrEmpty(input)) return (0, 0); + var ratio = input.Split(separator); + return (ratio.Length > 0 ? ParseDoubleInvariant(ratio[0]) : 0, ratio.Length > 1 ? ParseDoubleInvariant(ratio[1]) : 0); + } + + private static double ParseDoubleInvariant(string line) => + double.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + private static int ParseIntInvariant(string line) => + int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + } +} \ No newline at end of file diff --git a/FFMpegCore/FFProbe/MediaFormat.cs b/FFMpegCore/FFProbe/MediaFormat.cs new file mode 100644 index 0000000..ea5c6f3 --- /dev/null +++ b/FFMpegCore/FFProbe/MediaFormat.cs @@ -0,0 +1,14 @@ +using System; + +namespace FFMpegCore +{ + public class MediaFormat + { + public TimeSpan Duration { get; set; } + public string FormatName { get; set; } = null!; + public string FormatLongName { get; set; } = null!; + public int StreamCount { get; set; } + public double ProbeScore { get; set; } + public double BitRate { get; set; } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFProbe/MediaStream.cs b/FFMpegCore/FFProbe/MediaStream.cs new file mode 100644 index 0000000..61d548f --- /dev/null +++ b/FFMpegCore/FFProbe/MediaStream.cs @@ -0,0 +1,17 @@ +using FFMpegCore.Enums; +using System; + +namespace FFMpegCore +{ + public class MediaStream + { + public int Index { get; internal set; } + public string CodecName { get; internal set; } = null!; + public string CodecLongName { get; internal set; } = null!; + public int BitRate { get; internal set; } + public TimeSpan Duration { get; internal set; } + public string? Language { get; internal set; } + + public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName); + } +} \ No newline at end of file diff --git a/FFMpegCore/FFProbe/VideoStream.cs b/FFMpegCore/FFProbe/VideoStream.cs new file mode 100644 index 0000000..0bcfc09 --- /dev/null +++ b/FFMpegCore/FFProbe/VideoStream.cs @@ -0,0 +1,19 @@ +using FFMpegCore.Enums; + +namespace FFMpegCore +{ + public class VideoStream : MediaStream + { + public double AvgFrameRate { get; internal set; } + public int BitsPerRawSample { get; internal set; } + public (int Width, int Height) DisplayAspectRatio { get; internal set; } + public string Profile { get; internal set; } = null!; + public int Width { get; internal set; } + public int Height { get; internal set; } + public double FrameRate { get; internal set; } + public string PixelFormat { get; internal set; } = null!; + public int Rotation { get; set; } + + public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat); + } +} \ No newline at end of file diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs index 9804869..f2a214e 100644 --- a/FFMpegCore/Helpers/FFMpegHelper.cs +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -1,77 +1,53 @@ using System; using System.Drawing; using System.IO; -using FFMpegCore.FFMPEG.Exceptions; +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); } - public static void ConversionSizeExceptionCheck(VideoInfo info) + public static void ConversionSizeExceptionCheck(IMediaAnalysis info) { - ConversionSizeExceptionCheck(new Size(info.Width, info.Height)); + ConversionSizeExceptionCheck(new Size(info.PrimaryVideoStream.Width, info.PrimaryVideoStream.Height)); } - public static void ConversionSizeExceptionCheck(Size size) + private static void ConversionSizeExceptionCheck(Size size) { - if ( - size.Height % 2 != 0 || - size.Width % 2 != 0 - ) + if (size.Height % 2 != 0 || size.Width % 2 != 0 ) { throw new ArgumentException("FFMpeg yuv420p encoding requires the width and height to be a multiple of 2!"); } } - public static void OutputExistsExceptionCheck(FileInfo output) + public static void ExtensionExceptionCheck(string filename, string extension) { - if (File.Exists(output.FullName)) - { + if (!extension.Equals(Path.GetExtension(filename), StringComparison.OrdinalIgnoreCase)) throw new FFMpegException(FFMpegExceptionType.File, - $"The output file: {output} already exists!"); - } + $"Invalid output file. File extension should be '{extension}' required."); } - public static void InputExistsExceptionCheck(FileInfo input) + public static void RootExceptionCheck() { - if (!File.Exists(input.FullName)) - { - throw new FFMpegException(FFMpegExceptionType.File, - $"Input {input.FullName} does not exist!"); - } - } - - public static void ConversionExceptionCheck(FileInfo originalVideo, FileInfo convertedPath) - { - OutputExistsExceptionCheck(convertedPath); - InputExistsExceptionCheck(originalVideo); - } - - public static void InputsExistExceptionCheck(params FileInfo[] paths) - { - foreach (var path in paths) - { - InputExistsExceptionCheck(path); - } - } - - public static void ExtensionExceptionCheck(FileInfo output, string expected) - { - if (!expected.Equals(new FileInfo(output.FullName).Extension, StringComparison.OrdinalIgnoreCase)) - throw new FFMpegException(FFMpegExceptionType.File, - $"Invalid output file. File extension should be '{expected}' required."); - } - - public static void RootExceptionCheck(string root) - { - 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 5b6b423..1e833e0 100644 --- a/FFMpegCore/Helpers/FFProbeHelper.cs +++ b/FFMpegCore/Helpers/FFProbeHelper.cs @@ -1,9 +1,12 @@ -using FFMpegCore.FFMPEG.Exceptions; +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/FFMpegCore/ImageInfo.cs b/FFMpegCore/ImageInfo.cs index 9c9c044..cf8561e 100644 --- a/FFMpegCore/ImageInfo.cs +++ b/FFMpegCore/ImageInfo.cs @@ -1,8 +1,8 @@ -using FFMpegCore.Enums; -using FFMpegCore.Helpers; -using System; +using System; using System.Drawing; using System.IO; +using FFMpegCore.Enums; +using FFMpegCore.Helpers; namespace FFMpegCore { @@ -16,21 +16,21 @@ public class ImageInfo /// Image file information. public ImageInfo(FileInfo fileInfo) { - if (!fileInfo.Extension.ToLower().EndsWith(FileExtension.Png)) + if (!fileInfo.Extension.ToLowerInvariant().EndsWith(FileExtension.Png)) { throw new Exception("Image joining currently suppors only .png file types"); } fileInfo.Refresh(); - this.Size = fileInfo.Length / (1024 * 1024); + Size = fileInfo.Length / (1024 * 1024); using (var image = Image.FromFile(fileInfo.FullName)) { - this.Width = image.Width; - this.Height = image.Height; - var cd = FFProbeHelper.Gcd(this.Width, this.Height); - this.Ratio = $"{this.Width / cd}:{this.Height / cd}"; + Width = image.Width; + Height = image.Height; + var cd = FFProbeHelper.Gcd(Width, Height); + Ratio = $"{Width / cd}:{Height / cd}"; } @@ -46,9 +46,7 @@ public ImageInfo(FileInfo fileInfo) /// Create a image information object from a target path. /// /// Path to image. - public ImageInfo(string path) : this(new FileInfo(path)) - { - } + public ImageInfo(string path) : this(new FileInfo(path)) { } /// /// Aspect ratio. diff --git a/FFMpegCore/VideoInfo.cs b/FFMpegCore/VideoInfo.cs deleted file mode 100644 index a4556d3..0000000 --- a/FFMpegCore/VideoInfo.cs +++ /dev/null @@ -1,187 +0,0 @@ -using FFMpegCore.FFMPEG; -using System; -using System.IO; - -namespace FFMpegCore -{ - public class VideoInfo - { - private FileInfo _file; - - /// - /// Create a video information object from a file information object. - /// - /// Video file information. - public VideoInfo(FileInfo fileInfo) - { - fileInfo.Refresh(); - - if (!fileInfo.Exists) - throw new ArgumentException($"Input file {fileInfo.FullName} does not exist!"); - - _file = fileInfo; - - new FFProbe().ParseVideoInfo(this); - } - - /// - /// Create a video information object from a target path. - /// - /// Path to video. - public VideoInfo(string path) : this(new FileInfo(path)) - { - } - - /// - /// Duration of the video file. - /// - public TimeSpan Duration { get; internal set; } - - /// - /// Audio format of the video file. - /// - public string AudioFormat { get; internal set; } - - /// - /// Video format of the video file. - /// - public string VideoFormat { get; internal set; } - - /// - /// Aspect ratio. - /// - public string Ratio { get; internal set; } - - /// - /// Video frame rate. - /// - public double FrameRate { get; internal set; } - - /// - /// Height of the video file. - /// - public int Height { get; internal set; } - - /// - /// Width of the video file. - /// - public int Width { get; internal set; } - - /// - /// Video file size in MegaBytes (MB). - /// - public double Size { get; internal set; } - - /// - /// Gets the name of the file. - /// - public string Name => _file.Name; - - /// - /// Gets the full path of the file. - /// - public string FullName => _file.FullName; - - /// - /// Gets the file extension. - /// - public string Extension => _file.Extension; - - /// - /// Gets a flag indicating if the file is read-only. - /// - public bool IsReadOnly => _file.IsReadOnly; - - /// - /// Gets a flag indicating if the file exists (no cache, per call verification). - /// - public bool Exists => File.Exists(FullName); - - /// - /// Gets the creation date. - /// - public DateTime CreationTime => _file.CreationTime; - - /// - /// Gets the parent directory information. - /// - public DirectoryInfo Directory => _file.Directory; - - /// - /// Create a video information object from a file information object. - /// - /// Video file information. - /// - public static VideoInfo FromFileInfo(FileInfo fileInfo) - { - return FromPath(fileInfo.FullName); - } - - /// - /// Create a video information object from a target path. - /// - /// Path to video. - /// - public static VideoInfo FromPath(string path) - { - return new VideoInfo(path); - } - - /// - /// Pretty prints the video information. - /// - /// - public override string ToString() - { - return "Video Path : " + FullName + Environment.NewLine + - "Video Root : " + Directory.FullName + Environment.NewLine + - "Video Name: " + Name + Environment.NewLine + - "Video Extension : " + Extension + Environment.NewLine + - "Video Duration : " + Duration + Environment.NewLine + - "Audio Format : " + AudioFormat + Environment.NewLine + - "Video Format : " + VideoFormat + Environment.NewLine + - "Aspect Ratio : " + Ratio + Environment.NewLine + - "Framerate : " + FrameRate + "fps" + Environment.NewLine + - "Resolution : " + Width + "x" + Height + Environment.NewLine + - "Size : " + Size + " MB"; - } - - /// - /// Open a file stream. - /// - /// Opens a file in a specified mode. - /// File stream of the video file. - public FileStream FileOpen(FileMode mode) - { - return _file.Open(mode); - } - - /// - /// Move file to a specific directory. - /// - /// - public void MoveTo(DirectoryInfo destination) - { - var newLocation = $"{destination.FullName}{Path.DirectorySeparatorChar}{Name}{Extension}"; - _file.MoveTo(newLocation); - _file = new FileInfo(newLocation); - } - - /// - /// Delete the file. - /// - public void Delete() - { - _file.Delete(); - } - - /// - /// Converts video info to file info. - /// - /// FileInfo - public FileInfo ToFileInfo() - { - return new FileInfo(_file.FullName); - } - } -} diff --git a/README.md b/README.md index 6fddd05..6fb7abe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # FFMpegCore [![NuGet Badge](https://buildstats.info/nuget/FFMpegCore)](https://www.nuget.org/packages/FFMpegCore/) -![CI](https://github.com/rosenbjerg/FFMpegCore/workflows/CI/badge.svg) +[![CI](https://github.com/rosenbjerg/FFMpegCore/workflows/CI/badge.svg)](https://github.com/rosenbjerg/FFMpegCore/actions?query=workflow%3ACI) # Setup @@ -10,11 +10,140 @@ 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 all web formats: MP4, OGV, TS and methods of capturing screens from the videos. +A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your C# applications. Support both synchronous and asynchronous use + +# API + +## FFProbe + +FFProbe is used to gather media information: + +```csharp +var mediaInfo = FFProbe.Analyse(inputFile); +``` +or +```csharp +var mediaInfo = await FFProbe.AnalyseAsync(inputFile); +``` + + +## FFMpeg +FFMpeg is used for converting your media files to web ready formats. +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 + .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(inputPath); + +// process the snapshot in-memory and use the Bitmap directly +var bitmap = FFMpeg.Snapshot(mediaFileAnalysis, new Size(200, 400), TimeSpan.FromMinutes(1)); + +// or persists the image on the drive +FFMpeg.Snapshot(mediaFileAnalysis, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1)) +``` + +Convert to and/or from streams +```csharp +await FFMpegArguments + .FromPipeInput(new StreamPipeSource(inputStream)) + .OutputToPipe(new StreamPipeSink(outputStream), options => options + .WithVideoCodec("vp9") + .ForceFormat("webm")) + .ProcessAsynchronously(); +``` + +Join video parts into one single file: +```csharp +FFMpeg.Join(@"..\joined_video.mp4", + @"..\part1.mp4", + @"..\part2.mp4", + @"..\part3.mp4" +); +``` + +Join images into a video: +```csharp +FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, + ImageInfo.FromPath(@"..\1.png"), + ImageInfo.FromPath(@"..\2.png"), + ImageInfo.FromPath(@"..\3.png") +); +``` + +Mute videos: +```csharp +FFMpeg.Mute(inputFilePath, outputFilePath); +``` + +Save audio track from video: +```csharp +FFMpeg.ExtractAudio(inputVideoFilePath, outputAudioFilePath); +``` + +Add or replace audio track on video: +```csharp +FFMpeg.ReplaceAudio(inputVideoFilePath, inputAudioFilePath, outputVideoFilePath); +``` + +Add poster image to audio file (good for youtube videos): +```csharp +FFMpeg.PosterWithAudio(inputImageFilePath, inputAudioFilePath, outputVideoFilePath); +// or +var image = Image.FromFile(inputImageFile); +image.AddAudio(inputAudioFilePath, outputVideoFilePath); +``` + +Other available arguments could be found in `FFMpegCore.Arguments` namespace. + +### Input piping +With input piping it is possible to write video frames directly from program memory without saving them to jpeg or png and then passing path to input of ffmpeg. This feature also allows us to convert video on-the-fly while frames are being generated or received. + +The `IPipeSource` interface is used as the source of data. It could be represented as encoded video stream or raw frames stream. Currently, the `IPipeSource` interface has single implementation, `RawVideoPipeSource` that is used for raw stream encoding. + +For example: + +Method that is generating bitmap frames: +```csharp +IEnumerable CreateFrames(int count) +{ + for(int i = 0; i < count; i++) + { + yield return GetNextFrame(); //method of generating new frames + } +} +``` +Then create `ArgumentsContainer` with `InputPipeArgument` +```csharp +var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable or IEnumerator to constructor of RawVideoPipeSource +{ + FrameRate = 30 //set source frame rate +}; +FFMpegArguments + .FromPipeInput(videoFramesSource, ) + .OutputToFile("temporary.mp4", false, ) + .ProcessSynchronously(); +``` + +if you want to use `System.Drawing.Bitmap` as `IVideoFrame`, there is a `BitmapVideoFrameWrapper` wrapper class. + ## Binaries -If you prefer to manually download them, visit [ffbinaries](https://ffbinaries.com/downloads). +If you prefer to manually download them, visit [ffbinaries](https://ffbinaries.com/downloads) or [zeranoe Windows builds](https://ffmpeg.zeranoe.com/builds/). #### Windows @@ -52,335 +181,25 @@ The default value (`\\FFMPEG\\bin`) can be overwritten via the `FFMpegOptions` c ```c# public Startup() { - FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "./bin" }); + FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "./bin", TempDirectory = "/tmp" }); } ``` #### Option 2 -The root directory for the ffmpeg binaries can be configured via the `ffmpeg.config.json` file. +The root and temp directory for the ffmpeg binaries can be configured via the `ffmpeg.config.json` file. ```json { - "RootDirectory": "./bin" + "RootDirectory": "./bin", + "TempDirectory": "/tmp" } ``` # Compatibility - Some versions of FFMPEG might not have the same argument schema. The lib has been tested with version `3.3` to `4.1` + Some versions of FFMPEG might not have the same argument schema. The lib has been tested with version `3.3` to `4.2` - # API -## FFProbe - -FFProbe is used to gather video information -```csharp -static void Main(string[] args) -{ - string inputFile = "G:\\input.mp4"; - - // loaded from configuration - var video = new VideoInfo(inputFile); - - string output = video.ToString(); - - Console.WriteLine(output); -} -``` - -Sample output: -```csharp -Video Path : G:\input.mp4 -Video Root : G:\\ -Video Name: input.mp4 -Video Extension : .mp4 -Video Duration : 00:00:09 -Audio Format : none -Video Format : h264 -Aspect Ratio : 16:9 -Framerate : 30fps -Resolution : 1280x720 -Size : 2.88 Mb -``` - -## FFMpeg -Convert your video files to web ready formats: - -```csharp -static void Main(string[] args) -{ - string inputFile = "input_path_goes_here"; - var encoder = new FFMpeg(); - FileInfo outputFile = new FileInfo("output_path_goes_here"); - - var video = VideoInfo.FromPath(inputFile); - - // easily track conversion progress - encoder.OnProgress += (percentage) => Console.WriteLine("Progress {0}%", percentage); - - // MP4 conversion - encoder.Convert( - video, - outputFile, - VideoType.Mp4, - Speed.UltraFast, - VideoSize.Original, - AudioQuality.Hd, - true - ); - // OGV conversion - encoder.Convert( - video, - outputFile, - VideoType.Ogv, - Speed.UltraFast, - VideoSize.Original, - AudioQuality.Hd, - true - ); - // TS conversion - encoder.Convert( - video, - outputFile, - VideoType.Ts - ); -} -``` - -Easily capture screens from your videos: -```csharp -static void Main(string[] args) -{ - string inputFile = "input_path_goes_here"; - FileInfo output = new FileInfo("output_path_goes_here"); - - var video = VideoInfo.FromPath(inputFile); - - new FFMpeg() - .Snapshot( - video, - output, - new Size(200, 400), - TimeSpan.FromMinutes(1) - ); -} -``` - -Join video parts: -```csharp -static void Main(string[] args) -{ - FFMpeg encoder = new FFMpeg(); - - encoder.Join( - new FileInfo(@"..\joined_video.mp4"), - VideoInfo.FromPath(@"..\part1.mp4"), - VideoInfo.FromPath(@"..\part2.mp4"), - VideoInfo.FromPath(@"..\part3.mp4") - ); -} -``` - -Join image sequences: -```csharp -static void Main(string[] args) -{ - FFMpeg encoder = new FFMpeg(); - - encoder.JoinImageSequence( - new FileInfo(@"..\joined_video.mp4"), - 1, // FPS - ImageInfo.FromPath(@"..\1.png"), - ImageInfo.FromPath(@"..\2.png"), - ImageInfo.FromPath(@"..\3.png") - ); -} -``` - -Strip audio track from videos: -```csharp -static void Main(string[] args) -{ - string inputFile = "input_path_goes_here", - outputFile = "output_path_goes_here"; - - new FFMpeg() - .Mute( - VideoInfo.FromPath(inputFile), - new FileInfo(outputFile) - ); -} -``` - -Save audio track from video: -```csharp -static void Main(string[] args) -{ - string inputVideoFile = "input_path_goes_here", - outputAudioFile = "output_path_goes_here"; - - new FFMpeg() - .ExtractAudio( - VideoInfo.FromPath(inputVideoFile), - new FileInfo(outputAudioFile) - ); -} -``` - -Add audio track to video: -```csharp -static void Main(string[] args) -{ - string inputVideoFile = "input_path_goes_here", - inputAudioFile = "input_path_goes_here", - outputVideoFile = "output_path_goes_here"; - - FFMpeg encoder = new FFMpeg(); - - new FFMpeg() - .ReplaceAudio( - VideoInfo.FromPath(inputVideoFile), - new FileInfo(inputAudioFile), - new FileInfo(outputVideoFile) - ); -} -``` - -Add poster image to audio file (good for youtube videos): -```csharp -static void Main(string[] args) -{ - string inputImageFile = "input_path_goes_here", - inputAudioFile = "input_path_goes_here", - outputVideoFile = "output_path_goes_here"; - - FFMpeg encoder = new FFMpeg(); - - ((Bitmap)Image.FromFile(inputImageFile)) - .AddAudio( - new FileInfo(inputAudioFile), - new FileInfo(outputVideoFile) - ); - - /* OR */ - - new FFMpeg() - .PosterWithAudio( - inputImageFile, - new FileInfo(inputAudioFile), - new FileInfo(outputVideoFile) - ); -} -``` - -Control over the 'FFmpeg' process doing the job: -```csharp -static void Main(string[] args) -{ - string inputVideoFile = "input_path_goes_here", - outputVideoFile = "input_path_goes_here"; - - FFMpeg encoder = new FFMpeg(); - - // start the conversion process - Task.Run(() => { - encoder.Convert(new VideoInfo(inputVideoFile), new FileInfo(outputVideoFile)); - }); - - // stop encoding after 2 seconds (only for example purposes) - Thread.Sleep(2000); - encoder.Stop(); -} -``` -### Enums - -Video Size enumeration: - -```csharp -public enum VideoSize -{ - HD, - FullHD, - ED, - LD, - Original -} -``` - -Speed enumeration: - -```csharp -public enum Speed -{ - VerySlow, - Slower, - Slow, - Medium, - Fast, - Faster, - VeryFast, - SuperFast, - UltraFast -} -``` -Audio codecs enumeration: - -```csharp -public enum AudioCodec -{ - Aac, - LibVorbis -} -``` - -Audio quality presets enumeration: - -```csharp -public enum AudioQuality -{ - Ultra = 384, - Hd = 192, - Normal = 128, - Low = 64 -} -``` - -Video codecs enumeration: - -```csharp -public enum VideoCodec -{ - LibX264, - LibVpx, - LibTheora, - Png, - MpegTs -} -``` -### ArgumentBuilder -Custom video converting presets could be created with help of `ArgumentsContainer` class: -```csharp -var container = new ArgumentsContainer(); -container.Add(new VideoCodecArgument(VideoCodec.LibX264)); -container.Add(new ScaleArgument(VideoSize.Hd)); - -var ffmpeg = new FFMpeg(); -var result = ffmpeg.Convert(container, new FileInfo("input.mp4"), new FileInfo("output.mp4")); -``` - -Other availible arguments could be found in `FFMpegCore.FFMPEG.Arguments` namespace. - -If you need to create your custom argument, you just need to create new class, that is inherited from `Argument`, `Argument` or `Argument` -For example: -```csharp -public class OverrideArgument : Argument -{ - public override string GetStringValue() - { - return "-y"; - } -} -``` ## Contributors @@ -393,5 +212,6 @@ public class OverrideArgument : Argument ### License -Copyright © 2018, [Vlad Jerca](https://github.com/vladjerca). -Released under the [MIT license](https://github.com/jonschlinkert/github-contributors/blob/master/LICENSE). +Copyright © 2020 + +Released under [MIT license](https://github.com/rosenbjerg/FFMpegCore/blob/master/LICENSE) diff --git a/pack.sh b/pack.sh deleted file mode 100644 index 8bda521..0000000 --- a/pack.sh +++ /dev/null @@ -1 +0,0 @@ -./.nuget/nuget.exe pack ./FFMpegCore/ -Prop Configuration=Release \ No newline at end of file