creating repo for plugins

This commit is contained in:
reven
2021-11-21 07:07:37 +13:00
commit 0f8c544436
26 changed files with 1781 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
Plugin.dll
*/bin
*/obj
*.suo

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Plugin">
<HintPath>..\Plugin.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2035" />
</ItemGroup>
<ItemGroup>
<Content Include="BasicNodes.en.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,87 @@
{
"Enums":{
"LogType":{
"Info":"Information",
"Debug":"Debug",
"Warning":"Warning",
"Error":"Error"
}
},
"Flow":{
"Parts":{
"InputFile":{
"Description":"An input node for a library file. This is required and is the starting point of a flow. Any input node can be used, just one is required."
},
"CopyFile":{
"Description":"Copies a file to the destination path",
"Fields":{
"DestinationPath":"Destination Path",
"DestinationPath-Help":"The path where the file will be copied too",
"CopyFolder":"Copy Folder",
"CopyFolder-Help" :"If the relative library folder structure should be copied too"
}
},
"Log":{
"Description":"Logs a message to the flow log",
"Fields":{
"LogType":"Type",
"Message":"Message"
}
},
"FileExtension":{
"Description":"Checks if the file has one of the configured extensions.\n\nOutput 1: Matches\nOutput 2: Does not match",
"Fields":{
"Extensions":"Extensions",
"Extensions-Help":"A list of case insensitive file extensions that will be matched against.\nOuput 1 Matches\nOutput 2: Does not match"
}
},
"FileSize":{
"Description":"Checks if the file size matches the configured parameters.\n\nOutput 1: Matches\nOutput 2: Does not match",
"Fields":{
"Comparison":"Comparison",
"Lower":"Lower",
"Lower-Suffix":"MB",
"Lower-Help":"The value it must bet greater than",
"Upper":"Upper",
"Upper-Suffix":"MB",
"Upper-Help":"The value it must be less than. Leave as 0 to not test the upper limit."
}
},
"MoveFile":{
"Description":"Moves a file to the destination path",
"Fields":{
"DestinationPath":"Destination Path",
"DestinationPath-Help":"The path where the file will be moved too",
"MoveFolder":"Copy Folder",
"MoveFolder-Help" :"If the relative library folder structure should be copied too",
"DeleteOriginal":"Delete Original",
"DeleteOriginal-Help":"If the original file should be deleted, this will only happen if the working file is different to the original file"
}
},
"RenameFile":{
"Description":"Renames the working file",
"Fields":{
"FileName":"File Name",
"FileName-Help":"The new filename"
}
},
"DeleteSourceDirectory":{
"Description":"Deletes the source directory of the original library file",
"Fields":{
"IfEmpty":"If Empty",
"IfEmpty-Help":"Only delete the source directory if the it is empty",
"IncludePatterns":"Include Patterns",
"IncludePatterns-Help":"Optional, if set only files matching these patterns will be counted to see if the folder is empty. Any of these patterns can match."
}
},
"Function":{
"Fields":{
"Outputs":"Outputs",
"Outputs-Help":"The number of outputs this node can have.",
"Code":"Code",
"Code-Help":"return -1 for error and flow to stop\nreturn 0 for flow to complete\nreturn 1 or more for the desired output to be called"
}
}
}
}
}

107
BasicNodes/File/CopyFile.cs Normal file
View File

@@ -0,0 +1,107 @@
namespace FileFlows.BasicNodes.File
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class CopyFile : Node
{
public override int Inputs => 1;
public override int Outputs => 1;
public override FlowElementType Type => FlowElementType.Process;
public override string Icon => "far fa-copy";
private string _DestinationPath = string.Empty;
[Folder(1)]
public string DestinationPath
{
get => _DestinationPath;
set { _DestinationPath = value ?? ""; }
}
[Boolean(2)]
public bool CopyFolder { get; set; }
private bool Canceled;
public override Task Cancel()
{
Canceled = true;
return base.Cancel();
}
public override int Execute(NodeParameters args)
{
Canceled = false;
string dest = DestinationPath;
if (string.IsNullOrEmpty(dest))
{
args.Logger.ELog("No destination specified");
args.Result = NodeResult.Failure;
return -1;
}
args.Result = NodeResult.Failure;
if (CopyFolder)
dest = Path.Combine(dest, args.RelativeFile);
else
dest = Path.Combine(dest, new FileInfo(args.FileName).Name);
var destDir = new FileInfo(dest).DirectoryName;
if (Directory.Exists(destDir) == false)
Directory.CreateDirectory(destDir);
// have to use file streams so we can report progress
int bufferSize = 1024 * 1024;
using (FileStream fsOut = new FileStream(dest, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite))
{
using (FileStream fsIn = new FileStream(args.WorkingFile, FileMode.Open, FileAccess.Read))
{
try
{
long fileSize = fsIn.Length;
fsOut.SetLength(fileSize);
int bytesRead = -1;
byte[] bytes = new byte[bufferSize];
while ((bytesRead = fsIn.Read(bytes, 0, bufferSize)) > 0 && Canceled == false)
{
fsOut.Write(bytes, 0, bytesRead);
float percent = fsOut.Position / fileSize * 100;
if (percent > 100)
percent = 100;
args.PartPercentageUpdate(percent);
}
if (Canceled == false)
args.PartPercentageUpdate(100);
}
catch (Exception ex)
{
args.Logger.ELog("Failed to move file: " + ex.Message + Environment.NewLine + ex.StackTrace);
return -1;
}
}
}
if (Canceled)
{
try
{
System.IO.File.Delete(dest);
}
catch (Exception) { }
args.Logger.ELog("Action was canceled.");
return -1;
}
else
{
args.SetWorkingFile(dest);
return base.Execute(args);
}
}
}
}

View File

