From 73531b394742ab4d67098712109a0df7ae8247cb Mon Sep 17 00:00:00 2001 From: Thierry Fleury Date: Sun, 28 Feb 2021 14:50:55 +0100 Subject: [PATCH 01/22] Add InputDeviceArgument --- .../FFMpeg/Arguments/InputDeviceArgument.cs | 26 +++++++++++++++++++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 1 + 2 files changed, 27 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs diff --git a/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs new file mode 100644 index 0000000..f276bbb --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents an input device parameter + /// + public class InputDeviceArgument : IInputArgument + { + private readonly string Device; + + public InputDeviceArgument(string device) + { + Device = device; + } + + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; + + public void Pre() { } + + public void Post() { } + + public string Text => $"-i {Device}"; + } +} diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 44e20d2..8d62db0 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -23,6 +23,7 @@ private FFMpegArguments() { } public static FFMpegArguments FromFileInput(FileInfo fileInfo, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(fileInfo.FullName, false), addArguments); public static FFMpegArguments FromFileInput(IMediaAnalysis mediaAnalysis, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(mediaAnalysis.Path, false), addArguments); public static FFMpegArguments FromUrlInput(Uri uri, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); + public static FFMpegArguments FromDeviceInput(string device, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputDeviceArgument(device), addArguments); public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments); From 157a53690f92550ef3852c73dab239a25bcc979d Mon Sep 17 00:00:00 2001 From: Thierry Fleury Date: Sun, 28 Feb 2021 19:28:35 +0100 Subject: [PATCH 02/22] Add OutputStreamArgument (cherry picked from commit 0c64c4d81d7055a582d8377123dbc3b7ba86e444) --- .../FFMpeg/Arguments/OutputStreamArgument.cs | 26 +++++++++++++++++++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs diff --git a/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs new file mode 100644 index 0000000..5581929 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents output stream parameter + /// + public class OutputStreamArgument : IOutputArgument + { + public readonly string Stream; + + public OutputStreamArgument(string stream) + { + Stream = stream; + } + + public void Post() { } + + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; + + public void Pre() { } + + public string Text => Stream; + } +} diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 44e20d2..d2609eb 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -51,6 +51,8 @@ private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments = null) => ToProcessor(new OutputArgument(file, overwrite), addArguments); public FFMpegArgumentProcessor OutputToFile(Uri uri, bool overwrite = true, Action? addArguments = null) => ToProcessor(new OutputArgument(uri.AbsolutePath, overwrite), addArguments); + public FFMpegArgumentProcessor OutputToStream(string uri, Action? addArguments = null) => ToProcessor(new OutputStreamArgument(uri), addArguments); + public FFMpegArgumentProcessor OutputToStream(Uri uri, Action? addArguments = null) => ToProcessor(new OutputStreamArgument(uri.ToString()), addArguments); public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader, Action? addArguments = null) => ToProcessor(new OutputPipeArgument(reader), addArguments); private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action? addArguments) From 9672713e633ddcbaf7555e067bb7e7531624f444 Mon Sep 17 00:00:00 2001 From: Thierry Fleury Date: Tue, 2 Mar 2021 19:33:19 +0100 Subject: [PATCH 03/22] Add cancel timeout (cherry picked from commit 6383164f267516fbd50d50b2a511c15c25a168dc) --- FFMpegCore.Test/VideoTest.cs | 48 ++++++++++++++++---- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 26 +++++++---- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index eb5b46b..958f04e 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -12,6 +12,7 @@ using FFMpegCore.Arguments; using FFMpegCore.Exceptions; using FFMpegCore.Pipes; +using System.Threading; namespace FFMpegCore.Test { @@ -596,24 +597,55 @@ public void Video_TranscodeInMemory() public async Task Video_Cancel_Async() { var outputFile = new TemporaryFile("out.mp4"); - + var task = FFMpegArguments - .FromFileInput(TestResources.Mp4Video) + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) .OutputToFile(outputFile, false, opt => opt - .Resize(new Size(1000, 1000)) .WithAudioCodec(AudioCodec.Aac) .WithVideoCodec(VideoCodec.LibX264) - .WithConstantRateFactor(14) - .WithSpeedPreset(Speed.VerySlow) - .Loop(3)) + .WithSpeedPreset(Speed.VeryFast)) .CancellableThrough(out var cancel) .ProcessAsynchronously(false); - + await Task.Delay(300); cancel(); - + var result = await task; + Assert.IsFalse(result); } + + [TestMethod, Timeout(10000)] + public async Task Video_Cancel_Async_With_Timeout() + { + var outputFile = new TemporaryFile("out.mp4"); + + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(out var cancel, 10000) + .ProcessAsynchronously(false); + + await Task.Delay(300); + cancel(); + + var result = await task; + + var outputInfo = FFProbe.Analyse(outputFile); + + Assert.IsTrue(result); + Assert.IsNotNull(outputInfo); + Assert.AreEqual(320, outputInfo.PrimaryVideoStream.Width); + Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); + Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName); + Assert.AreEqual("aac", outputInfo.PrimaryAudioStream.CodecName); + } } } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index cfbe42a..5161fd4 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -27,7 +27,7 @@ internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) public string Arguments => _ffMpegArguments.Text; - private event EventHandler CancelEvent = null!; + private event EventHandler CancelEvent = null!; public FFMpegArgumentProcessor NotifyOnProgress(Action onPercentageProgress, TimeSpan totalTimeSpan) { @@ -45,9 +45,9 @@ public FFMpegArgumentProcessor NotifyOnOutput(Action onOutput) _onOutput = onOutput; return this; } - public FFMpegArgumentProcessor CancellableThrough(out Action cancel) + public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0) { - cancel = () => CancelEvent?.Invoke(this, EventArgs.Empty); + cancel = () => CancelEvent?.Invoke(this, timeout); return this; } public bool ProcessSynchronously(bool throwOnError = true) @@ -55,11 +55,15 @@ public bool ProcessSynchronously(bool throwOnError = true) using var instance = PrepareInstance(out var cancellationTokenSource); var errorCode = -1; - void OnCancelEvent(object sender, EventArgs args) + void OnCancelEvent(object sender, int timeout) { instance.SendInput("q"); - cancellationTokenSource.Cancel(); - instance.Started = false; + + if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) + { + cancellationTokenSource.Cancel(); + instance.Started = false; + } } CancelEvent += OnCancelEvent; instance.Exited += delegate { cancellationTokenSource.Cancel(); }; @@ -102,11 +106,15 @@ public async Task ProcessAsynchronously(bool throwOnError = true) using var instance = PrepareInstance(out var cancellationTokenSource); var errorCode = -1; - void OnCancelEvent(object sender, EventArgs args) + void OnCancelEvent(object sender, int timeout) { instance.SendInput("q"); - cancellationTokenSource.Cancel(); - instance.Started = false; + + if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) + { + cancellationTokenSource.Cancel(); + instance.Started = false; + } } CancelEvent += OnCancelEvent; From 4f2898397268193555a63507a98814f61c9623df Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 5 Mar 2021 18:06:40 +0100 Subject: [PATCH 04/22] Init --- FFMpegCore.Test/ArgumentBuilderTest.cs | 188 ++++++++---- FFMpegCore.Test/FFProbeTests.cs | 16 +- FFMpegCore.Test/TasksExtensions.cs | 10 - .../{ => Utilities}/BitmapSources.cs | 0 FFMpegCore.Test/VideoTest.cs | 273 ++++++++---------- .../FFMpeg/Arguments/DrawTextArgument.cs | 5 +- FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs | 7 +- FFMpegCore/FFMpeg/Arguments/SizeArgument.cs | 17 +- .../FFMpeg/Arguments/TransposeArgument.cs | 5 +- .../FFMpeg/Arguments/VideoFiltersArgument.cs | 50 ++++ .../FFMpeg/Exceptions/FFMpegException.cs | 44 ++- FFMpegCore/FFMpeg/FFMpeg.cs | 16 +- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 14 +- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 21 +- FFMpegCore/FFMpeg/FFMpegArguments.cs | 2 - FFMpegCore/FFMpeg/FFMpegOptions.cs | 2 +- FFMpegCore/FFMpegCore.csproj.DotSettings | 3 +- FFMpegCore/FFProbe/FFProbe.cs | 49 ++-- FFMpegCore/FFProbe/IMediaAnalysis.cs | 6 +- FFMpegCore/FFProbe/MediaAnalysis.cs | 12 +- FFMpegCore/Helpers/FFMpegHelper.cs | 16 +- 21 files changed, 432 insertions(+), 324 deletions(-) delete mode 100644 FFMpegCore.Test/TasksExtensions.cs rename FFMpegCore.Test/{ => Utilities}/BitmapSources.cs (100%) create mode 100644 FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index de625e2..543354f 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -8,7 +8,7 @@ namespace FFMpegCore.Test [TestClass] public class ArgumentBuilderTest { - private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4"}; + private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4" }; [TestMethod] @@ -21,28 +21,35 @@ public void Builder_BuildString_IO_1() [TestMethod] public void Builder_BuildString_Scale() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.Scale(VideoSize.Hd)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", true, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Scale(VideoSize.Hd))) + .Arguments; Assert.AreEqual("-i \"input.mp4\" -vf scale=-1:720 \"output.mp4\" -y", str); } - + [TestMethod] public void Builder_BuildString_AudioCodec() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.WithAudioCodec(AudioCodec.Aac)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", true, opt => opt.WithAudioCodec(AudioCodec.Aac)).Arguments; Assert.AreEqual("-i \"input.mp4\" -c:a aac \"output.mp4\" -y", str); } - + [TestMethod] public void Builder_BuildString_AudioBitrate() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.WithAudioBitrate(AudioQuality.Normal)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", true, opt => opt.WithAudioBitrate(AudioQuality.Normal)).Arguments; Assert.AreEqual("-i \"input.mp4\" -b:a 128k \"output.mp4\" -y", str); } - + [TestMethod] public void Builder_BuildString_Quiet() { - var str = FFMpegArguments.FromFileInput("input.mp4").WithGlobalOptions(opt => opt.WithVerbosityLevel()).OutputToFile("output.mp4", false).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").WithGlobalOptions(opt => opt.WithVerbosityLevel()) + .OutputToFile("output.mp4", false).Arguments; Assert.AreEqual("-hide_banner -loglevel error -i \"input.mp4\" \"output.mp4\"", str); } @@ -50,27 +57,32 @@ public void Builder_BuildString_Quiet() [TestMethod] public void Builder_BuildString_AudioCodec_Fluent() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithAudioCodec(AudioCodec.Aac).WithAudioBitrate(128)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, + opt => opt.WithAudioCodec(AudioCodec.Aac).WithAudioBitrate(128)).Arguments; Assert.AreEqual("-i \"input.mp4\" -c:a aac -b:a 128k \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_BitStream() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithBitStreamFilter(Channel.Audio, Filter.H264_Mp4ToAnnexB)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, + opt => opt.WithBitStreamFilter(Channel.Audio, Filter.H264_Mp4ToAnnexB)).Arguments; Assert.AreEqual("-i \"input.mp4\" -bsf:a h264_mp4toannexb \"output.mp4\"", str); } - + [TestMethod] public void Builder_BuildString_HardwareAcceleration_Auto() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithHardwareAcceleration()).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithHardwareAcceleration()).Arguments; Assert.AreEqual("-i \"input.mp4\" -hwaccel \"output.mp4\"", str); } + [TestMethod] public void Builder_BuildString_HardwareAcceleration_Specific() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithHardwareAcceleration(HardwareAccelerationDevice.CUVID)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, + opt => opt.WithHardwareAcceleration(HardwareAccelerationDevice.CUVID)).Arguments; Assert.AreEqual("-i \"input.mp4\" -hwaccel cuvid \"output.mp4\"", str); } @@ -84,140 +96,175 @@ public void Builder_BuildString_Concat() [TestMethod] public void Builder_BuildString_Copy_Audio() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Audio)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Audio)).Arguments; Assert.AreEqual("-i \"input.mp4\" -c:a copy \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Copy_Video() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Video)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Video)).Arguments; Assert.AreEqual("-i \"input.mp4\" -c:v copy \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Copy_Both() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.CopyChannel()).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.CopyChannel()).Arguments; Assert.AreEqual("-i \"input.mp4\" -c copy \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_DisableChannel_Audio() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Audio)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Audio)).Arguments; Assert.AreEqual("-i \"input.mp4\" -an \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_DisableChannel_Video() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Video)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Video)).Arguments; Assert.AreEqual("-i \"input.mp4\" -vn \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_AudioSamplingRate_Default() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate()).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate()).Arguments; Assert.AreEqual("-i \"input.mp4\" -ar 48000 \"output.mp4\"", str); } - + [TestMethod] public void Builder_BuildString_AudioSamplingRate() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate(44000)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate(44000)).Arguments; Assert.AreEqual("-i \"input.mp4\" -ar 44000 \"output.mp4\"", str); } - + [TestMethod] public void Builder_BuildString_VariableBitrate() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithVariableBitrate(5)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithVariableBitrate(5)).Arguments; Assert.AreEqual("-i \"input.mp4\" -vbr 5 \"output.mp4\"", str); } - + [TestMethod] public void Builder_BuildString_Faststart() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithFastStart()).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithFastStart()).Arguments; Assert.AreEqual("-i \"input.mp4\" -movflags faststart \"output.mp4\"", str); } - + [TestMethod] public void Builder_BuildString_Overwrite() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.OverwriteExisting()).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.OverwriteExisting()).Arguments; Assert.AreEqual("-i \"input.mp4\" -y \"output.mp4\"", str); } - + [TestMethod] public void Builder_BuildString_RemoveMetadata() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithoutMetadata()).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithoutMetadata()).Arguments; Assert.AreEqual("-i \"input.mp4\" -map_metadata -1 \"output.mp4\"", str); } - + [TestMethod] public void Builder_BuildString_Transpose() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Transpose(Transposition.CounterClockwise90)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Transpose(Transposition.CounterClockwise90))) + .Arguments; Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2\" \"output.mp4\"", str); } + [TestMethod] + public void Builder_BuildString_TransposeScale() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Transpose(Transposition.CounterClockwise90) + .Scale(200, 300))) + .Arguments; + Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2, scale=200:300\" \"output.mp4\"", str); + } + [TestMethod] public void Builder_BuildString_ForceFormat() { - var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)).OutputToFile("output.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)) + .OutputToFile("output.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)).Arguments; Assert.AreEqual("-f mp4 -i \"input.mp4\" -f mp4 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_FrameOutputCount() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithFrameOutputCount(50)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithFrameOutputCount(50)).Arguments; Assert.AreEqual("-i \"input.mp4\" -vframes 50 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_FrameRate() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithFramerate(50)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithFramerate(50)).Arguments; Assert.AreEqual("-i \"input.mp4\" -r 50 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Loop() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Loop(50)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Loop(50)) + .Arguments; Assert.AreEqual("-i \"input.mp4\" -loop 50 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Seek() { - var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).OutputToFile("output.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))) + .OutputToFile("output.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).Arguments; Assert.AreEqual("-ss 00:00:10 -i \"input.mp4\" -ss 00:00:10 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Shortest() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.UsingShortest()).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.UsingShortest()).Arguments; Assert.AreEqual("-i \"input.mp4\" -shortest \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Size() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Resize(1920, 1080)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.Resize(1920, 1080)).Arguments; Assert.AreEqual("-i \"input.mp4\" -s 1920x1080 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Speed() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithSpeedPreset(Speed.Fast)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithSpeedPreset(Speed.Fast)).Arguments; Assert.AreEqual("-i \"input.mp4\" -preset fast \"output.mp4\"", str); } @@ -227,18 +274,21 @@ public void Builder_BuildString_DrawtextFilter() var str = FFMpegArguments .FromFileInput("input.mp4") .OutputToFile("output.mp4", false, opt => opt - .DrawText(DrawTextOptions - .Create("Stack Overflow", "/path/to/font.ttf") - .WithParameter("fontcolor", "white") - .WithParameter("fontsize", "24") - .WithParameter("box", "1") - .WithParameter("boxcolor", "black@0.5") - .WithParameter("boxborderw", "5") - .WithParameter("x", "(w-text_w)/2") - .WithParameter("y", "(h-text_h)/2"))) + .WithVideoFilters(filterOptions => filterOptions + .DrawText(DrawTextOptions + .Create("Stack Overflow", "/path/to/font.ttf") + .WithParameter("fontcolor", "white") + .WithParameter("fontsize", "24") + .WithParameter("box", "1") + .WithParameter("boxcolor", "black@0.5") + .WithParameter("boxborderw", "5") + .WithParameter("x", "(w-text_w)/2") + .WithParameter("y", "(h-text_h)/2")))) .Arguments; - Assert.AreEqual("-i \"input.mp4\" -vf drawtext=\"text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=(h-text_h)/2\" \"output.mp4\"", str); + Assert.AreEqual( + "-i \"input.mp4\" -vf drawtext=\"text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=(h-text_h)/2\" \"output.mp4\"", + str); } [TestMethod] @@ -247,45 +297,53 @@ public void Builder_BuildString_DrawtextFilter_Alt() var str = FFMpegArguments .FromFileInput("input.mp4") .OutputToFile("output.mp4", false, opt => opt - .DrawText(DrawTextOptions - .Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24")))) + .WithVideoFilters(filterOptions => filterOptions + .DrawText(DrawTextOptions + .Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24"))))) .Arguments; - Assert.AreEqual("-i \"input.mp4\" -vf drawtext=\"text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24\" \"output.mp4\"", str); + Assert.AreEqual( + "-i \"input.mp4\" -vf drawtext=\"text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24\" \"output.mp4\"", + str); } - + [TestMethod] public void Builder_BuildString_StartNumber() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithStartNumber(50)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithStartNumber(50)).Arguments; Assert.AreEqual("-i \"input.mp4\" -start_number 50 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Threads_1() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.UsingThreads(50)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.UsingThreads(50)).Arguments; Assert.AreEqual("-i \"input.mp4\" -threads 50 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Threads_2() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.UsingMultithreading(true)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.UsingMultithreading(true)).Arguments; Assert.AreEqual($"-i \"input.mp4\" -threads {Environment.ProcessorCount} \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Codec() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithVideoCodec(VideoCodec.LibX264)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithVideoCodec(VideoCodec.LibX264)).Arguments; Assert.AreEqual("-i \"input.mp4\" -c:v libx264 \"output.mp4\"", str); } [TestMethod] public void Builder_BuildString_Codec_Override() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, opt => opt.WithVideoCodec(VideoCodec.LibX264).ForcePixelFormat("yuv420p")).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, + opt => opt.WithVideoCodec(VideoCodec.LibX264).ForcePixelFormat("yuv420p")).Arguments; Assert.AreEqual("-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\" -y", str); } @@ -293,17 +351,20 @@ public void Builder_BuildString_Codec_Override() [TestMethod] public void Builder_BuildString_Duration() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithDuration(TimeSpan.FromSeconds(20))).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithDuration(TimeSpan.FromSeconds(20))).Arguments; Assert.AreEqual("-i \"input.mp4\" -t 00:00:20 \"output.mp4\"", str); } - + [TestMethod] public void Builder_BuildString_Raw() { - var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.WithCustomArgument(null!)).OutputToFile("output.mp4", false, opt => opt.WithCustomArgument(null!)).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.WithCustomArgument(null!)) + .OutputToFile("output.mp4", false, opt => opt.WithCustomArgument(null!)).Arguments; Assert.AreEqual(" -i \"input.mp4\" \"output.mp4\"", str); - str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.WithCustomArgument("-acodec copy")).Arguments; + str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithCustomArgument("-acodec copy")).Arguments; Assert.AreEqual("-i \"input.mp4\" -acodec copy \"output.mp4\"", str); } @@ -311,7 +372,8 @@ public void Builder_BuildString_Raw() [TestMethod] public void Builder_BuildString_ForcePixelFormat() { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments; + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments; Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str); } } diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 21d9d34..5ac83a1 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Threading.Tasks; using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -23,16 +24,21 @@ public async Task Audio_FromStream_Duration() var streamAnalysis = await FFProbe.AnalyseAsync(inputStream); Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); } + + [TestMethod] + public async Task Uri_Duration() + { + var fileAnalysis = await FFProbe.AnalyseAsync(new Uri("https://github.com/rosenbjerg/FFMpegCore/raw/master/FFMpegCore.Test/Resources/input_3sec.webm")); + Assert.IsNotNull(fileAnalysis); + } [TestMethod] public void Probe_Success() { var info = FFProbe.Analyse(TestResources.Mp4Video); Assert.AreEqual(3, info.Duration.Seconds); - Assert.AreEqual(".mp4", info.Extension); - Assert.AreEqual(TestResources.Mp4Video, info.Path); - Assert.AreEqual("5.1", info.PrimaryAudioStream.ChannelLayout); + Assert.AreEqual("5.1", info.PrimaryAudioStream!.ChannelLayout); Assert.AreEqual(6, info.PrimaryAudioStream.Channels); Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName); Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName); @@ -40,7 +46,7 @@ public void Probe_Success() Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate); Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz); - Assert.AreEqual(1471810, info.PrimaryVideoStream.BitRate); + Assert.AreEqual(1471810, info.PrimaryVideoStream!.BitRate); Assert.AreEqual(16, info.PrimaryVideoStream.DisplayAspectRatio.Width); Assert.AreEqual(9, info.PrimaryVideoStream.DisplayAspectRatio.Height); Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat); diff --git a/FFMpegCore.Test/TasksExtensions.cs b/FFMpegCore.Test/TasksExtensions.cs deleted file mode 100644 index c9549ca..0000000 --- a/FFMpegCore.Test/TasksExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; - -namespace FFMpegCore.Test -{ - static class TasksExtensions - { - public static T WaitForResult(this Task task) => - task.ConfigureAwait(false).GetAwaiter().GetResult(); - } -} diff --git a/FFMpegCore.Test/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs similarity index 100% rename from FFMpegCore.Test/BitmapSources.cs rename to FFMpegCore.Test/Utilities/BitmapSources.cs diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index eb5b46b..ec80fe3 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -57,95 +57,6 @@ public bool Convert(ContainerFormat type, bool multithreaded = false, VideoSize ); } - private void ConvertFromStreamPipe(ContainerFormat type, params IArgument[] arguments) - { - using var outputFile = new TemporaryFile($"out{type.Extension}"); - - var input = FFProbe.Analyse(TestResources.WebmVideo); - using var inputStream = File.OpenRead(input.Path); - var processor = FFMpegArguments - .FromPipeInput(new StreamPipeSource(inputStream)) - .OutputToFile(outputFile, false, opt => - { - foreach (var arg in arguments) - opt.WithArgument(arg); - }); - - var scaling = arguments.OfType().FirstOrDefault(); - - var success = processor.ProcessSynchronously(); - - var outputVideo = FFProbe.Analyse(outputFile); - - Assert.IsTrue(success); - Assert.IsTrue(File.Exists(outputFile)); - Assert.IsTrue(Math.Abs((outputVideo.Duration - input.Duration).TotalMilliseconds) < 1000.0 / input.PrimaryVideoStream.FrameRate); - - if (scaling?.Size == null) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - else - { - if (scaling.Size.Value.Width != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); - } - - if (scaling.Size.Value.Height != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); - } - - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - } - - private void ConvertToStreamPipe(params IArgument[] arguments) - { - using var ms = new MemoryStream(); - var processor = FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToPipe(new StreamPipeSink(ms), opt => - { - foreach (var arg in arguments) - opt.WithArgument(arg); - }); - - var scaling = arguments.OfType().FirstOrDefault(); - - processor.ProcessSynchronously(); - - ms.Position = 0; - var outputVideo = FFProbe.Analyse(ms); - - var input = FFProbe.Analyse(TestResources.Mp4Video); - // Assert.IsTrue(Math.Abs((outputVideo.Duration - input.Duration).TotalMilliseconds) < 1000.0 / input.PrimaryVideoStream.FrameRate); - - if (scaling?.Size == null) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - else - { - if (scaling.Size.Value.Width != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); - } - - if (scaling.Size.Value.Height != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); - } - - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - } - public void Convert(ContainerFormat type, Action validationMethod, params IArgument[] arguments) { using var outputFile = new TemporaryFile($"out{type.Extension}"); @@ -195,45 +106,6 @@ public void Convert(ContainerFormat type, params IArgument[] inputArguments) Convert(type, null, inputArguments); } - public void ConvertFromPipe(ContainerFormat type, System.Drawing.Imaging.PixelFormat fmt, params IArgument[] arguments) - { - using var outputFile = new TemporaryFile($"out{type.Extension}"); - - var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, fmt, 256, 256)); - var processor = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(outputFile, false, opt => - { - foreach (var arg in arguments) - opt.WithArgument(arg); - }); - var scaling = arguments.OfType().FirstOrDefault(); - processor.ProcessSynchronously(); - - var outputVideo = FFProbe.Analyse(outputFile); - - Assert.IsTrue(File.Exists(outputFile)); - - if (scaling?.Size == null) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height); - } - else - { - if (scaling.Size.Value.Width != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); - } - - if (scaling.Size.Value.Height != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); - } - - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width); - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height); - } - } - [TestMethod, Timeout(10000)] public void Video_ToMP4() { @@ -258,13 +130,31 @@ public void Video_ToMP4_Args() [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) { - ConvertFromPipe(VideoType.Mp4, pixelFormat, new VideoCodecArgument(VideoCodec.LibX264)); + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); + var success = FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); } [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamPipe() { - ConvertFromStreamPipe(VideoType.Mp4, new VideoCodecArgument(VideoCodec.LibX264)); + using var input = File.OpenRead(TestResources.WebmVideo); + using var output = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments + .FromPipeInput(new StreamPipeSource(input)) + .OutputToFile(output, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); + + var outputVideo = FFProbe.Analyse(output); } [TestMethod, Timeout(10000)] @@ -286,8 +176,9 @@ public void Video_StreamFile_OutputToMemoryStream() var output = new MemoryStream(); FFMpegArguments - .FromPipeInput(new StreamPipeSource(File.OpenRead(TestResources.WebmVideo)), options => options.ForceFormat("webm")) - .OutputToPipe(new StreamPipeSink(output), options => options + .FromPipeInput(new StreamPipeSource(File.OpenRead(TestResources.WebmVideo)), opt => opt + .ForceFormat("webm")) + .OutputToPipe(new StreamPipeSink(output), opt => opt .ForceFormat("mpegts")) .ProcessSynchronously(); @@ -299,22 +190,31 @@ public void Video_StreamFile_OutputToMemoryStream() [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamOutputPipe_Failure() { - Assert.ThrowsException(() => ConvertToStreamPipe(new ForceFormatArgument("mkv"))); + Assert.ThrowsException(() => + { + using var ms = new MemoryStream(); + var processor = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToPipe(new StreamPipeSink(ms), opt => opt + .ForceFormat("mkv")) + .ProcessSynchronously(); + ms.Position = 0; + var outputVideo = FFProbe.Analyse(ms); + }); } [TestMethod, Timeout(10000)] - public void Video_ToMP4_Args_StreamOutputPipe_Async() + public async Task Video_ToMP4_Args_StreamOutputPipe_Async() { - using var ms = new MemoryStream(); + await using var ms = new MemoryStream(); var pipeSource = new StreamPipeSink(ms); - FFMpegArguments + await FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToPipe(pipeSource, opt => opt .WithVideoCodec(VideoCodec.LibX264) .ForceFormat("matroska")) - .ProcessAsynchronously() - .WaitForResult(); + .ProcessAsynchronously(); } [TestMethod, Timeout(10000)] @@ -334,7 +234,19 @@ await FFMpegArguments.FromFileInput(TestResources.Mp4Video) [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamOutputPipe() { - ConvertToStreamPipe(new VideoCodecArgument(VideoCodec.LibX264), new ForceFormatArgument("matroska")); + using var input = new MemoryStream(); + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToPipe(new StreamPipeSink(input), opt => opt + .WithVideoCodec(VideoCodec.LibVpx) + .ForceFormat("matroska")) + .ProcessSynchronously(); + Assert.IsTrue(success); + + input.Position = 0; + var inputAnalysis = FFProbe.Analyse(TestResources.Mp4Video); + var outputAnalysis = FFProbe.Analyse(input); + Assert.AreEqual(inputAnalysis.Duration.TotalSeconds, outputAnalysis.Duration.TotalSeconds, 0.3); } [TestMethod, Timeout(10000)] @@ -355,42 +267,73 @@ public void Video_ToTS_Args() [DataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - public void Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) + public async Task Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) { - ConvertFromPipe(VideoType.Ts, pixelFormat, new ForceFormatArgument(VideoType.Ts)); + using var output = new TemporaryFile($"out{VideoType.Ts.Extension}"); + var input = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); + + var success = await FFMpegArguments + .FromPipeInput(input) + .OutputToFile(output, false, opt => opt + .ForceFormat(VideoType.Ts)) + .ProcessAsynchronously(); + Assert.IsTrue(success); + + var analysis = await FFProbe.AnalyseAsync(output); + Assert.AreEqual(VideoType.Ts.Name, analysis.Format.FormatName); } [TestMethod, Timeout(10000)] - public void Video_ToOGV_Resize() + public async Task Video_ToOGV_Resize() { - Convert(VideoType.Ogv, true, VideoSize.Ed); - } - - [TestMethod, Timeout(10000)] - public void Video_ToOGV_Resize_Args() - { - Convert(VideoType.Ogv, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora)); + using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); + var success = await FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt + .Resize(VideoSize.Ed) + .WithVideoCodec(VideoCodec.LibTheora)) + .ProcessAsynchronously(); + Assert.IsTrue(success); } [DataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] // [DataRow(PixelFormat.Format48bppRgb)] - public void Video_ToOGV_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) + public void RawVideoPipeSource_Ogv_Scale(System.Drawing.Imaging.PixelFormat pixelFormat) { - ConvertFromPipe(VideoType.Ogv, pixelFormat, new ScaleArgument(VideoSize.Ed), new VideoCodecArgument(VideoCodec.LibTheora)); + using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); + var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); + + FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Scale(VideoSize.Ed)) + .WithVideoCodec(VideoCodec.LibTheora)) + .ProcessSynchronously(); + + var analysis = FFProbe.Analyse(outputFile); + Assert.Equals((int)VideoSize.Ed, analysis!.PrimaryVideoStream.Width); } [TestMethod, Timeout(10000)] - public void Video_ToMP4_Resize() + public void Scale_Mp4_Multithreaded() { - Convert(VideoType.Mp4, true, VideoSize.Ed); - } + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt + .UsingMultithreading(true) + .WithVideoFilters(filterOptions => filterOptions + .Scale(VideoSize.Ld)) + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); - [TestMethod, Timeout(10000)] - public void Video_ToMP4_Resize_Args() - { - Convert(VideoType.Mp4, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264)); + var analysis = FFProbe.Analyse(outputFile); + Assert.AreEqual((int)VideoSize.Ld, analysis!.PrimaryVideoStream.Width); } [DataTestMethod, Timeout(10000)] @@ -399,7 +342,19 @@ public void Video_ToMP4_Resize_Args() // [DataRow(PixelFormat.Format48bppRgb)] public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) { - ConvertFromPipe(VideoType.Mp4, pixelFormat, new ScaleArgument(VideoSize.Ld), new VideoCodecArgument(VideoCodec.LibX264)); + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); + + var success = FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .Resize(VideoSize.Ld) + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); + + var analysis = FFProbe.Analyse(outputFile); + Assert.AreEqual((int)VideoSize.Ld, analysis!.PrimaryVideoStream.Width); } [TestMethod, Timeout(10000)] diff --git a/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs index d4eabb8..c148328 100644 --- a/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs @@ -6,7 +6,7 @@ namespace FFMpegCore.Arguments /// /// Drawtext video filter argument /// - public class DrawTextArgument : IArgument + public class DrawTextArgument : IVideoFilterArgument { public readonly DrawTextOptions Options; @@ -15,7 +15,8 @@ public DrawTextArgument(DrawTextOptions options) Options = options; } - public string Text => $"-vf drawtext=\"{Options.TextInternal}\""; + public string Key { get; } = "drawtext"; + public string Value => Options.TextInternal; } public class DrawTextOptions diff --git a/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs b/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs index 40e98d0..6ed2b31 100644 --- a/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs @@ -6,7 +6,7 @@ namespace FFMpegCore.Arguments /// /// Represents scale parameter /// - public class ScaleArgument : IArgument + public class ScaleArgument : IVideoFilterArgument { public readonly Size? Size; public ScaleArgument(Size? size) @@ -18,9 +18,10 @@ public ScaleArgument(int width, int height) : this(new Size(width, height)) { } public ScaleArgument(VideoSize videosize) { - Size = videosize == VideoSize.Original ? new Size(-1, -1) : new Size(-1, (int)videosize); + Size = videosize == VideoSize.Original ? null : (Size?)new Size(-1, (int)videosize); } - public virtual string Text => Size.HasValue ? $"-vf scale={Size.Value.Width}:{Size.Value.Height}" : string.Empty; + public string Key { get; } = "scale"; + public string Value => Size == null ? string.Empty : $"{Size.Value.Width}:{Size.Value.Height}"; } } diff --git a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs index 2ccde92..e22f29c 100644 --- a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs @@ -6,14 +6,21 @@ namespace FFMpegCore.Arguments /// /// Represents size parameter /// - public class SizeArgument : ScaleArgument + public class SizeArgument : IArgument { - public SizeArgument(Size? value) : base(value) { } + public readonly Size? Size; + public SizeArgument(Size? size) + { + Size = size; + } - public SizeArgument(VideoSize videosize) : base(videosize) { } + public SizeArgument(int width, int height) : this(new Size(width, height)) { } - public SizeArgument(int width, int height) : base(width, height) { } + public SizeArgument(VideoSize videosize) + { + Size = videosize == VideoSize.Original ? new Size(-1, -1) : new Size(-1, (int)videosize); + } - public override string Text => Size.HasValue ? $"-s {Size.Value.Width}x{Size.Value.Height}" : string.Empty; + public string Text => Size == null ? string.Empty : $"-s {Size.Value.Width}x{Size.Value.Height}"; } } diff --git a/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs index acc26f4..bd15c47 100644 --- a/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs @@ -9,7 +9,7 @@ namespace FFMpegCore.Arguments /// 2 = 90CounterClockwise /// 3 = 90Clockwise and Vertical Flip /// - public class TransposeArgument : IArgument + public class TransposeArgument : IVideoFilterArgument { public readonly Transposition Transposition; public TransposeArgument(Transposition transposition) @@ -17,6 +17,7 @@ public TransposeArgument(Transposition transposition) Transposition = transposition; } - public string Text => $"-vf \"transpose={(int)Transposition}\""; + public string Key { get; } = "transpose"; + public string Value => ((int)Transposition).ToString(); } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs new file mode 100644 index 0000000..317d4be --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using FFMpegCore.Enums; +using FFMpegCore.Exceptions; + +namespace FFMpegCore.Arguments +{ + public class VideoFiltersArgument : IArgument + { + public readonly VideoFilterOptions Options; + + public VideoFiltersArgument(VideoFilterOptions options) + { + Options = options; + } + public string Text { get; set; } + + public string GetText() + { + if (!Options.Arguments.Any()) + throw new FFMpegArgumentException("No video-filter arguments provided"); + + return $"-vf \"{string.Join(", ", Options.Arguments.Where(arg => !string.IsNullOrEmpty(arg.Value)).Select(arg => $"{arg.Key}={arg.Value.Replace(",", "\\,")}"))}\""; + } + } + + public interface IVideoFilterArgument + { + public string Key { get; } + public string Value { get; } + } + + public class VideoFilterOptions + { + public List Arguments { get; } = new List(); + + public VideoFilterOptions Scale(VideoSize videoSize) => WithArgument(new ScaleArgument(videoSize)); + public VideoFilterOptions Scale(int width, int height) => WithArgument(new ScaleArgument(width, height)); + public VideoFilterOptions Scale(Size size) => WithArgument(new ScaleArgument(size)); + public VideoFilterOptions Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition)); + public VideoFilterOptions DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions)); + + private VideoFilterOptions WithArgument(IVideoFilterArgument argument) + { + Arguments.Add(argument); + return this; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs index fc154ac..bf7c41f 100644 --- a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs +++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs @@ -11,18 +11,52 @@ public enum FFMpegExceptionType Process } + public abstract class FFException : Exception + { + protected FFException(string message) : base(message) { } + protected FFException(string message, Exception innerException) : base(message, innerException) { } + } + public abstract class FFProcessException : FFException + { + protected FFProcessException(string process, int exitCode, string errorOutput) + : base($"{process} exited with non-zero exit-code {exitCode}\n{errorOutput}") + { + ExitCode = exitCode; + ErrorOutput = errorOutput; + } + + public int ExitCode { get; } + public string ErrorOutput { get; } + } + public class FFMpegProcessException : FFProcessException + { + public FFMpegProcessException(int exitCode, string errorOutput) + : base("ffmpeg", exitCode, errorOutput) { } + } + public class FFProbeProcessException : FFProcessException + { + public FFProbeProcessException(int exitCode, string errorOutput) + : base("ffprobe", exitCode, errorOutput) { } + } + public class FFMpegException : Exception { - public FFMpegException(FFMpegExceptionType type, string? message = null, Exception? innerException = null, string ffmpegErrorOutput = "", string ffmpegOutput = "") + public FFMpegException(FFMpegExceptionType type, string? message = null, Exception? innerException = null, string ffMpegErrorOutput = "") : base(message, innerException) { - FfmpegOutput = ffmpegOutput; - FfmpegErrorOutput = ffmpegErrorOutput; + FFMpegErrorOutput = ffMpegErrorOutput; Type = type; } public FFMpegExceptionType Type { get; } - public string FfmpegOutput { get; } - public string FfmpegErrorOutput { get; } + public string FFMpegErrorOutput { get; } + } + + public class FFMpegArgumentException : Exception + { + public FFMpegArgumentException(string? message = null, Exception? innerException = null) + : base(message, innerException) + { + } } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 90c2265..5de955a 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using FFMpegCore.Arguments; namespace FFMpegCore { @@ -171,7 +172,8 @@ public static bool Convert( .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibX264) .WithVideoBitrate(2400) - .Scale(outputSize) + .WithVideoFilters(filterOptions => filterOptions + .Scale(outputSize)) .WithSpeedPreset(speed) .WithAudioCodec(AudioCodec.Aac) .WithAudioBitrate(audioQuality)) @@ -182,7 +184,8 @@ public static bool Convert( .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibTheora) .WithVideoBitrate(2400) - .Scale(outputSize) + .WithVideoFilters(filterOptions => filterOptions + .Scale(outputSize)) .WithSpeedPreset(speed) .WithAudioCodec(AudioCodec.LibVorbis) .WithAudioBitrate(audioQuality)) @@ -200,7 +203,8 @@ public static bool Convert( .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibVpx) .WithVideoBitrate(2400) - .Scale(outputSize) + .WithVideoFilters(filterOptions => filterOptions + .Scale(outputSize)) .WithSpeedPreset(speed) .WithAudioCodec(AudioCodec.LibVorbis) .WithAudioBitrate(audioQuality)) @@ -398,7 +402,7 @@ internal static IReadOnlyList GetPixelFormatsInternal() FFMpegHelper.RootExceptionCheck(); var list = new List(); - using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), "-pix_fmts"); + using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), "-pix_fmts"); instance.DataReceived += (e, args) => { if (PixelFormat.TryParse(args.Data, out var format)) @@ -443,7 +447,7 @@ private static void ParsePartOfCodecs(Dictionary codecs, string a { FFMpegHelper.RootExceptionCheck(); - using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), arguments); + using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), arguments); instance.DataReceived += (e, args) => { var codec = parser(args.Data); @@ -527,7 +531,7 @@ internal static IReadOnlyList GetContainersFormatsInternal() FFMpegHelper.RootExceptionCheck(); var list = new List(); - using var instance = new Instances.Instance(FFMpegOptions.Options.FFmpegBinary(), "-formats"); + using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), "-formats"); instance.DataReceived += (e, args) => { if (ContainerFormat.TryParse(args.Data, out var fmt)) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index e41f8ee..9872e0a 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -20,9 +20,7 @@ internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions Resize(int width, int height) => WithArgument(new SizeArgument(width, height)); public FFMpegArgumentOptions Resize(Size? size) => WithArgument(new SizeArgument(size)); - public FFMpegArgumentOptions Scale(VideoSize videoSize) => WithArgument(new ScaleArgument(videoSize)); - public FFMpegArgumentOptions Scale(int width, int height) => WithArgument(new ScaleArgument(width, height)); - public FFMpegArgumentOptions Scale(Size size) => WithArgument(new ScaleArgument(size)); + public FFMpegArgumentOptions WithBitStreamFilter(Channel channel, Filter filter) => WithArgument(new BitStreamFilterArgument(channel, filter)); public FFMpegArgumentOptions WithConstantRateFactor(int crf) => WithArgument(new ConstantRateFactorArgument(crf)); @@ -40,6 +38,13 @@ internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions WithVideoCodec(Codec videoCodec) => WithArgument(new VideoCodecArgument(videoCodec)); public FFMpegArgumentOptions WithVideoCodec(string videoCodec) => WithArgument(new VideoCodecArgument(videoCodec)); public FFMpegArgumentOptions WithVideoBitrate(int bitrate) => WithArgument(new VideoBitrateArgument(bitrate)); + public FFMpegArgumentOptions WithVideoFilters(Action videoFilterOptions) + { + var videoFilterOptionsObj = new VideoFilterOptions(); + videoFilterOptions(videoFilterOptionsObj); + return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj)); + } + public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate)); public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument()); public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed)); @@ -47,7 +52,6 @@ internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions WithCustomArgument(string argument) => WithArgument(new CustomArgument(argument)); public FFMpegArgumentOptions Seek(TimeSpan? seekTo) => WithArgument(new SeekArgument(seekTo)); - public FFMpegArgumentOptions Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition)); public FFMpegArgumentOptions Loop(int times) => WithArgument(new LoopArgument(times)); public FFMpegArgumentOptions OverwriteExisting() => WithArgument(new OverwriteArgument()); @@ -56,8 +60,6 @@ internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); public FFMpegArgumentOptions ForcePixelFormat(PixelFormat pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); - public FFMpegArgumentOptions DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions)); - public FFMpegArgumentOptions WithArgument(IArgument argument) { Arguments.Add(argument); diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index cfbe42a..9d0a4ad 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -83,18 +83,18 @@ void OnCancelEvent(object sender, EventArgs args) CancelEvent -= OnCancelEvent; } - return HandleCompletion(throwOnError, errorCode, instance.ErrorData, instance.OutputData); + return HandleCompletion(throwOnError, errorCode, instance.ErrorData); } - private bool HandleCompletion(bool throwOnError, int errorCode, IReadOnlyList errorData, IReadOnlyList outputData) + private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) { - if (throwOnError && errorCode != 0) - throw new FFMpegException(FFMpegExceptionType.Conversion, "FFMpeg exited with non-zero exitcode.", null, string.Join("\n", errorData), string.Join("\n", outputData)); + if (throwOnError && exitCode != 0) + throw new FFMpegProcessException(exitCode, string.Join("\n", errorData)); _onPercentageProgress?.Invoke(100.0); if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value); - return errorCode == 0; + return exitCode == 0; } public async Task ProcessAsynchronously(bool throwOnError = true) @@ -122,14 +122,14 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => } catch (Exception e) { - if (!HandleException(throwOnError, e, instance.ErrorData, instance.OutputData)) return false; + if (!HandleException(throwOnError, e, instance.ErrorData)) return false; } finally { CancelEvent -= OnCancelEvent; } - return HandleCompletion(throwOnError, errorCode, instance.ErrorData, instance.OutputData); + return HandleCompletion(throwOnError, errorCode, instance.ErrorData); } private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSource) @@ -138,7 +138,7 @@ private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSo FFMpegHelper.VerifyFFMpegExists(); var startInfo = new ProcessStartInfo { - FileName = FFMpegOptions.Options.FFmpegBinary(), + FileName = FFMpegOptions.Options.FFMpegBinary(), Arguments = _ffMpegArguments.Text, StandardOutputEncoding = FFMpegOptions.Options.Encoding, StandardErrorEncoding = FFMpegOptions.Options.Encoding, @@ -153,12 +153,13 @@ private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSo } - private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData, IReadOnlyList outputData) + private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData) { if (!throwOnError) return false; - throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, string.Join("\n", errorData), string.Join("\n", outputData)); + throw new FFMpegProcessException(exitCode, string.Join("\n", errorData)); + throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, string.Join("\n", errorData)); } private void OutputData(object sender, (DataType Type, string Data) msg) diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 44e20d2..37dc36f 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -21,7 +21,6 @@ private FFMpegArguments() { } 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); @@ -36,7 +35,6 @@ public FFMpegArguments WithGlobalOptions(Action configureOp public FFMpegArguments AddDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) => WithInput(new DemuxConcatArgument(filePaths), addArguments); public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) => WithInput(new InputArgument(verifyExists, filePath), addArguments); public FFMpegArguments AddFileInput(FileInfo fileInfo, Action? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments); - public FFMpegArguments AddFileInput(IMediaAnalysis mediaAnalysis, Action? addArguments = null) => WithInput(new InputArgument(mediaAnalysis.Path, false), addArguments); public FFMpegArguments AddUrlInput(Uri uri, Action? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments); diff --git a/FFMpegCore/FFMpeg/FFMpegOptions.cs b/FFMpegCore/FFMpeg/FFMpegOptions.cs index a7d29b4..4a09d7a 100644 --- a/FFMpegCore/FFMpeg/FFMpegOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegOptions.cs @@ -44,7 +44,7 @@ static FFMpegOptions() public bool UseCache { get; set; } = true; public Encoding Encoding { get; set; } = Encoding.Default; - public string FFmpegBinary() => FFBinary("FFMpeg"); + public string FFMpegBinary() => FFBinary("FFMpeg"); public string FFProbeBinary() => FFBinary("FFProbe"); diff --git a/FFMpegCore/FFMpegCore.csproj.DotSettings b/FFMpegCore/FFMpegCore.csproj.DotSettings index 69be7ec..7a8d17a 100644 --- a/FFMpegCore/FFMpegCore.csproj.DotSettings +++ b/FFMpegCore/FFMpegCore.csproj.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 8bb68db..573a2ad 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -12,22 +12,28 @@ namespace FFMpegCore { public static class FFProbe { - public static IMediaAnalysis? Analyse(string filePath, int outputCapacity = int.MaxValue) + public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue) { 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); + var exitCode = instance.BlockUntilFinished(); + if (exitCode != 0) + throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData)); + + return ParseOutput(instance); } - public static IMediaAnalysis? Analyse(Uri uri, int outputCapacity = int.MaxValue) + public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue) { using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); - instance.BlockUntilFinished(); - return ParseOutput(uri.AbsoluteUri, instance); + var exitCode = instance.BlockUntilFinished(); + if (exitCode != 0) + throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData)); + + return ParseOutput(instance); } - public static IMediaAnalysis? Analyse(Stream stream, int outputCapacity = int.MaxValue) + public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue) { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); @@ -46,26 +52,26 @@ public static class FFProbe } var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult(); if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}", null, string.Join("\n", instance.ErrorData), string.Join("\n", instance.OutputData)); + throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData)); - return ParseOutput(pipeArgument.PipePath, instance); + return ParseOutput(instance); } - public static async Task AnalyseAsync(string filePath, int outputCapacity = int.MaxValue) + public static async Task AnalyseAsync(string filePath, int outputCapacity = int.MaxValue) { 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); + return ParseOutput(instance); } - public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue) + public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue) { using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); await instance.FinishedRunning(); - return ParseOutput(uri.AbsoluteUri, instance); + return ParseOutput(instance); } - public static async Task AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue) + public static async Task AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue) { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); @@ -86,21 +92,24 @@ public static class FFProbe } var exitCode = await task; if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}", null, string.Join("\n", instance.ErrorData), string.Join("\n", instance.OutputData)); + throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}", null, string.Join("\n", instance.ErrorData)); pipeArgument.Post(); - return ParseOutput(pipeArgument.PipePath, instance); + return ParseOutput(instance); } - private static IMediaAnalysis? ParseOutput(string filePath, Instance instance) + private static IMediaAnalysis ParseOutput(Instance instance) { var json = string.Join(string.Empty, instance.OutputData); var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true - })!; - if (ffprobeAnalysis?.Format == null) return null; - return new MediaAnalysis(filePath, ffprobeAnalysis); + }); + + if (ffprobeAnalysis?.Format == null) + throw new Exception(); + + return new MediaAnalysis(ffprobeAnalysis); } private static Instance PrepareInstance(string filePath, int outputCapacity) diff --git a/FFMpegCore/FFProbe/IMediaAnalysis.cs b/FFMpegCore/FFProbe/IMediaAnalysis.cs index 660d776..4e67d4f 100644 --- a/FFMpegCore/FFProbe/IMediaAnalysis.cs +++ b/FFMpegCore/FFProbe/IMediaAnalysis.cs @@ -5,12 +5,10 @@ namespace FFMpegCore { public interface IMediaAnalysis { - string Path { get; } - string Extension { get; } TimeSpan Duration { get; } MediaFormat Format { get; } - AudioStream PrimaryAudioStream { get; } - VideoStream PrimaryVideoStream { get; } + AudioStream? PrimaryAudioStream { get; } + VideoStream? PrimaryVideoStream { get; } List VideoStreams { get; } List AudioStreams { get; } } diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 5a43aa2..f1b2f82 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -9,14 +9,11 @@ internal class MediaAnalysis : IMediaAnalysis { private static readonly Regex DurationRegex = new Regex("^(\\d{1,2}:\\d{1,2}:\\d{1,2}(.\\d{1,7})?)", RegexOptions.Compiled); - internal MediaAnalysis(string path, FFProbeAnalysis analysis) + internal MediaAnalysis(FFProbeAnalysis analysis) { Format = ParseFormat(analysis.Format); VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList(); AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList(); - PrimaryVideoStream = VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault(); - PrimaryAudioStream = AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault(); - Path = path; } private MediaFormat ParseFormat(Format analysisFormat) @@ -33,9 +30,6 @@ private MediaFormat ParseFormat(Format analysisFormat) }; } - public string Path { get; } - public string Extension => System.IO.Path.GetExtension(Path); - public TimeSpan Duration => new[] { Format.Duration, @@ -44,9 +38,9 @@ private MediaFormat ParseFormat(Format analysisFormat) }.Max(); public MediaFormat Format { get; } - public AudioStream PrimaryAudioStream { get; } + public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault(); - public VideoStream PrimaryVideoStream { get; } + public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault(); public List VideoStreams { get; } public List AudioStreams { get; } diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs index f2a214e..676f6d3 100644 --- a/FFMpegCore/Helpers/FFMpegHelper.cs +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -11,21 +11,15 @@ public static class FFMpegHelper private static bool _ffmpegVerified; public static void ConversionSizeExceptionCheck(Image image) - { - ConversionSizeExceptionCheck(image.Size); - } + => ConversionSizeExceptionCheck(image.Size.Width, image.Size.Height); public static void ConversionSizeExceptionCheck(IMediaAnalysis info) - { - ConversionSizeExceptionCheck(new Size(info.PrimaryVideoStream.Width, info.PrimaryVideoStream.Height)); - } + => ConversionSizeExceptionCheck(info.PrimaryVideoStream!.Width, info.PrimaryVideoStream.Height); - private static void ConversionSizeExceptionCheck(Size size) + private static void ConversionSizeExceptionCheck(int width, int height) { - if (size.Height % 2 != 0 || size.Width % 2 != 0 ) - { + if (height % 2 != 0 || width % 2 != 0 ) throw new ArgumentException("FFMpeg yuv420p encoding requires the width and height to be a multiple of 2!"); - } } public static void ExtensionExceptionCheck(string filename, string extension) @@ -45,7 +39,7 @@ public static void RootExceptionCheck() public static void VerifyFFMpegExists() { if (_ffmpegVerified) return; - var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFmpegBinary(), "-version"); + 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"); } From e49290b217e5b9c8da82a6cca3cdba1e4a7b5781 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 6 Mar 2021 21:25:17 +0100 Subject: [PATCH 05/22] Fix tests --- FFMpegCore.Test/ArgumentBuilderTest.cs | 6 +- FFMpegCore.Test/VideoTest.cs | 55 ++++++------- FFMpegCore/FFMpeg/Arguments/SizeArgument.cs | 5 -- .../FFMpeg/Arguments/VideoFiltersArgument.cs | 3 +- FFMpegCore/FFMpeg/FFMpeg.cs | 78 +++++++++---------- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 2 - FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 3 +- FFMpegCore/FFMpegCore.csproj | 2 +- 8 files changed, 73 insertions(+), 81 deletions(-) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 543354f..f0792ef 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -26,7 +26,7 @@ public void Builder_BuildString_Scale() .WithVideoFilters(filterOptions => filterOptions .Scale(VideoSize.Hd))) .Arguments; - Assert.AreEqual("-i \"input.mp4\" -vf scale=-1:720 \"output.mp4\" -y", str); + Assert.AreEqual("-i \"input.mp4\" -vf \"scale=-1:720\" \"output.mp4\" -y", str); } [TestMethod] @@ -287,7 +287,7 @@ public void Builder_BuildString_DrawtextFilter() .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\"", + "-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); } @@ -303,7 +303,7 @@ public void Builder_BuildString_DrawtextFilter_Alt() .Arguments; Assert.AreEqual( - "-i \"input.mp4\" -vf drawtext=\"text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24\" \"output.mp4\"", + "-i \"input.mp4\" -vf \"drawtext=text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24\" \"output.mp4\"", str); } diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 33f51e4..ec6b8e2 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -24,7 +24,7 @@ public bool Convert(ContainerFormat type, bool multithreaded = false, VideoSize using var outputFile = new TemporaryFile($"out{type.Extension}"); var input = FFProbe.Analyse(TestResources.Mp4Video); - FFMpeg.Convert(input, outputFile, type, size: size, multithreaded: multithreaded); + FFMpeg.Convert(TestResources.Mp4Video, outputFile, type, size: size, multithreaded: multithreaded); var outputVideo = FFProbe.Analyse(outputFile); Assert.IsTrue(File.Exists(outputFile)); @@ -116,6 +116,16 @@ public void Video_ToMP4() [TestMethod, Timeout(10000)] public void Video_ToMP4_YUV444p() { + using var outputFile = new TemporaryFile($"out{VideoType.WebM.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); + var analysis = FFProbe.Analyse(outputFile); + Convert(VideoType.Mp4, (a) => Assert.IsTrue(a.VideoStreams.First().PixelFormat == "yuv444p"), new ForcePixelFormat("yuv444p")); } @@ -161,13 +171,13 @@ public void Video_ToMP4_Args_StreamPipe() [TestMethod, Timeout(10000)] public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure() { - await Assert.ThrowsExceptionAsync(async () => + await Assert.ThrowsExceptionAsync(async () => { await using var ms = new MemoryStream(); var pipeSource = new StreamPipeSink(ms); await FFMpegArguments .FromFileInput(TestResources.Mp4Video) - .OutputToPipe(pipeSource, opt => opt.ForceFormat("mkv")) + .OutputToPipe(pipeSource, opt => opt.ForceFormat("mp4")) .ProcessAsynchronously(); }); } @@ -191,7 +201,7 @@ public void Video_StreamFile_OutputToMemoryStream() [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamOutputPipe_Failure() { - Assert.ThrowsException(() => + Assert.ThrowsException(() => { using var ms = new MemoryStream(); var processor = FFMpegArguments @@ -221,11 +231,13 @@ await FFMpegArguments [TestMethod, Timeout(10000)] public async Task TestDuplicateRun() { - FFMpegArguments.FromFileInput(TestResources.Mp4Video) + FFMpegArguments + .FromFileInput(TestResources.Mp4Video) .OutputToFile("temporary.mp4") .ProcessSynchronously(); - await FFMpegArguments.FromFileInput(TestResources.Mp4Video) + await FFMpegArguments + .FromFileInput(TestResources.Mp4Video) .OutputToFile("temporary.mp4") .ProcessAsynchronously(); @@ -233,20 +245,20 @@ await FFMpegArguments.FromFileInput(TestResources.Mp4Video) } [TestMethod, Timeout(10000)] - public void Video_ToMP4_Args_StreamOutputPipe() + public void TranscodeToMemoryStream_Success() { - using var input = new MemoryStream(); + using var output = new MemoryStream(); var success = FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToPipe(new StreamPipeSink(input), opt => opt + .FromFileInput(TestResources.WebmVideo) + .OutputToPipe(new StreamPipeSink(output), opt => opt .WithVideoCodec(VideoCodec.LibVpx) .ForceFormat("matroska")) .ProcessSynchronously(); Assert.IsTrue(success); - input.Position = 0; - var inputAnalysis = FFProbe.Analyse(TestResources.Mp4Video); - var outputAnalysis = FFProbe.Analyse(input); + output.Position = 0; + var inputAnalysis = FFProbe.Analyse(TestResources.WebmVideo); + var outputAnalysis = FFProbe.Analyse(output); Assert.AreEqual(inputAnalysis.Duration.TotalSeconds, outputAnalysis.Duration.TotalSeconds, 0.3); } @@ -291,7 +303,7 @@ public async Task Video_ToOGV_Resize() var success = await FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToFile(outputFile, false, opt => opt - .Resize(VideoSize.Ed) + .Resize(200, 200) .WithVideoCodec(VideoCodec.LibTheora)) .ProcessAsynchronously(); Assert.IsTrue(success); @@ -315,7 +327,7 @@ public void RawVideoPipeSource_Ogv_Scale(System.Drawing.Imaging.PixelFormat pixe .ProcessSynchronously(); var analysis = FFProbe.Analyse(outputFile); - Assert.Equals((int)VideoSize.Ed, analysis!.PrimaryVideoStream.Width); + Assert.AreEqual((int)VideoSize.Ed, analysis!.PrimaryVideoStream.Width); } [TestMethod, Timeout(10000)] @@ -327,14 +339,9 @@ public void Scale_Mp4_Multithreaded() .FromFileInput(TestResources.Mp4Video) .OutputToFile(outputFile, false, opt => opt .UsingMultithreading(true) - .WithVideoFilters(filterOptions => filterOptions - .Scale(VideoSize.Ld)) .WithVideoCodec(VideoCodec.LibX264)) .ProcessSynchronously(); Assert.IsTrue(success); - - var analysis = FFProbe.Analyse(outputFile); - Assert.AreEqual((int)VideoSize.Ld, analysis!.PrimaryVideoStream.Width); } [DataTestMethod, Timeout(10000)] @@ -349,13 +356,9 @@ public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixe var success = FFMpegArguments .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt - .Resize(VideoSize.Ld) .WithVideoCodec(VideoCodec.LibX264)) .ProcessSynchronously(); Assert.IsTrue(success); - - var analysis = FFProbe.Analyse(outputFile); - Assert.AreEqual((int)VideoSize.Ld, analysis!.PrimaryVideoStream.Width); } [TestMethod, Timeout(10000)] @@ -386,7 +389,7 @@ public void Video_ToOGV_MultiThread() public void Video_Snapshot_InMemory() { var input = FFProbe.Analyse(TestResources.Mp4Video); - using var bitmap = FFMpeg.Snapshot(input); + using var bitmap = FFMpeg.Snapshot(TestResources.Mp4Video); Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); @@ -399,7 +402,7 @@ public void Video_Snapshot_PersistSnapshot() var outputPath = new TemporaryFile("out.png"); var input = FFProbe.Analyse(TestResources.Mp4Video); - FFMpeg.Snapshot(input, outputPath); + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); using var bitmap = Image.FromFile(outputPath); Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); diff --git a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs index e22f29c..04fe615 100644 --- a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs @@ -16,11 +16,6 @@ public SizeArgument(Size? size) public SizeArgument(int width, int height) : this(new Size(width, height)) { } - public SizeArgument(VideoSize videosize) - { - Size = videosize == VideoSize.Original ? new Size(-1, -1) : new Size(-1, (int)videosize); - } - public string Text => Size == null ? string.Empty : $"-s {Size.Value.Width}x{Size.Value.Height}"; } } diff --git a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs index 317d4be..f7fef93 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs @@ -14,7 +14,8 @@ public VideoFiltersArgument(VideoFilterOptions options) { Options = options; } - public string Text { get; set; } + + public string Text => GetText(); public string GetText() { diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 5de955a..b242c7b 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -8,7 +8,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using FFMpegCore.Arguments; namespace FFMpegCore { @@ -17,17 +16,18 @@ public static class FFMpeg /// /// Saves a 'png' thumbnail from the input video to drive /// - /// Source video analysis + /// Source video analysis /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. /// Bitmap with the requested snapshot. - public static bool Snapshot(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null) + public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; - var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); + var source = FFProbe.Analyse(input); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime); return arguments .OutputToFile(output, true, outputOptions) @@ -36,32 +36,35 @@ public static bool Snapshot(IMediaAnalysis source, string output, Size? size = n /// /// Saves a 'png' thumbnail from the input video to drive /// - /// Source video analysis + /// Source video analysis /// Output video file path /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. /// Bitmap with the requested snapshot. - public static Task SnapshotAsync(IMediaAnalysis source, string output, Size? size = null, TimeSpan? captureTime = null) + public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null) { if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; - var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); + var source = await FFProbe.AnalyseAsync(input); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime); - return arguments + return await arguments .OutputToFile(output, true, outputOptions) .ProcessAsynchronously(); } + /// /// Saves a 'png' thumbnail to an in-memory bitmap /// - /// Source video file. + /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. /// Bitmap with the requested snapshot. - public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null) { - var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); + var source = FFProbe.Analyse(input); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime); using var ms = new MemoryStream(); arguments @@ -76,13 +79,14 @@ public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan /// /// Saves a 'png' thumbnail to an in-memory bitmap /// - /// Source video file. + /// Source video file. /// Seek position where the thumbnail should be taken. /// Thumbnail size. If width or height equal 0, the other will be computed automatically. /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null) { - var (arguments, outputOptions) = BuildSnapshotArguments(source, size, captureTime); + var source = await FFProbe.AnalyseAsync(input); + var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime); using var ms = new MemoryStream(); await arguments @@ -94,13 +98,13 @@ await arguments return new Bitmap(ms); } - private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) + private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments(string input, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null) { captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); size = PrepareSnapshotSize(source, size); return (FFMpegArguments - .FromFileInput(source, options => options + .FromFileInput(input, false, options => options .Seek(captureTime)), options => options .WithVideoCodec(VideoCodec.Png) @@ -110,7 +114,7 @@ private static (FFMpegArguments, Action outputOptions) Bu private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize) { - if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0)) + if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0) || source.PrimaryVideoStream == null) return null; var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height); @@ -147,7 +151,7 @@ private static (FFMpegArguments, Action outputOptions) Bu /// Is encoding multithreaded. /// Output video information. public static bool Convert( - IMediaAnalysis source, + string input, string output, ContainerFormat format, Speed speed = Speed.SuperFast, @@ -156,6 +160,7 @@ public static bool Convert( bool multithreaded = false) { FFMpegHelper.ExtensionExceptionCheck(output, format.Extension); + var source = FFProbe.Analyse(input); FFMpegHelper.ConversionSizeExceptionCheck(source); var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream.Height / (int)size; @@ -167,7 +172,7 @@ public static bool Convert( return format.Name switch { "mp4" => FFMpegArguments - .FromFileInput(source) + .FromFileInput(input) .OutputToFile(output, true, options => options .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibX264) @@ -179,7 +184,7 @@ public static bool Convert( .WithAudioBitrate(audioQuality)) .ProcessSynchronously(), "ogv" => FFMpegArguments - .FromFileInput(source) + .FromFileInput(input) .OutputToFile(output, true, options => options .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibTheora) @@ -191,14 +196,14 @@ public static bool Convert( .WithAudioBitrate(audioQuality)) .ProcessSynchronously(), "mpegts" => FFMpegArguments - .FromFileInput(source) + .FromFileInput(input) .OutputToFile(output, true, options => options .CopyChannel() .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) .ForceFormat(VideoType.Ts)) .ProcessSynchronously(), "webm" => FFMpegArguments - .FromFileInput(source) + .FromFileInput(input) .OutputToFile(output, true, options => options .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibVpx) @@ -236,21 +241,22 @@ public static bool PosterWithAudio(string image, string audio, string output) .UsingShortest()) .ProcessSynchronously(); } - + /// /// Joins a list of video files. /// /// Output video file. /// List of vides that need to be joined together. /// Output video information. - public static bool Join(string output, params IMediaAnalysis[] videos) + public static bool Join(string output, params string[] videos) { - var temporaryVideoParts = videos.Select(video => + var temporaryVideoParts = videos.Select(videoPath => { + var video = FFProbe.Analyse(videoPath); FFMpegHelper.ConversionSizeExceptionCheck(video); - var destinationPath = Path.Combine(FFMpegOptions.Options.TempDirectory, $"{Path.GetFileNameWithoutExtension(video.Path)}{FileExtension.Ts}"); + var destinationPath = Path.Combine(FFMpegOptions.Options.TempDirectory, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}"); Directory.CreateDirectory(FFMpegOptions.Options.TempDirectory); - Convert(video, destinationPath, VideoType.Ts); + Convert(videoPath, destinationPath, VideoType.Ts); return destinationPath; }).ToArray(); @@ -268,16 +274,6 @@ public static bool Join(string output, params IMediaAnalysis[] videos) Cleanup(temporaryVideoParts); } } - /// - /// Joins a list of video files. - /// - /// Output video file. - /// List of vides that need to be joined together. - /// Output video information. - public static bool Join(string output, params string[] videos) - { - return Join(output, videos.Select(videoPath => FFProbe.Analyse(videoPath)).ToArray()); - } /// /// Converts an image sequence to a video. @@ -344,10 +340,10 @@ public static bool Mute(string input, string output) { var source = FFProbe.Analyse(input); FFMpegHelper.ConversionSizeExceptionCheck(source); - FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); + // FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); return FFMpegArguments - .FromFileInput(source) + .FromFileInput(input) .OutputToFile(output, true, options => options .CopyChannel(Channel.Video) .DisableChannel(Channel.Audio)) @@ -383,10 +379,10 @@ public static bool ReplaceAudio(string input, string inputAudio, string output, { var source = FFProbe.Analyse(input); FFMpegHelper.ConversionSizeExceptionCheck(source); - FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); + // FFMpegHelper.ExtensionExceptionCheck(output, source.Format.); return FFMpegArguments - .FromFileInput(source) + .FromFileInput(input) .AddFileInput(inputAudio) .OutputToFile(output, true, options => options .CopyChannel() diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 9872e0a..c87e029 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -15,8 +15,6 @@ internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions WithAudioBitrate(int bitrate) => WithArgument(new AudioBitrateArgument(bitrate)); public FFMpegArgumentOptions WithAudioSamplingRate(int samplingRate = 48000) => WithArgument(new AudioSamplingRateArgument(samplingRate)); public FFMpegArgumentOptions WithVariableBitrate(int vbr) => WithArgument(new VariableBitRateArgument(vbr)); - - public FFMpegArgumentOptions Resize(VideoSize videoSize) => WithArgument(new SizeArgument(videoSize)); public FFMpegArgumentOptions Resize(int width, int height) => WithArgument(new SizeArgument(width, height)); public FFMpegArgumentOptions Resize(Size? size) => WithArgument(new SizeArgument(size)); diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 75b98f4..ecc568f 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -80,7 +80,7 @@ void OnCancelEvent(object sender, int timeout) } catch (Exception e) { - if (!HandleException(throwOnError, e, instance.ErrorData, instance.OutputData)) return false; + if (!HandleException(throwOnError, e, instance.ErrorData)) return false; } finally { @@ -166,7 +166,6 @@ private static bool HandleException(bool throwOnError, Exception e, IReadOnlyLis if (!throwOnError) return false; - throw new FFMpegProcessException(exitCode, string.Join("\n", errorData)); throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, string.Join("\n", errorData)); } diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index cb5d906..ffbc8b8 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -12,7 +12,7 @@ - return null from FFProbe.Analyse* when no media format was detected - Expose tags as string dictionary on IMediaAnalysis (thanks hey-red) 8 - 3.4.0 + 4.0.0 MIT Malte Rosenbjerg, Vlad Jerca ffmpeg ffprobe convert video audio mediafile resize analyze muxing From a12c322de9183307693f923c6a8dba958329dee4 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 6 Mar 2021 23:05:38 +0100 Subject: [PATCH 06/22] Add ConfigureAwait(false) --- FFMpegCore/FFProbe/FFProbe.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 573a2ad..fab4b2e 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -62,13 +62,13 @@ public static async Task AnalyseAsync(string filePath, int outpu throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); using var instance = PrepareInstance(filePath, outputCapacity); - await instance.FinishedRunning(); + await instance.FinishedRunning().ConfigureAwait(false); return ParseOutput(instance); } public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue) { using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); - await instance.FinishedRunning(); + await instance.FinishedRunning().ConfigureAwait(false); return ParseOutput(instance); } public static async Task AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue) @@ -81,7 +81,7 @@ public static async Task AnalyseAsync(Stream stream, int outputC var task = instance.FinishedRunning(); try { - await pipeArgument.During(); + await pipeArgument.During().ConfigureAwait(false); } catch(IOException) { @@ -90,7 +90,7 @@ public static async Task AnalyseAsync(Stream stream, int outputC { pipeArgument.Post(); } - var exitCode = await task; + var exitCode = await task.ConfigureAwait(false); if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}", null, string.Join("\n", instance.ErrorData)); From e0819c89bfa7b5ddf7509533adecbb70232446db Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 6 Mar 2021 23:12:53 +0100 Subject: [PATCH 07/22] Cleanup tests --- FFMpegCore.Test/VideoTest.cs | 203 +++++++++++------------------------ 1 file changed, 61 insertions(+), 142 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index ec6b8e2..45072c1 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -12,111 +12,56 @@ using FFMpegCore.Arguments; using FFMpegCore.Exceptions; using FFMpegCore.Pipes; -using System.Threading; namespace FFMpegCore.Test { [TestClass] public class VideoTest { - public bool Convert(ContainerFormat type, bool multithreaded = false, VideoSize size = VideoSize.Original) + [TestMethod, Timeout(10000)] + public void Video_ToOGV() { - using var outputFile = new TemporaryFile($"out{type.Extension}"); - - var input = FFProbe.Analyse(TestResources.Mp4Video); - FFMpeg.Convert(TestResources.Mp4Video, outputFile, type, size: size, multithreaded: multithreaded); - var outputVideo = FFProbe.Analyse(outputFile); - - Assert.IsTrue(File.Exists(outputFile)); - Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1); - if (size == VideoSize.Original) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - else - { - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, (int)size); - } - - return File.Exists(outputFile) && - outputVideo.Duration == input.Duration && - ( - ( - size == VideoSize.Original && - outputVideo.PrimaryVideoStream.Width == input.PrimaryVideoStream.Width && - outputVideo.PrimaryVideoStream.Height == input.PrimaryVideoStream.Height - ) || - ( - size != VideoSize.Original && - outputVideo.PrimaryVideoStream.Width != input.PrimaryVideoStream.Width && - outputVideo.PrimaryVideoStream.Height != input.PrimaryVideoStream.Height && - outputVideo.PrimaryVideoStream.Height == (int)size - ) - ); - } - - public void Convert(ContainerFormat type, Action validationMethod, params IArgument[] arguments) - { - using var outputFile = new TemporaryFile($"out{type.Extension}"); - - var input = FFProbe.Analyse(TestResources.Mp4Video); - - var processor = FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToFile(outputFile, false, opt => - { - foreach (var arg in arguments) - opt.WithArgument(arg); - }); - - var scaling = arguments.OfType().FirstOrDefault(); - processor.ProcessSynchronously(); - - var outputVideo = FFProbe.Analyse(outputFile); - - Assert.IsTrue(File.Exists(outputFile)); - Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1); - validationMethod?.Invoke(outputVideo); - if (scaling?.Size == null) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - else - { - if (scaling.Size.Value.Width != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); - } - - if (scaling.Size.Value.Height != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); - } - - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - } - - public void Convert(ContainerFormat type, params IArgument[] inputArguments) - { - Convert(type, null, inputArguments); + using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false) + .ProcessSynchronously(); + Assert.IsTrue(success); } [TestMethod, Timeout(10000)] public void Video_ToMP4() { - Convert(VideoType.Mp4); + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false) + .ProcessSynchronously(); + Assert.IsTrue(success); } [TestMethod, Timeout(10000)] public void Video_ToMP4_YUV444p() { - using var outputFile = new TemporaryFile($"out{VideoType.WebM.Extension}"); + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264) + .ForcePixelFormat("yuv444p")) + .ProcessSynchronously(); + Assert.IsTrue(success); + var analysis = FFProbe.Analyse(outputFile); + Assert.IsTrue(analysis.VideoStreams.First().PixelFormat == "yuv444p"); + } + + [TestMethod, Timeout(10000)] + public void Video_ToMP4_Args() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); var success = FFMpegArguments .FromFileInput(TestResources.WebmVideo) @@ -124,16 +69,6 @@ public void Video_ToMP4_YUV444p() .WithVideoCodec(VideoCodec.LibX264)) .ProcessSynchronously(); Assert.IsTrue(success); - var analysis = FFProbe.Analyse(outputFile); - - Convert(VideoType.Mp4, (a) => Assert.IsTrue(a.VideoStreams.First().PixelFormat == "yuv444p"), - new ForcePixelFormat("yuv444p")); - } - - [TestMethod, Timeout(10000)] - public void Video_ToMP4_Args() - { - Convert(VideoType.Mp4, new VideoCodecArgument(VideoCodec.LibX264)); } [DataTestMethod, Timeout(10000)] @@ -164,8 +99,6 @@ public void Video_ToMP4_Args_StreamPipe() .WithVideoCodec(VideoCodec.LibX264)) .ProcessSynchronously(); Assert.IsTrue(success); - - var outputVideo = FFProbe.Analyse(output); } [TestMethod, Timeout(10000)] @@ -204,13 +137,11 @@ public void Video_ToMP4_Args_StreamOutputPipe_Failure() Assert.ThrowsException(() => { using var ms = new MemoryStream(); - var processor = FFMpegArguments + FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToPipe(new StreamPipeSink(ms), opt => opt .ForceFormat("mkv")) .ProcessSynchronously(); - ms.Position = 0; - var outputVideo = FFProbe.Analyse(ms); }); } @@ -265,16 +196,28 @@ public void TranscodeToMemoryStream_Success() [TestMethod, Timeout(10000)] public void Video_ToTS() { - Convert(VideoType.Ts); + using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false) + .ProcessSynchronously(); + Assert.IsTrue(success); } [TestMethod, Timeout(10000)] public void Video_ToTS_Args() { - Convert(VideoType.Ts, - new CopyArgument(), - new BitStreamFilterArgument(Channel.Video, Filter.H264_Mp4ToAnnexB), - new ForceFormatArgument(VideoType.MpegTs)); + using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt + .CopyChannel() + .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) + .ForceFormat(VideoType.MpegTs)) + .ProcessSynchronously(); + Assert.IsTrue(success); } [DataTestMethod, Timeout(10000)] @@ -327,7 +270,7 @@ public void RawVideoPipeSource_Ogv_Scale(System.Drawing.Imaging.PixelFormat pixe .ProcessSynchronously(); var analysis = FFProbe.Analyse(outputFile); - Assert.AreEqual((int)VideoSize.Ed, analysis!.PrimaryVideoStream.Width); + Assert.AreEqual((int)VideoSize.Ed, analysis.PrimaryVideoStream!.Width); } [TestMethod, Timeout(10000)] @@ -361,37 +304,13 @@ public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixe Assert.IsTrue(success); } - [TestMethod, Timeout(10000)] - public void Video_ToOGV() - { - Convert(VideoType.Ogv); - } - - [TestMethod, Timeout(10000)] - public void Video_ToMP4_MultiThread() - { - Convert(VideoType.Mp4, true); - } - - [TestMethod, Timeout(10000)] - public void Video_ToTS_MultiThread() - { - Convert(VideoType.Ts, true); - } - - [TestMethod, Timeout(10000)] - public void Video_ToOGV_MultiThread() - { - Convert(VideoType.Ogv, true); - } - [TestMethod, Timeout(10000)] public void Video_Snapshot_InMemory() { var input = FFProbe.Analyse(TestResources.Mp4Video); using var bitmap = FFMpeg.Snapshot(TestResources.Mp4Video); - Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); + Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); } @@ -405,7 +324,7 @@ public void Video_Snapshot_PersistSnapshot() FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); using var bitmap = Image.FromFile(outputPath); - Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); + Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); } @@ -428,7 +347,7 @@ public void Video_Join() Assert.AreEqual(expectedDuration.Hours, result.Duration.Hours); Assert.AreEqual(expectedDuration.Minutes, result.Duration.Minutes); Assert.AreEqual(expectedDuration.Seconds, result.Duration.Seconds); - Assert.AreEqual(input.PrimaryVideoStream.Height, result.PrimaryVideoStream.Height); + Assert.AreEqual(input.PrimaryVideoStream!.Height, result.PrimaryVideoStream!.Height); Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width); } @@ -452,7 +371,7 @@ public void Video_Join_Image_Sequence() Assert.IsTrue(success); var result = FFProbe.Analyse(outputFile); Assert.AreEqual(3, result.Duration.Seconds); - Assert.AreEqual(imageSet.First().Width, result.PrimaryVideoStream.Width); + Assert.AreEqual(imageSet.First().Width, result.PrimaryVideoStream!.Width); Assert.AreEqual(imageSet.First().Height, result.PrimaryVideoStream.Height); } @@ -461,7 +380,7 @@ public void Video_With_Only_Audio_Should_Extract_Metadata() { var video = FFProbe.Analyse(TestResources.Mp4WithoutVideo); Assert.AreEqual(null, video.PrimaryVideoStream); - Assert.AreEqual("aac", video.PrimaryAudioStream.CodecName); + Assert.AreEqual("aac", video.PrimaryAudioStream!.CodecName); Assert.AreEqual(10, video.Duration.TotalSeconds, 0.5); } @@ -547,7 +466,7 @@ public void Video_TranscodeInMemory() resStream.Position = 0; var vi = FFProbe.Analyse(resStream); - Assert.AreEqual(vi.PrimaryVideoStream.Width, 128); + Assert.AreEqual(vi.PrimaryVideoStream!.Width, 128); Assert.AreEqual(vi.PrimaryVideoStream.Height, 128); } @@ -596,14 +515,14 @@ public async Task Video_Cancel_Async_With_Timeout() var result = await task; - var outputInfo = FFProbe.Analyse(outputFile); + var outputInfo = await FFProbe.AnalyseAsync(outputFile); Assert.IsTrue(result); Assert.IsNotNull(outputInfo); - Assert.AreEqual(320, outputInfo.PrimaryVideoStream.Width); + Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width); Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName); - Assert.AreEqual("aac", outputInfo.PrimaryAudioStream.CodecName); + Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName); } } } From f40479e6978626f1b15e638858030951c43bbf0f Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 6 Mar 2021 23:17:23 +0100 Subject: [PATCH 08/22] Update nuget info --- FFMpegCore/FFMpegCore.csproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index ffbc8b8..69a8e61 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -9,8 +9,10 @@ 3.0.0.0 3.0.0.0 3.0.0.0 - - return null from FFProbe.Analyse* when no media format was detected -- Expose tags as string dictionary on IMediaAnalysis (thanks hey-red) + - Video filter args refactored to support multiple arguments +- Cancel improved with timeout (thanks TFleury) +- Basic support for webcam/mic input through InputDeviceArgument (thanks TFleury) +- Other fixes and improvements 8 4.0.0 MIT From cfda317883432f399d9520a495c9d6425d27fe3e Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 6 Mar 2021 23:26:15 +0100 Subject: [PATCH 09/22] change FrameRate on RawVideoPipeSource to double --- FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs index f61bb7c..378cead 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using FFMpegCore.Exceptions; @@ -14,7 +15,7 @@ public class RawVideoPipeSource : IPipeSource public string StreamFormat { get; private set; } = null!; public int Width { get; private set; } public int Height { get; private set; } - public int FrameRate { get; set; } = 25; + public double FrameRate { get; set; } = 25; private bool _formatInitialized; private readonly IEnumerator _framesEnumerator; @@ -42,7 +43,7 @@ public string GetStreamArguments() _formatInitialized = true; } - return $"-f rawvideo -r {FrameRate} -pix_fmt {StreamFormat} -s {Width}x{Height}"; + return $"-f rawvideo -r {FrameRate.ToString(CultureInfo.InvariantCulture)} -pix_fmt {StreamFormat} -s {Width}x{Height}"; } public async Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) From 744489910610f4b2c72cf599c183581e7cef7096 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 6 Mar 2021 23:33:48 +0100 Subject: [PATCH 10/22] reorder method --- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index ecc568f..06fca1a 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -90,17 +90,6 @@ void OnCancelEvent(object sender, int timeout) return HandleCompletion(throwOnError, errorCode, instance.ErrorData); } - private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) - { - if (throwOnError && exitCode != 0) - throw new FFMpegProcessException(exitCode, string.Join("\n", errorData)); - - _onPercentageProgress?.Invoke(100.0); - if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value); - - return exitCode == 0; - } - public async Task ProcessAsynchronously(bool throwOnError = true) { using var instance = PrepareInstance(out var cancellationTokenSource); @@ -140,6 +129,17 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => return HandleCompletion(throwOnError, errorCode, instance.ErrorData); } + private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) + { + if (throwOnError && exitCode != 0) + throw new FFMpegProcessException(exitCode, string.Join("\n", errorData)); + + _onPercentageProgress?.Invoke(100.0); + if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value); + + return exitCode == 0; + } + private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSource) { FFMpegHelper.RootExceptionCheck(); From df0205fb11482a6c7190073d89988982b4ab7bb2 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sun, 7 Mar 2021 00:26:08 +0100 Subject: [PATCH 11/22] Cleanup --- FFMpegCore.Test/FFMpegOptionsTests.cs | 16 ++--- FFMpegCore.Test/VideoTest.cs | 6 +- .../FFMpeg/Arguments/DemuxConcatArgument.cs | 2 +- FFMpegCore/FFMpeg/Enums/ContainerFormat.cs | 4 +- .../FFMpeg/Exceptions/FFMpegException.cs | 52 ++++++--------- FFMpegCore/FFMpeg/FFMpeg.cs | 26 ++++---- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 2 +- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 21 +++--- FFMpegCore/FFMpeg/FFMpegArguments.cs | 10 +-- ...gOptionsBase.cs => FFMpegArgumentsBase.cs} | 2 +- FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs | 18 +++++ FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs | 18 ----- FFMpegCore/FFMpeg/FFMpegOptions.cs | 66 ------------------- FFMpegCore/FFOptions.cs | 37 +++++++++++ FFMpegCore/FFProbe/FFProbe.cs | 36 +++++----- FFMpegCore/GlobalFFOptions.cs | 52 +++++++++++++++ FFMpegCore/Helpers/FFMpegHelper.cs | 12 ++-- FFMpegCore/Helpers/FFProbeHelper.cs | 13 ++-- 18 files changed, 203 insertions(+), 190 deletions(-) rename FFMpegCore/FFMpeg/{FFMpegOptionsBase.cs => FFMpegArgumentsBase.cs} (79%) create mode 100644 FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs delete mode 100644 FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs delete mode 100644 FFMpegCore/FFMpeg/FFMpegOptions.cs create mode 100644 FFMpegCore/FFOptions.cs create mode 100644 FFMpegCore/GlobalFFOptions.cs diff --git a/FFMpegCore.Test/FFMpegOptionsTests.cs b/FFMpegCore.Test/FFMpegOptionsTests.cs index d175644..2be810f 100644 --- a/FFMpegCore.Test/FFMpegOptionsTests.cs +++ b/FFMpegCore.Test/FFMpegOptionsTests.cs @@ -10,39 +10,39 @@ public class FFMpegOptionsTest [TestMethod] public void Options_Initialized() { - Assert.IsNotNull(FFMpegOptions.Options); + Assert.IsNotNull(GlobalFFOptions.Current); } [TestMethod] public void Options_Defaults_Configured() { - Assert.AreEqual(new FFMpegOptions().RootDirectory, $""); + Assert.AreEqual(new FFOptions().BinaryFolder, $""); } [TestMethod] public void Options_Loaded_From_File() { Assert.AreEqual( - FFMpegOptions.Options.RootDirectory, - JsonConvert.DeserializeObject(File.ReadAllText("ffmpeg.config.json")).RootDirectory + GlobalFFOptions.Current.BinaryFolder, + JsonConvert.DeserializeObject(File.ReadAllText("ffmpeg.config.json")).BinaryFolder ); } [TestMethod] public void Options_Set_Programmatically() { - var original = FFMpegOptions.Options; + var original = GlobalFFOptions.Current; try { - FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "Whatever" }); + GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" }); Assert.AreEqual( - FFMpegOptions.Options.RootDirectory, + GlobalFFOptions.Current.BinaryFolder, "Whatever" ); } finally { - FFMpegOptions.Configure(original); + GlobalFFOptions.Configure(original); } } } diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 45072c1..30e6a9a 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -104,7 +104,7 @@ public void Video_ToMP4_Args_StreamPipe() [TestMethod, Timeout(10000)] public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure() { - await Assert.ThrowsExceptionAsync(async () => + await Assert.ThrowsExceptionAsync(async () => { await using var ms = new MemoryStream(); var pipeSource = new StreamPipeSink(ms); @@ -134,7 +134,7 @@ public void Video_StreamFile_OutputToMemoryStream() [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamOutputPipe_Failure() { - Assert.ThrowsException(() => + Assert.ThrowsException(() => { using var ms = new MemoryStream(); FFMpegArguments @@ -435,7 +435,7 @@ public void Video_OutputsData() var outputFile = new TemporaryFile("out.mp4"); var dataReceived = false; - FFMpegOptions.Configure(opt => opt.Encoding = Encoding.UTF8); + GlobalFFOptions.Configure(opt => opt.Encoding = Encoding.UTF8); var success = FFMpegArguments .FromFileInput(TestResources.Mp4Video) .WithGlobalOptions(options => options diff --git a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs index 5651802..c672c74 100644 --- a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs @@ -18,7 +18,7 @@ public DemuxConcatArgument(IEnumerable values) { Values = values.Select(value => $"file '{value}'"); } - private readonly string _tempFileName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid() + ".txt"); + private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"concat_{Guid.NewGuid()}.txt"); public void Pre() => File.WriteAllLines(_tempFileName, Values); public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; diff --git a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs index 90da0d2..2da1572 100644 --- a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs +++ b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs @@ -15,8 +15,8 @@ public string Extension { get { - if (FFMpegOptions.Options.ExtensionOverrides.ContainsKey(Name)) - return FFMpegOptions.Options.ExtensionOverrides[Name]; + if (GlobalFFOptions.Current.ExtensionOverrides.ContainsKey(Name)) + return GlobalFFOptions.Current.ExtensionOverrides[Name]; return "." + Name; } } diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs index bf7c41f..dad6ef1 100644 --- a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs +++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs @@ -4,53 +4,43 @@ namespace FFMpegCore.Exceptions { public enum FFMpegExceptionType { - Dependency, Conversion, File, Operation, Process } - public abstract class FFException : Exception - { - protected FFException(string message) : base(message) { } - protected FFException(string message, Exception innerException) : base(message, innerException) { } - } - public abstract class FFProcessException : FFException - { - protected FFProcessException(string process, int exitCode, string errorOutput) - : base($"{process} exited with non-zero exit-code {exitCode}\n{errorOutput}") - { - ExitCode = exitCode; - ErrorOutput = errorOutput; - } - - public int ExitCode { get; } - public string ErrorOutput { get; } - } - public class FFMpegProcessException : FFProcessException - { - public FFMpegProcessException(int exitCode, string errorOutput) - : base("ffmpeg", exitCode, errorOutput) { } - } - public class FFProbeProcessException : FFProcessException - { - public FFProbeProcessException(int exitCode, string errorOutput) - : base("ffprobe", exitCode, errorOutput) { } - } - public class FFMpegException : Exception { - public FFMpegException(FFMpegExceptionType type, string? message = null, Exception? innerException = null, string ffMpegErrorOutput = "") + public FFMpegException(FFMpegExceptionType type, string message, Exception? innerException = null, string ffMpegErrorOutput = "") : base(message, innerException) { FFMpegErrorOutput = ffMpegErrorOutput; Type = type; } - + public FFMpegException(FFMpegExceptionType type, string message, string ffMpegErrorOutput = "") + : base(message) + { + FFMpegErrorOutput = ffMpegErrorOutput; + Type = type; + } + public FFMpegException(FFMpegExceptionType type, string message) + : base(message) + { + FFMpegErrorOutput = string.Empty; + Type = type; + } + public FFMpegExceptionType Type { get; } public string FFMpegErrorOutput { get; } } + public class FFOptionsException : Exception + { + public FFOptionsException(string message, Exception? innerException = null) + : base(message, innerException) + { + } + } public class FFMpegArgumentException : Exception { diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index b242c7b..3f3d162 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -254,8 +254,8 @@ public static bool Join(string output, params string[] videos) { var video = FFProbe.Analyse(videoPath); FFMpegHelper.ConversionSizeExceptionCheck(video); - var destinationPath = Path.Combine(FFMpegOptions.Options.TempDirectory, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}"); - Directory.CreateDirectory(FFMpegOptions.Options.TempDirectory); + var destinationPath = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}"); + Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder); Convert(videoPath, destinationPath, VideoType.Ts); return destinationPath; }).ToArray(); @@ -284,7 +284,7 @@ public static bool Join(string output, params string[] videos) /// Output video information. public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images) { - var tempFolderName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid().ToString()); + var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); var temporaryImageFiles = images.Select((image, index) => { FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName)); @@ -398,7 +398,7 @@ internal static IReadOnlyList GetPixelFormatsInternal() FFMpegHelper.RootExceptionCheck(); var list = new List(); - using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), "-pix_fmts"); + using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts"); instance.DataReceived += (e, args) => { if (PixelFormat.TryParse(args.Data, out var format)) @@ -413,14 +413,14 @@ internal static IReadOnlyList GetPixelFormatsInternal() public static IReadOnlyList GetPixelFormats() { - if (!FFMpegOptions.Options.UseCache) + if (!GlobalFFOptions.Current.UseCache) return GetPixelFormatsInternal(); return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly(); } public static bool TryGetPixelFormat(string name, out PixelFormat fmt) { - if (!FFMpegOptions.Options.UseCache) + if (!GlobalFFOptions.Current.UseCache) { fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); return fmt != null; @@ -443,7 +443,7 @@ private static void ParsePartOfCodecs(Dictionary codecs, string a { FFMpegHelper.RootExceptionCheck(); - using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), arguments); + using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), arguments); instance.DataReceived += (e, args) => { var codec = parser(args.Data); @@ -485,14 +485,14 @@ internal static Dictionary GetCodecsInternal() public static IReadOnlyList GetCodecs() { - if (!FFMpegOptions.Options.UseCache) + if (!GlobalFFOptions.Current.UseCache) return GetCodecsInternal().Values.ToList().AsReadOnly(); return FFMpegCache.Codecs.Values.ToList().AsReadOnly(); } public static IReadOnlyList GetCodecs(CodecType type) { - if (!FFMpegOptions.Options.UseCache) + if (!GlobalFFOptions.Current.UseCache) return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly(); return FFMpegCache.Codecs.Values.Where(x=>x.Type == type).ToList().AsReadOnly(); } @@ -504,7 +504,7 @@ public static IReadOnlyList GetCodecs(CodecType type) public static bool TryGetCodec(string name, out Codec codec) { - if (!FFMpegOptions.Options.UseCache) + if (!GlobalFFOptions.Current.UseCache) { codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); return codec != null; @@ -527,7 +527,7 @@ internal static IReadOnlyList GetContainersFormatsInternal() FFMpegHelper.RootExceptionCheck(); var list = new List(); - using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), "-formats"); + using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats"); instance.DataReceived += (e, args) => { if (ContainerFormat.TryParse(args.Data, out var fmt)) @@ -542,14 +542,14 @@ internal static IReadOnlyList GetContainersFormatsInternal() public static IReadOnlyList GetContainerFormats() { - if (!FFMpegOptions.Options.UseCache) + if (!GlobalFFOptions.Current.UseCache) return GetContainersFormatsInternal(); return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly(); } public static bool TryGetContainerFormat(string name, out ContainerFormat fmt) { - if (!FFMpegOptions.Options.UseCache) + if (!GlobalFFOptions.Current.UseCache) { fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); return fmt != null; diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index c87e029..be342c4 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -5,7 +5,7 @@ namespace FFMpegCore { - public class FFMpegArgumentOptions : FFMpegOptionsBase + public class FFMpegArgumentOptions : FFMpegArgumentsBase { internal FFMpegArgumentOptions() { } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 06fca1a..67607af 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -50,9 +50,9 @@ public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout cancel = () => CancelEvent?.Invoke(this, timeout); return this; } - public bool ProcessSynchronously(bool throwOnError = true) + public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { - using var instance = PrepareInstance(out var cancellationTokenSource); + using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource); var errorCode = -1; void OnCancelEvent(object sender, int timeout) @@ -90,9 +90,9 @@ void OnCancelEvent(object sender, int timeout) return HandleCompletion(throwOnError, errorCode, instance.ErrorData); } - public async Task ProcessAsynchronously(bool throwOnError = true) + public async Task ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { - using var instance = PrepareInstance(out var cancellationTokenSource); + using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource); var errorCode = -1; void OnCancelEvent(object sender, int timeout) @@ -132,7 +132,7 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) { if (throwOnError && exitCode != 0) - throw new FFMpegProcessException(exitCode, string.Join("\n", errorData)); + throw new FFMpegException(FFMpegExceptionType.Process, $"ffmpeg exited with non-zero exit-code ({exitCode} - {string.Join("\n", errorData)})", null, string.Join("\n", errorData)); _onPercentageProgress?.Invoke(100.0); if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value); @@ -140,16 +140,17 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList string.Join(" ", _globalOptions.Arguments.Concat(Arguments).Select(arg => arg.Text)); + public string Text => string.Join(" ", _globalArguments.Arguments.Concat(Arguments).Select(arg => arg.Text)); public static FFMpegArguments FromConcatInput(IEnumerable filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments); public static FFMpegArguments FromDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments); @@ -26,9 +26,9 @@ private FFMpegArguments() { } public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments); - public FFMpegArguments WithGlobalOptions(Action configureOptions) + public FFMpegArguments WithGlobalOptions(Action configureOptions) { - configureOptions(_globalOptions); + configureOptions(_globalArguments); return this; } diff --git a/FFMpegCore/FFMpeg/FFMpegOptionsBase.cs b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs similarity index 79% rename from FFMpegCore/FFMpeg/FFMpegOptionsBase.cs rename to FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs index 015e609..fc51ab1 100644 --- a/FFMpegCore/FFMpeg/FFMpegOptionsBase.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs @@ -3,7 +3,7 @@ namespace FFMpegCore { - public abstract class FFMpegOptionsBase + public abstract class FFMpegArgumentsBase { internal readonly List Arguments = new List(); } diff --git a/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs new file mode 100644 index 0000000..e7d6e24 --- /dev/null +++ b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs @@ -0,0 +1,18 @@ +using FFMpegCore.Arguments; + +namespace FFMpegCore +{ + public sealed class FFMpegGlobalArguments : FFMpegArgumentsBase + { + internal FFMpegGlobalArguments() { } + + public FFMpegGlobalArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel)); + + private FFMpegGlobalArguments WithOption(IArgument argument) + { + Arguments.Add(argument); + return this; + } + + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs b/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs deleted file mode 100644 index 00dc66f..0000000 --- a/FFMpegCore/FFMpeg/FFMpegGlobalOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FFMpegCore.Arguments; - -namespace FFMpegCore -{ - public sealed class FFMpegGlobalOptions : FFMpegOptionsBase - { - internal FFMpegGlobalOptions() { } - - public FFMpegGlobalOptions WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel)); - - private FFMpegGlobalOptions WithOption(IArgument argument) - { - Arguments.Add(argument); - return this; - } - - } -} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegOptions.cs b/FFMpegCore/FFMpeg/FFMpegOptions.cs deleted file mode 100644 index 4a09d7a..0000000 --- a/FFMpegCore/FFMpeg/FFMpegOptions.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.Json; - -namespace FFMpegCore -{ - public class FFMpegOptions - { - private static readonly string ConfigFile = "ffmpeg.config.json"; - private static readonly string DefaultRoot = ""; - private static readonly string DefaultTemp = Path.GetTempPath(); - private static readonly Dictionary DefaultExtensionsOverrides = new Dictionary - { - { "mpegts", ".ts" }, - }; - - public static FFMpegOptions Options { get; private set; } = new FFMpegOptions(); - - public static void Configure(Action optionsAction) - { - optionsAction?.Invoke(Options); - } - - public static void Configure(FFMpegOptions options) - { - Options = options ?? throw new ArgumentNullException(nameof(options)); - } - - static FFMpegOptions() - { - if (File.Exists(ConfigFile)) - { - Options = JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))!; - foreach (var pair in DefaultExtensionsOverrides) - if (!Options.ExtensionOverrides.ContainsKey(pair.Key)) Options.ExtensionOverrides.Add(pair.Key, pair.Value); - } - } - - public string RootDirectory { get; set; } = DefaultRoot; - public string TempDirectory { get; set; } = DefaultTemp; - public bool UseCache { get; set; } = true; - public Encoding Encoding { get; set; } = Encoding.Default; - - public string FFMpegBinary() => FFBinary("FFMpeg"); - - public string FFProbeBinary() => FFBinary("FFProbe"); - - public Dictionary ExtensionOverrides { get; private set; } = new Dictionary(); - - private static string FFBinary(string name) - { - var ffName = name.ToLowerInvariant(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - ffName += ".exe"; - - var target = Environment.Is64BitProcess ? "x64" : "x86"; - if (Directory.Exists(Path.Combine(Options.RootDirectory, target))) - ffName = Path.Combine(target, ffName); - - return Path.Combine(Options.RootDirectory, ffName); - } - } -} diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs new file mode 100644 index 0000000..1f7e497 --- /dev/null +++ b/FFMpegCore/FFOptions.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace FFMpegCore +{ + public class FFOptions + { + /// + /// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH + /// + public string BinaryFolder { get; set; } = string.Empty; + + /// + /// Folder used for temporary files necessary for static methods on FFMpeg class + /// + public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); + + /// + /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes + /// + public Encoding Encoding { get; set; } = Encoding.Default; + + /// + /// + /// + public Dictionary ExtensionOverrides { get; set; } = new Dictionary + { + { "mpegts", ".ts" }, + }; + + /// + /// Whether to cache calls to get ffmpeg codec, pixel- and container-formats + /// + public bool UseCache { get; set; } = true; + } +} \ No newline at end of file diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index fab4b2e..ab35457 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -12,32 +12,32 @@ namespace FFMpegCore { public static class FFProbe { - public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue) + public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareInstance(filePath, outputCapacity); + using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var exitCode = instance.BlockUntilFinished(); if (exitCode != 0) - throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData)); + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); return ParseOutput(instance); } - public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue) + public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { - using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); + using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var exitCode = instance.BlockUntilFinished(); if (exitCode != 0) - throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData)); + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); return ParseOutput(instance); } - public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue) + public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); - using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity); + using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); var task = instance.FinishedRunning(); @@ -52,30 +52,30 @@ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.Max } var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult(); if (exitCode != 0) - throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData)); + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); return ParseOutput(instance); } - public static async Task AnalyseAsync(string filePath, int outputCapacity = int.MaxValue) + public static async Task AnalyseAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareInstance(filePath, outputCapacity); + using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); await instance.FinishedRunning().ConfigureAwait(false); return ParseOutput(instance); } - public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue) + public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { - using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity); + using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); await instance.FinishedRunning().ConfigureAwait(false); return ParseOutput(instance); } - public static async Task AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue) + public static async Task AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); - using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity); + using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); var task = instance.FinishedRunning(); @@ -112,12 +112,12 @@ private static IMediaAnalysis ParseOutput(Instance instance) return new MediaAnalysis(ffprobeAnalysis); } - private static Instance PrepareInstance(string filePath, int outputCapacity) + private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions) { FFProbeHelper.RootExceptionCheck(); - FFProbeHelper.VerifyFFProbeExists(); + FFProbeHelper.VerifyFFProbeExists(ffOptions); var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\""; - var instance = new Instance(FFMpegOptions.Options.FFProbeBinary(), arguments) {DataBufferCapacity = outputCapacity}; + var instance = new Instance(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) {DataBufferCapacity = outputCapacity}; return instance; } } diff --git a/FFMpegCore/GlobalFFOptions.cs b/FFMpegCore/GlobalFFOptions.cs new file mode 100644 index 0000000..358787a --- /dev/null +++ b/FFMpegCore/GlobalFFOptions.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.Json; + +namespace FFMpegCore +{ + public static class GlobalFFOptions + { + private static readonly string ConfigFile = "ffmpeg.config.json"; + + public static FFOptions Current { get; private set; } + static GlobalFFOptions() + { + if (File.Exists(ConfigFile)) + { + Current = JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))!; + } + else + { + Current = new FFOptions(); + } + } + + public static void Configure(Action optionsAction) + { + optionsAction?.Invoke(Current); + } + public static void Configure(FFOptions ffOptions) + { + Current = ffOptions ?? throw new ArgumentNullException(nameof(ffOptions)); + } + + + public static string GetFFMpegBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFMpeg", ffOptions ?? Current); + + public static string GetFFProbeBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFProbe", ffOptions ?? Current); + + private static string GetFFBinaryPath(string name, FFOptions ffOptions) + { + var ffName = name.ToLowerInvariant(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + ffName += ".exe"; + + var target = Environment.Is64BitProcess ? "x64" : "x86"; + if (Directory.Exists(Path.Combine(ffOptions.BinaryFolder, target))) + ffName = Path.Combine(target, ffName); + + return Path.Combine(ffOptions.BinaryFolder, ffName); + } + } +} diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs index 676f6d3..12e52c3 100644 --- a/FFMpegCore/Helpers/FFMpegHelper.cs +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -31,17 +31,17 @@ public static void ExtensionExceptionCheck(string filename, string extension) public static void RootExceptionCheck() { - if (FFMpegOptions.Options.RootDirectory == null) - throw new FFMpegException(FFMpegExceptionType.Dependency, - "FFMpeg root is not configured in app config. Missing key 'ffmpegRoot'."); + if (GlobalFFOptions.Current.BinaryFolder == null) + throw new FFOptionsException("FFMpeg root is not configured in app config. Missing key 'BinaryFolder'."); } - public static void VerifyFFMpegExists() + public static void VerifyFFMpegExists(FFOptions ffMpegOptions) { if (_ffmpegVerified) return; - var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFMpegBinary(), "-version"); + var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version"); _ffmpegVerified = exitCode == 0; - if (!_ffmpegVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system"); + if (!_ffmpegVerified) + throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system"); } } } diff --git a/FFMpegCore/Helpers/FFProbeHelper.cs b/FFMpegCore/Helpers/FFProbeHelper.cs index 1e833e0..d0064e4 100644 --- a/FFMpegCore/Helpers/FFProbeHelper.cs +++ b/FFMpegCore/Helpers/FFProbeHelper.cs @@ -20,18 +20,17 @@ public static int Gcd(int first, int second) public static void RootExceptionCheck() { - if (FFMpegOptions.Options.RootDirectory == null) - throw new FFMpegException(FFMpegExceptionType.Dependency, - "FFProbe root is not configured in app config. Missing key 'ffmpegRoot'."); - + if (GlobalFFOptions.Current.BinaryFolder == null) + throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'."); } - public static void VerifyFFProbeExists() + public static void VerifyFFProbeExists(FFOptions ffMpegOptions) { if (_ffprobeVerified) return; - var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFProbeBinary(), "-version"); + var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version"); _ffprobeVerified = exitCode == 0; - if (!_ffprobeVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system"); + if (!_ffprobeVerified) + throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system"); } } } From 928ef40f21848a1544ce205b2fc6bcfa8e36e6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20K=C3=BChner?= Date: Wed, 10 Mar 2021 07:15:36 +0100 Subject: [PATCH 12/22] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a9fc35..e4abc3e 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ The default value (`\\FFMPEG\\bin`) can be overwritten via the `FFMpegOptions` c ```c# public Startup() { - FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "./bin", TempDirectory = "/tmp" }); + GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); } ``` @@ -194,8 +194,8 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f ```json { - "RootDirectory": "./bin", - "TempDirectory": "/tmp" + "BinaryFolder": "./bin", + "TemporaryFilesFolder": "/tmp" } ``` From fe646752d366a5a9c33fcc30ec457991241e6d41 Mon Sep 17 00:00:00 2001 From: Maxim Bagryantsev Date: Mon, 15 Mar 2021 20:35:19 +0300 Subject: [PATCH 13/22] Fixed process hang on pipe images format mismatch --- FFMpegCore.Test/Utilities/BitmapSources.cs | 2 +- FFMpegCore.Test/VideoTest.cs | 88 +++++++++++++++++++ FFMpegCore/FFMpeg/Arguments/PipeArgument.cs | 7 +- .../FFMpeg/Exceptions/FFMpegException.cs | 8 ++ FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs | 2 +- 5 files changed, 103 insertions(+), 4 deletions(-) diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs index c3e8d40..8ea02e8 100644 --- a/FFMpegCore.Test/Utilities/BitmapSources.cs +++ b/FFMpegCore.Test/Utilities/BitmapSources.cs @@ -21,7 +21,7 @@ public static IEnumerable CreateBitmaps(int count, PixelFormat fmt, } } - private static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset) + public static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset) { var bitmap = new Bitmap(w, h, fmt); diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 30e6a9a..149dabd 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -87,6 +87,92 @@ public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat Assert.IsTrue(success); } + [TestMethod, Timeout(10000)] + public void Video_ToMP4_Args_Pipe_DifferentImageSizes() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var frames = new List + { + BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 256, 256, 1, 0) + }; + + var videoFramesSource = new RawVideoPipeSource(frames); + var ex = Assert.ThrowsException(() => FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously()); + + Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException)); + } + + + [TestMethod, Timeout(10000)] + public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var frames = new List + { + BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 256, 256, 1, 0) + }; + + var videoFramesSource = new RawVideoPipeSource(frames); + var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessAsynchronously()); + + Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException)); + } + + [TestMethod, Timeout(10000)] + public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var frames = new List + { + BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format32bppRgb, 255, 255, 1, 0) + }; + + var videoFramesSource = new RawVideoPipeSource(frames); + var ex = Assert.ThrowsException(() => FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously()); + + Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException)); + } + + + [TestMethod, Timeout(10000)] + public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var frames = new List + { + BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0), + BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format32bppRgb, 255, 255, 1, 0) + }; + + var videoFramesSource = new RawVideoPipeSource(frames); + var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessAsynchronously()); + + Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException)); + } + [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamPipe() { @@ -114,6 +200,8 @@ await FFMpegArguments .ProcessAsynchronously(); }); } + + [TestMethod, Timeout(10000)] public void Video_StreamFile_OutputToMemoryStream() { diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index 428f21b..e169400 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -41,13 +41,16 @@ public async Task During(CancellationToken cancellationToken = default) try { await ProcessDataAsync(cancellationToken); - Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); - Pipe?.Disconnect(); + Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); } catch (TaskCanceledException) { Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled"); } + finally + { + Pipe?.Disconnect(); + } } protected abstract Task ProcessDataAsync(CancellationToken token); diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs index dad6ef1..485cf20 100644 --- a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs +++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs @@ -49,4 +49,12 @@ public FFMpegArgumentException(string? message = null, Exception? innerException { } } + + public class FFMpegStreamFormatException : FFMpegException + { + public FFMpegStreamFormatException(FFMpegExceptionType type, string message, Exception? innerException = null) + : base(type, message, innerException) + { + } + } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs index 378cead..65f622e 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs @@ -64,7 +64,7 @@ public async Task WriteAsync(System.IO.Stream outputStream, CancellationToken ca private void CheckFrameAndThrow(IVideoFrame frame) { if (frame.Width != Width || frame.Height != Height || frame.Format != StreamFormat) - throw new FFMpegException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" + + throw new FFMpegStreamFormatException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" + $"Frame format: {frame.Width}x{frame.Height} pix_fmt: {frame.Format}\r\n" + $"Stream format: {Width}x{Height} pix_fmt: {StreamFormat}"); } From bbd9b7f55cc95b3b648e7a67fff31057b3e2d62b Mon Sep 17 00:00:00 2001 From: Maxim Bagryantsev Date: Mon, 15 Mar 2021 20:44:48 +0300 Subject: [PATCH 14/22] Moved Debug.WriteLine to Pipe disconnect --- FFMpegCore/FFMpeg/Arguments/PipeArgument.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index e169400..fcb944a 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -40,8 +40,7 @@ public async Task During(CancellationToken cancellationToken = default) { try { - await ProcessDataAsync(cancellationToken); - Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); + await ProcessDataAsync(cancellationToken); } catch (TaskCanceledException) { @@ -49,6 +48,7 @@ public async Task During(CancellationToken cancellationToken = default) } finally { + Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); Pipe?.Disconnect(); } } From eba1dac0b975328bb5dc13e61eebf211d30f0278 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 18:49:20 +0100 Subject: [PATCH 15/22] Update README.md --- README.md | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e4abc3e..89b8f35 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,11 @@ A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and FFProbe is used to gather media information: ```csharp -var mediaInfo = FFProbe.Analyse(inputFile); +var mediaInfo = FFProbe.Analyse(inputPath); ``` or ```csharp -var mediaInfo = await FFProbe.AnalyseAsync(inputFile); +var mediaInfo = await FFProbe.AnalyseAsync(inputPath); ``` @@ -43,20 +43,19 @@ FFMpegArguments .WithConstantRateFactor(21) .WithAudioCodec(AudioCodec.Aac) .WithVariableBitrate(4) - .WithFastStart() - .Scale(VideoSize.Hd)) + .WithVideoFilters(filterOptions => filterOptions + .Scale(VideoSize.Hd)) + .WithFastStart()) .ProcessSynchronously(); ``` Easily capture screens from your videos: ```csharp -var mediaFileAnalysis = FFProbe.Analyse(inputPath); - // process the snapshot in-memory and use the Bitmap directly -var bitmap = FFMpeg.Snapshot(mediaFileAnalysis, new Size(200, 400), TimeSpan.FromMinutes(1)); +var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); // or persists the image on the drive -FFMpeg.Snapshot(mediaFileAnalysis, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1)) +FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); ``` Convert to and/or from streams @@ -89,25 +88,25 @@ FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, Mute videos: ```csharp -FFMpeg.Mute(inputFilePath, outputFilePath); +FFMpeg.Mute(inputPath, outputPath); ``` Save audio track from video: ```csharp -FFMpeg.ExtractAudio(inputVideoFilePath, outputAudioFilePath); +FFMpeg.ExtractAudio(inputPath, outputPath); ``` Add or replace audio track on video: ```csharp -FFMpeg.ReplaceAudio(inputVideoFilePath, inputAudioFilePath, outputVideoFilePath); +FFMpeg.ReplaceAudio(inputPath, inputAudioPath, outputPath); ``` Add poster image to audio file (good for youtube videos): ```csharp -FFMpeg.PosterWithAudio(inputImageFilePath, inputAudioFilePath, outputVideoFilePath); +FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath); // or -var image = Image.FromFile(inputImageFile); -image.AddAudio(inputAudioFilePath, outputVideoFilePath); +var image = Image.FromFile(inputImagePath); +image.AddAudio(inputAudioPath, outputPath); ``` Other available arguments could be found in `FFMpegCore.Arguments` namespace. @@ -135,10 +134,11 @@ var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumera { FrameRate = 30 //set source frame rate }; -FFMpegArguments - .FromPipeInput(videoFramesSource, ) - .OutputToFile("temporary.mp4", false, ) - .ProcessSynchronously(); +await FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputPath, false, options => options + .WithVideoCodec(VideoCodec.LibVpx)) + .ProcessAsynchronously(); ``` if you want to use `System.Drawing.Bitmap` as `IVideoFrame`, there is a `BitmapVideoFrameWrapper` wrapper class. @@ -179,13 +179,19 @@ If these folders are not defined, it will try to find the binaries in `/root/(ff #### Option 1 -The default value (`\\FFMPEG\\bin`) can be overwritten via the `FFMpegOptions` class: +The default value of an empty string (expecting ffmpeg to be found through PATH) can be overwritten via the `FFOptions` class: ```c# -public Startup() -{ - GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); -} +// setting global options +GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); +// or +GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin"); + +// or individual, per-run options +await FFMpegArguments + .FromFileInput(inputPath) + .OutputToFile(outputPath) + .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); ``` #### Option 2 From d44863747a57ba871105e1d1976cd99201c8e0d8 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 18:49:31 +0100 Subject: [PATCH 16/22] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 89b8f35..9dce345 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,6 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f ### License -Copyright © 2020 +Copyright © 2021 Released under [MIT license](https://github.com/rosenbjerg/FFMpegCore/blob/master/LICENSE) From cbf241ca3c8f2825a66282ad05f904769e565386 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 22:45:30 +0100 Subject: [PATCH 17/22] Add examples from readme --- .../FFMpegCore.Examples.csproj | 12 ++ FFMpegCore.Examples/Program.cs | 124 ++++++++++++++++++ FFMpegCore.sln | 6 + 3 files changed, 142 insertions(+) create mode 100644 FFMpegCore.Examples/FFMpegCore.Examples.csproj create mode 100644 FFMpegCore.Examples/Program.cs diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj new file mode 100644 index 0000000..f9daae7 --- /dev/null +++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs new file mode 100644 index 0000000..256ef3c --- /dev/null +++ b/FFMpegCore.Examples/Program.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using FFMpegCore; +using FFMpegCore.Enums; +using FFMpegCore.Pipes; +using FFMpegCore.Extend; + +var inputPath = "/path/to/input"; +var outputPath = "/path/to/output"; + +{ + var mediaInfo = FFProbe.Analyse(inputPath); +} + +{ + var mediaInfo = await FFProbe.AnalyseAsync(inputPath); +} + +{ + FFMpegArguments + .FromFileInput(inputPath) + .OutputToFile(outputPath, false, options => options + .WithVideoCodec(VideoCodec.LibX264) + .WithConstantRateFactor(21) + .WithAudioCodec(AudioCodec.Aac) + .WithVariableBitrate(4) + .WithVideoFilters(filterOptions => filterOptions + .Scale(VideoSize.Hd)) + .WithFastStart()) + .ProcessSynchronously(); +} + +{ + // process the snapshot in-memory and use the Bitmap directly + var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); + + // or persists the image on the drive + FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); +} + +var inputStream = new MemoryStream(); +var outputStream = new MemoryStream(); + +{ + await FFMpegArguments + .FromPipeInput(new StreamPipeSource(inputStream)) + .OutputToPipe(new StreamPipeSink(outputStream), options => options + .WithVideoCodec("vp9") + .ForceFormat("webm")) + .ProcessAsynchronously(); +} + +{ + FFMpeg.Join(@"..\joined_video.mp4", + @"..\part1.mp4", + @"..\part2.mp4", + @"..\part3.mp4" + ); +} + +{ + FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, + ImageInfo.FromPath(@"..\1.png"), + ImageInfo.FromPath(@"..\2.png"), + ImageInfo.FromPath(@"..\3.png") + ); +} + +{ + FFMpeg.Mute(inputPath, outputPath); +} + +{ + FFMpeg.ExtractAudio(inputPath, outputPath); +} + +var inputAudioPath = "/path/to/input/audio"; +{ + FFMpeg.ReplaceAudio(inputPath, inputAudioPath, outputPath); +} + +var inputImagePath = "/path/to/input/image"; +{ + FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath); + // or + var image = Image.FromFile(inputImagePath); + image.AddAudio(inputAudioPath, outputPath); +} + +IVideoFrame GetNextFrame() => throw new NotImplementedException(); +{ + IEnumerable CreateFrames(int count) + { + for(int i = 0; i < count; i++) + { + yield return GetNextFrame(); //method of generating new frames + } + } + + var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable or IEnumerator to constructor of RawVideoPipeSource + { + FrameRate = 30 //set source frame rate + }; + await FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputPath, false, options => options + .WithVideoCodec(VideoCodec.LibVpx)) + .ProcessAsynchronously(); +} + +{ + // setting global options + GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); + // or + GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin"); + + // or individual, per-run options + await FFMpegArguments + .FromFileInput(inputPath) + .OutputToFile(outputPath) + .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); +} \ No newline at end of file diff --git a/FFMpegCore.sln b/FFMpegCore.sln index eab20fd..27eab0a 100644 --- a/FFMpegCore.sln +++ b/FFMpegCore.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore", "FFMpegCore\FF EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Test", "FFMpegCore.Test\FFMpegCore.Test.csproj", "{F20C8353-72D9-454B-9F16-3624DBAD2328}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore.Examples", "FFMpegCore.Examples\FFMpegCore.Examples.csproj", "{3125CF91-FFBD-4E4E-8930-247116AFE772}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {F20C8353-72D9-454B-9F16-3624DBAD2328}.Debug|Any CPU.Build.0 = Debug|Any CPU {F20C8353-72D9-454B-9F16-3624DBAD2328}.Release|Any CPU.ActiveCfg = Release|Any CPU {F20C8353-72D9-454B-9F16-3624DBAD2328}.Release|Any CPU.Build.0 = Release|Any CPU + {3125CF91-FFBD-4E4E-8930-247116AFE772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3125CF91-FFBD-4E4E-8930-247116AFE772}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 68822845933343f554866373dd4462bbc7c72f54 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 22:48:43 +0100 Subject: [PATCH 18/22] Move extension method from Bitmap to Image --- FFMpegCore/Extend/BitmapExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/Extend/BitmapExtensions.cs b/FFMpegCore/Extend/BitmapExtensions.cs index bf10336..e2f5505 100644 --- a/FFMpegCore/Extend/BitmapExtensions.cs +++ b/FFMpegCore/Extend/BitmapExtensions.cs @@ -6,7 +6,7 @@ namespace FFMpegCore.Extend { public static class BitmapExtensions { - public static bool AddAudio(this Bitmap poster, string audio, string output) + public static bool AddAudio(this Image poster, string audio, string output) { var destination = $"{Environment.TickCount}.png"; poster.Save(destination); From 0face0b6e4b36429ea85e1c4a8405d4d7875f6a2 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 22:48:53 +0100 Subject: [PATCH 19/22] Bump nuget dependencies --- FFMpegCore.Test/FFMpegCore.Test.csproj | 8 ++++---- FFMpegCore/FFMpegCore.csproj | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 2cd97bb..98c9274 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -39,10 +39,10 @@ - - - - + + + + diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 69a8e61..615b71a 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -32,7 +32,7 @@ - + From fc2802d5fbe7cffc8ea1d4e3ced145abc85c634e Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:04:59 +0100 Subject: [PATCH 20/22] Renaming to OutputUrlArgument --- ...OutputStreamArgument.cs => OutputUrlArgument.cs} | 13 +++++++------ FFMpegCore/FFMpeg/FFMpegArguments.cs | 5 ++--- 2 files changed, 9 insertions(+), 9 deletions(-) rename FFMpegCore/FFMpeg/Arguments/{OutputStreamArgument.cs => OutputUrlArgument.cs} (51%) diff --git a/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs similarity index 51% rename from FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs rename to FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs index 5581929..15cbef9 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs @@ -4,15 +4,16 @@ namespace FFMpegCore.Arguments { /// - /// Represents output stream parameter + /// Represents outputting to url using supported protocols + /// See http://ffmpeg.org/ffmpeg-protocols.html /// - public class OutputStreamArgument : IOutputArgument + public class OutputUrlArgument : IOutputArgument { - public readonly string Stream; + public readonly string Url; - public OutputStreamArgument(string stream) + public OutputUrlArgument(string url) { - Stream = stream; + Url = url; } public void Post() { } @@ -21,6 +22,6 @@ public void Post() { } public void Pre() { } - public string Text => Stream; + public string Text => Url; } } diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index cdce6be..847e68c 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -49,9 +49,8 @@ private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments = null) => ToProcessor(new OutputArgument(file, overwrite), addArguments); - public FFMpegArgumentProcessor OutputToFile(Uri uri, bool overwrite = true, Action? addArguments = null) => ToProcessor(new OutputArgument(uri.AbsolutePath, overwrite), addArguments); - public FFMpegArgumentProcessor OutputToStream(string uri, Action? addArguments = null) => ToProcessor(new OutputStreamArgument(uri), addArguments); - public FFMpegArgumentProcessor OutputToStream(Uri uri, Action? addArguments = null) => ToProcessor(new OutputStreamArgument(uri.ToString()), addArguments); + public FFMpegArgumentProcessor OutputToUrl(string uri, Action? addArguments = null) => ToProcessor(new OutputUrlArgument(uri), addArguments); + public FFMpegArgumentProcessor OutputToUrl(Uri uri, Action? addArguments = null) => ToProcessor(new OutputUrlArgument(uri.ToString()), addArguments); public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader, Action? addArguments = null) => ToProcessor(new OutputPipeArgument(reader), addArguments); private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action? addArguments) From 0a146251e78a786d5a58b75eac0504309670f7a3 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:06:34 +0100 Subject: [PATCH 21/22] Update nuget meta --- FFMpegCore/FFMpegCore.csproj | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 615b71a..0f43758 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -5,18 +5,16 @@ https://github.com/rosenbjerg/FFMpegCore https://github.com/rosenbjerg/FFMpegCore - A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your C# applications + A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications 3.0.0.0 3.0.0.0 3.0.0.0 - - Video filter args refactored to support multiple arguments -- Cancel improved with timeout (thanks TFleury) -- Basic support for webcam/mic input through InputDeviceArgument (thanks TFleury) -- Other fixes and improvements + - Fixes for RawVideoPipeSource hanging (thanks to max619) +- Added .OutputToUrl(..) method for outputting to url using supported protocol (thanks to TFleury) 8 - 4.0.0 + 4.1.0 MIT - Malte Rosenbjerg, Vlad Jerca + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev ffmpeg ffprobe convert video audio mediafile resize analyze muxing GitHub true From 8a314f02ae9b0ce037998b66764c150ed0a1afc8 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:17:56 +0100 Subject: [PATCH 22/22] Move MediaAnalysis parsing helper methods to static class --- FFMpegCore/FFProbe/MediaAnalysis.cs | 105 +++++++++++++++------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index f1b2f82..1ee7b18 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -7,8 +7,6 @@ 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(FFProbeAnalysis analysis) { Format = ParseFormat(analysis.Format); @@ -50,14 +48,14 @@ private VideoStream ParseVideoStream(FFProbeStream stream) return new VideoStream { Index = stream.Index, - AvgFrameRate = DivideRatio(ParseRatioDouble(stream.AvgFrameRate, '/')), - BitRate = !string.IsNullOrEmpty(stream.BitRate) ? ParseIntInvariant(stream.BitRate) : default, - BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? ParseIntInvariant(stream.BitsPerRawSample) : default, + AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')), + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default, + BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default, CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, - DisplayAspectRatio = ParseRatioInt(stream.DisplayAspectRatio, ':'), - Duration = ParseDuration(stream), - FrameRate = DivideRatio(ParseRatioDouble(stream.FrameRate, '/')), + DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'), + Duration = MediaAnalysisUtils.ParseDuration(stream), + FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')), Height = stream.Height ?? 0, Width = stream.Width ?? 0, Profile = stream.Profile, @@ -68,7 +66,56 @@ private VideoStream ParseVideoStream(FFProbeStream stream) }; } - private static TimeSpan ParseDuration(FFProbeStream ffProbeStream) + + private AudioStream ParseAudioStream(FFProbeStream stream) + { + return new AudioStream + { + Index = stream.Index, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default, + CodecName = stream.CodecName, + CodecLongName = stream.CodecLongName, + Channels = stream.Channels ?? default, + ChannelLayout = stream.ChannelLayout, + Duration = MediaAnalysisUtils.ParseDuration(stream), + SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default, + Profile = stream.Profile, + Language = stream.GetLanguage(), + Tags = stream.Tags, + }; + } + + + } + + public static class MediaAnalysisUtils + { + private static readonly Regex DurationRegex = new Regex("^(\\d{1,5}:\\d{1,2}:\\d{1,2}(.\\d{1,7})?)", RegexOptions.Compiled); + + public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2; + + public static (int, int) ParseRatioInt(string input, char separator) + { + if (string.IsNullOrEmpty(input)) return (0, 0); + var ratio = input.Split(separator); + return (ParseIntInvariant(ratio[0]), ParseIntInvariant(ratio[1])); + } + + public static (double, double) ParseRatioDouble(string input, char separator) + { + if (string.IsNullOrEmpty(input)) return (0, 0); + var ratio = input.Split(separator); + return (ratio.Length > 0 ? ParseDoubleInvariant(ratio[0]) : 0, ratio.Length > 1 ? ParseDoubleInvariant(ratio[1]) : 0); + } + + public static double ParseDoubleInvariant(string line) => + double.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + + public static int ParseIntInvariant(string line) => + int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + + + public static TimeSpan ParseDuration(FFProbeStream ffProbeStream) { return !string.IsNullOrEmpty(ffProbeStream.Duration) ? TimeSpan.Parse(ffProbeStream.Duration) @@ -80,45 +127,5 @@ private static TimeSpan ParseDuration(FFProbeStream ffProbeStream) 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(), - Tags = stream.Tags, - }; - } - - 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