Merge branch 'develop'

# Conflicts:
#	VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs
This commit is contained in:
John Andrews
2022-06-17 12:55:13 +12:00
89 changed files with 2389 additions and 1367 deletions
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+2 -1
View File
@@ -234,7 +234,8 @@
"1": "File renamed"
},
"Fields": {
"Pattern": "Pattern",
"Pattern": "New Name",
"Pattern-Help": "The new name of the file. Can use variables. Any empty () and '{}' will be removed.",
"DestinationPath": "Destination Folder",
"DestinationPath-Help": "If the file should be moved to a different folder.",
"LogOnly": "Log Only",
+89 -88
View File
@@ -1,114 +1,115 @@
namespace FileFlows.BasicNodes.File
namespace FileFlows.BasicNodes.File;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class Renamer : Node
{
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public override int Inputs => 1;
public override int Outputs => 1;
public override string Icon => "fas fa-font";
public class Renamer : Node
public string _Pattern = string.Empty;
public override string HelpUrl => "https://docs.fileflows.com/plugins/basic-nodes/renamer";
public override FlowElementType Type => FlowElementType.Process;
[Required]
[TextVariable(1)]
public string? Pattern
{
public override int Inputs => 1;
public override int Outputs => 1;
public override string Icon => "fas fa-font";
get => _Pattern;
set { _Pattern = value ?? ""; }
}
public string _Pattern = string.Empty;
private string _DestinationPath = string.Empty;
public override FlowElementType Type => FlowElementType.Process;
[Folder(2)]
public string DestinationPath
{
get => _DestinationPath;
set { _DestinationPath = value ?? ""; }
}
[Required]
[TextVariable(1)]
public string? Pattern
[Boolean(3)]
public bool LogOnly { get; set; }
[File(4)]
public string CsvFile { get; set; }
public override int Execute(NodeParameters args)
{
if(string.IsNullOrEmpty(Pattern))
{
get => _Pattern;
set { _Pattern = value ?? ""; }
args.Logger?.ELog("No pattern specified");
return -1;
}
private string _DestinationPath = string.Empty;
string newFile = Pattern;
// incase they set a linux path on windows or vice versa
newFile = newFile.Replace('\\', Path.DirectorySeparatorChar);
newFile = newFile.Replace('/', Path.DirectorySeparatorChar);
[Folder(2)]
public string DestinationPath
newFile = args.ReplaceVariables(newFile, stripMissing: true, cleanSpecialCharacters: true);
newFile = Regex.Replace(newFile, @"\.(\.[\w\d]+)$", "$1");
// remove empty [], (), {}
newFile = newFile.Replace("()", "").Replace("{}", "").Replace("[]", "");
// remove double space that may have been introduced by empty [], () removals
while (newFile.IndexOf(" ") >= 0)
newFile = newFile.Replace(" ", " ");
newFile = Regex.Replace(newFile, @"\s(\.[\w\d]+)$", "$1");
newFile = newFile.Replace(" \\", "\\");
string destFolder = args.ReplaceVariables(DestinationPath ?? string.Empty, stripMissing: true, cleanSpecialCharacters: true);
if (string.IsNullOrEmpty(destFolder))
destFolder = new FileInfo(args.WorkingFile).Directory?.FullName ?? "";
var dest = args.GetSafeName(Path.Combine(destFolder, newFile));
if (string.IsNullOrEmpty(dest.Extension) == false)
{
get => _DestinationPath;
set { _DestinationPath = value ?? ""; }
// just ensures extensions are lowercased
dest = new FileInfo(dest.FullName.Substring(0, dest.FullName.LastIndexOf(dest.Extension)) + dest.Extension.ToLower());
}
[Boolean(3)]
public bool LogOnly { get; set; }
[File(4)]
public string CsvFile { get; set; }
public override int Execute(NodeParameters args)
args.Logger?.ILog("Renaming file to: " + dest.FullName);
if (string.IsNullOrEmpty(CsvFile) == false)
{
if(string.IsNullOrEmpty(Pattern))
try
{
args.Logger?.ELog("No pattern specified");
return -1;
System.IO.File.AppendAllText(CsvFile, EscapeForCsv(args.FileName) + "," + EscapeForCsv(dest.FullName) + Environment.NewLine);
}
string newFile = Pattern;
// incase they set a linux path on windows or vice versa
newFile = newFile.Replace('\\', Path.DirectorySeparatorChar);
newFile = newFile.Replace('/', Path.DirectorySeparatorChar);
newFile = args.ReplaceVariables(newFile, stripMissing: true, cleanSpecialCharacters: true);
newFile = Regex.Replace(newFile, @"\.(\.[\w\d]+)$", "$1");
// remove empty [], (), {}
newFile = newFile.Replace("()", "").Replace("{}", "").Replace("[]", "");
// remove double space that may have been introduced by empty [], () removals
while (newFile.IndexOf(" ") >= 0)
newFile = newFile.Replace(" ", " ");
newFile = Regex.Replace(newFile, @"\s(\.[\w\d]+)$", "$1");
newFile = newFile.Replace(" \\", "\\");
string destFolder = args.ReplaceVariables(DestinationPath ?? string.Empty, stripMissing: true, cleanSpecialCharacters: true);
if (string.IsNullOrEmpty(destFolder))
destFolder = new FileInfo(args.WorkingFile).Directory?.FullName ?? "";
var dest = args.GetSafeName(Path.Combine(destFolder, newFile));
if (string.IsNullOrEmpty(dest.Extension) == false)
catch (Exception ex)
{
// just ensures extensions are lowercased
dest = new FileInfo(dest.FullName.Substring(0, dest.FullName.LastIndexOf(dest.Extension)) + dest.Extension.ToLower());
args.Logger?.ELog("Failed to append to CSV file: " + ex.Message);
}
args.Logger?.ILog("Renaming file to: " + dest.FullName);
if (string.IsNullOrEmpty(CsvFile) == false)
{
try
{
System.IO.File.AppendAllText(CsvFile, EscapeForCsv(args.FileName) + "," + EscapeForCsv(dest.FullName) + Environment.NewLine);
}
catch (Exception ex)
{
args.Logger?.ELog("Failed to append to CSV file: " + ex.Message);
}
}
if (LogOnly)
return 1;
return args.MoveFile(dest.FullName) ? 1 : -1;
}
private string EscapeForCsv(string input)
if (LogOnly)
return 1;
return args.MoveFile(dest.FullName) ? 1 : -1;
}
private string EscapeForCsv(string input)
{
StringBuilder sb = new StringBuilder();
sb.Append('"');
foreach(char c in input)
{
StringBuilder sb = new StringBuilder();
sb.Append('"');
foreach(char c in input)
{
sb.Append(c);
if(c == '"')
sb.Append('"');
}
sb.Append('"');
return sb.ToString();
sb.Append(c);
if(c == '"')
sb.Append('"');
}
sb.Append('"');
return sb.ToString();
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -6,7 +6,7 @@
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v6.0": {
"FileFlows.Plugin/0.0.1.0": {
"FileFlows.Plugin/1.0.0": {
"runtime": {
"FileFlows.Plugin.dll": {}
}
@@ -14,7 +14,7 @@
}
},
"libraries": {
"FileFlows.Plugin/0.0.1.0": {
"FileFlows.Plugin/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
Binary file not shown.
Binary file not shown.
+7 -1
View File
@@ -27,7 +27,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby", "Emby\Emby.csproj",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apprise", "Apprise\Apprise.csproj", "{CA750701-C4CF-482F-B5F3-A40E188F3E14}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageNodes", "ImageNodes\ImageNodes.csproj", "{3C6B9933-B6BC-4C00-9247-71F575AA276B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageNodes", "ImageNodes\ImageNodes.csproj", "{3C6B9933-B6BC-4C00-9247-71F575AA276B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoLegacyNodes", "VideoLegacyNodes\VideoLegacyNodes.csproj", "{4339720B-5061-431F-9080-FC087ACFDB3B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -87,6 +89,10 @@ Global
{3C6B9933-B6BC-4C00-9247-71F575AA276B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C6B9933-B6BC-4C00-9247-71F575AA276B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C6B9933-B6BC-4C00-9247-71F575AA276B}.Release|Any CPU.Build.0 = Release|Any CPU
{4339720B-5061-431F-9080-FC087ACFDB3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4339720B-5061-431F-9080-FC087ACFDB3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4339720B-5061-431F-9080-FC087ACFDB3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4339720B-5061-431F-9080-FC087ACFDB3B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+64
View File
@@ -0,0 +1,64 @@
namespace FileFlows.VideoNodes
{
using System.Linq;
using System.Text.RegularExpressions;
internal static class ExtensionMethods
{
public static void AddOrUpdate(this Dictionary<string, object> dict, string key, object value) {
if (dict.ContainsKey(key))
dict[key] = value;
else
dict.Add(key, value);
}
public static string? EmptyAsNull(this string str)
{
return str == string.Empty ? null : str;
}
public static bool TryMatch(this Regex regex, string input, out Match match)
{
match = regex.Match(input);
return match.Success;
}
public static IEnumerable<string> SplitCommandLine(this string commandLine)
{
bool inQuotes = false;
return commandLine.Split(c =>
{
if (c == '\"')
inQuotes = !inQuotes;
return !inQuotes && c == ' ';
})
.Select(arg => arg.Trim().TrimMatchingQuotes('\"'))
.Where(arg => !string.IsNullOrEmpty(arg));
}
public static IEnumerable<string> Split(this string str,
Func<char, bool> controller)
{
int nextPiece = 0;
for (int c = 0; c < str.Length; c++)
{
if (controller(str[c]))
{
yield return str.Substring(nextPiece, c - nextPiece);
nextPiece = c + 1;
}
}
yield return str.Substring(nextPiece);
}
public static string TrimMatchingQuotes(this string input, char quote)
{
if ((input.Length >= 2) &&
(input[0] == quote) && (input[input.Length - 1] == quote))
return input.Substring(1, input.Length - 2);
return input;
}
}
}
+235
View File
@@ -0,0 +1,235 @@
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 successs, string output) Encode(string input, string output, List<string> arguments, bool dontAddInputFile = false, bool dontAddOutputFile = false)
{
arguments ??= new List<string> ();
// -y means it will overwrite a file if output already exists
if (dontAddInputFile == false) {
arguments.Insert(0, "-i");
arguments.Insert(1, input);
arguments.Insert(2, "-y");
}
if (dontAddOutputFile == false)
{
if (arguments.Last() != "-")
arguments.Add(output);
else
Logger.ILog("Last argument '-' skipping adding output file");
}
string argsString = String.Join(" ", arguments.Select(x => x.IndexOf(" ") > 0 ? "\"" + x + "\"" : x));
Logger.ILog(new string('=', ("FFMpeg.Arguments: " + argsString).Length));
Logger.ILog("FFMpeg.Arguments: " + argsString);
Logger.ILog(new string('=', ("FFMpeg.Arguments: " + argsString).Length));
var task = ExecuteShellCommand(ffMpegExe, arguments, 0);
task.Wait();
Logger.ILog("Exit Code: " + task.Result.ExitCode);
return (task.Result.ExitCode == 0, task.Result.Output); // 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, List<string> arguments, int timeout = 0)
{
var result = new ProcessResult();
using (var process = new Process())
{
this.process = process;
process.StartInfo.FileName = command;
if (arguments?.Any() == true)
{
foreach (string arg in arguments)
process.StartInfo.ArgumentList.Add(arg);
}
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
outputBuilder = new StringBuilder();
outputCloseEvent = new TaskCompletionSource<bool>();
process.OutputDataReceived += OnOutputDataReceived;
errorBuilder = new StringBuilder();
errorCloseEvent = new TaskCompletionSource<bool>();
process.ErrorDataReceived += OnErrorDataReceived;
bool isStarted;
try
{
isStarted = process.Start();
}
catch (Exception error)
{
// Usually it occurs when an executable file is not found or is not executable
result.Completed = true;
result.ExitCode = -1;
result.Output = error.Message;
isStarted = false;
}
if (isStarted)
{
// Reads the output stream first and then waits because deadlocks are possible
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// Creates task to wait for process exit using timeout
var waitForExit = WaitForExitAsync(process, timeout);
// Create task to wait for process exit and closing all output streams
var processTask = Task.WhenAll(waitForExit, outputCloseEvent.Task, errorCloseEvent.Task);
// Waits process completion and then checks it was not completed by timeout
if (
(
(timeout > 0 && await Task.WhenAny(Task.Delay(timeout), processTask) == processTask) ||
(timeout == 0 && await Task.WhenAny(processTask) == processTask)
)
&& waitForExit.Result)
{
result.Completed = true;
result.ExitCode = process.ExitCode;
result.Output = $"{outputBuilder}{errorBuilder}";
}
else
{
try
{
// Kill hung process
process.Kill();
}
catch
{
}
}
}
}
process = null;
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 (e.Data.Contains("Skipping NAL unit"))
return; // just slighlty ignore these
if (rgxTime.IsMatch(e.Data))
{
var timeString = rgxTime.Match(e.Data).Value;
var ts = TimeSpan.Parse(timeString);
Logger.DLog("TimeSpan Detected: " + ts);
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 (e.Data.Contains("Skipping NAL unit"))
{
return; // just slighlty ignore these
}
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;
}
}
}
+9
View File
@@ -0,0 +1,9 @@
global using System;
global using System.Linq;
global using System.Collections.Generic;
global using System.Text.RegularExpressions;
global using System.ComponentModel.DataAnnotations;
global using FileFlows.Plugin;
global using FileFlows.Plugin.Attributes;
global using System.ComponentModel;
@@ -0,0 +1,124 @@
namespace FileFlows.VideoNodes;
class CanUseHardwareEncoding
{
public enum HardwareEncoder
{
Nvidia_H264 = 1,
Amd_H264 = 2,
Qsv_H264 = 3,
Vaapi_H264 = 4,
Nvidia_Hevc = 11,
Amd_Hevc = 12,
Qsv_Hevc = 13,
Vaapi_Hevc = 14,
}
/// <summary>
/// Checks if this flow runner can use NVIDIA HEVC encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Nvidia_Hevc(NodeParameters args) => CanProcess(args, "hevc_nvenc");
/// <summary>
/// Checks if this flow runner can use NVIDIA H.264 encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Nvidia_H264(NodeParameters args) => CanProcess(args, "h264_nvenc");
/// <summary>
/// Checks if this flow runner can use AND HEVC encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Amd_Hevc(NodeParameters args) => CanProcess(args, "hevc_amf");
/// <summary>
/// Checks if this flow runner can use AND H.264 encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Amd_H264(NodeParameters args) => CanProcess(args, "h264_amf");
/// <summary>
/// Checks if this flow runner can use Intels QSV HEVC encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Qsv_Hevc(NodeParameters args) => CanProcess(args, "hevc_qsv -global_quality 28 -load_plugin hevc_hw");
/// <summary>
/// Checks if this flow runner can use Intels QSV H.264 encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Qsv_H264(NodeParameters args) => CanProcess(args, "h264_qsv");
/// <summary>
/// Checks if this flow runner can use VAAPI HEVC encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Vaapi_Hevc(NodeParameters args) => CanProcess(args, "hevc_vaapi");
/// <summary>
/// Checks if this flow runner can use VAAPI H.264 encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Vaapi_H264(NodeParameters args) => CanProcess(args, "h264_vaapi");
private static bool CanProcess(NodeParameters args, string encodingParams)
{
string ffmpeg = args.GetToolPath("FFMpeg");
if (string.IsNullOrEmpty(ffmpeg))
{
args.Logger.ELog("FFMpeg tool not found.");
return false;
}
return CanProcess(args, ffmpeg, encodingParams);
}
/// <summary>
/// Tests if the encoding parameters can be executed
/// </summary>
/// <param name="args">the node paramterse</param>
/// <param name="ffmpeg">the location of ffmpeg</param>
/// <param name="encodingParams">the encoding parameter to test</param>
/// <returns>true if can be processed</returns>
internal static bool CanProcess(NodeParameters args, string ffmpeg, string encodingParams)
{
bool can = CanExecute();
if (can == false && encodingParams?.Contains("amf") == true)
{
// AMD/AMF has a issue where it reports false at first but then passes
// https://github.com/revenz/FileFlows/issues/106
Thread.Sleep(2000);
can = CanExecute();
}
return can;
bool CanExecute()
{
string cmdArgs = $"-loglevel error -f lavfi -i color=black:s=1080x1080 -vframes 1 -an -c:v {encodingParams} -f null -\"";
var cmd = args.Process.ExecuteShellCommand(new ExecuteArgs
{
Command = ffmpeg,
Arguments = cmdArgs,
Silent = true
}).Result;
if (cmd.ExitCode != 0 || string.IsNullOrWhiteSpace(cmd.Output) == false)
{
args.Logger?.WLog($"Cant process '{encodingParams}': {cmd.Output ?? ""}");
return false;
}
return true;
}
}
}
@@ -8,6 +8,9 @@ namespace FileFlows.VideoNodes
public class VideoCodec : VideoNode
{
public override string ObsoleteMessage => "This node has been combined into 'Video Has Track'";
public override int Inputs => 1;
public override int Outputs => 2;
public override FlowElementType Type => FlowElementType.Logic;
Binary file not shown.
+43
View File
@@ -0,0 +1,43 @@
namespace FileFlows.VideoNodes
{
internal class ResolutionHelper
{
public enum Resolution
{
Unknown,
r480p,
r720p,
r1080p,
r4k,
}
public static Resolution GetResolution(VideoInfo videoInfo)
{
var video = videoInfo?.VideoStreams?.FirstOrDefault();
if (video == null)
return Resolution.Unknown;
return GetResolution(video.Width, video.Height);
}
public static Resolution GetResolution(int width, int height)
{
// so if the video is in portait mode, we test the height as if it were the width
int w = Math.Max(width, height);
int h = Math.Min(width, height);
if (Between(w, 1860, 1980))
return Resolution.r1080p;
else if (Between(w, 3780, 3900))
return Resolution.r4k;
else if (Between(w, 1220, 1340))
return Resolution.r720p;
else if (Between(w, 600, 700))
return Resolution.r480p;
return Resolution.Unknown;
}
private static bool Between(int value, int lower, int max) => value >= lower && value <= max;
}
}
+113
View File
@@ -0,0 +1,113 @@
namespace FileFlows.VideoNodes
{
public class VideoInfo
{
public string FileName { get; set; }
/// <summary>
/// Gets or sets the bitrate in bytes per second
/// </summary>
public float Bitrate { 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 List<Chapter> Chapters { get; set; } = new List<Chapter>();
}
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 bitrate(BPS) of the video stream in bytes per second
/// </summary>
public float Bitrate { get; set; }
/// <summary>
/// The codec of the stream
/// </summary>
public string Codec { get; set; } = "";
public string IndexString { get; set; }
/// <summary>
/// Gets or sets if the stream is HDR
/// </summary>
public bool HDR { 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; }
/// <summary>
/// The sample rate of the audio stream
/// </summary>
public int SampleRate { 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; }
}
public class Chapter
{
public string Title { get; set; }
public TimeSpan Start { get; set; }
public TimeSpan End { get; set; }
}
}
+320
View File
@@ -0,0 +1,320 @@
namespace FileFlows.VideoNodes
{
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
public class VideoInfoHelper
{
private string ffMpegExe;
private ILogger Logger;
static Regex rgxTitle = new Regex(@"(?<=((^[\s]+title[\s]+:[\s])))(.*?)$", RegexOptions.Multiline);
static Regex rgxDuration = new Regex(@"(?<=((^[\s]+DURATION(\-[\w]+)?[\s]+:[\s])))([\d]+:?)+\.[\d]{1,7}", RegexOptions.Multiline);
static Regex rgxDuration2 = new Regex(@"(?<=((^[\s]+Duration:[\s])))([\d]+:?)+\.[\d]{1,7}", RegexOptions.Multiline);
static Regex rgxAudioSampleRate = new Regex(@"(?<=((,|\s)))[\d]+(?=([\s]?hz))", RegexOptions.IgnoreCase);
static int _ProbeSize = 25;
internal static int ProbeSize
{
get => _ProbeSize;
set
{
if (value < 5)
_ProbeSize = 5;
else if (value > 1000)
_ProbeSize = 1000;
else
_ProbeSize = value;
}
}
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();
vi.FileName = filename;
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;
foreach (var arg in new[]
{
"-hide_banner",
"-probesize", ProbeSize + "M",
"-i",
filename,
})
{
process.StartInfo.ArgumentList.Add(arg);
}
process.Start();
string output = process.StandardError.ReadToEnd();
output = output.Replace("At least one output file must be specified", string.Empty).Trim();
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);
vi = ParseOutput(Logger, output);
}
}
catch (Exception ex)
{
Logger.ELog(ex.Message, ex.StackTrace.ToString());
}
return vi;
}
public static VideoInfo ParseOutput(ILogger logger, string output)
{
var vi = new VideoInfo();
var rgxStreams = new Regex(@"Stream\s#[\d]+:[\d]+(.*?)(?=(Stream\s#[\d]|$))", RegexOptions.Singleline);
var streamMatches = rgxStreams.Matches(output);
int streamIndex = 0;
// get a rough estimate, bitrate: 346 kb/s
var rgxBitrate = new Regex(@"(?<=(bitrate: ))[\d\.]+(?!=( kb/s))");
var brMatch = rgxBitrate.Match(output);
if (brMatch.Success)
{
vi.Bitrate = float.Parse(brMatch.Value) * 1_000; // to convert to b/s
}
vi.Chapters = ParseChapters(output);
int subtitleIndex = 0;
int videoIndex = 0;
int audioIndex = 0;
foreach (Match sm in streamMatches)
{
if (sm.Value.Contains(" Video: "))
{
var vs = ParseVideoStream(logger, sm.Value, output);
if (vs != null)
{
vs.Index = streamIndex;
vs.TypeIndex = videoIndex;
var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
if (match.Success)
vs.IndexString = match.Value;
vi.VideoStreams.Add(vs);
}
++videoIndex;
}
else if (sm.Value.Contains(" Audio: "))
{
var audio = ParseAudioStream(sm.Value);
if (audio != null)
{
audio.TypeIndex = audioIndex;
audio.Index = streamIndex;
var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
if (match.Success)
audio.IndexString = match.Value;
vi.AudioStreams.Add(audio);
}
++audioIndex;
}
else if (sm.Value.Contains(" Subtitle: "))
{
var sub = ParseSubtitleStream(sm.Value);
if (sub != null)
{
sub.Index = streamIndex;
sub.TypeIndex = subtitleIndex;
var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
if (match.Success)
sub.IndexString = match.Value;
vi.SubtitleStreams.Add(sub);
}
++subtitleIndex;
}
++streamIndex;
}
return vi;
}
private static List<Chapter> ParseChapters(string output)
{
try
{
var rgxChatpers = new Regex("(?<=(Chapters:))(.*?)(?=(Stream))", RegexOptions.Singleline);
string strChapters;
if (rgxChatpers.TryMatch(output, out Match matchChapters))
strChapters = matchChapters.Value.Trim();
else
return new List<Chapter>();
var rgxChapter = new Regex("Chapter #(.*?)(?=(Chapter #|$))", RegexOptions.Singleline);
var chapters = new List<Chapter>();
var rgxTitle = new Regex(@"title[\s]*:[\s]*(.*?)$");
var rgxStart = new Regex(@"(?<=(start[\s]))[\d]+\.[\d]+");
var rgxEnd = new Regex(@"(?<=(end[\s]))[\d]+\.[\d]+");
foreach (Match match in rgxChapter.Matches(strChapters))
{
try
{
Chapter chapter = new Chapter();
if (rgxTitle.TryMatch(match.Value.Trim(), out Match title))
chapter.Title = title.Groups[1].Value;
if (rgxStart.TryMatch(match.Value, out Match start))
{
double startSeconds = double.Parse(start.Value);
chapter.Start = TimeSpan.FromSeconds(startSeconds);
}
if (rgxEnd.TryMatch(match.Value, out Match end))
{
double endSeconds = double.Parse(end.Value);
chapter.End = TimeSpan.FromSeconds(endSeconds);
}
if (chapter.Start > TimeSpan.Zero || chapter.End > TimeSpan.Zero)
{
chapters.Add(chapter);
}
}
catch (Exception ) { }
}
return chapters;
}catch (Exception) { return new List<Chapter>(); }
}
public static VideoStream ParseVideoStream(ILogger logger, 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 (Regex.IsMatch(line, @"([\d]+(\.[\d]+)?)\sfps") && float.TryParse(Regex.Match(line, @"([\d]+(\.[\d]+)?)\sfps").Groups[1].Value, out float fps))
vs.FramesPerSecond = fps;
var rgxBps = new Regex(@"(?<=((BPS(\-[\w]+)?[\s]*:[\s])))([\d]+)");
if (rgxBps.IsMatch(info) && float.TryParse(rgxBps.Match(info).Value, out float bps))
vs.Bitrate = bps;
if (rgxDuration.IsMatch(info) && TimeSpan.TryParse(rgxDuration.Match(info).Value, out TimeSpan duration) && duration.TotalSeconds > 0)
{
vs.Duration = duration;
logger?.ILog("Video stream duration: " + vs.Duration);
}
else if (rgxDuration2.IsMatch(fullOutput) && TimeSpan.TryParse(rgxDuration2.Match(fullOutput).Value, out TimeSpan duration2) && duration2.TotalSeconds > 0)
{
vs.Duration = duration2;
logger?.ILog("Video stream duration: " + vs.Duration);
}
else
{
logger?.ILog("Failed to read duration for VideoStream: " + info);
}
vs.HDR = info.Contains("bt2020nc") && info.Contains("smpte2084");
return vs;
}
public static 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 = "";
// this isnt type index, this is overall index
audio.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value) - 1;
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() ?? "";
if (info.IndexOf("0 channels") >= 0)
{
audio.Channels = 0;
}
else
{
try
{
//Logger.ILog("codec: " + vs.Codec);
if (parts[2] == "stereo")
audio.Channels = 2;
else if (parts[2] == "mono")
audio.Channels = 1;
else if (Regex.IsMatch(parts[2], @"^[\d]+(\.[\d]+)?"))
{
audio.Channels = float.Parse(Regex.Match(parts[2], @"^[\d]+(\.[\d]+)?").Value);
}
}
catch (Exception) { }
}
var match = rgxAudioSampleRate.Match(info);
if (match.Success)
audio.SampleRate = int.Parse(match.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;
}
public static 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;
}
}
}
Binary file not shown.
+254
View File
@@ -0,0 +1,254 @@
{
"H": {
"264": "H.264",
"265": "H.265"
},
"5": { "1": "5.1" },
"7": { "1": "7.1" },
"Flow": {
"Parts": {
"AudioAddTrack": {
"Outputs": {
"1": "Audio track added and saved to temporary file"
},
"Description": "Adds a new audio track to the video file, all other audio tracks will remain. This will use the first audio track of the file as the source audio track to convert.",
"Fields": {
"Index": "Index",
"Index-Help": "The index where to insert the new audio track. 0 based, so to insert the new audio track as the first track set this to 0.",
"Channels": "Channels",
"Channels-Help": "The number of channels to convert this audio track to.",
"Bitrate": "Bitrate",
"Bitrate-Help": "Bitrate of the new audio track"
}
},
"AudioAdjustVolume": {
"Outputs": {
"1": "Audio tracks volume was adjusted and saved to temporary file",
"2": "Audio tracks were not adjusted"
},
"Description": "Adjusts audio tracks volume in a video file using FFMPEG",
"Fields": {
"VolumePercent": "Volume Percent",
"VolumePercent-Help": "The percent of the adjusted volume.\n100 means no adjustment\n50 means half volume\n0 means muted"
}
},
"AudioNormalization": {
"Outputs": {
"1": "Audio tracks were normalized and saved to temporary file",
"2": "No audio tracks were found to be normalized"
},
"Description": "Normalizes all audio tracks in a video file using FFMPEGs loudnorm filter",
"Fields": {
"AllAudio": "All Audio Tracks",
"AllAudio-Help": "If all audio tracks should be normalized or if just the first track should be",
"TwoPass": "Two Pass",
"TwoPass-Help": "If the audio tracks should use two pass normalization. This improves the normalization but increases the processing time.",
"Pattern": "Pattern",
"Pattern-Help": "An optional regular expression to filter out audio tracks to normalize. Will match against the title, codec and language",
"NotMatching": "Not Matching",
"NotMatching-Help": "If the pattern should be used to exclude audio tracks from normalization, otherwise if the audio track matches they will be normalized."
}
},
"AudioTrackRemover": {
"Outputs": {
"1": "Audio tracks were removed",
"2": "Audio tracks were NOT removed"
},
"Description": "Allows you to remove audio tracks based on either their title or their language codes.\n\nAny title (or language code if set to \"Use Language Code\") that is blank will NOT be removed regardless of the pattern.",
"Fields": {
"Pattern": "Pattern",
"Pattern-Help": "A regular expression to match against, eg \"commentary\" to remove commentary tracks",
"NotMatching": "Not Matching",
"NotMatching-Help": "If audio tracks NOT matching the pattern should be removed",
"UseLanguageCode": "Use Language Code",
"UseLanguageCode-Help": "If the language code of the audio track should be used instead of the title"
}
},
"AudioTrackReorder": {
"Outputs": {
"1": "Audio tracks re-ordered in new temporary file",
"2": "Audio tracks NOT re-ordered"
},
"Description": "Allows you to reorder audio tracks in the preferred order.\n\nEnter the languages/audio codecs/channels in the order you want. Any not listed will be ordered after the ones entered in their original order.\nIf there are multiple tracks with same language/codec/channels, they will be ordered first by the order you entered, then in their original order.\n\nOutput 1: Tracks were reordered\nOutput 2: Tracks did not need reordering",
"Fields": {
"OrderedTracks": "Ordered Audio Codecs",
"OrderedTracks-Help": "The order of audio codecs to the audio tracks by. This is done after the languages (if any)",
"Languages": "Languages",
"Languages-Help": "The order of languages to sort the audio tracks by. This sorting is done before the codec.",
"Channels": "Channels",
"Channels-Help": "The order of audio channels to sort the audio tracks by. This sorting is done before languages.\nFor example \"5.1\",\"7.1\",\"6.2\",\"2\""
}
},
"AudioTrackSetLanguage": {
"Label": "Audio Set Language",
"Outputs": {
"1": "Audio tracks updated to new temporary file",
"2": "Audio tracks NOT updated"
},
"Description": "Allows you to set the language for any audio tracks that have no language set. If the audio track does have a language set, it will be skipped.\n\nOutput 1: Audio Tracks were updated\nOutput 2: No audio tracks were needing to be updated",
"Fields": {
"Language": "Language",
"Language-Help": "The ISO 639-2 language code to use. \nhttps://en.wikipedia.org/wiki/List_of_ISO_639-2_codes"
}
},
"AutoChapters": {
"Description": "Automatically detect scene changes in the video to generate chapters.",
"Outputs": {
"1": "Chapters generated and saved to temporary file",
"2": "No chapters detected or video already had chapters"
},
"Fields": {
"MinimumLength": "Minimum Length",
"MinimumLength-Suffix": "seconds",
"MinimumLength-Help": "The minimum length of a chapter in seconds",
"Percent": "Percent",
"Percent-Suffix": "%",
"Percent-Help": "The threshold percentage value to use for scene detection changes. A good value is 45%"
}
},
"ComskipChapters": {
"Description": "Uses a comskip EDL file and will create chapters given that EDL comskip file.",
"Outputs": {
"1": "Commercials chapters created, saved to temporary file",
"2": "No commercials detected"
}
},
"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",
"Outputs": {
"1": "Black bars detected",
"2": "No black bars detected"
},
"Fields": {
"CroppingThreshold": "Threshold",
"CroppingThreshold-Help": "The amount of pixels that must be greater than to crop. E.g. if there's only 5 pixels detected as black space, you may consider this too small to crop."
}
},
"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",
"Outputs": {
"1": "Video processed"
},
"Fields": {
"Extension": "Extension",
"Extension-Help": "The file extension to use on the newly created file",
"CommandLine": "Command Line",
"CommandLine-Help": "The command line to run with FFMPEG.\n'{WorkingFile}': the working file of the flow\n'{Output}': The output file that will be passed as the last parameter to FFMPEG including the extension defined above."
}
},
"RemuxToMKV": {
"Descritption": "Remuxes a video file into a MKV container. All streams will be copied to the new container",
"Outputs": {
"1": "File remuxed to temporary file",
"2": "File was already in a MKV container"
},
"Fields": {
"Force": "Force",
"Force-Help": "If the file should be always remuxed into a MKV container even when it already is in a MKV container.\ni.e. a new temporary file will always be created."
}
},
"RemuxToMP4": {
"Descritption": "Remuxes a video file into a MP4 container. All streams will be copied to the new container",
"Outputs": {
"1": "File remuxed to temporary file",
"2": "File was already in a MP4 container"
},
"Fields": {
"Force": "Force",
"Force-Help": "If the file should be always remuxed into a MP4 container even when it already is in a MP4 container.\ni.e. a new temporary file will always be created."
}
},
"SubtitleRemover": {
"Description": "Removes subtitles from a video file if found.\n\nOutput 1: Subtitles were removed\nOutput 2: No subtitles found that needed to be removed",
"Outputs": {
"1": "Subtitles removed in new temporary file",
"2": "No subtitles to remove"
},
"Fields": {
"SubtitlesToRemove": "Subtitles To Remove",
"RemoveAll": "Remove All",
"RemoveAll-Help": "When checked, all subtitles will be removed from the file, otherwise only those selected below will be"
}
},
"SubtitleLanguageRemover": {
"Outputs": {
"1": "Subtitles were removed",
"2": "Subtitles were NOT removed"
},
"Description": "Allows you to remove subtitles based on either their title or their language codes.\n\nAny language (or title if set to \"Use Title\") that is blank will NOT be removed regardless of the pattern.",
"Fields": {
"Pattern": "Pattern",
"Pattern-Help": "A regular expression to match against, eg \"eng\" to remove English tracks",
"NotMatching": "Not Matching",
"NotMatching-Help": "If subtitles NOT matching the pattern should be removed",
"UseTitle": "Use Title",
"UseTitle-Help": "If the title of the subtitle should be used for matching instead of the language"
}
},
"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",
"Outputs": {
"1": "Video re-encoded to temporary file",
"2": "Video not re-encoded"
},
"Fields": {
"Extension": "Extension",
"Extension-Help": "The file extension to use on the newly created file.",
"VideoCodec": "Video Codec",
"VideoCodec-Help": "The video codec the video should be in, for example hevc, h264.\nIf left empty all original video tracks will be copied.",
"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.\nIf left empty all original audio tracks will be copied.",
"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"
}
},
"VideoHasStream": {
"Description": "Tests if a video file contains a stream",
"Outputs": {
"1": "Contains the matching stream",
"2": "Does not contain the matching stream"
},
"Fields": {
"Stream": "Type",
"Stream-Help": "The type of stream to look for",
"Title": "Title",
"Title-Help": "A regular expression used to test the stream title",
"Codec": "Codec",
"Codec-Help": "A regular expression used to test the stream codec",
"Language": "Language",
"Language-Help": "A regular expression used to test the stream language",
"Channels": "Channels",
"Channels-Help": "The number of channels to test for. Set to 0 to ignore this check"
}
},
"VideoScaler": {
"Description": "This allows you to scale a video to the specified dimensions. It will retain the aspect ratio of the video so if the video was 1920x1000 it would scale to 1280x668 if you select 720P.",
"Outputs": {
"1": "Video rescaled to temporary file",
"2": "Video was already in/near the scaled resolution"
},
"Fields": {
"VideoCodec": "Video Codec",
"Language-Help": "The video codec to encode the scaled video in",
"Extension": "Extension",
"Extension-Help": "The file extension to use on the newly created file",
"Force": "Force",
"Force-Help": "When checked the video will be force scaled even if the working file is already in this resolution (or near this resolution).",
"Resolution": "Resolution",
"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."
}
}
}
}
}
+236
View File
@@ -0,0 +1,236 @@
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 FFMpegEncoder Encoder;
public bool Encode(NodeParameters args, string ffmpegExe, List<string> ffmpegParameters, string extension = "mkv", string outputFile = "", bool updateWorkingFile = true, bool dontAddInputFile = false, bool dontAddOutputFile = false)
{
string output;
return Encode(args, ffmpegExe, ffmpegParameters, out output, extension, outputFile, updateWorkingFile, dontAddInputFile: dontAddInputFile, dontAddOutputFile: dontAddOutputFile);
}
public bool Encode(NodeParameters args, string ffmpegExe, List<string> ffmpegParameters, out string output, string extension = "mkv", string outputFile = "", bool updateWorkingFile = true, bool dontAddInputFile = false, bool dontAddOutputFile = false)
{
if (string.IsNullOrEmpty(extension))
extension = "mkv";
Encoder = new FFMpegEncoder(ffmpegExe, args.Logger);
Encoder.AtTime += AtTimeEvent;
if (string.IsNullOrEmpty(outputFile))
outputFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + "." + extension);
if (TotalTime.TotalMilliseconds == 0)
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo != null)
{
TotalTime = videoInfo.VideoStreams[0].Duration;
args.Logger.ILog("### Total Time: " + TotalTime);
}
}
var success = Encoder.Encode(args.WorkingFile, outputFile, ffmpegParameters, dontAddInputFile: dontAddInputFile, dontAddOutputFile: dontAddOutputFile);
args.Logger.ILog("Encoding successful: " + success.successs);
if (success.successs && updateWorkingFile)
{
args.SetWorkingFile(outputFile);
// get the new video info
var videoInfo = new VideoInfoHelper(ffmpegExe, args.Logger).Read(outputFile);
SetVideoInfo(args, videoInfo, this.Variables ?? new Dictionary<string, object>());
}
Encoder.AtTime -= AtTimeEvent;
Encoder = null;
output = success.output;
return success.successs;
}
public override Task Cancel()
{
if (Encoder != null)
Encoder.Cancel();
return base.Cancel();
}
void AtTimeEvent(TimeSpan time)
{
if (TotalTime.TotalMilliseconds == 0)
{
Args?.Logger?.DLog("Can't report time progress as total time is 0");
return;
}
float percent = (float)((time.TotalMilliseconds / TotalTime.TotalMilliseconds) * 100);
if(Args?.PartPercentageUpdate != null)
Args.PartPercentageUpdate(percent);
}
public string CheckVideoCodec(string ffmpeg, string vidparams)
{
if (string.IsNullOrEmpty(vidparams))
return string.Empty;
if(vidparams.ToLower() == "hevc" || vidparams.ToLower() == "h265")
{
// try find best hevc encoder
foreach(string vidparam in new [] { "hevc_nvenc -preset hq", "hevc_qsv -global_quality 28 -load_plugin hevc_hw", "hevc_amf", "hevc_vaapi" })
{
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, vidparam);
if (canProcess)
return vidparam;
}
return "libx265";
}
if (vidparams.ToLower() == "h264")
{
// try find best hevc encoder
foreach (string vidparam in new[] { "h264_nvenc", "h264_qsv", "h264_amf", "h264_vaapi" })
{
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, vidparam);
if (canProcess)
return vidparam;
}
return "libx264";
}
if (vidparams.ToLower().Contains("hevc_nvenc"))
{
// nvidia h265 encoding, check can
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, vidparams);
if (canProcess == false)
{
// change to cpu encoding
Args.Logger?.ILog("Can't encode using hevc_nvenc, falling back to CPU encoding H265 (libx265)");
return "libx265";
}
return vidparams;
}
else if (vidparams.ToLower().Contains("h264_nvenc"))
{
// nvidia h264 encoding, check can
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, vidparams);
if (canProcess == false)
{
// change to cpu encoding
Args.Logger?.ILog("Can't encode using h264_nvenc, falling back to CPU encoding H264 (libx264)");
return "libx264";
}
return vidparams;
}
else if (vidparams.ToLower().Contains("hevc_qsv"))
{
// nvidia h265 encoding, check can
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, vidparams);
if (canProcess == false)
{
// change to cpu encoding
Args.Logger?.ILog("Can't encode using hevc_qsv, falling back to CPU encoding H265 (libx265)");
return "libx265";
}
return vidparams;
}
else if (vidparams.ToLower().Contains("h264_qsv"))
{
// nvidia h264 encoding, check can
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, vidparams);
if (canProcess == false)
{
// change to cpu encoding
Args.Logger?.ILog("Can't encode using h264_qsv, falling back to CPU encoding H264 (libx264)");
return "libx264";
}
return vidparams;
}
return vidparams;
}
public bool HasNvidiaCard(string ffmpeg)
{
try
{
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
var cmd = Args.Process.ExecuteShellCommand(new ExecuteArgs
{
Command = "wmic",
Arguments = "path win32_VideoController get name"
}).Result;
if (cmd.ExitCode == 0)
{
// it worked
if (cmd.Output?.ToLower()?.Contains("nvidia") == false)
return false;
}
}
else
{
// linux, crude method, look for nvidia in the /dev dir
var dir = new DirectoryInfo("/dev");
if (dir.Exists == false)
return false;
bool dev = dir.GetDirectories().Any(x => x.Name.ToLower().Contains("nvidia"));
if (dev == false)
return false;
}
// check cuda in ffmpeg itself
var result = Args.Process.ExecuteShellCommand(new ExecuteArgs
{
Command = ffmpeg,
Arguments = "-hide_banner -init_hw_device list"
}).Result;
return result.Output?.Contains("cuda") == true;
}
catch (Exception ex)
{
Args.Logger?.ELog("Failed to detect NVIDIA card: " + ex.Message + Environment.NewLine + ex.StackTrace);
return false;
}
}
protected bool IsSameVideoCodec(string current, string wanted)
{
wanted = ReplaceCommon(wanted);
current = ReplaceCommon(current);
return wanted == current;
string ReplaceCommon(string input)
{
input = input.ToLower();
input = Regex.Replace(input, "^(divx|xvid|m(-)?peg(-)4)$", "mpeg4", RegexOptions.IgnoreCase);
input = Regex.Replace(input, "^(hevc|h[\\.x\\-]?265)$", "h265", RegexOptions.IgnoreCase);
input = Regex.Replace(input, "^(h[\\.x\\-]?264)$", "h264", RegexOptions.IgnoreCase);
return input;
}
}
protected bool SupportsSubtitles(NodeParameters args, VideoInfo videoInfo, string extension)
{
if (videoInfo?.SubtitleStreams?.Any() != true)
return false;
bool mov_text = videoInfo.SubtitleStreams.Any(x => x.Codec == "mov_text");
// if mov_text and going to mkv, we can't convert these subtitles
if (mov_text && extension?.ToLower()?.EndsWith("mkv") == true)
return false;
return true;
//if (Regex.IsMatch(container ?? string.Empty, "(mp(e)?(g)?4)|avi|divx|xvid", RegexOptions.IgnoreCase))
// return false;
//return true;
}
}
}
+164
View File
@@ -0,0 +1,164 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using System.Text.Json;
public abstract class VideoNode : Node
{
/// <summary>
/// Gets the Node Parameters
/// </summary>
protected NodeParameters Args { get; private set; }
/// <summary>
/// Gets if this node is obsolete and should be phased out
/// </summary>
public override bool Obsolete => true;
/// <summary>
/// Gets a message to show when the user tries to use this obsolete node
/// </summary>
public override string ObsoleteMessage => "This node has been replaced by the FFMPEG Builder version";
#if (DEBUG)
/// <summary>
/// Used for unit tests
/// </summary>
/// <param name="args">the args</param>
public void SetArgs(NodeParameters args)
{
this.Args = args;
}
#endif
/// <summary>
/// Gets the FFMPEG executable location
/// </summary>
protected string FFMPEG { get; private set; }
public override string Icon => "fas fa-video";
/// <summary>
/// Executed before execute, sets ffmpegexe etc
/// </summary>
/// <param name="args">the node parametes</param>
/// <returns>true if successfully</returns>
public override bool PreExecute(NodeParameters args)
{
this.Args = args;
this.FFMPEG = GetFFMpegExe();
return string.IsNullOrEmpty(this.FFMPEG) == false;
}
private string GetFFMpegExe()
{
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;
// }
private const string VIDEO_INFO = "VideoInfo";
protected void SetVideoInfo(NodeParameters args, VideoInfo videoInfo, Dictionary<string, object> variables)
{
if (videoInfo.VideoStreams?.Any() == false)
return;
if (args.Parameters.ContainsKey(VIDEO_INFO))
args.Parameters[VIDEO_INFO] = videoInfo;
else
args.Parameters.Add(VIDEO_INFO, videoInfo);
variables.AddOrUpdate("vi.VideoInfo", videoInfo);
variables.AddOrUpdate("vi.Width", videoInfo.VideoStreams[0].Width);
variables.AddOrUpdate("vi.Height", videoInfo.VideoStreams[0].Height);
variables.AddOrUpdate("vi.Duration", videoInfo.VideoStreams[0].Duration.TotalSeconds);
variables.AddOrUpdate("vi.Video.Codec", videoInfo.VideoStreams[0].Codec);
if (videoInfo.AudioStreams?.Any() == true)
{
variables.AddOrUpdate("vi.Audio.Codec", videoInfo.AudioStreams[0].Codec?.EmptyAsNull());
variables.AddOrUpdate("vi.Audio.Channels", videoInfo.AudioStreams[0].Channels > 0 ? (object)videoInfo.AudioStreams[0].Channels : null);
variables.AddOrUpdate("vi.Audio.Language", videoInfo.AudioStreams[0].Language?.EmptyAsNull());
variables.AddOrUpdate("vi.Audio.Codecs", string.Join(", ", videoInfo.AudioStreams.Select(x => x.Codec).Where(x => string.IsNullOrEmpty(x) == false)));
variables.AddOrUpdate("vi.Audio.Languages", string.Join(", ", videoInfo.AudioStreams.Select(x => x.Language).Where(x => string.IsNullOrEmpty(x) == false)));
}
var resolution = ResolutionHelper.GetResolution(videoInfo.VideoStreams[0].Width, videoInfo.VideoStreams[0].Height);
if(resolution == ResolutionHelper.Resolution.r1080p)
variables.AddOrUpdate("vi.Resolution", "1080p");
else if (resolution == ResolutionHelper.Resolution.r4k)
variables.AddOrUpdate("vi.Resolution", "4K");
else if (resolution == ResolutionHelper.Resolution.r720p)
variables.AddOrUpdate("vi.Resolution", "720p");
else if (resolution == ResolutionHelper.Resolution.r480p)
variables.AddOrUpdate("vi.Resolution", "480p");
else if (videoInfo.VideoStreams[0].Width < 900 && videoInfo.VideoStreams[0].Height < 800)
variables.AddOrUpdate("vi.Resolution", "SD");
else
variables.AddOrUpdate("vi.Resolution", videoInfo.VideoStreams[0].Width + "x" + videoInfo.VideoStreams[0].Height);
args.UpdateVariables(variables);
}
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;
}
if(args.Parameters[VIDEO_INFO] == null)
{
args.Logger.WLog("VideoInfo not found for file");
return null;
}
var result = args.Parameters[VIDEO_INFO] as VideoInfo;
if (result != null)
return result;
// may be from non Legacy VideoNodes
try
{
string json = JsonSerializer.Serialize(args.Parameters[VIDEO_INFO]);
var vi = JsonSerializer.Deserialize<VideoInfo>(json);
if (vi == null)
throw new Exception("Failed to deserailize object");
return vi;
}
catch(Exception ex)
{
args.Logger.WLog("VideoInfo could not be deserialized: " + ex.Message);
return null;
}
}
}
}
@@ -95,6 +95,11 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
public override int Execute(NodeParameters args)
{
if (string.IsNullOrEmpty(Codec) || Codec == "ORIGINAL")
{
// this is a special case we use in the templates, to not add an audio track and use original
return 1;
}
var audio = new FfmpegAudioStream();
var bestAudio = GetBestAudioTrack(args, Model.AudioStreams.Select(x => x.Stream));
@@ -1,5 +1,6 @@
using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
@@ -21,6 +22,8 @@ public class FfmpegBuilderAudioNormalization : FfmpegBuilderNode
[Boolean(4)]
public bool NotMatching { get; set; }
internal const string LOUDNORM_TARGET = "I=-24:LRA=7:TP=-2.0";
[RequiresUnreferencedCode("")]
public override int Execute(NodeParameters args)
{
@@ -54,14 +57,14 @@ public class FfmpegBuilderAudioNormalization : FfmpegBuilderNode
item.stream.Filter.Add(normalizedTracks[audio.TypeIndex]);
else
{
string twoPass = AudioNormalization.DoTwoPass(this, args, FFMPEG, audio.TypeIndex);
string twoPass = DoTwoPass(this, args, FFMPEG, audio.TypeIndex);
item.stream.Filter.Add(twoPass);
normalizedTracks.Add(audio.TypeIndex, twoPass);
}
}
else
{
item.stream.Filter.Add($"loudnorm={AudioNormalization.LOUDNORM_TARGET}");
item.stream.Filter.Add($"loudnorm={LOUDNORM_TARGET}");
}
normalizing = true;
}
@@ -69,4 +72,70 @@ public class FfmpegBuilderAudioNormalization : FfmpegBuilderNode
return normalizing ? 1 : 2;
}
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize<FileFlows.VideoNodes.AudioNormalization.LoudNormStats>(string, System.Text.Json.JsonSerializerOptions?)")]
public static string DoTwoPass(EncodingNode node, NodeParameters args, string ffmpegExe, int audioIndex)
{
//-af loudnorm=I=-24:LRA=7:TP=-2.0"
string output;
var result = node.Encode(args, ffmpegExe, new List<string>
{
"-hide_banner",
"-i", args.WorkingFile,
"-strict", "-2", // allow experimental stuff
"-map", "0:a:" + audioIndex,
"-af", "loudnorm=" + LOUDNORM_TARGET + ":print_format=json",
"-f", "null",
"-"
}, out output, updateWorkingFile: false, dontAddInputFile: true);
if (result == false)
throw new Exception("Failed to prcoess audio track");
int index = output.LastIndexOf("{");
if (index == -1)
throw new Exception("Failed to detected json in output");
string json = output.Substring(index);
json = json.Substring(0, json.IndexOf("}") + 1);
if (string.IsNullOrEmpty(json))
throw new Exception("Failed to parse TwoPass json");
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
LoudNormStats stats = JsonSerializer.Deserialize<LoudNormStats>(json);
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
if (stats.input_i == "-inf" || stats.input_lra == "-inf" || stats.input_tp == "-inf" || stats.input_thresh == "-inf" || stats.target_offset == "-inf")
{
args.Logger?.WLog("-inf detected in loud norm two pass, falling back to single pass loud norm");
return $"loudnorm={LOUDNORM_TARGET}";
}
string ar = $"loudnorm=print_format=summary:linear=true:{LOUDNORM_TARGET}:measured_I={stats.input_i}:measured_LRA={stats.input_lra}:measured_tp={stats.input_tp}:measured_thresh={stats.input_thresh}:offset={stats.target_offset}";
return ar;
}
private class LoudNormStats
{
/*
{
"input_i" : "-7.47",
"input_tp" : "12.33",
"input_lra" : "6.70",
"input_thresh" : "-18.13",
"output_i" : "-24.25",
"output_tp" : "-3.60",
"output_lra" : "5.90",
"output_thresh" : "-34.74",
"normalization_type" : "dynamic",
"target_offset" : "0.25"
}
*/
public string input_i { get; set; }
public string input_tp { get; set; }
public string input_lra { get; set; }
public string input_thresh { get; set; }
public string target_offset { get; set; }
}
}
@@ -1,39 +1,76 @@
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
public class FfmpegBuilderAudioTrackRemover: FfmpegBuilderNode
{
public override string HelpUrl => "https://docs.fileflows.com/plugins/video-nodes/ffmpeg-builder/audio-track-remover";
public override string HelpUrl => "https://docs.fileflows.com/plugins/video-nodes/ffmpeg-builder/track-remover";
public override string Icon => "fas fa-volume-off";
public override string Icon => "fas fa-eraser";
public override int Outputs => 2;
public override int Outputs => 2;
[Boolean(1)]
[Select(nameof(StreamTypeOptions), 1)]
public string StreamType { get; set; }
[Boolean(2)]
[ConditionEquals(nameof(StreamType), "Video", inverse: true)]
public bool RemoveAll { get; set; }
[NumberInt(2)]
[NumberInt(3)]
public int RemoveIndex { get; set; }
[TextVariable(3)]
[TextVariable(4)]
[ConditionEquals(nameof(RemoveAll), false)]
public string Pattern { get; set; }
[Boolean(4)]
[Boolean(5)]
[ConditionEquals(nameof(RemoveAll), false)]
public bool NotMatching { get; set; }
[Boolean(5)]
[Boolean(6)]
[ConditionEquals(nameof(RemoveAll), false)]
public bool UseLanguageCode { get; set; }
private static List<ListOption> _StreamTypeOptions;
public static List<ListOption> StreamTypeOptions
{
get
{
if (_StreamTypeOptions == null)
{
_StreamTypeOptions = new List<ListOption>
{
new ListOption { Label = "Audio", Value = "Audio" },
new ListOption { Label = "Video", Value = "Video" },
new ListOption { Label = "Subtitle", Value = "Subtitle" }
};
}
return _StreamTypeOptions;
}
}
public override int Execute(NodeParameters args)
{
if(string.IsNullOrEmpty(StreamType) || StreamType.ToLower() == "audio")
return RemoveTracks(Model.AudioStreams) ? 1 : 2;
if (StreamType.ToLower() == "subtitle")
return RemoveTracks(Model.SubtitleStreams) ? 1 : 2;
if (StreamType.ToLower() == "video")
return RemoveTracks(Model.VideoStreams) ? 1 : 2;
return 2;
}
private bool RemoveTracks<T>(List<T> tracks) where T: FfmpegStream
{
bool removing = false;
Regex? regex = null;
int index = -1;
foreach(var audio in Model.AudioStreams)
foreach (var track in tracks)
{
if (audio.Deleted == false)
if (track.Deleted == false)
{
// only record indexes of tracks that have not been deleted
++index;
@@ -41,15 +78,22 @@ public class FfmpegBuilderAudioTrackRemover: FfmpegBuilderNode
continue;
}
if (RemoveAll)
if (RemoveAll || string.IsNullOrEmpty(this.Pattern))
{
audio.Deleted = true;
track.Deleted = true;
removing = true;
continue;
}
if(regex == null)
if (regex == null)
regex = new Regex(this.Pattern, RegexOptions.IgnoreCase);
string str = UseLanguageCode ? audio.Stream.Language : audio.Stream.Title;
string str = "";
if(track is FfmpegAudioStream audio)
str = UseLanguageCode ? audio.Stream.Language : audio.Stream.Title;
else if (track is FfmpegSubtitleStream subtitle)
str = UseLanguageCode ? subtitle.Stream.Language : subtitle.Stream.Title;
else if (track is FfmpegVideoStream video)
str = video.Stream.Title;
if (string.IsNullOrEmpty(str) == false) // if empty we always use this since we have no info to go on
{
bool matches = regex.IsMatch(str);
@@ -57,11 +101,11 @@ public class FfmpegBuilderAudioTrackRemover: FfmpegBuilderNode
matches = !matches;
if (matches)
{
audio.Deleted = true;
track.Deleted = true;
removing = true;
}
}
}
return removing ? 1 : 2;
return removing;
}
}
@@ -22,8 +22,6 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes
{
var model = this.Model;
List<string> ffArgs = new List<string>();
ffArgs.AddRange(new[] { "-strict", "-2" }); // allow experimental stuff
ffArgs.AddRange(new[] { "-fflags", "+genpts" }); //Generate missing PTS if DTS is present.
if(model.CustomParameters?.Any() == true)
ffArgs.AddRange(model.CustomParameters);
@@ -68,6 +66,9 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes
else
model.InputFiles[0] = args.WorkingFile;
startArgs.AddRange(new[] { "-strict", "-2" }); // allow experimental stuff
startArgs.AddRange(new[] { "-fflags", "+genpts" }); //Generate missing PTS if DTS is present.
startArgs.AddRange(new[] {
"-probesize", VideoInfoHelper.ProbeSize + "M"
});
@@ -95,7 +96,7 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes
internal string[] GetHardwareDecodingArgs()
{
string testFile = Path.Combine(Args.TempPath, Guid.NewGuid() + ".hwtest.mkv");
foreach(var hw in new [] { "cuda", "dxva2", "qsv", "d3d11va", "opencl" })
foreach(var hw in new [] { "cuda", "qsv", "dxva2", "d3d11va", "opencl" })
{
// ffmpeg -y -hwaccel qsvf -f lavfi -i color=color=red -frames:v 10 test.mkv
try
@@ -0,0 +1,128 @@
using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
/// <summary>
/// Node that removes metadata from a file
/// </summary>
public class FfmpegBuilderMetadataRemover : FfmpegBuilderNode
{
/// <summary>
/// Gets the Help URL for this node
/// </summary>
public override string HelpUrl => "https://docs.fileflows.com/plugins/video-nodes/ffmpeg-builder/metadata-remover";
/// <summary>
/// Gets the icon for this node
/// </summary>
public override string Icon => "fas fa-remove-format";
/// <summary>
/// Gets the number of outputs for this node
/// </summary>
public override int Outputs => 1;
/// <summary>
/// Gets or sets if should run against video tracks
/// </summary>
[Boolean(1)]
public bool Video { get; set; }
/// <summary>
/// Gets or sets if should run against audio tracks
/// </summary>
[Boolean(2)]
public bool Audio { get; set; }
/// <summary>
/// Gets or sets if should run against subtitle tracks
/// </summary>
[Boolean(3)]
public bool Subtitle { get; set; }
/// <summary>
/// Gets or sets if images should be removed
/// </summary>
[Boolean(4)]
public bool RemoveImages { get; set; }
/// <summary>
/// Gets or sets if title should be removed
/// </summary>
[Boolean(5)]
public bool RemoveTitle { get; set; }
/// <summary>
/// Gets or sets if language should be removed
/// </summary>
[Boolean(6)]
public bool RemoveLanguage { get; set; }
/// <summary>
/// Gets or sets if additional metadata should be removed
/// </summary>
[Boolean(6)]
public bool RemoveAdditionalMetadata { get; set; }
/// <summary>
/// Executes the node
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>the output number to execute next</returns>
public override int Execute(NodeParameters args)
{
if (Video)
Process(Model.VideoStreams);
if (Audio)
Process(Model.AudioStreams);
if (Subtitle)
Process(Model.SubtitleStreams);
if (RemoveAdditionalMetadata)
{
Model.CustomParameters.AddRange(new[] { "-map_metadata", "-1" });
Model.ForceEncode = true;
}
if (RemoveImages)
{
foreach (var video in Model.VideoStreams)
{
if (video.Stream.IsImage)
video.Deleted = true;
}
}
return 1;
}
private void Process<T>(List<T> streams) where T : FfmpegStream
{
if (streams == null)
return;
foreach (var stream in streams)
Process(stream);
}
private void Process(FfmpegStream stream)
{
if (RemoveTitle)
{
stream.Title = string.Empty;
}
if (stream is FfmpegAudioStream audio)
{
if (RemoveLanguage)
audio.Language = string.Empty;
}
else if (stream is FfmpegSubtitleStream sub)
{
if (RemoveLanguage)
sub.Language = string.Empty;
}
}
}
@@ -1,4 +1,6 @@
namespace FileFlows.VideoNodes.FfmpegBuilderNodes
using System.Text;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes
{
public class FfmpegBuilderAutoChapters : FfmpegBuilderNode
{
@@ -25,7 +27,7 @@
return 2;
}
string tempMetaDataFile = AutoChapters.GenerateMetaDataFile(this, args, videoInfo, FFMPEG, this.Percent, this.MinimumLength);
string tempMetaDataFile = GenerateMetaDataFile(this, args, videoInfo, FFMPEG, this.Percent, this.MinimumLength);
if (string.IsNullOrEmpty(tempMetaDataFile))
return 2;
@@ -33,5 +35,78 @@
Model.MetadataParameters.AddRange(new[] { "-map_metadata", (Model.InputFiles.Count - 1).ToString() });
return 1;
}
string GenerateMetaDataFile(EncodingNode node, NodeParameters args, VideoInfo videoInfo, string ffmpegExe, int percent, int minimumLength)
{
string output;
var result = node.Encode(args, ffmpegExe, new List<string>
{
"-hide_banner",
"-i", args.WorkingFile,
"-filter:v", $"select='gt(scene,{percent / 100f})',showinfo",
"-f", "null",
"-"
}, out output, updateWorkingFile: false, dontAddInputFile: true);
if (result == false)
{
args.Logger?.WLog("Failed to detect scenes");
return string.Empty;
}
if (minimumLength < 30)
{
args.Logger?.ILog("Mimium length set to invalid number, defaulting to 60 seconds");
minimumLength = 60;
}
else
{
args.Logger?.ILog($"Minimum length of chapter {minimumLength} seconds");
}
StringBuilder metadata = new StringBuilder();
metadata.AppendLine(";FFMETADATA1");
metadata.AppendLine("");
int chapter = 0;
TimeSpan previous = TimeSpan.Zero;
foreach (Match match in Regex.Matches(output, @"(?<=(pts_time:))[\d]+\.[\d]+"))
{
TimeSpan time = TimeSpan.FromSeconds(double.Parse(match.Value));
if (Math.Abs((time - previous).TotalSeconds) < minimumLength)
continue;
AddChapter(previous, time);
previous = time;
}
var totalTime = TimeSpan.FromSeconds(videoInfo.VideoStreams[0].Duration.TotalSeconds);
if (Math.Abs((totalTime - previous).TotalSeconds) > minimumLength)
AddChapter(previous, totalTime);
if (chapter == 0)
{
args.Logger?.ILog("Failed to detect any scene changes");
return string.Empty;
}
string tempMetaDataFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + ".txt");
File.WriteAllText(tempMetaDataFile, metadata.ToString());
return tempMetaDataFile;
void AddChapter(TimeSpan start, TimeSpan end)
{
metadata.AppendLine("[CHAPTER]");
metadata.AppendLine("TIMEBASE=1/1000");
metadata.AppendLine("START=" + ((int)(start.TotalSeconds * 1000)));
metadata.AppendLine("END=" + ((int)(end.TotalSeconds * 1000)));
metadata.AppendLine("title=Chapter " + (++chapter));
metadata.AppendLine();
}
}
}
}
@@ -1,4 +1,6 @@
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
using System.Text;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
public class FfmpegBuilderComskipChapters : FfmpegBuilderNode
{
@@ -17,7 +19,7 @@ public class FfmpegBuilderComskipChapters : FfmpegBuilderNode
return 2;
}
string tempMetaDataFile = ComskipChapters.GenerateMetaDataFile(args, videoInfo);
string tempMetaDataFile = GenerateMetaDataFile(args, videoInfo);
if (string.IsNullOrEmpty(tempMetaDataFile))
return 2;
@@ -25,4 +27,67 @@ public class FfmpegBuilderComskipChapters : FfmpegBuilderNode
Model.MetadataParameters.AddRange(new[] { "-map_metadata", (Model.InputFiles.Count - 1).ToString() });
return 1;
}
string GenerateMetaDataFile(NodeParameters args, VideoInfo videoInfo)
{
float totalTime = (float)videoInfo.VideoStreams[0].Duration.TotalSeconds;
string edlFile = args.WorkingFile.Substring(0, args.WorkingFile.LastIndexOf(".") + 1) + "edl";
if (File.Exists(edlFile) == false)
edlFile = args.WorkingFile.Substring(0, args.WorkingFile.LastIndexOf(".") + 1) + "edl";
if (File.Exists(edlFile) == false)
{
args.Logger?.ILog("No EDL file found for file");
return string.Empty;
}
string text = File.ReadAllText(edlFile) ?? string.Empty;
float last = 0;
StringBuilder metadata = new StringBuilder();
metadata.AppendLine(";FFMETADATA1");
metadata.AppendLine("");
int chapter = 0;
foreach (string line in text.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries))
{
// 93526.47 93650.13 0
string[] parts = line.Split(new[] { " ", "\t" }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
continue;
float start = 0;
float end = 0;
if (float.TryParse(parts[0], out start) == false || float.TryParse(parts[1], out end) == false)
continue;
if (start < last)
continue;
AddChapter(last, start);
last = end;
}
if (chapter == 0)
{
args.Logger?.ILog("No ads found in edl file");
return string.Empty;
}
AddChapter(last, totalTime);
string tempMetaDataFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + ".txt");
File.WriteAllText(tempMetaDataFile, metadata.ToString());
return tempMetaDataFile;
void AddChapter(float start, float end)
{
metadata.AppendLine("[CHAPTER]");
metadata.AppendLine("TIMEBASE=1/1000");
metadata.AppendLine("START=" + ((int)(start * 1000)));
metadata.AppendLine("END=" + ((int)(end * 1000)));
metadata.AppendLine("title=Chapter " + (++chapter));
metadata.AppendLine();
}
}
}
@@ -6,7 +6,17 @@ public class FfmpegBuilderSubtitleTrackRemover : FfmpegBuilderNode
public override string Icon => "fas fa-comment";
public override int Outputs => 2;
public override int Outputs => 2;
/// <summary>
/// This node is obsolete
/// </summary>
public override bool Obsolete => true;
/// <summary>
/// Gets the obsolete message
/// </summary>
public override string ObsoleteMessage => "This node has been merged into FFMPEG Builder: Track Remover.";
[TextVariable(1)]
@@ -1,4 +1,6 @@
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
using System.Diagnostics;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
{
[NumberInt(1)]
@@ -15,7 +17,7 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
return -1;
string crop = DetectBlackBars.Detect(FFMPEG, videoInfo, args, this.CroppingThreshold);
string crop = Detect(FFMPEG, videoInfo, args, this.CroppingThreshold);
if (string.IsNullOrWhiteSpace(crop))
return 2;
@@ -30,4 +32,106 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
video.Filter.AddRange(new[] { "crop=" + crop });
return 1;
}
public string Detect(string ffmpeg, VideoInfo videoInfo, NodeParameters args, int threshold)
{
int vidWidth = videoInfo.VideoStreams[0].Width;
int vidHeight = videoInfo.VideoStreams[0].Height;
if (vidWidth < 1)
{
args.Logger?.ELog("Failed to find video width");
return string.Empty;
}
if (vidHeight < 1)
{
args.Logger?.ELog("Failed to find video height");
return string.Empty;
}
return Execute(ffmpeg, args.WorkingFile, args, vidWidth, vidHeight, threshold);
}
string Execute(string ffplay, string file, NodeParameters args, int vidWidth, int vidHeight, int threshold)
{
try
{
int x = int.MaxValue;
int y = int.MaxValue;
int width = 0;
int height = 0;
foreach (int ss in new int[] { 60, 120, 240, 360 }) // check at multiple times
{
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 = $" -ss {ss} -i \"{file}\" -hide_banner -vframes 25 -vf cropdetect -f null -";
args.Logger?.DLog("Executing ffmpeg " + process.StartInfo.Arguments);
process.Start();
string output = process.StandardError.ReadToEnd();
Console.WriteLine(output);
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
var dimMatch = Regex.Match(output, @"Stream #[\d]+:[\d]+(.*?)Video:(.*?)([\d]+)x([\d]+)", RegexOptions.Multiline);
if (dimMatch.Success == false)
{
args.Logger?.WLog("Can't find dimensions for video");
continue;
}
var matches = Regex.Matches(output, @"(?<=(crop=))([\d]+:){3}[\d]+");
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 (width == 0 || height == 0)
{
args.Logger?.WLog("Width/Height not detected: " + width + "x" + height);
return String.Empty;
}
if (x == 0 && y == 0)
{
// nothing to do
return String.Empty;
}
if (x == int.MaxValue)
x = 0;
if (y == int.MaxValue)
y = 0;
if (threshold < 0)
threshold = 0;
args.Logger?.DLog($"Video dimensions: {vidWidth}x{vidHeight}");
var willCrop = TestAboveThreshold(vidWidth, vidHeight, width, height, threshold);
args.Logger?.ILog($"Crop detection, x:{x}, y:{y}, width: {width}, height: {height}, total:{willCrop.diff}, threshold:{threshold}, above threshold: {willCrop}");
return willCrop.crop ? $"{width}:{height}:{x}:{y}" : string.Empty;
}
catch (Exception)
{
return string.Empty;
}
}
(bool crop, int diff) TestAboveThreshold(int vidWidth, int vidHeight, int detectedWidth, int detectedHeight, int threshold)
{
int diff = (vidWidth - detectedWidth) + (vidHeight - detectedHeight);
return (diff > threshold, diff);
}
}
@@ -147,6 +147,10 @@ public class FfmpegBuilderVideoEncode:FfmpegBuilderNode
"-preset", "slow",
"-crf", Quality.ToString()
});
bit10Filters = new[]
{
"-pix_fmt:v:{index}", "yuv420p10le", "-profile:v:{index}", "main10"
};
}
private void H26x_Nvidia(FfmpegVideoStream stream, bool h265)
Binary file not shown.
-46
View File
@@ -1,46 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests
{
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class AudioAddTrackTests
{
[TestMethod]
public void AudioAddTrackTests_Mono_First()
{
const string file = @"D:\videos\unprocessed\The Witcher - S02E05 - Turn Your Back.mkv";
var logger = new TestLogger();
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", logger);
var vii = vi.Read(file);
const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
AudioAddTrack node = new();
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => ffmpeg;
args.TempPath = @"D:\videos\temp";
new VideoFile().Execute(args);
node.Bitrate = 128;
node.Channels = 0;
node.Index = 2;
node.Codec = "aac";
int output = node.Execute(args);
string log = logger.ToString();
Assert.AreEqual(1, output);
}
}
}
#endif
-148
View File
@@ -1,148 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests;
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class AudioNormalizationTests:TestBase
{
[TestMethod]
public void AudioNormalization_Test_DoTwoPassMethod()
{
string file = TestFile_BasicMkv;
var vi = new VideoInfoHelper(FfmpegPath, new TestLogger());
var vii = vi.Read(file);
AudioNormalization node = new();
//node.OutputFile = file + ".sup";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
new VideoFile().Execute(args);
string output = AudioNormalization.DoTwoPass(node, args, FfmpegPath, 0);
Assert.IsFalse(string.IsNullOrWhiteSpace(output));
}
[TestMethod]
public void AudioNormalization_Test_TwoPass()
{
string file = TestFile_BasicMkv;
var vi = new VideoInfoHelper(FfmpegPath, new TestLogger());
var vii = vi.Read(file);
AudioNormalization node = new();
node.TwoPass = true;
//node.OutputFile = file + ".sup";
var args = new NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
new VideoFile().Execute(args);
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
[TestMethod]
public void AudioNormalization_Pattern_Test()
{
const string file = @"D:\videos\unprocessed\Masters of the Universe (1987) Bluray-1080p.mkv";
var logger = new TestLogger();
var vi = new VideoInfoHelper(FfmpegPath, logger);
var vii = vi.Read(file);
AudioNormalization node = new();
node.AllAudio = true;
node.Pattern = "";
node.NotMatching = true;
var args = new NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
new VideoFile().Execute(args);
int output = node.Execute(args);
string log = logger.ToString();
Assert.AreEqual(1, output);
}
[TestMethod]
public void AudioNormalization_Pattern_Test3()
{
const string file = @"D:\videos\unprocessed\test_orig.mkv";
var logger = new TestLogger();
var vi = new VideoInfoHelper(FfmpegPath, logger);
var vii = vi.Read(file);
AudioNormalization node = new();
node.AllAudio = true;
node.Pattern = "flac";
node.NotMatching = false;
var args = new NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
new VideoFile().Execute(args);
int output = node.Execute(args);
string log = logger.ToString();
Assert.AreEqual(2, output);
}
[TestMethod]
public void AudioNormalization_Pattern_Test4()
{
const string file = @"D:\videos\unprocessed\test_orig.mkv";
var logger = new TestLogger();
var vi = new VideoInfoHelper(FfmpegPath, logger);
var vii = vi.Read(file);
AudioNormalization node = new();
node.AllAudio = true;
var args = new NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
new VideoFile().Execute(args);
int output = node.Execute(args);
string log = logger.ToString();
Assert.AreEqual(1, output);
}
[TestMethod]
public void AudioNormalization_Test_TwoPass_NegInfinity()
{
string file = TestFile_TwoPassNegInifinity;
var vi = new VideoInfoHelper(FfmpegPath, new TestLogger());
var vii = vi.Read(file);
AudioNormalization node = new();
node.TwoPass = true;
var args = new NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
new VideoFile().Execute(args);
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
}
#endif
@@ -1,40 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests
{
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class AudioTrackRemovalTests
{
[TestMethod]
public void AudoTrackRemoval_Test_01()
{
const string file = @"D:\videos\unprocessed\Masters of the Universe (1987) Bluray-1080p.mkv";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(file);
AudioTrackRemover node = new();
node.Pattern = "commentary";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
new VideoFile().Execute(args);
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
}
}
#endif
-39
View File
@@ -1,39 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests
{
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class AutoChaptersTests
{
[TestMethod]
public void AutoChaptersTests_Test_01()
{
const string file = @"D:\videos\unprocessed\The IT Crowd - 2x04 - The Dinner Party - No English.mkv";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(file);
AutoChapters node = new();
//node.OutputFile = file + ".sup";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
new VideoFile().Execute(args);
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
}
}
#endif
-49
View File
@@ -1,49 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests
{
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class DetectBlackBarsTests
{
[TestMethod]
public void DetectBlackBars_Test_01()
{
const string file = @"D:\videos\Dexter - New Blood - S01E02 - Storm of Fuck.mkv";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(file);
DetectBlackBars node = new();
//node.OutputFile = file + ".sup";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
new VideoFile().Execute(args);
int output = node.Execute(args);
string crop = args.Variables[DetectBlackBars.CROP_KEY] as string;
Assert.IsFalse(string.IsNullOrWhiteSpace(crop));
Assert.AreEqual(1, output);
}
[TestMethod]
public void DetectBlackBars_Test_02()
{
var crop = DetectBlackBars.TestAboveThreshold(1920, 1080, 1920, 1072, 20);
Assert.IsFalse(crop.crop);
}
}
}
#endif
-47
View File
@@ -1,47 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests
{
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class FFMPEGTests
{
[TestMethod]
public void FFMPEG_Variables_Test()
{
const string file = @"D:\videos\unprocessed\Home and Away - 2022-02-23 18 30 00 - Home And Away.ts";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(file);
FFMPEG node = new();
//node.OutputFile = file + ".sup";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
args.Variables.Add("SomeVars", "i am batman");
node.CommandLine = "-i {workingFile} {SomeVars} -o {output}";
node.Extension = ".mkv";
var results = node.GetFFMPEGArgs(args, "file");
Assert.AreEqual("-i", results[0]);
Assert.AreEqual(args.WorkingFile, results[1]);
Assert.AreEqual("i", results[2]);
Assert.AreEqual("am", results[3]);
Assert.AreEqual("batman", results[4]);
Assert.AreEqual("-o", results[5]);
Assert.AreEqual("file", results[6]);
}
}
}
#endif
@@ -1102,6 +1102,47 @@ public class FfmpegBuilder_BasicTests
}
[TestMethod]
public void FfmpegBuilder_ImageStream()
{
const string file = @"D:\videos\testfiles\img_stream.mp4";
var logger = new TestLogger();
const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
var vi = new VideoInfoHelper(ffmpeg, logger);
var vii = vi.Read(file);
var args = new NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => ffmpeg;
args.TempPath = @"D:\videos\temp";
args.Parameters.Add("VideoInfo", vii);
FfmpegBuilderStart ffStart = new();
ffStart.PreExecute(args);
Assert.AreEqual(1, ffStart.Execute(args));
FfmpegBuilderRemuxToMkv ffMkv = new();
ffMkv.PreExecute(args);
Assert.AreEqual(1, ffMkv.Execute(args));
FfmpegBuilderAudioTrackRemover ffRemover = new();
ffRemover.StreamType = "Video";
ffRemover.RemoveIndex = 1;
ffRemover.PreExecute(args);
Assert.AreEqual(1, ffRemover.Execute(args));
FfmpegBuilderAudioTrackRemover ffRemoverSubtitles = new();
ffRemoverSubtitles.StreamType = "Subtitle";
ffRemoverSubtitles.RemoveAll = true;
ffRemoverSubtitles.PreExecute(args);
Assert.AreEqual(1, ffRemoverSubtitles.Execute(args));
FfmpegBuilderExecutor ffExecutor = new();
ffExecutor.PreExecute(args);
int result = ffExecutor.Execute(args);
string log = logger.ToString();
Assert.AreEqual(1, result);
}
}
#endif
@@ -1,44 +1,88 @@
//#if(DEBUG)
#if(DEBUG)
//using FileFlows.VideoNodes.FfmpegBuilderNodes;
//using Microsoft.VisualStudio.TestTools.UnitTesting;
//using VideoNodes.Tests;
using FileFlows.VideoNodes.FfmpegBuilderNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VideoNodes.Tests;
//namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests
//{
// [TestClass]
// public class FfmpegBuilder_MetadataTests
// {
// [TestMethod]
// public void FfmpegBuilder_MetadataJson()
// {
// const string file = @"D:\videos\unprocessed\basic.mkv";
// var logger = new TestLogger();
// const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
// var vi = new VideoInfoHelper(ffmpeg, logger);
// var vii = vi.Read(file);
// var args = new NodeParameters(file, logger, false, string.Empty);
// VideoMetadata md = System.Text.Json.JsonSerializer.Deserialize<VideoMetadata>(File.ReadAllText(@"D:\videos\metadata.json"));
// args.Variables.Add("VideoMetadata", md);
// args.GetToolPathActual = (string tool) => ffmpeg;
// args.TempPath = @"D:\videos\temp";
// args.Parameters.Add("VideoInfo", vii);
namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests;
[TestClass]
public class FfmpegBuilder_MetadataTests: TestBase
{
//[TestMethod]
//public void FfmpegBuilder_MetadataJson()
//{
// const string file = @"D:\videos\unprocessed\basic.mkv";
// var logger = new TestLogger();
// const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
// var vi = new VideoInfoHelper(ffmpeg, logger);
// var vii = vi.Read(file);
// var args = new NodeParameters(file, logger, false, string.Empty);
// VideoMetadata md = System.Text.Json.JsonSerializer.Deserialize<VideoMetadata>(File.ReadAllText(@"D:\videos\metadata.json"));
// args.Variables.Add("VideoMetadata", md);
// args.GetToolPathActual = (string tool) => ffmpeg;
// args.TempPath = @"D:\videos\temp";
// args.Parameters.Add("VideoInfo", vii);
// FfmpegBuilderStart ffStart = new ();
// Assert.AreEqual(1, ffStart.Execute(args));
// FfmpegBuilderStart ffStart = new ();
// Assert.AreEqual(1, ffStart.Execute(args));
// FfmpegBuilderVideoMetadata ffMetadata = new();
// Assert.AreEqual(1, ffMetadata.Execute(args));
// FfmpegBuilderVideoMetadata ffMetadata = new();
// Assert.AreEqual(1, ffMetadata.Execute(args));
// FfmpegBuilderExecutor ffExecutor = new();
// int result = ffExecutor.Execute(args);
// FfmpegBuilderExecutor ffExecutor = new();
// int result = ffExecutor.Execute(args);
// string log = logger.ToString();
// Assert.AreEqual(1, result);
// }
// }
//}
// string log = logger.ToString();
// Assert.AreEqual(1, result);
//}
//#endif
[TestMethod]
public void FfmpegBuilder_Metadata_Remover()
{
string file = TestFile_MovText_Mp4;
var logger = new TestLogger();
const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
var vi = new VideoInfoHelper(ffmpeg, logger);
var vii = vi.Read(file);
var args = new NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => ffmpeg;
args.TempPath = @"D:\videos\temp";
args.Parameters.Add("VideoInfo", vii);
FfmpegBuilderStart ffStart = new ();
ffStart.PreExecute(args);
Assert.AreEqual(1, ffStart.Execute(args));
FfmpegBuilderAudioTrackRemover ffRemover = new();
ffRemover.StreamType = "subtitle";
ffRemover.RemoveAll = true;
ffRemover.PreExecute(args);
Assert.AreEqual(1, ffRemover.Execute(args));
FfmpegBuilderMetadataRemover ffMetadata = new();
ffMetadata.RemoveImages = true;
ffMetadata.RemoveTitle = true;
ffMetadata.RemoveLanguage = true;
ffMetadata.Video = true;
ffMetadata.Audio= true;
ffMetadata.Subtitle = true;
ffMetadata.RemoveAdditionalMetadata = true;
ffMetadata.PreExecute(args);
Assert.AreEqual(1, ffMetadata.Execute(args));
FfmpegBuilderExecutor ffExecutor = new();
ffExecutor.PreExecute(args);
int result = ffExecutor.Execute(args);
string log = logger.ToString();
Assert.AreEqual(1, result);
}
}
#endif
@@ -1,41 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests
{
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class SubtitleLanguageRemoverTests
{
[TestMethod]
public void SubtitleLanguageRemover_Test_01()
{
const string file = @"D:\videos\unprocessed\Injustice.mkv";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(file);
SubtitleLanguageRemover node = new();
node.Pattern = "eng";
node.NotMatching = true;
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
new VideoFile().Execute(args);
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
}
}
#endif
-39
View File
@@ -1,39 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests
{
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class SubtitleRemoverTests
{
[TestMethod]
public void SubtitleRemover_Test_01()
{
const string file = @"D:\videos\unprocessed\Home and Away - 2022-02-23 18 30 00 - Home And Away.ts";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(file);
SubtitleRemover node = new();
//node.OutputFile = file + ".sup";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
new VideoFile().Execute(args);
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
}
}
#endif
-58
View File
@@ -1,58 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests
{
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class VideoEncodeTests
{
[TestMethod]
public void VideoEncode_EAC3_Test()
{
const string file = @"D:\videos\problemfile\sample fileflows.mkv";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(file);
VideoEncode node = new();
//node.OutputFile = file + ".sup";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
node.VideoCodec = "h265";
node.AudioCodec = "aac";
node.Language = "DE";
new VideoFile().Execute(args);
TestVideoInfo(args, "h264", "eac3");
int output = node.Execute(args);
Assert.AreEqual(1, output);
TestVideoInfo(args, "hevc", "aac");
}
private void TestVideoInfo(FileFlows.Plugin.NodeParameters parameters, string videoCodec, string audioCodec)
{
Assert.AreEqual(videoCodec, parameters.Variables["vi.Video.Codec"]);
Assert.AreEqual(audioCodec, parameters.Variables["vi.Audio.Codec"]);
var videoInfo = parameters.Variables["vi.VideoInfo"] as VideoInfo;
Assert.AreEqual(videoCodec, videoInfo.VideoStreams[0].Codec);
Assert.AreEqual(audioCodec, videoInfo.AudioStreams[0].Codec);
}
}
}
#endif
-300
View File
@@ -22,285 +22,6 @@ namespace VideoNodes.Tests
}
[TestMethod]
public void VideoInfoTest_SubtitleRemover()
{
const string file = @"D:\videos\unprocessed\Bourne.mkv";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(@"D:\videos\unprocessed\Masters of the Universe (1987) Bluray-1080p.mkv.skip");
SubtitleRemover remover = new SubtitleRemover();
remover.SubtitlesToRemove = new List<string>
{
"subrip", "srt"
};
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
new VideoFile().Execute(args);
int output = remover.Execute(args);
Assert.AreEqual(1, output);
}
[TestMethod]
public void VideoInfoTest_DetectBlackBars()
{
//const string file = @"D:\videos\unprocessed\The Witcher - S02E05 - Turn Your Back.mkv";
//const string file = @"D:\videos\unprocessed\Hawkeye (2021) - S01E05 - Ronin.mkv";
const string file = @"\\ORACLE\tv\Dexter - New Blood\Season 1\Dexter - New Blood - S01E07 - Skin of Her Teeth.mkv";
//var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger(), false, string.Empty);
//vi.Read(@"D:\videos\unprocessed\Bourne.mkv");
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
int result = new DetectBlackBars().Execute(args);
Assert.IsTrue(result > 0);
}
[TestMethod]
public void VideoInfoTest_NvidiaCard()
{
const string file = @"D:\videos\unprocessed\Bourne.mkv";
const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
//args.Process = new FileFlows.Plugin.ProcessHelper(args.Logger);
var node = new VideoEncode();
node.SetArgs(args);
bool result = node.HasNvidiaCard(ffmpeg);
Assert.IsTrue(result);
}
//[TestMethod]
//public void VideoInfoTest_CanEncodeNvidia()
//{
// const string file = @"D:\videos\unprocessed\Bourne.mkv";
// const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
// var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
// //args.Process = new FileFlows.Plugin.ProcessHelper(args.Logger);
// var node = new VideoEncode();
// node.SetArgs(args);
// bool result = node.CanProcessEncoder(ffmpeg, "hevc_nvenc -preset hq");
// Assert.IsTrue(result);
//}
//[TestMethod]
//public void VideoInfoTest_CanEncodeIntel()
//{
// const string file = @"D:\videos\unprocessed\Bourne.mkv";
// const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
// var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
// //args.Process = new FileFlows.Plugin.ProcessHelper(args.Logger);
// var node = new VideoEncode();
// node.SetArgs(args);
// bool result = node.CanProcessEncoder(ffmpeg, "h264_qsv");
// Assert.IsTrue(result);
//}
[TestMethod]
public void VideoInfoTest_AudioTrackReorder()
{
var node = new AudioTrackReorder();
var original = new List<AudioStream>
{
new AudioStream{ Codec = "aac", Language = "fre"},
new AudioStream{ Codec = "dts", Language = "fre"},
new AudioStream{ Codec = "aac", Language = "eng"},
new AudioStream{ Codec = "aac", Language = "mao"},
new AudioStream{ Codec = "dts", Language = "mao"},
new AudioStream{ Codec = "ac3", Language = "mao"},
new AudioStream{ Codec = "ac3", Language = "eng"},
new AudioStream{ Codec = "ac3", Language = "fre"},
};
node.Languages = new List<string> { "eng" };
node.OrderedTracks = new List<string> { "ac3", "aac" };
var reordered = node.Reorder(original);
Assert.AreEqual("ac3", reordered[0].Codec);
Assert.AreEqual("eng", reordered[0].Language);
Assert.AreEqual("aac", reordered[1].Codec);
Assert.AreEqual("eng", reordered[1].Language);
Assert.AreEqual("ac3", reordered[2].Codec);
Assert.AreEqual("mao", reordered[2].Language);
Assert.AreEqual("ac3", reordered[3].Codec);
Assert.AreEqual("fre", reordered[3].Language);
Assert.AreEqual("aac", reordered[4].Codec);
Assert.AreEqual("fre", reordered[4].Language);
Assert.AreEqual("aac", reordered[5].Codec);
Assert.AreEqual("mao", reordered[5].Language);
Assert.AreEqual("dts", reordered[6].Codec);
Assert.AreEqual("fre", reordered[6].Language);
Assert.AreEqual("dts", reordered[7].Codec);
Assert.AreEqual("mao", reordered[7].Language);
}
[TestMethod]
public void VideoInfoTest_AudioTrackReorder_Channels()
{
var node = new AudioTrackReorder();
var original = new List<AudioStream>
{
new AudioStream{ Codec = "aac", Language = "fre", Channels = 5.1f},
new AudioStream{ Codec = "dts", Language = "fre", Channels = 2},
new AudioStream{ Codec = "aac", Language = "eng", Channels = 2.1f},
new AudioStream{ Codec = "aac", Language = "mao", Channels = 8},
new AudioStream{ Codec = "dts", Language = "mao" , Channels=7.1f} ,
new AudioStream{ Codec = "ac3", Language = "mao", Channels = 6.2f},
new AudioStream{ Codec = "ac3", Language = "eng", Channels = 5.1f},
new AudioStream{ Codec = "ac3", Language = "fre", Channels = 8},
};
node.Channels = new List<string> { "8", "5.1", "7.1", "6.2" };
var reordered = node.Reorder(original);
int count = 0;
foreach (var chan in new[] { ("aac", "mao", 8f), ("ac3", "fre", 8), ("aac", "fre", 5.1f), ("ac3", "eng", 5.1f), ("dts", "mao", 7.1f),
("ac3", "mao", 6.2f), ("dts", "fre", 2), ("aac", "eng", 2.1f) })
{
Assert.AreEqual(chan.Item1, reordered[count].Codec);
Assert.AreEqual(chan.Item2, reordered[count].Language);
Assert.AreEqual(chan.Item3, reordered[count].Channels);
++count;
}
}
[TestMethod]
public void VideoInfoTest_AudioTrackReorder_NothingConfigured()
{
var node = new AudioTrackReorder();
var original = new List<AudioStream>
{
new AudioStream{ Codec = "aac", Language = "fre"},
new AudioStream{ Codec = "dts", Language = "fre"},
new AudioStream{ Codec = "aac", Language = "eng"},
new AudioStream{ Codec = "aac", Language = "mao"},
new AudioStream{ Codec = "dts", Language = "mao"},
new AudioStream{ Codec = "ac3", Language = "mao"},
new AudioStream{ Codec = "ac3", Language = "eng"},
new AudioStream{ Codec = "ac3", Language = "fre"},
};
node.Languages = null;
node.OrderedTracks = new List<string>();
var reordered = node.Reorder(original);
for(int i = 0; i < original.Count; i++)
{
Assert.AreEqual(original[i].Codec, reordered[i].Codec);
Assert.AreEqual(original[i].Language, reordered[i].Language);
}
}
[TestMethod]
public void VideoInfoTest_AudioTrackReorder_NoLanguage()
{
var node = new AudioTrackReorder();
var original = new List<AudioStream>
{
new AudioStream{ Codec = "aac", Language = "fre"},
new AudioStream{ Codec = "dts", Language = "fre"},
new AudioStream{ Codec = "aac", Language = "eng"},
new AudioStream{ Codec = "aac", Language = "mao"},
new AudioStream{ Codec = "dts", Language = "mao"},
new AudioStream{ Codec = "ac3", Language = "mao"},
new AudioStream{ Codec = "ac3", Language = "eng"},
new AudioStream{ Codec = "ac3", Language = "fre"},
};
node.OrderedTracks = new List<string> { "ac3", "aac" };
var reordered = node.Reorder(original);
Assert.AreEqual("ac3", reordered[0].Codec);
Assert.AreEqual("mao", reordered[0].Language);
Assert.AreEqual("ac3", reordered[1].Codec);
Assert.AreEqual("eng", reordered[1].Language);
Assert.AreEqual("ac3", reordered[2].Codec);
Assert.AreEqual("fre", reordered[2].Language);
Assert.AreEqual("aac", reordered[3].Codec);
Assert.AreEqual("fre", reordered[3].Language);
Assert.AreEqual("aac", reordered[4].Codec);
Assert.AreEqual("eng", reordered[4].Language);
Assert.AreEqual("aac", reordered[5].Codec);
Assert.AreEqual("mao", reordered[5].Language);
Assert.AreEqual("dts", reordered[6].Codec);
Assert.AreEqual("fre", reordered[6].Language);
Assert.AreEqual("dts", reordered[7].Codec);
Assert.AreEqual("mao", reordered[7].Language);
}
[TestMethod]
public void VideoInfoTest_AudioTrackReorder_NoCodec()
{
var node = new AudioTrackReorder();
var original = new List<AudioStream>
{
new AudioStream{ Codec = "aac", Language = "fre"},
new AudioStream{ Codec = "dts", Language = "fre"},
new AudioStream{ Codec = "aac", Language = "eng"},
new AudioStream{ Codec = "aac", Language = "mao"},
new AudioStream{ Codec = "dts", Language = "mao"},
new AudioStream{ Codec = "ac3", Language = "mao"},
new AudioStream{ Codec = "ac3", Language = "eng"},
new AudioStream{ Codec = "ac3", Language = "fre"},
};
node.Languages = new List<string> { "eng" };
var reordered = node.Reorder(original);
Assert.AreEqual("aac", reordered[0].Codec);
Assert.AreEqual("eng", reordered[0].Language);
Assert.AreEqual("ac3", reordered[1].Codec);
Assert.AreEqual("eng", reordered[1].Language);
Assert.AreEqual("aac", reordered[2].Codec);
Assert.AreEqual("fre", reordered[2].Language);
Assert.AreEqual("dts", reordered[3].Codec);
Assert.AreEqual("fre", reordered[3].Language);
Assert.AreEqual("aac", reordered[4].Codec);
Assert.AreEqual("mao", reordered[4].Language);
Assert.AreEqual("dts", reordered[5].Codec);
Assert.AreEqual("mao", reordered[5].Language);
Assert.AreEqual("ac3", reordered[6].Codec);
Assert.AreEqual("mao", reordered[6].Language);
Assert.AreEqual("ac3", reordered[7].Codec);
Assert.AreEqual("fre", reordered[7].Language);
}
[TestMethod]
public void ComskipTest()
{
@@ -322,27 +43,6 @@ namespace VideoNodes.Tests
Assert.AreEqual(1, output);
}
[TestMethod]
public void Comskip_Chapters()
{
const string file = @"D:\videos\recordings\Rescue My Renovation (2001).ts";
const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
var logger = new TestLogger();
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(file);
args.SetParameter("VideoInfo", vii);
//args.Process = new FileFlows.Plugin.ProcessHelper(args.Logger);
var node = new ComskipChapters();
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
-168
View File
@@ -1,168 +0,0 @@
#if(DEBUG)
namespace VideoNodes.Tests
{
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class VideoScalerTests
{
[TestMethod]
public void VideoScaler_Resolution_Tests()
{
const string file = @"D:\videos\problemfile\sample fileflows.mkv";
VideoScaler node = new();
foreach (var test in new[]
{
// 480p
(600, 640, 2),
(640, 640, 2),
(700, 640, 2),
(599, 640, -1),
(701, 640, -1),
// 720p
(1280, 1280, 2),
(1220, 1280, 2),
(1340, 1280, 2),
(1219, 1280, -1),
(1341, 1280, -1),
// 1080p
(1860, 1920, 2),
(1920, 1920, 2),
(1980, 1920, 2),
(1859, 1920, -1),
(1981, 1920, -1),
// 4k
(3780, 3840, 2),
(3840, 3840, 2),
(3900, 3840, 2),
(3779, 3840, -1),
(3901, 3840, -1),
})
{
node.Resolution = test.Item2 + ":-2";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.Parameters.Add("VideoInfo", new VideoInfo
{
VideoStreams = new List<VideoStream>
{
new VideoStream
{
Width = test.Item1
}
}
});
int output = node.Execute(args);
Assert.AreEqual(test.Item3, output);
}
}
[TestMethod]
public void VideoScaler_Force_Tests()
{
const string file = @"D:\videos\problemfile\sample fileflows.mkv";
VideoScaler node = new();
foreach (var test in new[]
{
// 480p
(600, 640, 2),
(640, 640, 2),
(700, 640, 2),
(599, 640, -1),
(701, 640, -1),
// 720p
(1280, 1280, 2),
(1220, 1280, 2),
(1340, 1280, 2),
(1219, 1280, -1),
(1341, 1280, -1),
// 1080p
(1860, 1920, 2),
(1920, 1920, 2),
(1980, 1920, 2),
(1859, 1920, -1),
(1981, 1920, -1),
// 4k
(3780, 3840, 2),
(3840, 3840, 2),
(3900, 3840, 2),
(3779, 3840, -1),
(3901, 3840, -1),
})
{
node.Resolution = test.Item2 + ":-2";
node.Force = true;
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.Parameters.Add("VideoInfo", new VideoInfo
{
VideoStreams = new List<VideoStream>
{
new VideoStream
{
Width = test.Item1
}
}
});
int output = node.Execute(args);
Assert.AreEqual(-1, output);
}
}
[TestMethod]
public void VideoScaler_VideoInfoUpdated_Test()
{
const string file = @"D:\videos\problemfile\sample fileflows.mkv";
var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger());
var vii = vi.Read(file);
VideoScaler node = new();
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\videos\temp";
node.VideoCodec = "h265";
new VideoFile().Execute(args);
TestVideoInfo(args, "h264", 1280, 720, "720p");
node.Resolution = "1920:-2";
int output = node.Execute(args);
Assert.AreEqual(1, output);
TestVideoInfo(args, "hevc", 1920, 1080, "1080p");
}
private void TestVideoInfo(FileFlows.Plugin.NodeParameters parameters, string videoCodec, int width, int height, string resolution)
{
Assert.AreEqual(videoCodec, parameters.Variables["vi.Video.Codec"]);
Assert.AreEqual(resolution, parameters.Variables["vi.Resolution"]);
var videoInfo = parameters.Variables["vi.VideoInfo"] as VideoInfo;
Assert.AreEqual(videoCodec, videoInfo.VideoStreams[0].Codec);
}
}
}
#endif
+8
View File
@@ -39,6 +39,14 @@ namespace FileFlows.VideoNodes
/// </summary>
public string Codec { get; set; } = "";
/// <summary>
/// If this stream is an image
/// </summary>
public bool IsImage { get; set; }
/// <summary>
/// Gets or sets the index string of this track
/// </summary>
public string IndexString { get; set; }
/// <summary>
+2
View File
@@ -220,6 +220,8 @@ namespace FileFlows.VideoNodes
// 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.IsImage = info.Contains("(attached pic)");
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))
Binary file not shown.
+36 -233
View File
@@ -7,106 +7,6 @@
"7": { "1": "7.1" },
"Flow": {
"Parts": {
"AudioAddTrack": {
"Outputs": {
"1": "Audio track added and saved to temporary file"
},
"Description": "Adds a new audio track to the video file, all other audio tracks will remain. This will use the first audio track of the file as the source audio track to convert.",
"Fields": {
"Index": "Index",
"Index-Help": "The index where to insert the new audio track. 0 based, so to insert the new audio track as the first track set this to 0.",
"Channels": "Channels",
"Channels-Help": "The number of channels to convert this audio track to.",
"Bitrate": "Bitrate",
"Bitrate-Help": "Bitrate of the new audio track"
}
},
"AudioAdjustVolume": {
"Outputs": {
"1": "Audio tracks volume was adjusted and saved to temporary file",
"2": "Audio tracks were not adjusted"
},
"Description": "Adjusts audio tracks volume in a video file using FFMPEG",
"Fields": {
"VolumePercent": "Volume Percent",
"VolumePercent-Help": "The percent of the adjusted volume.\n100 means no adjustment\n50 means half volume\n0 means muted"
}
},
"AudioNormalization": {
"Outputs": {
"1": "Audio tracks were normalized and saved to temporary file",
"2": "No audio tracks were found to be normalized"
},
"Description": "Normalizes all audio tracks in a video file using FFMPEGs loudnorm filter",
"Fields": {
"AllAudio": "All Audio Tracks",
"AllAudio-Help": "If all audio tracks should be normalized or if just the first track should be",
"TwoPass": "Two Pass",
"TwoPass-Help": "If the audio tracks should use two pass normalization. This improves the normalization but increases the processing time.",
"Pattern": "Pattern",
"Pattern-Help": "An optional regular expression to filter out audio tracks to normalize. Will match against the title, codec and language",
"NotMatching": "Not Matching",
"NotMatching-Help": "If the pattern should be used to exclude audio tracks from normalization, otherwise if the audio track matches they will be normalized."
}
},
"AudioTrackRemover": {
"Outputs": {
"1": "Audio tracks were removed",
"2": "Audio tracks were NOT removed"
},
"Description": "Allows you to remove audio tracks based on either their title or their language codes.\n\nAny title (or language code if set to \"Use Language Code\") that is blank will NOT be removed regardless of the pattern.",
"Fields": {
"Pattern": "Pattern",
"Pattern-Help": "A regular expression to match against, eg \"commentary\" to remove commentary tracks",
"NotMatching": "Not Matching",
"NotMatching-Help": "If audio tracks NOT matching the pattern should be removed",
"UseLanguageCode": "Use Language Code",
"UseLanguageCode-Help": "If the language code of the audio track should be used instead of the title"
}
},
"AudioTrackReorder": {
"Outputs": {
"1": "Audio tracks re-ordered in new temporary file",
"2": "Audio tracks NOT re-ordered"
},
"Description": "Allows you to reorder audio tracks in the preferred order.\n\nEnter the languages/audio codecs/channels in the order you want. Any not listed will be ordered after the ones entered in their original order.\nIf there are multiple tracks with same language/codec/channels, they will be ordered first by the order you entered, then in their original order.\n\nOutput 1: Tracks were reordered\nOutput 2: Tracks did not need reordering",
"Fields": {
"OrderedTracks": "Ordered Audio Codecs",
"OrderedTracks-Help": "The order of audio codecs to the audio tracks by. This is done after the languages (if any)",
"Languages": "Languages",
"Languages-Help": "The order of languages to sort the audio tracks by. This sorting is done before the codec.",
"Channels": "Channels",
"Channels-Help": "The order of audio channels to sort the audio tracks by. This sorting is done before languages.\nFor example \"5.1\",\"7.1\",\"6.2\",\"2\""
}
},
"AudioTrackSetLanguage": {
"Label": "Audio Set Language",
"Outputs": {
"1": "Audio tracks updated to new temporary file",
"2": "Audio tracks NOT updated"
},
"Description": "Allows you to set the language for any audio tracks that have no language set. If the audio track does have a language set, it will be skipped.\n\nOutput 1: Audio Tracks were updated\nOutput 2: No audio tracks were needing to be updated",
"Fields": {
"Language": "Language",
"Language-Help": "The ISO 639-2 language code to use. \nhttps://en.wikipedia.org/wiki/List_of_ISO_639-2_codes"
}
},
"AutoChapters": {
"Description": "Automatically detect scene changes in the video to generate chapters.",
"Outputs": {
"1": "Chapters generated and saved to temporary file",
"2": "No chapters detected or video already had chapters"
},
"Fields": {
"MinimumLength": "Minimum Length",
"MinimumLength-Suffix": "seconds",
"MinimumLength-Help": "The minimum length of a chapter in seconds",
"Percent": "Percent",
"Percent-Suffix": "%",
"Percent-Help": "The threshold percentage value to use for scene detection changes. A good value is 45%"
}
},
"CanUseHardwareEncoding": {
"Description": "Checks if the specified hardware encoder is currently available to the Flow.",
"Fields": {
@@ -114,13 +14,6 @@
"Encoder-Help": "The hardware decoder to check"
}
},
"ComskipChapters": {
"Description": "Uses a comskip EDL file and will create chapters given that EDL comskip file.",
"Outputs": {
"1": "Commercials chapters created, saved to temporary file",
"2": "No commercials detected"
}
},
"ComskipRemoveAds": {
"Description": "Uses a comskip EDL file and will remove commercials using that file.",
"Outputs": {
@@ -139,29 +32,6 @@
"ProbeSize-Help": "The probe size to use in FFMPEG when executing."
}
},
"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",
"Outputs": {
"1": "Black bars detected",
"2": "No black bars detected"
},
"Fields": {
"CroppingThreshold": "Threshold",
"CroppingThreshold-Help": "The amount of pixels that must be greater than to crop. E.g. if there's only 5 pixels detected as black space, you may consider this too small to crop."
}
},
"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",
"Outputs": {
"1": "Video processed"
},
"Fields": {
"Extension": "Extension",
"Extension-Help": "The file extension to use on the newly created file",
"CommandLine": "Command Line",
"CommandLine-Help": "The command line to run with FFMPEG.\n'{WorkingFile}': the working file of the flow\n'{Output}': The output file that will be passed as the last parameter to FFMPEG including the extension defined above."
}
},
"FfmpegBuilderStart": {
"Label": "FFMPEG Builder: Start",
"Outputs": {
@@ -254,23 +124,25 @@
}
},
"FfmpegBuilderAudioTrackRemover": {
"Label": "FFMPEG Builder: Audio Track Remover",
"Label": "FFMPEG Builder: Track Remover",
"Outputs": {
"1": "Audio tracks set to remove",
"2": "Audio tracks NOT set to removed"
"1": "Tracks set to remove",
"2": "Tracks NOT set to removed"
},
"Description": "Allows you to remove audio tracks based on either their title or their language codes.\n\nAny title (or language code if set to \"Use Language Code\") that is blank will NOT be removed regardless of the pattern.",
"Description": "Allows you to remove tracks based on either their title or their language codes.\n\nAny title (or language code if set to \"Use Language Code\") that is blank will NOT be removed regardless of the pattern.",
"Fields": {
"StreamType": "Type",
"StreamType-Help": "The type of tracks to reorder in the FFMPEG Builder",
"RemoveAll": "Remove All",
"RemoveAll-Help": "Remove all current tracks from the output file. You can use Add Audio Track afterwards to add tracks of specific codecs",
"RemoveAll-Help": "Remove all current tracks from the output file. You can use Add Track afterwards to add tracks of specific codecs",
"Pattern": "Pattern",
"Pattern-Help": "A regular expression to match against, eg \"commentary\" to remove commentary tracks",
"NotMatching": "Not Matching",
"NotMatching-Help": "If audio tracks NOT matching the pattern should be removed",
"UseLanguageCode": "Use Language Code",
"UseLanguageCode-Help": "If the language code of the audio track should be used instead of the title",
"UseLanguageCode-Help": "If the language code of the track should be used instead of the title",
"RemoveIndex": "Remove Index",
"RemoveIndex-Help": "The start index where to remove the audio tracks from. This allows you to remove all, or all matching, audio streams after the starting index.\nSet to zero to remove all matching the parameters.\nSet to 1 to keep the first audio track and remove any after the first matching the parameters"
"RemoveIndex-Help": "The start index where to remove the tracks from. This allows you to remove all, or all matching, tracks after the starting index.\nSet to zero to remove all matching the parameters.\nSet to 1 to keep the first track and remove any after the first matching the parameters"
}
},
"FfmpegBuilderAudioTrackReorder": {
@@ -350,6 +222,29 @@
"2": "No HDR stream found"
}
},
"FfmpegBuilderMetadataRemover": {
"Label": "FFMPEG Builder: Metadata Remover",
"Description": "Removes metadata from the FFMPEG Builder so when the file is processed the selected metadata will be removed.\n\nNote: Only the metadata when this node is effected, if metadata is added after this node runs, that will not be effected.",
"Outputs": {
"1": "Metadata removed from FFMPEG Builder"
},
"Fields": {
"Video": "Video",
"Video-Help": "If video tracks should have this metadata removed",
"Audio": "Audio",
"Audio-Help": "If audio tracks should have this metadata removed",
"Subtitle": "Subtitle",
"Subtitle-Help": "If subtitle tracks should have this metadata removed",
"RemoveImages": "Remove Images",
"RemoveImages-Help": "If any images found in the metadata should be removed",
"RemoveTitle": "Remove Title",
"RemoveTitle-Help": "If the title should be removed from the tracks",
"RemoveLanguage": "Remove Language",
"RemoveLanguage-Help": "If the language should be removed from the tracks",
"RemoveAdditionalMetadata": "Remove Additional Metadata",
"RemoveAdditionalMetadata-Help": "If additional metadata should be removed. Additional metadata is non-standard metadata that may have been added to a video file, eg by iTunes."
}
},
"FfmpegBuilderSubtitleFormatRemover": {
"Label": "FFMPEG Builder: Subtitle Format Remover",
"Description": "Removes subtitles from a video file if found.",
@@ -496,17 +391,6 @@
"Tag-Help": "The tag to add to the video file"
}
},
"RemuxToMKV": {
"Descritption": "Remuxes a video file into a MKV container. All streams will be copied to the new container",
"Outputs": {
"1": "File remuxed to temporary file",
"2": "File was already in a MKV container"
},
"Fields": {
"Force": "Force",
"Force-Help": "If the file should be always remuxed into a MKV container even when it already is in a MKV container.\ni.e. a new temporary file will always be created."
}
},
"ReadVideoInfo": {
"Descritption": "Reads the video information from the current working file and updates the vi variables.",
"Outputs": {
@@ -514,29 +398,6 @@
"2": "File was not a video file or failed to be read"
}
},
"RemuxToMP4": {
"Descritption": "Remuxes a video file into a MP4 container. All streams will be copied to the new container",
"Outputs": {
"1": "File remuxed to temporary file",
"2": "File was already in a MP4 container"
},
"Fields": {
"Force": "Force",
"Force-Help": "If the file should be always remuxed into a MP4 container even when it already is in a MP4 container.\ni.e. a new temporary file will always be created."
}
},
"SubtitleRemover": {
"Description": "Removes subtitles from a video file if found.\n\nOutput 1: Subtitles were removed\nOutput 2: No subtitles found that needed to be removed",
"Outputs": {
"1": "Subtitles removed in new temporary file",
"2": "No subtitles to remove"
},
"Fields": {
"SubtitlesToRemove": "Subtitles To Remove",
"RemoveAll": "Remove All",
"RemoveAll-Help": "When checked, all subtitles will be removed from the file, otherwise only those selected below will be"
}
},
"SubtitleExtractor": {
"Description": "Extract a single subtitle tracks and saves it to the destination. Can extract srt, ass, and ssa format.\nThis will NOT update the working file, and will keep the working file the same as the input file.\n\nOutput 1: Subtitles were extracted\nOutput 2: No subtitles found to extract",
"Outputs": {
@@ -552,52 +413,12 @@
"SetWorkingFile-Help": "When this is checked, if a subtitle is extracted, the working file will be changed to this extracted subtitle. The original working file will NOT be deleted."
}
},
"SubtitleLanguageRemover": {
"Outputs": {
"1": "Subtitles were removed",
"2": "Subtitles were NOT removed"
},
"Description": "Allows you to remove subtitles based on either their title or their language codes.\n\nAny language (or title if set to \"Use Title\") that is blank will NOT be removed regardless of the pattern.",
"Fields": {
"Pattern": "Pattern",
"Pattern-Help": "A regular expression to match against, eg \"eng\" to remove English tracks",
"NotMatching": "Not Matching",
"NotMatching-Help": "If subtitles NOT matching the pattern should be removed",
"UseTitle": "Use Title",
"UseTitle-Help": "If the title of the subtitle should be used for matching instead of the language"
}
},
"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",
"Outputs": {
"1": "Video re-encoded to temporary file",
"2": "Video not re-encoded"
},
"Fields": {
"Extension": "Extension",
"Extension-Help": "The file extension to use on the newly created file.",
"VideoCodec": "Video Codec",
"VideoCodec-Help": "The video codec the video should be in, for example hevc, h264.\nIf left empty all original video tracks will be copied.",
"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.\nIf left empty all original audio tracks will be copied.",
"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"
}
},
"VideoHasStream": {
"Description": "Tests if a video file contains a stream",
"Title": "Video Has Track",
"Description": "Tests if a video file contains a track",
"Outputs": {
"1": "Contains the matching stream",
"2": "Does not contain the matching stream"
"1": "Contains the matching track",
"2": "Does not contain the matching track"
},
"Fields": {
"Stream": "Type",
@@ -611,24 +432,6 @@
"Channels": "Channels",
"Channels-Help": "The number of channels to test for. Set to 0 to ignore this check"
}
},
"VideoScaler": {
"Description": "This allows you to scale a video to the specified dimensions. It will retain the aspect ratio of the video so if the video was 1920x1000 it would scale to 1280x668 if you select 720P.",
"Outputs": {
"1": "Video rescaled to temporary file",
"2": "Video was already in/near the scaled resolution"
},
"Fields": {
"VideoCodec": "Video Codec",
"Language-Help": "The video codec to encode the scaled video in",
"Extension": "Extension",
"Extension-Help": "The file extension to use on the newly created file",
"Force": "Force",
"Force-Help": "When checked the video will be force scaled even if the working file is already in this resolution (or near this resolution).",
"Resolution": "Resolution",
"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."
}
}
}
}
+2
View File
@@ -7,6 +7,8 @@ namespace FileFlows.VideoNodes
public override string Icon => "fas fa-video";
public override int Outputs => 2;
public override FlowElementType Type => FlowElementType.Logic;
private Dictionary<string, object> _Variables;
public override Dictionary<string, object> Variables => _Variables;
public ReadVideoInfo()
+22 -3
View File
@@ -1,6 +1,7 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using System.Text.Json;
public abstract class VideoNode : Node
{
@@ -121,13 +122,31 @@ namespace FileFlows.VideoNodes
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)
if (args.Parameters[VIDEO_INFO] == null)
{
args.Logger.WLog("VideoInfo not found for file");
return null;
}
return result;
var result = args.Parameters[VIDEO_INFO] as VideoInfo;
if (result != null)
return result;
// may be from non Legacy VideoNodes
try
{
string json = JsonSerializer.Serialize(args.Parameters[VIDEO_INFO]);
var vi = JsonSerializer.Deserialize<VideoInfo>(json);
if (vi == null)
throw new Exception("Failed to deserailize object");
return vi;
}
catch (Exception ex)
{
args.Logger.WLog("VideoInfo could not be deserialized: " + ex.Message);
return null;
}
}