@@ -0,0 +1,82 @@
namespace FileFlows.BasicNodes.File
{
using System.ComponentModel;
using System.Threading.Tasks;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class DeleteSourceDirectory : Node
{
public override int Inputs => 1;
public override int Outputs => 2;
public override FlowElementType Type => FlowElementType.Process;
public override string Icon => "far fa-trash-alt";
[Boolean(1)]
public bool IfEmpty { get; set; }
[StringArray(2)]
public string[] IncludePatterns { get; set; }
public override int Execute(NodeParameters args)
{
string path = args.FileName.Substring(0, args.FileName.Length - args.RelativeFile.Length);
args.Logger.ILog("Library path: " + path);
int pathIndex = args.RelativeFile.IndexOf(System.IO.Path.DirectorySeparatorChar);
if (pathIndex < 0)
{
args.Logger.ILog("File is in library root, will not delete");
return base.Execute(args);
}
string topdir = args.RelativeFile.Substring(0, pathIndex);
string pathToDelete = Path.Combine(path, topdir);
if (IfEmpty)
{
var files = new System.IO.DirectoryInfo(pathToDelete).GetFiles("*.*", SearchOption.AllDirectories);
if (IncludePatterns?.Any() == true)
{
var count = files.Where(x =>
{
foreach (var pattern in IncludePatterns)
{
if (x.FullName.Contains(pattern))
return true;
try
{
if (System.Text.RegularExpressions.Regex.IsMatch(x.FullName, pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase))
return true;
}
catch (Exception) { }
}
return false;
}).Count();
if (count > 0)
{
args.Logger.ILog("Directory is not empty, cannot delete: " + pathToDelete);
return 2;
}
}
else if (files.Length == 0)
{
args.Logger.ILog("Directory is not empty, cannot delete: " + pathToDelete);
return 2;
}
}
args.Logger.ILog("Deleting directory: " + pathToDelete);
try
{
System.IO.Directory.Delete(pathToDelete, true);
}
catch (Exception ex)
{
args.Logger.ELog("Failed to delete directory: " + ex.Message);
return -1;
}
return base.Execute(args);
}
}
}

View File

@@ -0,0 +1,17 @@
namespace FileFlows.BasicNodes.File
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class FileExtension : Node
{
public override int Inputs => 1;
public override int Outputs => 2;
public override string Icon => "far fa-file-excel";
[StringArray(1)]
public string[] Extensions { get; set; }
public override FlowElementType Type => FlowElementType.Logic;
}
}

View File

@@ -0,0 +1,31 @@
namespace FileFlows.BasicNodes.File
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class FileSize : Node
{
public override int Inputs => 1;
public override int Outputs => 2;
public override FlowElementType Type => FlowElementType.Logic;
public override string Icon => "fas fa-balance-scale-right";
[NumberInt(1)]
public int Lower { get; set; }
[NumberInt(2)]
public int Upper { get; set; }
public override int Execute(NodeParameters args)
{
long size = new FileInfo(args.WorkingFile).Length;
if (size < (Lower * 1024 * 1024))
return 2;
if (Upper > 0 && size > (Upper * 1024 * 1024))
return 2;
return 1;
}
}
}

View File

@@ -0,0 +1,13 @@
namespace FileFlows.BasicNodes.File
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class InputFile : Node
{
public override int Outputs => 1;
public override FlowElementType Type => FlowElementType.Input;
public override string Icon => "far fa-file";
}
}

View File

@@ -0,0 +1,89 @@
namespace FileFlows.BasicNodes.File
{
using System.ComponentModel;
using System.Threading.Tasks;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class MoveFile : Node
{
public override int Inputs => 1;
public override int Outputs => 1;
public override FlowElementType Type => FlowElementType.Process;
public override string Icon => "fas fa-file-export";
[Folder(1)]
public string DestinationPath { get; set; }
[Boolean(2)]
public bool MoveFolder { get; set; }
[Boolean(3)]
public bool DeleteOriginal { get; set; }
public override int Execute(NodeParameters args)
{
string dest = DestinationPath;
if (string.IsNullOrEmpty(dest))
{
args.Logger.ELog("No destination specified");
args.Result = NodeResult.Failure;
return -1;
}
args.Result = NodeResult.Failure;
if (MoveFolder)
dest = Path.Combine(dest, args.RelativeFile);
else
dest = Path.Combine(dest, new FileInfo(args.FileName).Name);
var destDir = new FileInfo(dest).DirectoryName;
if (Directory.Exists(destDir) == false)
Directory.CreateDirectory(destDir);
long fileSize = new FileInfo(args.WorkingFile).Length;
bool moved = false;
Task task = Task.Run(() =>
{
try
{
if (System.IO.File.Exists(dest))
System.IO.File.Delete(dest);
System.IO.File.Move(args.WorkingFile, dest, true);
if (DeleteOriginal && args.WorkingFile != args.FileName)
{
System.IO.File.Delete(args.FileName);
}
args.SetWorkingFile(dest);
moved = true;
}
catch (Exception ex)
{
args.Logger.ELog("Failed to move file: " + ex.Message);
}
});
while (task.IsCompleted == false)
{
long currentSize = 0;
var destFileInfo = new FileInfo(dest);
if (destFileInfo.Exists)
currentSize = destFileInfo.Length;
args.PartPercentageUpdate(currentSize / fileSize * 100);
System.Threading.Thread.Sleep(50);
}
if (moved == false)
return -1;
args.PartPercentageUpdate(100);
return base.Execute(args);
}
}
}

View File

