mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2025-12-20 15:49:32 -06:00
creating repo for plugins
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
Plugin.dll
|
||||
*/bin
|
||||
*/obj
|
||||
*.suo
|
||||
|
||||
25
BasicNodes/BasicNodes.csproj
Normal file
25
BasicNodes/BasicNodes.csproj
Normal 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>
|
||||
87
BasicNodes/BasicNodes.en.json
Normal file
87
BasicNodes/BasicNodes.en.json
Normal 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
107
BasicNodes/File/CopyFile.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
BasicNodes/File/DeleteSourceDirectory.cs
Normal file
82
BasicNodes/File/DeleteSourceDirectory.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
BasicNodes/File/FileExtension.cs
Normal file
17
BasicNodes/File/FileExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
31
BasicNodes/File/FileSize.cs
Normal file
31
BasicNodes/File/FileSize.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
BasicNodes/File/InputFile.cs
Normal file
13
BasicNodes/File/InputFile.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
89
BasicNodes/File/MoveFile.cs
Normal file
89
BasicNodes/File/MoveFile.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
BasicNodes/Functions/Function.cs
Normal file
57
BasicNodes/Functions/Function.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
BasicNodes/Functions/Log.cs
Normal file
36
BasicNodes/Functions/Log.cs
Normal 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
11
BasicNodes/Plugin.cs
Normal 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() { }
|
||||
}
|
||||
}
|
||||
96
VideoNodes/DetectBlackBars.cs
Normal file
96
VideoNodes/DetectBlackBars.cs
Normal 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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
VideoNodes/EncodingNode.cs
Normal file
53
VideoNodes/EncodingNode.cs
Normal 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
52
VideoNodes/FFMPEG.cs
Normal 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
216
VideoNodes/FFMpegEncoder.cs
Normal 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
20
VideoNodes/Plugin.cs
Normal 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
41
VideoNodes/VideoCodec.cs
Normal 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
137
VideoNodes/VideoEncode.cs
Normal 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
52
VideoNodes/VideoFile.cs
Normal 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
83
VideoNodes/VideoInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
177
VideoNodes/VideoInfoHelper.cs
Normal file
177
VideoNodes/VideoInfoHelper.cs
Normal 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
89
VideoNodes/VideoNode.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
VideoNodes/VideoNodes.csproj
Normal file
27
VideoNodes/VideoNodes.csproj
Normal 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>
|
||||
52
VideoNodes/VideoNodes.en.json
Normal file
52
VideoNodes/VideoNodes.en.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
VideoNodes/Video_H265_AC3.cs
Normal file
126
VideoNodes/Video_H265_AC3.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user