Files
FileFlowsPlugins/VideoNodes/FFMpegEncoder.cs

377 lines
13 KiB
C#

using System.Diagnostics;
using System.Text;
namespace FileFlows.VideoNodes;
public class FFMpegEncoder
{
private string ffMpegExe;
private ILogger Logger;
StringBuilder outputBuilder, errorBuilder;
TaskCompletionSource<bool> outputCloseEvent, errorCloseEvent;
private Regex rgxTime = new Regex(@"(?<=(time=))([\d]+:?)+\.[\d]+");
private Regex rgxFps = new Regex(@"(?<=(fps=[\s]?))([\d]+(\.[\d]+)?)");
private Regex rgxSpeed = new Regex(@"(?<=(speed=[\s]?))([\d]+(\.[\d]+)?)");
private Regex rgxBitrate = new Regex(@"(?<=(bitrate=[\s]?))([\d]+(\.[\d]+)?)kbits");
public delegate void TimeEvent(TimeSpan time, DateTime startedAt);
public event TimeEvent AtTime;
public delegate void StatChange(string name, string value, bool recordStatistic = false);
public event StatChange OnStatChange;
private Process process;
DateTime startedAt;
private string? AbortReason;
public FFMpegEncoder(string ffMpegExe, ILogger logger)
{
this.ffMpegExe = ffMpegExe;
this.Logger = logger;
}
/// <summary>
/// Encodes using FFmpeg
/// </summary>
/// <param name="input">the input file</param>
/// <param name="output">the output file</param>
/// <param name="arguments">the FFmpeg arguments</param>
/// <param name="dontAddInputFile">if the input file should not be added to the arguments</param>
/// <param name="dontAddOutputFile">if the output file should not be added to the arguments</param>
/// <param name="strictness">the strictness to use</param>
/// <returns>the result and output of the encode</returns>
public (bool successs, string output, string? abortReason) Encode(string input, string output, List<string> arguments, bool dontAddInputFile = false, bool dontAddOutputFile = false, string strictness = "-2")
{
arguments ??= new List<string> ();
if (string.IsNullOrWhiteSpace(strictness))
strictness = "-2";
// -y means it will overwrite a file if output already exists
if (dontAddInputFile == false) {
arguments.Insert(0, "-i");
arguments.Insert(1, input);
arguments.Insert(2, "-y");
}
if (arguments.Any(x => x.Equals("webvtt", StringComparison.InvariantCultureIgnoreCase)))
{
arguments.Insert(0, "-c:s");
arguments.Insert(1, "webvtt");
}
if (dontAddOutputFile == false)
{
if (arguments.Last() != "-")
{
arguments.AddRange(new string[]
{ "-metadata", "comment=Created by FileFlows\nhttps://fileflows.com" });
// strict -2 needs to be just before the output file
arguments.AddRange(new[] { "-strict", strictness });
arguments.Add(output);
}
else
Logger.ILog("Last argument '-' skipping adding output file");
}
string argsString = String.Join(" ", arguments.Select(x => x.IndexOf(" ") > 0 ? "\"" + x + "\"" : x));
Logger.ILog(new string('-', ("FFmpeg.Arguments: " + argsString).Length));
Logger.ILog("FFmpeg.Arguments: " + argsString);
Logger.ILog(new string('-', ("FFmpeg.Arguments: " + argsString).Length));
var task = ExecuteShellCommand(ffMpegExe, arguments, 0);
task.Wait();
Logger.ILog("Exit Code: " + task.Result.ExitCode);
return (task.Result.ExitCode == 0, task.Result.Output, task.Result.AbortReason); // exitcode 0 means it was successful
}
/// <summary>
/// Aborts the file process
/// </summary>
/// <param name="reason">the reason for the abort</param>
private void Abort(string reason)
{
this.AbortReason = reason;
Cancel();
}
internal void Cancel()
{
try
{
if (this.process != null)
{
this.process.Kill();
this.process = null;
}
}
catch (Exception) { }
}
public async Task<ProcessResult> ExecuteShellCommand(string command, List<string> arguments, int timeout = 0)
{
var result = new ProcessResult();
var hwDecoderIndex = arguments.FindIndex(x => x == "-hwaccel");
string? decoder = null;
if (hwDecoderIndex >= 0 && hwDecoderIndex < arguments.Count - 2)
{
var decoder2 = arguments[hwDecoderIndex + 1].ToLowerInvariant();
foreach(var dec in new []
{
("qsv", "QSV"), ("cuda", "NVIDIA"), ("amf", "AMD"), ("vulkan", "Vulkan"), ("vaapi", "VAAPI"), ("dxva2", "dxva2"),
("d3d11va", "d3d11va"), ("opencl", "opencl")
})
{
if (decoder2 == dec.Item1)
{
decoder = dec.Item2;
break;
}
}
}
else
{
var index = arguments.FindIndex(x => x.Contains("-c:v:"));
if (index > 0 && index < arguments.Count - 2)
{
if(arguments[index + 1] != "copy")
decoder = "CPU";
else
OnStatChange?.Invoke("Decoder", "Copy", recordStatistic: false);
}
}
if (decoder != null)
{
Logger?.ILog("Decoder: " + decoder);
OnStatChange?.Invoke("Decoder", decoder, recordStatistic: true);
}
string? encoder = null;
if (arguments.Any(x =>
x.ToLowerInvariant().Contains("hevc_qsv") || x.ToLowerInvariant().Contains("h264_qsv") ||
x.ToLowerInvariant().Contains("av1_qsv")))
encoder = "QSV";
else if (arguments.Any(x => x.ToLowerInvariant().Contains("_nvenc")))
encoder = "NVIDIA";
else if (arguments.Any(x => x.ToLowerInvariant().Contains("_amf")))
encoder = "AMF";
else if (arguments.Any(x => x.ToLowerInvariant().Contains("_vaapi")))
encoder = "VAAPI";
else if (arguments.Any(x => x.ToLowerInvariant().Contains("_videotoolbox")))
encoder = "VideoToolbox";
else if (arguments.Any(x => x.ToLowerInvariant().Contains("libx") || x.ToLowerInvariant().Contains("libvpx")))
encoder = "CPU";
if (encoder != null)
{
Logger?.ILog("Encoder: " + encoder);
OnStatChange?.Invoke("Encoder", encoder, recordStatistic: true);
}
using (var process = new Process())
{
this.process = process;
process.StartInfo.FileName = command;
if (arguments?.Any() == true)
{
foreach (string arg in arguments)
process.StartInfo.ArgumentList.Add(arg);
}
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
outputBuilder = new StringBuilder();
outputCloseEvent = new TaskCompletionSource<bool>();
process.OutputDataReceived += OnOutputDataReceived;
errorBuilder = new StringBuilder();
errorCloseEvent = new TaskCompletionSource<bool>();
process.ErrorDataReceived += OnErrorDataReceived;
bool isStarted;
startedAt = DateTime.Now;
try
{
isStarted = process.Start();
}
catch (Exception error)
{
// Usually it occurs when an executable file is not found or is not executable
result.Completed = true;
result.ExitCode = -1;
result.Output = error.Message;
isStarted = false;
}
if (isStarted)
{
// Reads the output stream first and then waits because deadlocks are possible
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// Creates task to wait for process exit using timeout
var waitForExit = WaitForExitAsync(process, timeout);
// Create task to wait for process exit and closing all output streams
var processTask = Task.WhenAll(waitForExit, outputCloseEvent.Task, errorCloseEvent.Task);
// Waits process completion and then checks it was not completed by timeout
if (
(
(timeout > 0 && await Task.WhenAny(Task.Delay(timeout), processTask) == processTask) ||
(timeout == 0 && await Task.WhenAny(processTask) == processTask)
)
&& waitForExit.Result)
{
result.Completed = true;
result.ExitCode = process.ExitCode;
result.Output = $"{outputBuilder}{errorBuilder}";
}
else
{
try
{
// Kill hung process
process.Kill();
}
catch
{
}
}
}
}
process = null;
if (this.AbortReason != null)
result.AbortReason = this.AbortReason;
return result;
}
public void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
{
// The output stream has been closed i.e. the process has terminated
if (e.Data == null)
{
outputCloseEvent.SetResult(true);
}
else
{
CheckOutputLine(e.Data);
}
}
public void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
{
// The error stream has been closed i.e. the process has terminated
if (e.Data == null)
{
errorCloseEvent.SetResult(true);
}
else if (e.Data.ToLower().Contains("failed") || e.Data.Contains("No capable devices found") || e.Data.ToLower().Contains("error"))
{
Logger.ELog(e.Data);
errorBuilder.AppendLine(e.Data);
}
else
{
CheckOutputLine(e.Data);
}
}
private int ErrorCount = 0;
private void CheckOutputLine(string line)
{
if (line.Contains("Skipping NAL unit"))
return; // just slightly ignore these
if (line.Contains("error ", StringComparison.InvariantCultureIgnoreCase))
{
if (++ErrorCount > 20)
{
// Abort
Logger.ELog("Maximum number of errors triggered: " + line);
Abort(line);
}
}
if (rgxTime.IsMatch(line))
{
var timeString = rgxTime.Match(line).Value;
var ts = TimeSpan.Parse(timeString);
AtTime?.Invoke(ts, startedAt);
}
if (line.Contains("Exit Code"))
{
OnStatChange?.Invoke("Speed", null);
OnStatChange?.Invoke("Bitrate", null);
OnStatChange?.Invoke("FPS", null);
}
else
{
if (rgxSpeed.TryMatch(line, out Match matchSpeed))
OnStatChange?.Invoke("Speed", matchSpeed.Value);
if (rgxBitrate.TryMatch(line, out Match matchBitrate))
OnStatChange?.Invoke("Bitrate", matchBitrate.Value);
if (rgxFps.TryMatch(line, out Match matchFps))
OnStatChange?.Invoke("FPS", matchFps.Value);
}
Logger.ILog(line);
outputBuilder.AppendLine(line);
}
private static Task<bool> WaitForExitAsync(Process process, int timeout)
{
if (timeout > 0)
return Task.Run(() => process.WaitForExit(timeout));
return Task.Run(() =>
{
process.WaitForExit();
return Task.FromResult<bool>(true);
});
}
/// <summary>
/// Represents the result of a process execution.
/// </summary>
public struct ProcessResult
{
/// <summary>
/// Indicates whether the process completed successfully.
/// </summary>
public bool Completed;
/// <summary>
/// The exit code of the process, if available.
/// </summary>
public int? ExitCode;
/// <summary>
/// The standard output of the process.
/// </summary>
public string Output;
/// <summary>
/// The reason for process abortion, if the process was aborted.
/// </summary>
public string AbortReason;
}
}