@@ -0,0 +1,57 @@
namespace FileFlows.BasicNodes.Functions
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using Jint.Runtime;
using Jint.Native.Object;
using Jint;
using System.Text;
public class Function : Node
{
public override int Inputs => 1;
public override FlowElementType Type => FlowElementType.Logic;
public override string Icon => "fas fa-code";
[DefaultValue(1)]
[NumberIntAttribute(1)]
public new int Outputs { get; set; }
[DefaultValue("// VideoFile object contains info about the video file\n\n// return 0 to complete the flow.\n// return -1 to signal an error in the flow\n// return 1+ to indicate which output to process next\n\n return 0;")]
[Code(2)]
public string Code { get; set; }
delegate void LogDelegate(params object[] values);
public override int Execute(NodeParameters args)
{
args.Logger.DLog("Code: ", Environment.NewLine + new string('=', 40) + Environment.NewLine + Code + Environment.NewLine + new string('=', 40));
if (string.IsNullOrEmpty(Code))
return base.Execute(args); // no code, means will run fine... i think... maybe... depends what i do
var sb = new StringBuilder();
var log = new
{
ILog = new LogDelegate(args.Logger.ILog),
DLog = new LogDelegate(args.Logger.DLog),
WLog = new LogDelegate(args.Logger.WLog),
ELog = new LogDelegate(args.Logger.ELog),
};
var engine = new Engine(options =>
{
options.LimitMemory(4_000_000);
options.MaxStatements(100);
})
.SetValue("Logger", args.Logger)
.SetValue("FileSize", new FileInfo(args.WorkingFile).Length)
//.SetValue("ILog", log.ILog)
;
var result = engine.Evaluate(Code).ToObject();
if (result as bool? != true)
args.Result = NodeResult.Failure;
return base.Execute(args);
}
}
}

View File

@@ -0,0 +1,36 @@
namespace FileFlows.BasicNodes.Functions
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using Jint.Runtime;
using Jint.Native.Object;
using Jint;
using System.Text;
public class Log : Node
{
public override int Inputs => 1;
public override int Outputs => 1;
public override FlowElementType Type => FlowElementType.Logic;
public override string Icon => "far fa-file-alt";
[Enum(1, LogType.Info, LogType.Debug, LogType.Warning, LogType.Error)]
public LogType LogType { get; set; }
[TextArea(2)]
public string Message { get; set; }
public override int Execute(NodeParameters args)
{
switch (LogType)
{
case LogType.Error: args.Logger.ELog(Message); break;
case LogType.Warning: args.Logger.WLog(Message); break;
case LogType.Debug: args.Logger.DLog(Message); break;
case LogType.Info: args.Logger.ILog(Message); break;
}
return base.Execute(args);
}
}
}

11
BasicNodes/Plugin.cs Normal file
View File

@@ -0,0 +1,11 @@
namespace FileFlows.BasicNodes
{
using System.ComponentModel.DataAnnotations;
public class Plugin : FileFlows.Plugin.IPlugin
{
public string Name => "Basic Nodes";
public void Init() { }
}
}

View File

@@ -0,0 +1,96 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class DetectBlackBars : VideoNode
{
public override int Outputs => 2;
public override int Inputs => 1;
public override FlowElementType Type => FlowElementType.Logic;
public override string Icon => "fas fa-film";
internal const string CROP_KEY = "VideoCrop";
public override int Execute(NodeParameters args)
{
string ffplay = GetFFMpegExe(args);
if (string.IsNullOrEmpty(ffplay))
return -1;
string crop = Execute(ffplay, args.WorkingFile, args.TempPath);
if (crop == string.Empty)
return 2;
args.Logger.ILog("Black bars detcted, crop: " + crop);
args.Parameters.Add(CROP_KEY, crop);
return 1;
}
public string Execute(string ffplay, string file, string tempDir)
{
string tempFile = Path.Combine(tempDir, Guid.NewGuid().ToString() + ".mkv");
try
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo();
process.StartInfo.FileName = ffplay;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.Arguments = $"-i \"{file}\" -hide_banner -t 10 -ss 60 -vf cropdetect=24:16:0 {tempFile}";
process.Start();
string output = process.StandardError.ReadToEnd();
Console.WriteLine(output);
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
var matches = Regex.Matches(output, @"(?<=(crop=))([\d]+:){3}[\d]+");
int x = int.MaxValue;
int y = int.MaxValue;
int width = 0;
int height = 0;
foreach (Match match in matches)
{
int[] parts = match.Value.Split(':').Select(x => int.Parse(x)).ToArray();
x = Math.Min(x, parts[2]);
y = Math.Min(y, parts[3]);
width = Math.Max(width, parts[0]);
height = Math.Max(height, parts[1]);
}
if (x == int.MaxValue)
x = 0;
if (y == int.MaxValue)
y = 0;
if (x + y < 28) // to small to bother croping
return string.Empty;
return $"{width}:{height}:{x}:{y}";
}
}
catch (Exception)
{
return string.Empty;
}
finally
{
if (System.IO.File.Exists(tempFile))
{
try
{
System.IO.File.Delete(tempFile);
}
catch (Exception) { }
}
}
}
}
}

View File

@@ -0,0 +1,53 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public abstract class EncodingNode : VideoNode
{
public override int Outputs => 2;
public override int Inputs => 1;
public override FlowElementType Type => FlowElementType.Process;
protected TimeSpan TotalTime;
private NodeParameters args;
private FFMpegEncoder Encoder;
protected bool Encode(NodeParameters args, string ffmpegExe, string ffmpegParameters)
{
this.args = args;
Encoder = new FFMpegEncoder(ffmpegExe, args.Logger);
Encoder.AtTime += AtTimeEvent;
string output = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + ".mkv");
args.Logger.DLog("New Temp file: " + output);
bool success = Encoder.Encode(args.WorkingFile, output, ffmpegParameters);
if (success)
args.SetWorkingFile(output);
Encoder.AtTime -= AtTimeEvent;
Encoder = null;
return success;
}
public override Task Cancel()
{
if (Encoder != null)
Encoder.Cancel();
return base.Cancel();
}
void AtTimeEvent(TimeSpan time)
{
if (TotalTime.TotalMilliseconds == 0)
return;
float percent = (float)((time.TotalMilliseconds / TotalTime.TotalMilliseconds) * 100);
args.PartPercentageUpdate(percent);
}
}
}

