From 2c2ceacb41d0dc30ed6726a0a1e1a284e3f3471d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=91=D0=B0=D0=B3?= =?UTF-8?q?=D1=80=D1=8F=D0=BD=D1=86=D0=B5=D0=B2?= Date: Mon, 27 Apr 2020 19:23:31 +0300 Subject: [PATCH] Added input piping Former-commit-id: 13d5e3d1910868610c3add8af6d24f69879ef43f --- FFMpegCore/Extend/BitmapVideoFrameWrapper.cs | 89 ++++++++++++++ .../FFMPEG/Argument/ArgumentContainer.cs | 11 +- .../Argument/Atoms/InputPipeArgument.cs | 74 +++++++++++ FFMpegCore/FFMPEG/FFMpeg.cs | 115 +++++++++++++----- FFMpegCore/FFMPEG/FFProbe.cs | 24 +++- FFMpegCore/FFMPEG/Pipes/IInputPipe.cs | 13 ++ FFMpegCore/FFMPEG/Pipes/IPipeSource.cs | 15 +++ FFMpegCore/FFMPEG/Pipes/IVideoFrame.cs | 17 +++ FFMpegCore/FFMPEG/Pipes/RawVideoPipeSource.cs | 67 ++++++++++ FFMpegCore/VideoInfo.cs | 2 + 10 files changed, 389 insertions(+), 38 deletions(-) create mode 100644 FFMpegCore/Extend/BitmapVideoFrameWrapper.cs create mode 100644 FFMpegCore/FFMPEG/Argument/Atoms/InputPipeArgument.cs create mode 100644 FFMpegCore/FFMPEG/Pipes/IInputPipe.cs create mode 100644 FFMpegCore/FFMPEG/Pipes/IPipeSource.cs create mode 100644 FFMpegCore/FFMPEG/Pipes/IVideoFrame.cs create mode 100644 FFMpegCore/FFMPEG/Pipes/RawVideoPipeSource.cs diff --git a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs new file mode 100644 index 0000000..8b86461 --- /dev/null +++ b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs @@ -0,0 +1,89 @@ +using FFMpegCore.FFMPEG.Pipes; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.Extend +{ + public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable + { + public int Width => Source.Width; + + public int Height => Source.Height; + + public string Format { get; private set; } + + public Bitmap Source { get; private set; } + + public BitmapVideoFrameWrapper(Bitmap bitmap) + { + Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap)); + Format = ConvertStreamFormat(bitmap.PixelFormat); + } + + public void Serialize(IInputPipe pipe) + { + var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); + + try + { + var buffer = new byte[data.Stride * data.Height]; + Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); + pipe.Write(buffer, 0, buffer.Length); + } + finally + { + Source.UnlockBits(data); + } + } + + public async Task SerializeAsync(IInputPipe pipe) + { + var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); + + try + { + var buffer = new byte[data.Stride * data.Height]; + Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); + await pipe.WriteAsync(buffer, 0, buffer.Length); + } + finally + { + Source.UnlockBits(data); + } + } + + public void Dispose() + { + Source.Dispose(); + } + + private static string ConvertStreamFormat(PixelFormat fmt) + { + switch (fmt) + { + case PixelFormat.Format16bppGrayScale: + return "gray16le"; + case PixelFormat.Format16bppRgb565: + return "bgr565le"; + case PixelFormat.Format24bppRgb: + return "rgb24"; + case PixelFormat.Format32bppArgb: + return "rgba"; + case PixelFormat.Format32bppPArgb: + //This is not really same as argb32 + return "argb"; + case PixelFormat.Format32bppRgb: + return "rgba"; + case PixelFormat.Format48bppRgb: + return "rgb48le"; + default: + throw new NotSupportedException($"Not supported pixel format {fmt}"); + } + } + } +} diff --git a/FFMpegCore/FFMPEG/Argument/ArgumentContainer.cs b/FFMpegCore/FFMPEG/Argument/ArgumentContainer.cs index d831904..d2e5532 100644 --- a/FFMpegCore/FFMPEG/Argument/ArgumentContainer.cs +++ b/FFMpegCore/FFMPEG/Argument/ArgumentContainer.cs @@ -15,7 +15,7 @@ public ArgumentContainer(params Argument[] arguments) { _args = new Dictionary(); - foreach(var argument in arguments) + foreach (var argument in arguments) { Add(argument); } @@ -28,7 +28,7 @@ public bool TryGetArgument(out T output) { if (_args.TryGetValue(typeof(T), out var arg)) { - output = (T) arg; + output = (T)arg; return true; } @@ -90,7 +90,7 @@ public bool Contains(KeyValuePair item) /// Argument that should be added to collection public void Add(params Argument[] values) { - foreach(var value in values) + foreach (var value in values) { _args.Add(value.GetType(), value); } @@ -102,8 +102,9 @@ public void Add(params Argument[] values) /// public bool ContainsInputOutput() { - return ((ContainsKey(typeof(InputArgument)) && !ContainsKey(typeof(ConcatArgument))) || - (!ContainsKey(typeof(InputArgument)) && ContainsKey(typeof(ConcatArgument)))) + return ((ContainsKey(typeof(InputArgument)) && !ContainsKey(typeof(ConcatArgument)) && !ContainsKey(typeof(InputPipeArgument))) || + (!ContainsKey(typeof(InputArgument)) && ContainsKey(typeof(ConcatArgument)) && !ContainsKey(typeof(InputPipeArgument))) || + (!ContainsKey(typeof(InputArgument)) && !ContainsKey(typeof(ConcatArgument)) && ContainsKey(typeof(InputPipeArgument)))) && ContainsKey(typeof(OutputArgument)); } diff --git a/FFMpegCore/FFMPEG/Argument/Atoms/InputPipeArgument.cs b/FFMpegCore/FFMPEG/Argument/Atoms/InputPipeArgument.cs new file mode 100644 index 0000000..1d715a1 --- /dev/null +++ b/FFMpegCore/FFMPEG/Argument/Atoms/InputPipeArgument.cs @@ -0,0 +1,74 @@ +using FFMpegCore.FFMPEG.Pipes; +using Instances; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Argument +{ + public class InputPipeArgument : Argument, IInputPipe + { + public string PipeName { get; private set; } + public IPipeSource Source { get; private set; } + + private NamedPipeServerStream pipe; + + public InputPipeArgument(IPipeSource source) + { + Source = source; + PipeName = "FFMpegCore_Pipe_" + Guid.NewGuid(); + } + + public void OpenPipe() + { + if (pipe != null) + throw new InvalidOperationException("Pipe already has been opened"); + + pipe = new NamedPipeServerStream(PipeName); + } + + public void ClosePipe() + { + pipe?.Dispose(); + pipe = null; + } + + public void Write(byte[] buffer, int offset, int count) + { + if(pipe == null) + throw new InvalidOperationException("Pipe shouled be opened before"); + + pipe.Write(buffer, offset, count); + } + + public Task WriteAsync(byte[] buffer, int offset, int count) + { + if (pipe == null) + throw new InvalidOperationException("Pipe shouled be opened before"); + + return pipe.WriteAsync(buffer, offset, count); + } + + public override string GetStringValue() + { + return $"-y {Source.GetFormat()} -i \\\\.\\pipe\\{PipeName}"; + } + + public void FlushPipe() + { + pipe.WaitForConnection(); + Source.FlushData(this); + } + + + public async Task FlushPipeAsync() + { + await pipe.WaitForConnectionAsync(); + await Source.FlushDataAsync(this); + } + } +} diff --git a/FFMpegCore/FFMPEG/FFMpeg.cs b/FFMpegCore/FFMPEG/FFMpeg.cs index d98f24b..f1bd402 100644 --- a/FFMpegCore/FFMPEG/FFMpeg.cs +++ b/FFMpegCore/FFMPEG/FFMpeg.cs @@ -65,16 +65,16 @@ public Bitmap Snapshot(VideoInfo source, FileInfo output, Size? size = null, Tim { if (size.Value.Width == 0) { - var ratio = source.Width / (double) size.Value.Width; + var ratio = source.Width / (double)size.Value.Width; - size = new Size((int) (source.Width * ratio), (int) (source.Height * ratio)); + size = new Size((int)(source.Width * ratio), (int)(source.Height * ratio)); } if (size.Value.Height == 0) { - var ratio = source.Height / (double) size.Value.Height; + var ratio = source.Height / (double)size.Value.Height; - size = new Size((int) (source.Width * ratio), (int) (source.Height * ratio)); + size = new Size((int)(source.Width * ratio), (int)(source.Height * ratio)); } } @@ -96,7 +96,7 @@ public Bitmap Snapshot(VideoInfo source, FileInfo output, Size? size = null, Tim output.Refresh(); Bitmap result; - using (var bmp = (Bitmap) Image.FromFile(output.FullName)) + using (var bmp = (Bitmap)Image.FromFile(output.FullName)) { using var ms = new MemoryStream(); bmp.Save(ms, ImageFormat.Png); @@ -135,8 +135,8 @@ public VideoInfo Convert( FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.ForType(type)); FFMpegHelper.ConversionSizeExceptionCheck(source); - var scale = VideoSize.Original == size ? 1 : (double) source.Height / (int) size; - var outputSize = new Size((int) (source.Width / scale), (int) (source.Height / scale)); + var scale = VideoSize.Original == size ? 1 : (double)source.Height / (int)size; + var outputSize = new Size((int)(source.Width / scale), (int)(source.Height / scale)); if (outputSize.Width % 2 != 0) outputSize.Width += 1; @@ -279,7 +279,7 @@ public VideoInfo JoinImageSequence(FileInfo output, double frameRate = 30, param throw new FFMpegException(FFMpegExceptionType.Operation, "Could not join the provided image sequence."); } - + return new VideoInfo(output); } finally @@ -380,11 +380,12 @@ public VideoInfo ReplaceAudio(VideoInfo source, FileInfo audio, FileInfo output, new OutputArgument(output) )); } - + public VideoInfo Convert(ArgumentContainer arguments) { var (sources, output) = GetInputOutput(arguments); - _totalTime = TimeSpan.FromSeconds(sources.Sum(source => source.Duration.TotalSeconds)); + if (sources != null) + _totalTime = TimeSpan.FromSeconds(sources.Sum(source => source.Duration.TotalSeconds)); if (!RunProcess(arguments, output)) throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio."); @@ -395,7 +396,8 @@ public VideoInfo Convert(ArgumentContainer arguments) public async Task ConvertAsync(ArgumentContainer arguments) { var (sources, output) = GetInputOutput(arguments); - _totalTime = TimeSpan.FromSeconds(sources.Sum(source => source.Duration.TotalSeconds)); + if (sources != null) + _totalTime = TimeSpan.FromSeconds(sources.Sum(source => source.Duration.TotalSeconds)); if (!await RunProcessAsync(arguments, output)) throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio."); @@ -406,12 +408,14 @@ public async Task ConvertAsync(ArgumentContainer arguments) private static (VideoInfo[] Input, FileInfo Output) GetInputOutput(ArgumentContainer arguments) { - var output = ((OutputArgument) arguments[typeof(OutputArgument)]).GetAsFileInfo(); + var output = ((OutputArgument)arguments[typeof(OutputArgument)]).GetAsFileInfo(); VideoInfo[] sources; if (arguments.TryGetArgument(out var input)) sources = input.GetAsVideoInfo(); else if (arguments.TryGetArgument(out var concat)) sources = concat.GetAsVideoInfo(); + else if (arguments.TryGetArgument(out var pipe)) + sources = null; else throw new FFMpegException(FFMpegExceptionType.Operation, "No input or concat argument found"); return (sources, output); @@ -442,29 +446,82 @@ private bool RunProcess(ArgumentContainer container, FileInfo output) { _instance?.Dispose(); var arguments = ArgumentBuilder.BuildArguments(container); - - _instance = new Instance(_ffmpegPath, arguments); - _instance.DataReceived += OutputData; - var exitCode = _instance.BlockUntilFinished(); - - if (!File.Exists(output.FullName) || new FileInfo(output.FullName).Length == 0) - throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\n", _instance.ErrorData)); + var exitCode = -1; - return exitCode == 0; + if (container.TryGetArgument(out var inputPipeArgument)) + { + inputPipeArgument.OpenPipe(); + } + + try + { + _instance = new Instance(_ffmpegPath, arguments); + _instance.DataReceived += OutputData; + + if (inputPipeArgument != null) + { + try + { + var task = _instance.FinishedRunning(); + inputPipeArgument.FlushPipe(); + inputPipeArgument.ClosePipe(); + task.Wait(); + exitCode = task.Result; + } + catch (Exception ex) + { + throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\n", _instance.ErrorData), ex); + } + } + else + { + exitCode = _instance.BlockUntilFinished(); + } + + if (!File.Exists(output.FullName) || new FileInfo(output.FullName).Length == 0) + throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\n", _instance.ErrorData)); + + return exitCode == 0; + } + finally + { + if (inputPipeArgument != null) + inputPipeArgument.ClosePipe(); + } } private async Task RunProcessAsync(ArgumentContainer container, FileInfo output) { _instance?.Dispose(); var arguments = ArgumentBuilder.BuildArguments(container); - - _instance = new Instance(_ffmpegPath, arguments); - _instance.DataReceived += OutputData; - var exitCode = await _instance.FinishedRunning(); - - if (!File.Exists(output.FullName) || new FileInfo(output.FullName).Length == 0) - throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\n", _instance.ErrorData)); - return exitCode == 0; + if (container.TryGetArgument(out var inputPipeArgument)) + { + inputPipeArgument.OpenPipe(); + } + try + { + + _instance = new Instance(_ffmpegPath, arguments); + _instance.DataReceived += OutputData; + + if (inputPipeArgument != null) + { + await inputPipeArgument.FlushPipeAsync(); + } + var exitCode = await _instance.FinishedRunning(); + + if (!File.Exists(output.FullName) || new FileInfo(output.FullName).Length == 0) + throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\n", _instance.ErrorData)); + + return exitCode == 0; + } + finally + { + if (inputPipeArgument != null) + { + inputPipeArgument.ClosePipe(); + } + } } private void Cleanup(IEnumerable pathList) @@ -487,7 +544,7 @@ private void OutputData(object sender, (DataType Type, string Data) msg) Trace.WriteLine(msg.Data); #endif if (OnProgress == null) return; - + var match = ProgressRegex.Match(msg.Data); if (!match.Success) return; diff --git a/FFMpegCore/FFMPEG/FFProbe.cs b/FFMpegCore/FFMPEG/FFProbe.cs index 52cb0b8..827ea7f 100644 --- a/FFMpegCore/FFMPEG/FFProbe.cs +++ b/FFMpegCore/FFMPEG/FFProbe.cs @@ -47,7 +47,7 @@ public Task ParseVideoInfoAsync(string source) /// A video info object containing all details necessary. public VideoInfo ParseVideoInfo(VideoInfo info) { - var instance = new Instance(_ffprobePath, BuildFFProbeArguments(info)) {DataBufferCapacity = _outputCapacity}; + var instance = new Instance(_ffprobePath, BuildFFProbeArguments(info.FullName)) {DataBufferCapacity = _outputCapacity}; instance.BlockUntilFinished(); var output = string.Join("", instance.OutputData); return ParseVideoInfoInternal(info, output); @@ -59,14 +59,14 @@ public VideoInfo ParseVideoInfo(VideoInfo info) /// A video info object containing all details necessary. public async Task ParseVideoInfoAsync(VideoInfo info) { - var instance = new Instance(_ffprobePath, BuildFFProbeArguments(info)) {DataBufferCapacity = _outputCapacity}; + var instance = new Instance(_ffprobePath, BuildFFProbeArguments(info.FullName)) {DataBufferCapacity = _outputCapacity}; await instance.FinishedRunning(); var output = string.Join("", instance.OutputData); return ParseVideoInfoInternal(info, output); } - private static string BuildFFProbeArguments(VideoInfo info) => - $"-v quiet -print_format json -show_streams \"{info.FullName}\""; + private static string BuildFFProbeArguments(string fullPath) => + $"-v quiet -print_format json -show_streams \"{fullPath}\""; private VideoInfo ParseVideoInfoInternal(VideoInfo info, string probeOutput) { @@ -133,5 +133,21 @@ private VideoInfo ParseVideoInfoInternal(VideoInfo info, string probeOutput) return info; } + + internal FFMpegStreamMetadata GetMetadata(string path) + { + var instance = new Instance(_ffprobePath, BuildFFProbeArguments(path)) { DataBufferCapacity = _outputCapacity }; + instance.BlockUntilFinished(); + var output = string.Join("", instance.OutputData); + return JsonConvert.DeserializeObject(output); + } + + internal async Task GetMetadataAsync(string path) + { + var instance = new Instance(_ffprobePath, BuildFFProbeArguments(path)) { DataBufferCapacity = _outputCapacity }; + await instance.FinishedRunning(); + var output = string.Join("", instance.OutputData); + return JsonConvert.DeserializeObject(output); + } } } diff --git a/FFMpegCore/FFMPEG/Pipes/IInputPipe.cs b/FFMpegCore/FFMPEG/Pipes/IInputPipe.cs new file mode 100644 index 0000000..d31d047 --- /dev/null +++ b/FFMpegCore/FFMPEG/Pipes/IInputPipe.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Pipes +{ + public interface IInputPipe + { + void Write(byte[] buffer, int offset, int count); + Task WriteAsync(byte[] buffer, int offset, int count); + } +} diff --git a/FFMpegCore/FFMPEG/Pipes/IPipeSource.cs b/FFMpegCore/FFMPEG/Pipes/IPipeSource.cs new file mode 100644 index 0000000..943bec6 --- /dev/null +++ b/FFMpegCore/FFMPEG/Pipes/IPipeSource.cs @@ -0,0 +1,15 @@ +using FFMpegCore.FFMPEG.Argument; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Pipes +{ + public interface IPipeSource + { + string GetFormat(); + void FlushData(IInputPipe pipe); + Task FlushDataAsync(IInputPipe pipe); + } +} diff --git a/FFMpegCore/FFMPEG/Pipes/IVideoFrame.cs b/FFMpegCore/FFMPEG/Pipes/IVideoFrame.cs new file mode 100644 index 0000000..2458c30 --- /dev/null +++ b/FFMpegCore/FFMPEG/Pipes/IVideoFrame.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Pipes +{ + public interface IVideoFrame + { + int Width { get; } + int Height { get; } + string Format { get; } + + void Serialize(IInputPipe pipe); + Task SerializeAsync(IInputPipe pipe); + } +} diff --git a/FFMpegCore/FFMPEG/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMPEG/Pipes/RawVideoPipeSource.cs new file mode 100644 index 0000000..586ec0e --- /dev/null +++ b/FFMpegCore/FFMPEG/Pipes/RawVideoPipeSource.cs @@ -0,0 +1,67 @@ +using FFMpegCore.FFMPEG.Argument; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Pipes +{ + public class RawVideoPipeSource : IPipeSource + { + public string StreamFormat { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public int FrameRate { get; set; } = 25; + private IEnumerator framesEnumerator; + + public RawVideoPipeSource(IEnumerator framesEnumerator) + { + this.framesEnumerator = framesEnumerator; + } + + public RawVideoPipeSource(IEnumerable framesEnumerator) : this(framesEnumerator.GetEnumerator()) { } + + public string GetFormat() + { + //see input format references https://lists.ffmpeg.org/pipermail/ffmpeg-user/2012-July/007742.html + if (framesEnumerator.Current == null) + { + if (!framesEnumerator.MoveNext()) + throw new InvalidOperationException("Enumerator is empty, unable to get frame"); + + StreamFormat = framesEnumerator.Current.Format; + Width = framesEnumerator.Current.Width; + Height = framesEnumerator.Current.Height; + } + + return $"-f rawvideo -r {FrameRate} -pix_fmt {StreamFormat} -s {Width}x{Height}"; + } + + public void FlushData(IInputPipe pipe) + { + if (framesEnumerator.Current != null) + { + framesEnumerator.Current.Serialize(pipe); + } + + while (framesEnumerator.MoveNext()) + { + framesEnumerator.Current.Serialize(pipe); + } + } + + public async Task FlushDataAsync(IInputPipe pipe) + { + if (framesEnumerator.Current != null) + { + await framesEnumerator.Current.SerializeAsync(pipe); + } + + while (framesEnumerator.MoveNext()) + { + await framesEnumerator.Current.SerializeAsync(pipe); + } + } + + } +} diff --git a/FFMpegCore/VideoInfo.cs b/FFMpegCore/VideoInfo.cs index b6f97d7..f56145c 100644 --- a/FFMpegCore/VideoInfo.cs +++ b/FFMpegCore/VideoInfo.cs @@ -1,4 +1,6 @@ using FFMpegCore.FFMPEG; +using FFMpegCore.FFMPEG.Argument; +using FFMpegCore.FFMPEG.Pipes; using System; using System.IO;