Added input piping

This commit is contained in:
Максим Багрянцев 2020-04-27 19:23:31 +03:00
parent 7406cd0fa2
commit 13d5e3d191
10 changed files with 389 additions and 38 deletions

View file

@ -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}");
}
}
}
}

View file

@ -15,7 +15,7 @@ public ArgumentContainer(params Argument[] arguments)
{ {
_args = new Dictionary<Type, Argument>(); _args = new Dictionary<Type, Argument>();
foreach(var argument in arguments) foreach (var argument in arguments)
{ {
Add(argument); Add(argument);
} }
@ -28,7 +28,7 @@ public bool TryGetArgument<T>(out T output)
{ {
if (_args.TryGetValue(typeof(T), out var arg)) if (_args.TryGetValue(typeof(T), out var arg))
{ {
output = (T) arg; output = (T)arg;
return true; return true;
} }
@ -90,7 +90,7 @@ public bool Contains(KeyValuePair<Type, Argument> item)
/// <param name="value">Argument that should be added to collection</param> /// <param name="value">Argument that should be added to collection</param>
public void Add(params Argument[] values) public void Add(params Argument[] values)
{ {
foreach(var value in values) foreach (var value in values)
{ {
_args.Add(value.GetType(), value); _args.Add(value.GetType(), value);
} }
@ -102,8 +102,9 @@ public void Add(params Argument[] values)
/// <returns></returns> /// <returns></returns>
public bool ContainsInputOutput() public bool ContainsInputOutput()
{ {
return ((ContainsKey(typeof(InputArgument)) && !ContainsKey(typeof(ConcatArgument))) || return ((ContainsKey(typeof(InputArgument)) && !ContainsKey(typeof(ConcatArgument)) && !ContainsKey(typeof(InputPipeArgument))) ||
(!ContainsKey(typeof(InputArgument)) && ContainsKey(typeof(ConcatArgument)))) (!ContainsKey(typeof(InputArgument)) && ContainsKey(typeof(ConcatArgument)) && !ContainsKey(typeof(InputPipeArgument))) ||
(!ContainsKey(typeof(InputArgument)) && !ContainsKey(typeof(ConcatArgument)) && ContainsKey(typeof(InputPipeArgument))))
&& ContainsKey(typeof(OutputArgument)); && ContainsKey(typeof(OutputArgument));
} }

View file

@ -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);
}
}
}

View file

@ -65,16 +65,16 @@ public Bitmap Snapshot(VideoInfo source, FileInfo output, Size? size = null, Tim
{ {
if (size.Value.Width == 0) 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) 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(); output.Refresh();
Bitmap result; Bitmap result;
using (var bmp = (Bitmap) Image.FromFile(output.FullName)) using (var bmp = (Bitmap)Image.FromFile(output.FullName))
{ {
using var ms = new MemoryStream(); using var ms = new MemoryStream();
bmp.Save(ms, ImageFormat.Png); bmp.Save(ms, ImageFormat.Png);
@ -135,8 +135,8 @@ public VideoInfo Convert(
FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.ForType(type)); FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.ForType(type));
FFMpegHelper.ConversionSizeExceptionCheck(source); FFMpegHelper.ConversionSizeExceptionCheck(source);
var scale = VideoSize.Original == size ? 1 : (double) source.Height / (int) size; var scale = VideoSize.Original == size ? 1 : (double)source.Height / (int)size;
var outputSize = new Size((int) (source.Width / scale), (int) (source.Height / scale)); var outputSize = new Size((int)(source.Width / scale), (int)(source.Height / scale));
if (outputSize.Width % 2 != 0) if (outputSize.Width % 2 != 0)
outputSize.Width += 1; outputSize.Width += 1;
@ -279,7 +279,7 @@ public VideoInfo JoinImageSequence(FileInfo output, double frameRate = 30, param
throw new FFMpegException(FFMpegExceptionType.Operation, throw new FFMpegException(FFMpegExceptionType.Operation,
"Could not join the provided image sequence."); "Could not join the provided image sequence.");
} }
return new VideoInfo(output); return new VideoInfo(output);
} }
finally finally
@ -380,11 +380,12 @@ public VideoInfo ReplaceAudio(VideoInfo source, FileInfo audio, FileInfo output,
new OutputArgument(output) new OutputArgument(output)
)); ));
} }
public VideoInfo Convert(ArgumentContainer arguments) public VideoInfo Convert(ArgumentContainer arguments)
{ {
var (sources, output) = GetInputOutput(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)) if (!RunProcess(arguments, output))
throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio."); throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio.");
@ -395,7 +396,8 @@ public VideoInfo Convert(ArgumentContainer arguments)
public async Task<VideoInfo> ConvertAsync(ArgumentContainer arguments) public async Task<VideoInfo> ConvertAsync(ArgumentContainer arguments)
{ {
var (sources, output) = GetInputOutput(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)) if (!await RunProcessAsync(arguments, output))
throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio."); throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio.");
@ -406,12 +408,14 @@ public async Task<VideoInfo> ConvertAsync(ArgumentContainer arguments)
private static (VideoInfo[] Input, FileInfo Output) GetInputOutput(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; VideoInfo[] sources;
if (arguments.TryGetArgument<InputArgument>(out var input)) if (arguments.TryGetArgument<InputArgument>(out var input))
sources = input.GetAsVideoInfo(); sources = input.GetAsVideoInfo();
else if (arguments.TryGetArgument<ConcatArgument>(out var concat)) else if (arguments.TryGetArgument<ConcatArgument>(out var concat))
sources = concat.GetAsVideoInfo(); sources = concat.GetAsVideoInfo();
else if (arguments.TryGetArgument<InputPipeArgument>(out var pipe))
sources = null;
else else
throw new FFMpegException(FFMpegExceptionType.Operation, "No input or concat argument found"); throw new FFMpegException(FFMpegExceptionType.Operation, "No input or concat argument found");
return (sources, output); return (sources, output);
@ -442,29 +446,82 @@ private bool RunProcess(ArgumentContainer container, FileInfo output)
{ {
_instance?.Dispose(); _instance?.Dispose();
var arguments = ArgumentBuilder.BuildArguments(container); var arguments = ArgumentBuilder.BuildArguments(container);
var exitCode = -1;
_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));
return exitCode == 0; if (container.TryGetArgument<InputPipeArgument>(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<bool> RunProcessAsync(ArgumentContainer container, FileInfo output) private async Task<bool> RunProcessAsync(ArgumentContainer container, FileInfo output)
{ {
_instance?.Dispose(); _instance?.Dispose();
var arguments = ArgumentBuilder.BuildArguments(container); 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<InputPipeArgument>(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<string> pathList) private void Cleanup(IEnumerable<string> pathList)
@ -487,7 +544,7 @@ private void OutputData(object sender, (DataType Type, string Data) msg)
Trace.WriteLine(msg.Data); Trace.WriteLine(msg.Data);
#endif #endif
if (OnProgress == null) return; if (OnProgress == null) return;
var match = ProgressRegex.Match(msg.Data); var match = ProgressRegex.Match(msg.Data);
if (!match.Success) return; if (!match.Success) return;

View file

@ -47,7 +47,7 @@ public Task<VideoInfo> ParseVideoInfoAsync(string source)
/// <returns>A video info object containing all details necessary.</returns> /// <returns>A video info object containing all details necessary.</returns>
public VideoInfo ParseVideoInfo(VideoInfo info) 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(); instance.BlockUntilFinished();
var output = string.Join("", instance.OutputData); var output = string.Join("", instance.OutputData);
return ParseVideoInfoInternal(info, output); return ParseVideoInfoInternal(info, output);
@ -59,14 +59,14 @@ public VideoInfo ParseVideoInfo(VideoInfo info)
/// <returns>A video info object containing all details necessary.</returns> /// <returns>A video info object containing all details necessary.</returns>
public async Task<VideoInfo> ParseVideoInfoAsync(VideoInfo info) public async Task<VideoInfo> 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(); await instance.FinishedRunning();
var output = string.Join("", instance.OutputData); var output = string.Join("", instance.OutputData);
return ParseVideoInfoInternal(info, output); return ParseVideoInfoInternal(info, output);
} }
private static string BuildFFProbeArguments(VideoInfo info) => private static string BuildFFProbeArguments(string fullPath) =>
$"-v quiet -print_format json -show_streams \"{info.FullName}\""; $"-v quiet -print_format json -show_streams \"{fullPath}\"";
private VideoInfo ParseVideoInfoInternal(VideoInfo info, string probeOutput) private VideoInfo ParseVideoInfoInternal(VideoInfo info, string probeOutput)
{ {
@ -133,5 +133,21 @@ private VideoInfo ParseVideoInfoInternal(VideoInfo info, string probeOutput)
return info; 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<FFMpegStreamMetadata>(output);
}
internal async Task<FFMpegStreamMetadata> GetMetadataAsync(string path)
{
var instance = new Instance(_ffprobePath, BuildFFProbeArguments(path)) { DataBufferCapacity = _outputCapacity };
await instance.FinishedRunning();
var output = string.Join("", instance.OutputData);
return JsonConvert.DeserializeObject<FFMpegStreamMetadata>(output);
}
} }
} }

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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<IVideoFrame> framesEnumerator;
public RawVideoPipeSource(IEnumerator<IVideoFrame> framesEnumerator)
{
this.framesEnumerator = framesEnumerator;
}
public RawVideoPipeSource(IEnumerable<IVideoFrame> 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);
}
}
}
}

View file

@ -1,4 +1,6 @@
using FFMpegCore.FFMPEG; using FFMpegCore.FFMPEG;
using FFMpegCore.FFMPEG.Argument;
using FFMpegCore.FFMPEG.Pipes;
using System; using System;
using System.IO; using System.IO;