52
VideoNodes/FFMPEG.cs Normal file
View File

@@ -0,0 +1,52 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class FFMPEG : EncodingNode
{
public override int Outputs => 1;
[DefaultValue("-i {WorkingFile} {TempDir}output.mkv")]
[TextArea(1)]
public string CommandLine { get; set; }
public override string Icon => "far fa-file-video";
private NodeParameters args;
public override int Execute(NodeParameters args)
{
if (string.IsNullOrEmpty(CommandLine))
{
args.Logger.ELog("Command Line not set");
return -1;
}
this.args = args;
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
string ffmpegExe = GetFFMpegExe(args);
if (string.IsNullOrEmpty(ffmpegExe))
return -1;
string cmd = CommandLine.Replace("{WorkingFile}", "\"" + args.WorkingFile + "\"")
.Replace("{TempDir}", "\"" + args.TempPath + Path.DirectorySeparatorChar + "\"");
if (Encode(args, ffmpegExe, CommandLine) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

216
VideoNodes/FFMpegEncoder.cs Normal file
View File

@@ -0,0 +1,216 @@
namespace FileFlows.VideoNodes
{
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
public class FFMpegEncoder
{
private string ffMpegExe;
private ILogger Logger;
StringBuilder outputBuilder, errorBuilder;
TaskCompletionSource<bool> outputCloseEvent, errorCloseEvent;
private Regex rgxTime = new Regex(@"(?<=(time=))([\d]+:?)+\.[\d]+");
public delegate void TimeEvent(TimeSpan time);
public event TimeEvent AtTime;
private Process process;
public FFMpegEncoder(string ffMpegExe, ILogger logger)
{
this.ffMpegExe = ffMpegExe;
this.Logger = logger;
}
public bool Encode(string input, string output, string arguments)
{
// -y means it will overwrite a file if output already exists
arguments = $"-i \"{input}\" -y {arguments} \"{output}\"";
Logger.ILog(new string('=', ("FFMpeg.Arguments: " + arguments).Length));
Logger.ILog("FFMpeg.Arguments: " + arguments);
Logger.ILog(new string('=', ("FFMpeg.Arguments: " + arguments).Length));
var task = ExecuteShellCommand(ffMpegExe, arguments, 0);
task.Wait();
Logger.ILog("Exit Code: " + task.Result.ExitCode);
return task.Result.ExitCode == 0; // exitcode 0 means it was successful
}
internal void Cancel()
{
try
{
if (this.process != null)
{
this.process.Kill();
this.process = null;
}
}
catch (Exception) { }
}
public async Task<ProcessResult> ExecuteShellCommand(string command, string arguments, int timeout = 0)
{
var result = new ProcessResult();
using (var process = new Process())
{
this.process = process;
// If you run bash-script on Linux it is possible that ExitCode can be 255.
// To fix it you can try to add '#!/bin/bash' header to the script.
process.StartInfo.FileName = command;
process.StartInfo.Arguments = arguments;
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;
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;
// Adds process output if it was completed with error
if (process.ExitCode != 0)
{
result.Output = $"{outputBuilder}{errorBuilder}";
}
}
else
{
try
{
// Kill hung process
process.Kill();
}
catch
{
}
}
}
}
process = null;
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
{
if (rgxTime.IsMatch(e.Data))
{
var timeString = rgxTime.Match(e.Data).Value;
var ts = TimeSpan.Parse(timeString);
if (AtTime != null)
AtTime.Invoke(ts);
}
Logger.ILog(e.Data);
outputBuilder.AppendLine(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
{
if (rgxTime.IsMatch(e.Data))
{
var timeString = rgxTime.Match(e.Data).Value;
var ts = TimeSpan.Parse(timeString);
if (AtTime != null)
AtTime.Invoke(ts);
}
Logger.ILog(e.Data);
outputBuilder.AppendLine(e.Data);
}
}
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);
});
}
public struct ProcessResult
{
public bool Completed;
public int? ExitCode;
public string Output;
}
}
}

20
VideoNodes/Plugin.cs Normal file
View File

@@ -0,0 +1,20 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel.DataAnnotations;
using FileFlows.Plugin.Attributes;
public class Plugin : FileFlows.Plugin.IPlugin
{
public string Name => "Video Nodes";
[Required]
[File(2, "exe")]
public string FFProbeExe { get; set; }
public void Init()
{
//var context = new System.Runtime.Loader.AssemblyLoadContext(null, true);
//context.LoadFromAssemblyPath(@"C:\Users\john\src\ViWatcher\Plugins\VideoNodes\bin\Debug\net6.0\FFMpegCore.dll");
}
}
}

41
VideoNodes/VideoCodec.cs Normal file
View File

@@ -0,0 +1,41 @@
namespace FileFlows.VideoNodes
{
using System.Linq;
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class VideoCodec : VideoNode
{
public override int Inputs => 1;
public override int Outputs => 2;
public override FlowElementType Type => FlowElementType.Logic;
[StringArray(1)]
public string[] Codecs { get; set; }
public override int Execute(NodeParameters args)
{
var videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
var codec = videoInfo.VideoStreams.FirstOrDefault(x => Codecs.Contains(x.Codec.ToLower()));
if (codec != null)
{
args.Logger.ILog($"Matching video codec found[{codec.Index}]: {codec.Codec}");
return 1;
}
var acodec = videoInfo.AudioStreams.FirstOrDefault(x => Codecs.Contains(x.Codec.ToLower()));
if (acodec != null)
{
args.Logger.ILog($"Matching audio codec found[{acodec.Index}]: {acodec.Codec}, language: {acodec.Language}");
return 1;
}
// not found, execute 2nd outputacodec
return 2;
}
}
}

137
VideoNodes/VideoEncode.cs Normal file
View File

@@ -0,0 +1,137 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class VideoEncode : EncodingNode
{
[DefaultValue("hevc")]
[Text(1)]
public string VideoCodec { get; set; }
[DefaultValue("hevc_nvenc -preset hq -crf 23")]
[Text(2)]
public string VideoCodecParameters { get; set; }
[DefaultValue("ac3")]
[Text(3)]
public string AudioCodec { get; set; }
[DefaultValue("eng")]
[Text(4)]
public string Language { get; set; }
public override string Icon => "far fa-file-video";
private NodeParameters args;
public override int Execute(NodeParameters args)
{
if (string.IsNullOrEmpty(VideoCodec))
{
args.Logger.ELog("Video codec not set");
return -1;
}
if (string.IsNullOrEmpty(AudioCodec))
{
args.Logger.ELog("Audeio codec not set");
return -1;
}
VideoCodec = VideoCodec.ToLower();
AudioCodec = AudioCodec.ToLower();
this.args = args;
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
Language = Language?.ToLower() ?? "";
// ffmpeg is one based for stream index, so video should be 1, audio should be 2
var videoIsRightCodec = videoInfo.VideoStreams.FirstOrDefault(x => x.Codec?.ToLower() == VideoCodec);
var videoTrack = videoIsRightCodec ?? videoInfo.VideoStreams[0];
args.Logger.ILog("Video: ", videoTrack);
var bestAudio = videoInfo.AudioStreams.Where(x => System.Text.Json.JsonSerializer.Serialize(x).ToLower().Contains("commentary") == false)
.OrderBy(x =>
{
if (Language != string.Empty)
{
args.Logger.ILog("Language: " + x.Language, x);
if (string.IsNullOrEmpty(x.Language))
return 50; // no language specified
if (x.Language?.ToLower() != Language)
return 100; // low priority not the desired language
}
return 0;
})
.ThenByDescending(x => x.Channels)
//.ThenBy(x => x.CodecName.ToLower() == "ac3" ? 0 : 1) // if we do this we can get commentary tracks...
.ThenBy(x => x.Index)
.FirstOrDefault();
bool audioRightCodec = bestAudio?.Codec?.ToLower() == AudioCodec && videoInfo.AudioStreams[0] == bestAudio;
args.Logger.ILog("Best Audio: ", (object)bestAudio ?? (object)"null");
string crop = args.GetParameter<string>(DetectBlackBars.CROP_KEY) ?? "";
if (crop != string.Empty)
crop = " -vf crop=" + crop;
if (audioRightCodec == true && videoIsRightCodec != null)
{
if (crop == string.Empty)
{
args.Logger.DLog($"File is {VideoCodec} with the first audio track is {AudioCodec}");
return 2;
}
else
{
args.Logger.ILog($"Video is {VideoCodec} and audio is {AudioCodec} but needs to be cropped");
}
}
string ffmpegExe = GetFFMpegExe(args);
if (string.IsNullOrEmpty(ffmpegExe))
return -1;
List<string> ffArgs = new List<string>();
if (videoIsRightCodec == null || crop != string.Empty)
ffArgs.Add($"-map 0:v:0 -c:v {VideoCodecParameters} {crop}");
else
ffArgs.Add($"-map 0:v:0 -c:v copy");
TotalTime = videoInfo.VideoStreams[0].Duration;
if (audioRightCodec == false)
ffArgs.Add($"-map 0:{bestAudio.Index} -c:a {AudioCodec}");
else
ffArgs.Add($"-map 0:{bestAudio.Index} -c:a copy");
if (Language != string.Empty)
ffArgs.Add($"-map 0:s:m:language:{Language}? -c:s copy");
else
ffArgs.Add($"-map 0:s? -c:s copy");
string ffArgsLine = string.Join(" ", ffArgs);
if (Encode(args, ffmpegExe, ffArgsLine) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

52
VideoNodes/VideoFile.cs Normal file
View File

@@ -0,0 +1,52 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class VideoFile : VideoNode
{
public override int Outputs => 1;
public override FlowElementType Type => FlowElementType.Input;
public override int Execute(NodeParameters args)
{
string ffmpegExe = GetFFMpegExe(args);
if (string.IsNullOrEmpty(ffmpegExe))
return -1;
try
{
var videoInfo = new VideoInfoHelper(ffmpegExe, args.Logger).Read(args.WorkingFile);
if (videoInfo.VideoStreams.Any() == false)
{
args.Logger.ILog("No video streams detected.");
return 0;
}
foreach (var vs in videoInfo.VideoStreams)
{
args.Logger.ILog($"Video stream '{vs.Codec}' '{vs.Index}'");
}
foreach (var vs in videoInfo.AudioStreams)
{
args.Logger.ILog($"Audio stream '{vs.Codec}' '{vs.Index}' '{vs.Language}");
}
SetVideoInfo(args, videoInfo);
return 1;
}
catch (Exception ex)
{
args.Logger.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

83
VideoNodes/VideoInfo.cs Normal file
View File

@@ -0,0 +1,83 @@
namespace FileFlows.VideoNodes
{
public class VideoInfo
{
public string FileName { get; set; }
public List<VideoStream> VideoStreams { get; set; } = new List<VideoStream>();
public List<AudioStream> AudioStreams { get; set; } = new List<AudioStream>();
public List<SubtitleStream> SubtitleStreams { get; set; } = new List<SubtitleStream>();
}
public class VideoFileStream
{
/// <summary>
/// The original index of the stream in the overall video
/// </summary>
public int Index { get; set; }
/// <summary>
/// The index of the specific type
/// </summary>
public int TypeIndex { get; set; }
/// <summary>
/// The stream title (name)
/// </summary>
public string Title { get; set; } = "";
/// <summary>
/// The codec of the stream
/// </summary>
public string Codec { get; set; } = "";
}
public class VideoStream : VideoFileStream
{
/// <summary>
/// The width of the video stream
/// </summary>
public int Width { get; set; }
/// <summary>
/// The height of the video stream
/// </summary>
public int Height { get; set; }
/// <summary>
/// The number of frames per second
/// </summary>
public float FramesPerSecond { get; set; }
/// <summary>
/// The duration of the stream
/// </summary>
public TimeSpan Duration { get; set; }
}
public class AudioStream : VideoFileStream
{
/// <summary>
/// The language of the stream
/// </summary>
public string Language { get; set; }
/// <summary>
/// The channels of the stream
/// </summary>
public float Channels { get; set; }
/// <summary>
/// The duration of the stream
/// </summary>
public TimeSpan Duration { get; set; }
}
public class SubtitleStream : VideoFileStream
{
/// <summary>
/// The language of the stream
/// </summary>
public string Language { get; set; }
/// <summary>
/// If this is a forced subtitle
/// </summary>
public bool Forced { get; set; }
}
}

View File

@@ -0,0 +1,177 @@
namespace FileFlows.VideoNodes
{
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
public class VideoInfoHelper
{
private string ffMpegExe;
private ILogger Logger;
Regex rgxTitle = new Regex(@"(?<=((^[\s]+title[\s]+:[\s])))(.*?)$", RegexOptions.Multiline);
Regex rgxDuration = new Regex(@"(?<=((^[\s]+DURATION(\-[\w]+)?[\s]+:[\s])))([\d]+:?)+\.[\d]+[1-9]", RegexOptions.Multiline);
Regex rgxDuration2 = new Regex(@"(?<=((^[\s]+Duration:[\s])))([\d]+:?)+\.[\d]+[1-9]", RegexOptions.Multiline);
public VideoInfoHelper(string ffMpegExe, ILogger logger)
{
this.ffMpegExe = ffMpegExe;
this.Logger = logger;
}
public static string GetFFMpegPath(NodeParameters args) => args.GetToolPath("FFMpeg");
public VideoInfo Read(string filename)
{
var vi = new VideoInfo();
if (File.Exists(filename) == false)
{
Logger.ELog("File not found: " + filename);
return vi;
}
if (string.IsNullOrEmpty(ffMpegExe) || File.Exists(ffMpegExe) == false)
{
Logger.ELog("FFMpeg not found: " + (ffMpegExe ?? "not passed in"));
return vi;
}
try
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo();
process.StartInfo.FileName = ffMpegExe;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.Arguments = $"-i \"{filename}\"";
process.Start();
string output = process.StandardError.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (string.IsNullOrEmpty(error) == false && error != "At least one output file must be specified")
{
Logger.ELog("Failed reading ffmpeg info: " + error);
return vi;
}
Logger.ILog("Video Information:" + Environment.NewLine + output);
var rgxStreams = new Regex(@"Stream\s#[\d]+:[\d]+(.*?)(?=(Stream\s#[\d]|$))", RegexOptions.Singleline);
var streamMatches = rgxStreams.Matches(output);
int streamIndex = 0;
foreach (Match sm in streamMatches)
{
if (sm.Value.Contains(" Video: "))
{
var vs = ParseVideoStream(sm.Value, output);
if (vs != null)
{
vs.Index = streamIndex;
vi.VideoStreams.Add(vs);
}
}
else if (sm.Value.Contains(" Audio: "))
{
var audio = ParseAudioStream(sm.Value);
if (audio != null)
{
audio.Index = streamIndex;
vi.AudioStreams.Add(audio);
}
}
else if (sm.Value.Contains(" Subtitle: "))
{
var sub = ParseSubtitleStream(sm.Value);
if (sub != null)
{
sub.Index = streamIndex;
vi.SubtitleStreams.Add(sub);
}
}
++streamIndex;
}
}
}
catch (Exception ex)
{
Logger.ELog(ex.Message, ex.StackTrace.ToString());
}
return vi;
}
VideoStream ParseVideoStream(string info, string fullOutput)
{
// Stream #0:0(eng): Video: h264 (High), yuv420p(tv, bt709/unknown/unknown, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 23.98 fps, 23.98 tbr, 1k tbn (default)
string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
VideoStream vs = new VideoStream();
vs.Codec = line.Substring(line.IndexOf("Video: ") + "Video: ".Length).Replace(",", "").Trim().Split(' ').First().ToLower();
var dimensions = Regex.Match(line, @"([\d]{3,})x([\d]{3,})");
if (int.TryParse(dimensions.Groups[1].Value, out int width))
vs.Width = width;
if (int.TryParse(dimensions.Groups[2].Value, out int height))
vs.Height = height;
if (int.TryParse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value, out int typeIndex))
vs.TypeIndex = typeIndex;
if (Regex.IsMatch(line, @"([\d]+(\.[\d]+)?)\sfps") && float.TryParse(Regex.Match(line, @"([\d]+(\.[\d]+)?)\sfps").Groups[1].Value, out float fps))
vs.FramesPerSecond = fps;
if (rgxDuration.IsMatch(info) && TimeSpan.TryParse(rgxDuration.Match(info).Value, out TimeSpan duration))
vs.Duration = duration;
else if (rgxDuration2.IsMatch(fullOutput) && TimeSpan.TryParse(rgxDuration2.Match(fullOutput).Value, out TimeSpan duration2))
vs.Duration = duration2;
return vs;
}
AudioStream ParseAudioStream(string info)
{
// Stream #0:1(eng): Audio: dts (DTS), 48000 Hz, stereo, fltp, 1536 kb/s (default)
string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
var parts = line.Split(",").Select(x => x?.Trim() ?? "").ToArray();
AudioStream audio = new AudioStream();
audio.Title = "";
audio.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value);
audio.Codec = parts[0].Substring(parts[0].IndexOf("Audio: ") + "Audio: ".Length).Trim().Split(' ').First().ToLower() ?? "";
audio.Language = Regex.Match(line, @"(?<=(Stream\s#[\d]+:[\d]+)\()[^\)]+").Value?.ToLower() ?? "";
//Logger.ILog("codec: " + vs.Codec);
if (parts[2] == "stereo")
audio.Channels = 2;
else if (Regex.IsMatch(parts[2], @"^[\d]+(\.[\d]+)?"))
{
audio.Channels = float.Parse(Regex.Match(parts[2], @"^[\d]+(\.[\d]+)?").Value);
}
if (rgxTitle.IsMatch(info))
audio.Title = rgxTitle.Match(info).Value.Trim();
if (rgxDuration.IsMatch(info))
audio.Duration = TimeSpan.Parse(rgxDuration.Match(info).Value);
return audio;
}
SubtitleStream ParseSubtitleStream(string info)
{
// Stream #0:1(eng): Audio: dts (DTS), 48000 Hz, stereo, fltp, 1536 kb/s (default)
string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
var parts = line.Split(",").Select(x => x?.Trim() ?? "").ToArray();
SubtitleStream sub = new SubtitleStream();
sub.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value);
sub.Codec = line.Substring(line.IndexOf("Subtitle: ") + "Subtitle: ".Length).Trim().Split(' ').First().ToLower();
sub.Language = Regex.Match(line, @"(?<=(Stream\s#[\d]+:[\d]+)\()[^\)]+").Value?.ToLower() ?? "";
if (rgxTitle.IsMatch(info))
sub.Title = rgxTitle.Match(info).Value.Trim();
sub.Forced = info.ToLower().Contains("forced");
return sub;
}
}
}

89
VideoNodes/VideoNode.cs Normal file
View File

@@ -0,0 +1,89 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
public abstract class VideoNode : Node
{
public override string Icon => "fas fa-video";
protected string GetFFMpegExe(NodeParameters args)
{
string ffmpeg = args.GetToolPath("FFMpeg");
if (string.IsNullOrEmpty(ffmpeg))
{
args.Logger.ELog("FFMpeg tool not found.");
return "";
}
var fileInfo = new FileInfo(ffmpeg);
if (fileInfo.Exists == false)
{
args.Logger.ELog("FFMpeg tool configured by ffmpeg.exe file does not exist.");
return "";
}
return fileInfo.FullName;
}
protected string GetFFMpegPath(NodeParameters args)
{
string ffmpeg = args.GetToolPath("FFMpeg");
if (string.IsNullOrEmpty(ffmpeg))
{
args.Logger.ELog("FFMpeg tool not found.");
return "";
}
var fileInfo = new FileInfo(ffmpeg);
if (fileInfo.Exists == false)
{
args.Logger.ELog("FFMpeg tool configured by ffmpeg.exe file does not exist.");
return "";
}
return fileInfo.DirectoryName;
}
protected string GetFFPlayExe(NodeParameters args)
{
string ffmpeg = args.GetToolPath("FFMpeg");
if (string.IsNullOrEmpty(ffmpeg))
{
args.Logger.ELog("FFMpeg tool not found.");
return "";
}
var fileInfo = new FileInfo(ffmpeg);
if (fileInfo.Exists == false)
{
args.Logger.ELog("FFMpeg tool configured by ffmpeg file does not exist.");
return "";
}
var ffplay = Path.Combine(fileInfo.DirectoryName, "ffplay" + fileInfo.Extension);
if (File.Exists(ffplay) == false)
{
args.Logger.ELog("FFMpeg tool configured by ffplay file does not exist.");
return "";
}
return ffplay;
}
private const string VIDEO_INFO = "VideoInfo";
protected void SetVideoInfo(NodeParameters args, VideoInfo info)
{
if (args.Parameters.ContainsKey(VIDEO_INFO))
args.Parameters[VIDEO_INFO] = info;
else
args.Parameters.Add(VIDEO_INFO, info);
}
protected VideoInfo GetVideoInfo(NodeParameters args)
{
if (args.Parameters.ContainsKey(VIDEO_INFO) == false)
{
args.Logger.WLog("No codec information loaded, use a 'VideoFile' node first");
return null;
}
var result = args.Parameters[VIDEO_INFO] as VideoInfo;
if (result == null)
{
args.Logger.WLog("VideoInfo not found for file");
return null;
}
return result;
}
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Instances" Version="1.6.0" />
</ItemGroup>
<ItemGroup>
<Content Include="VideoNodes.en.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Reference Include="Plugin">
<HintPath>..\Plugin.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,52 @@
{
"Flow":{
"Parts":{
"VideoFile":{
"Description":"An input video file that has had its VideoInformation read and can be processed"
},
"DetectBlackBars":{
"Description":"Processes a video file and scans for black bars in the video.\n\nIf found a parameter \"VideoCrop\" will be added.\n\nOutput 1: Black bars detected\nOutput 2: Not detected"
},
"VideoCodec":{
"Description":"This node will check the codecs in the input file, and trigger when matched.\n\nOutput 1: Matches\nOutput 2: Does not match",
"Fields":{
"Codecs":"Codecs",
"Codecs-Help":"Enter a list of case insensitive video or audio codecs.\nEg hevc, h265, mpeg4, ac3"
}
},
"VideoEncode":{
"Description":"A generic video encoding node, this lets you customize how to encode a video file using ffmpeg.\n\nOutput 1: Video was processed\nOutput 2: No processing required",
"Fields":{
"VideoCodec":"Video Codec",
"VideoCodec-Help":"The video codec the video should be in, for example hevc, h264",
"VideoCodecParameters":"Video Codec Parameters",
"VideoCodecParameters-Help":"The parameters to use to encode the video, eg. \"hevc_nvenc -preset hq -crf 23\" to encode into hevc using the HQ preset a constant rate factor of 23 and using NVIDIA hardware acceleration.",
"AudioCodec":"Audio Codec",
"AudioCodec-Help":"The audio codec to encode the video with",
"Language":"Language",
"Language-Help":"Optional ISO 639-2 language code to use. Will attempt to find an audio track with this language code if not the best audio track will be used.\nhttps://en.wikipedia.org/wiki/List_of_ISO_639-2_codes"
}
},
"Video_H265_AC3":{
"Description":"This will ensure all videos are encoded in H265 (if not already encoded) and that AC3 audio is the first audio channel\n\nOutput 1: Video was processed\nOutput 2: No processing required",
"Fields":{
"Language":"Language",
"Language-Help":"Optional ISO 639-2 language code to use. Will attempt to find an audio track with this language code if not the best audio track will be used.\nhttps://en.wikipedia.org/wiki/List_of_ISO_639-2_codes",
"Crf":"Constant Rate Factor",
"Crf-Help":"Refer to ffmpeg for more details, the lower the value the bigger the file. A good value is around 19-23. Default is 21.",
"NvidiaEncoding":"NVIDIA Encoding",
"NvidiaEncoding-Help":"If NVIDIA hardware encoding should be used. If you do not have a supported NVIDIA card the encoding will fail.",
"Threads":"Threads",
"Threads-Help":"Only used if not using NVIDIA. If set to 0, the threads will use FFMpegs defaults."
}
},
"FFMPEG":{
"Description":"The node lets you run any FFMPEG command you like. Giving you full control over what it can do.\n\nFor more information refer to the FFMPEG documentation",
"Fields":{
"CommandLine":"Command Line",
"CommandLine-Help":"The command line to run with FFMPEG.\n'{WorkingFile}': the working file of the flow\n'{TempDir}': The temp directory, including trailing directory separator, where files are generally created during the flow."
}
}
}
}
}

View File

@@ -0,0 +1,126 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class Video_H265_AC3 : EncodingNode
{
[DefaultValue("eng")]
[Text(1)]
public string Language { get; set; }
[DefaultValue(21)]
[NumberInt(2)]
public int Crf { get; set; }
[DefaultValue(true)]
[Boolean(3)]
public bool NvidiaEncoding { get; set; }
[DefaultValue(0)]
[NumberInt(4)]
public int Threads { get; set; }
public override string Icon => "far fa-file-video";
private NodeParameters args;
public override int Execute(NodeParameters args)
{
this.args = args;
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
Language = Language?.ToLower() ?? "";
// ffmpeg is one based for stream index, so video should be 1, audio should be 2
var videoH265 = videoInfo.VideoStreams.FirstOrDefault(x => Regex.IsMatch(x.Codec ?? "", @"^(hevc|h(\.)?265)$", RegexOptions.IgnoreCase));
var videoTrack = videoH265 ?? videoInfo.VideoStreams[0];
args.Logger.ILog("Video: ", videoTrack);
var bestAudio = videoInfo.AudioStreams.Where(x => System.Text.Json.JsonSerializer.Serialize(x).ToLower().Contains("commentary") == false)
.OrderBy(x =>
{
if (Language != string.Empty)
{
args.Logger.ILog("Language: " + x.Language, x);
if (string.IsNullOrEmpty(x.Language))
return 50; // no language specified
if (x.Language?.ToLower() != Language)
return 100; // low priority not the desired language
}
return 0;
})
.ThenByDescending(x => x.Channels)
//.ThenBy(x => x.CodecName.ToLower() == "ac3" ? 0 : 1) // if we do this we can get commentary tracks...
.ThenBy(x => x.Index)
.FirstOrDefault();
bool firstAc3 = bestAudio?.Codec?.ToLower() == "ac3" && videoInfo.AudioStreams[0] == bestAudio;
args.Logger.ILog("Best Audio: ", (object)bestAudio ?? (object)"null");
string crop = args.GetParameter<string>(DetectBlackBars.CROP_KEY) ?? "";
if (crop != string.Empty)
crop = " -vf crop=" + crop;
if (firstAc3 == true && videoH265 != null)
{
if (crop == string.Empty)
{
args.Logger.DLog("File is hevc with the first audio track being AC3");
return 2;
}
else
{
args.Logger.ILog("Video is hevc and ac3 but needs to be cropped");
}
}
string ffmpegExe = GetFFMpegExe(args);
if (string.IsNullOrEmpty(ffmpegExe))
return -1;
List<string> ffArgs = new List<string>();
if (NvidiaEncoding == false && Threads > 0)
ffArgs.Add($"-threads {Math.Min(Threads, 16)}");
if (videoH265 == null || crop != string.Empty)
ffArgs.Add($"-map 0:v:0 -c:v {(NvidiaEncoding ? "hevc_nvenc -preset hq" : "libx265")} -crf " + (Crf > 0 ? Crf : 21) + crop);
else
ffArgs.Add($"-map 0:v:0 -c:v copy");
TotalTime = videoInfo.VideoStreams[0].Duration;
if (bestAudio.Codec.ToLower() != "ac3")
ffArgs.Add($"-map 0:{bestAudio.Index} -c:a ac3");
else
ffArgs.Add($"-map 0:{bestAudio.Index} -c:a copy");
if (Language != string.Empty)
ffArgs.Add($"-map 0:s:m:language:{Language}? -c:s copy");
else
ffArgs.Add($"-map 0:s? -c:s copy");
string ffArgsLine = string.Join(" ", ffArgs);
if (Encode(args, ffmpegExe, ffArgsLine) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}