From 9072d32905b7dc302001baf00e9e74d75c85ddf8 Mon Sep 17 00:00:00 2001 From: John Andrews Date: Tue, 14 Jun 2022 20:21:44 +1200 Subject: [PATCH] moving obsolete nodes out of video nodes into legacy plugin --- Apprise/Apprise.csproj | Bin 2760 -> 2760 bytes BasicNodes/BasicNodes.csproj | Bin 3566 -> 3566 bytes ChecksumNodes/ChecksumNodes.csproj | Bin 2872 -> 2872 bytes CollectionNodes/CollectionNodes.csproj | Bin 3256 -> 3256 bytes DiscordNodes/DiscordNodes.csproj | Bin 2870 -> 2870 bytes EmailNodes/EmailNodes.csproj | Bin 3386 -> 3386 bytes Emby/Emby.csproj | Bin 2844 -> 2844 bytes FileFlows.Plugin.deps.json | 4 +- FileFlows.Plugin.dll | Bin 47616 -> 51200 bytes FileFlows.Plugin.pdb | Bin 24676 -> 27644 bytes FileFlowsPlugins.sln | 8 +- Gotify/Gotify.csproj | Bin 2752 -> 2752 bytes ImageNodes/ImageNodes.csproj | Bin 3232 -> 3232 bytes MetaNodes/MetaNodes.csproj | Bin 4504 -> 4504 bytes MusicNodes/MusicNodes.csproj | Bin 4228 -> 4228 bytes Plex/Plex.csproj | Bin 2844 -> 2844 bytes VideoLegacyNodes/ExtensionMethods.cs | 64 ++++ VideoLegacyNodes/FFMpegEncoder.cs | 235 +++++++++++++ VideoLegacyNodes/GlobalUsings.cs | 9 + .../LogicalNodes/CanUseHardwareEncoding.cs | 124 +++++++ .../LogicalNodes/DetectBlackBars.cs | 0 VideoLegacyNodes/Plugin.cs | Bin 0 -> 762 bytes VideoLegacyNodes/ResolutionHelper.cs | 43 +++ VideoLegacyNodes/VideoInfo.cs | 113 +++++++ VideoLegacyNodes/VideoInfoHelper.cs | 320 ++++++++++++++++++ VideoLegacyNodes/VideoLegacyNodes.csproj | Bin 0 -> 2994 bytes VideoLegacyNodes/VideoLegacyNodes.en.json | 254 ++++++++++++++ .../VideoNodes/AudioAddTrack.cs | 0 .../VideoNodes/AudioAdjustVolume.cs | 0 .../VideoNodes/AudioNormalization.cs | 0 .../VideoNodes/AudioTrackRemover.cs | 0 .../VideoNodes/AudioTrackReorder.cs | 0 .../VideoNodes/AudioTrackSetLanguage.cs | 0 .../VideoNodes/AutoChapters.cs | 0 .../VideoNodes/ComskipChapters.cs | 0 VideoLegacyNodes/VideoNodes/EncodingNode.cs | 236 +++++++++++++ .../VideoNodes/FFMPEG.cs | 0 .../VideoNodes/Remux.cs | 0 .../VideoNodes/SubtitleLanguageRemover.cs | 0 .../VideoNodes/SubtitleRemover.cs | 0 .../VideoNodes/VideoEncode.cs | 0 VideoLegacyNodes/VideoNodes/VideoNode.cs | 144 ++++++++ .../VideoNodes/VideoScaler.cs | 0 .../VideoNodes/Video_H265_AC3.cs | 0 .../Audio/FfmpegBuilderAudioNormalization.cs | 73 +++- .../Metadata/FfmpegBuilderAutoChapters.cs | 79 ++++- .../Metadata/FfmpegBuilderComskipChapters.cs | 69 +++- .../Video/FfmpegBuilderCropBlackBars.cs | 108 +++++- VideoNodes/Tests/AudioAddTrackTests.cs | 46 --- VideoNodes/Tests/AudioNormalizationTests.cs | 148 -------- VideoNodes/Tests/AudioTrackRemovalTests.cs | 40 --- VideoNodes/Tests/AutoChaptersTests.cs | 39 --- VideoNodes/Tests/DetectBlackBarsTests.cs | 49 --- VideoNodes/Tests/FFMPEGTests.cs | 47 --- .../Tests/SubtitleLanguageRemoverTests.cs | 41 --- VideoNodes/Tests/SubtitleRemoverTests.cs | 39 --- VideoNodes/Tests/VideoEncodeTests.cs | 58 ---- VideoNodes/Tests/VideoInfoHelperTests.cs | 300 ---------------- VideoNodes/Tests/VideoScalerTests.cs | 168 --------- VideoNodes/VideoNodes.csproj | Bin 4098 -> 4098 bytes 60 files changed, 1872 insertions(+), 986 deletions(-) create mode 100644 VideoLegacyNodes/ExtensionMethods.cs create mode 100644 VideoLegacyNodes/FFMpegEncoder.cs create mode 100644 VideoLegacyNodes/GlobalUsings.cs create mode 100644 VideoLegacyNodes/LogicalNodes/CanUseHardwareEncoding.cs rename {VideoNodes => VideoLegacyNodes}/LogicalNodes/DetectBlackBars.cs (100%) create mode 100644 VideoLegacyNodes/Plugin.cs create mode 100644 VideoLegacyNodes/ResolutionHelper.cs create mode 100644 VideoLegacyNodes/VideoInfo.cs create mode 100644 VideoLegacyNodes/VideoInfoHelper.cs create mode 100644 VideoLegacyNodes/VideoLegacyNodes.csproj create mode 100644 VideoLegacyNodes/VideoLegacyNodes.en.json rename {VideoNodes => VideoLegacyNodes}/VideoNodes/AudioAddTrack.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/AudioAdjustVolume.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/AudioNormalization.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/AudioTrackRemover.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/AudioTrackReorder.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/AudioTrackSetLanguage.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/AutoChapters.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/ComskipChapters.cs (100%) create mode 100644 VideoLegacyNodes/VideoNodes/EncodingNode.cs rename {VideoNodes => VideoLegacyNodes}/VideoNodes/FFMPEG.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/Remux.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/SubtitleLanguageRemover.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/SubtitleRemover.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/VideoEncode.cs (100%) create mode 100644 VideoLegacyNodes/VideoNodes/VideoNode.cs rename {VideoNodes => VideoLegacyNodes}/VideoNodes/VideoScaler.cs (100%) rename {VideoNodes => VideoLegacyNodes}/VideoNodes/Video_H265_AC3.cs (100%) delete mode 100644 VideoNodes/Tests/AudioAddTrackTests.cs delete mode 100644 VideoNodes/Tests/AudioNormalizationTests.cs delete mode 100644 VideoNodes/Tests/AudioTrackRemovalTests.cs delete mode 100644 VideoNodes/Tests/AutoChaptersTests.cs delete mode 100644 VideoNodes/Tests/DetectBlackBarsTests.cs delete mode 100644 VideoNodes/Tests/FFMPEGTests.cs delete mode 100644 VideoNodes/Tests/SubtitleLanguageRemoverTests.cs delete mode 100644 VideoNodes/Tests/SubtitleRemoverTests.cs delete mode 100644 VideoNodes/Tests/VideoEncodeTests.cs delete mode 100644 VideoNodes/Tests/VideoScalerTests.cs diff --git a/Apprise/Apprise.csproj b/Apprise/Apprise.csproj index 4596b27b63dde2c653a67c8252b88b5275713a6e..17902ed772610a2d3ed8a33e3e54d66e0890329d 100644 GIT binary patch delta 32 ocmX>hdO~!A3lpm;gAIfJWN9YJ$$OYMCTB6}F`90k$)wK#0F%%NU;qFB delta 32 ocmX>hdO~!A3lpmegAIfJWN9YJ$$OYMCTB6}F`8_i$)wK#0F$x^UH||9 diff --git a/BasicNodes/BasicNodes.csproj b/BasicNodes/BasicNodes.csproj index 541afdb9d3a422a1c85b1ca50538eefad59aae81..5e01a0279ab5309be11623791772584a7207a4dd 100644 GIT binary patch delta 20 ccmaDS{Z4wr8%9Rc$!{4W8BI64GcD%^09--`qyPW_ delta 20 ccmaDS{Z4wr8%9Qx$!{4W8BI33GcD%^09-H!q5uE@ diff --git a/ChecksumNodes/ChecksumNodes.csproj b/ChecksumNodes/ChecksumNodes.csproj index ddd2c5ad18c0a2ded2363af71f84507d9ab477d2..62d072ee9225c70695507a4f10beee9bc77642fa 100644 GIT binary patch delta 35 rcmdlXwnJ>gDJE7^1{((b$qShzC*R}ZnykjcH;Gw+(R8ySvp5F;!C47D delta 35 rcmdlXwnJ>gDJE7E1{((b$qShzC*R}ZnykjcH;Gw+(PXnCvp5F;!8i## diff --git a/CollectionNodes/CollectionNodes.csproj b/CollectionNodes/CollectionNodes.csproj index 9db42547b1f99949fabcc2cd7c26920eb054a462..65453ef1b0b866a25b1261796d0f9c1ed184d382 100644 GIT binary patch delta 36 scmdlXxkGZpEk;&T1{((b$r~9ZC&%${P2R)IH(7v5fzfocAyYdS0M0uK3IG5A delta 36 scmdlXxkGZpEk;%o1{((b$r~9ZC&%${P2R)IH(7v5fzf2MAyYdS0L~c-2mk;8 diff --git a/DiscordNodes/DiscordNodes.csproj b/DiscordNodes/DiscordNodes.csproj index 729f591c6fec1c0d57158a243148728390cf4872..7954babf28eb86d0bc71906b0882ba6694a7257e 100644 GIT binary patch delta 20 ccmdlcwoPoqBqm1F$&;Ca8BI4oXY%I&08A(bw*UYD delta 20 ccmdlcwoPoqBqm0a$&;Ca8BI1nXY%I&08ADJwEzGB diff --git a/EmailNodes/EmailNodes.csproj b/EmailNodes/EmailNodes.csproj index 46dab284938df6264bd1c90407fd182d9791c349..679fc1b17c22837104d0f20333678a04d9383e5f 100644 GIT binary patch delta 32 ocmdlbwM%M)3k$0$gAIfJWN8-3$$MD1Cg-r|Fq&?j$+D0G0FxjH%m4rY delta 32 ocmdlbwM%M)3k$0WgAIfJWN8-3$$MD1Cg-r|Fq&+h$+D0G0Fwd;$^ZZW diff --git a/Emby/Emby.csproj b/Emby/Emby.csproj index 4969388d3fa2dd9b4706dcbc105cd8d1d7e2fac7..39ac1bffc48bec936a7d6eb84bca8b6770d46c4b 100644 GIT binary patch delta 20 ccmbOuHb-p3Bqm1F$&;Ca8BI4oXENmg07w`HX#fBK delta 20 ccmbOuHb-p3Bqm0a$&;Ca8BI1nXENmg07wP~X8-^I diff --git a/FileFlows.Plugin.deps.json b/FileFlows.Plugin.deps.json index 88bfd6ce..468f7612 100644 --- a/FileFlows.Plugin.deps.json +++ b/FileFlows.Plugin.deps.json @@ -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": "" diff --git a/FileFlows.Plugin.dll b/FileFlows.Plugin.dll index 4a159fd7834bcbcf5bd523f6654657dd90bc412f..b68c3f5d8a6ba856168b374b8e6017e5295cdd07 100644 GIT binary patch literal 51200 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 literal 47616 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 EFVPT@jsO4v diff --git a/FileFlows.Plugin.pdb b/FileFlows.Plugin.pdb index af193c62c81a97166c60d9dbec07d0d199014fa1..3be45027a8489708d078c3fc9e41525d050b148d 100644 GIT binary patch delta 14084 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 delta 11209 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 delta 32 ocmX>gdO&o83lpmegAIfJWN9YJ$$OYMCTB6}F`8_i$)wH!0Fkl?MgRZ+ diff --git a/ImageNodes/ImageNodes.csproj b/ImageNodes/ImageNodes.csproj index d789e8af2ae3fd5ae82173f95ceaa0e4b461c573..87b6dd3dca576870d3c21138cd97222ab28ecc28 100644 GIT binary patch delta 28 kcmZ1=xj=Hm3?^1n1{((b$@xr@leaP1Fq&@u%(Rvh0D49Ung9R* delta 28 kcmZ1=xj=Hm3?^0+1{((b$@xr@leaP1Fq&-s%(Rvh0D3G4m;e9( diff --git a/MetaNodes/MetaNodes.csproj b/MetaNodes/MetaNodes.csproj index deabd4bd96b5dc6abc98d096cf7d3f9dd43c5556..a0b48439a3b3d6c50f6978ed9d6bd0eb0d87b614 100644 GIT binary patch delta 20 ccmbQCJVSZI9VSN8$#9m;e9( delta 20 ccmbQCJVSZI9VSMT$#6j3@?)k*M$^r<%6i;@?)k*Mw899% 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 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 Split(this string str, + Func 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; + } + } +} diff --git a/VideoLegacyNodes/FFMpegEncoder.cs b/VideoLegacyNodes/FFMpegEncoder.cs new file mode 100644 index 00000000..f07b911b --- /dev/null +++ b/VideoLegacyNodes/FFMpegEncoder.cs @@ -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 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 arguments, bool dontAddInputFile = false, bool dontAddOutputFile = false) + { + arguments ??= new List (); + + // -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 ExecuteShellCommand(string command, List 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(); + + process.OutputDataReceived += OnOutputDataReceived; + + errorBuilder = new StringBuilder(); + errorCloseEvent = new TaskCompletionSource(); + + 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 WaitForExitAsync(Process process, int timeout) + { + if (timeout > 0) + return Task.Run(() => process.WaitForExit(timeout)); + return Task.Run(() => + { + process.WaitForExit(); + return Task.FromResult(true); + }); + } + + + public struct ProcessResult + { + public bool Completed; + public int? ExitCode; + public string Output; + } + } +} \ No newline at end of file diff --git a/VideoLegacyNodes/GlobalUsings.cs b/VideoLegacyNodes/GlobalUsings.cs new file mode 100644 index 00000000..8c2a7e49 --- /dev/null +++ b/VideoLegacyNodes/GlobalUsings.cs @@ -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; + diff --git a/VideoLegacyNodes/LogicalNodes/CanUseHardwareEncoding.cs b/VideoLegacyNodes/LogicalNodes/CanUseHardwareEncoding.cs new file mode 100644 index 00000000..ea7b8df5 --- /dev/null +++ b/VideoLegacyNodes/LogicalNodes/CanUseHardwareEncoding.cs @@ -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, + } + + + /// + /// Checks if this flow runner can use NVIDIA HEVC encoder + /// + /// the node parameters + /// true if can use it, otherwise false + internal static bool CanProcess_Nvidia_Hevc(NodeParameters args) => CanProcess(args, "hevc_nvenc"); + + /// + /// Checks if this flow runner can use NVIDIA H.264 encoder + /// + /// the node parameters + /// true if can use it, otherwise false + internal static bool CanProcess_Nvidia_H264(NodeParameters args) => CanProcess(args, "h264_nvenc"); + + /// + /// Checks if this flow runner can use AND HEVC encoder + /// + /// the node parameters + /// true if can use it, otherwise false + internal static bool CanProcess_Amd_Hevc(NodeParameters args) => CanProcess(args, "hevc_amf"); + + /// + /// Checks if this flow runner can use AND H.264 encoder + /// + /// the node parameters + /// true if can use it, otherwise false + internal static bool CanProcess_Amd_H264(NodeParameters args) => CanProcess(args, "h264_amf"); + + + /// + /// Checks if this flow runner can use Intels QSV HEVC encoder + /// + /// the node parameters + /// true if can use it, otherwise false + internal static bool CanProcess_Qsv_Hevc(NodeParameters args) => CanProcess(args, "hevc_qsv -global_quality 28 -load_plugin hevc_hw"); + + /// + /// Checks if this flow runner can use Intels QSV H.264 encoder + /// + /// the node parameters + /// true if can use it, otherwise false + internal static bool CanProcess_Qsv_H264(NodeParameters args) => CanProcess(args, "h264_qsv"); + + /// + /// Checks if this flow runner can use VAAPI HEVC encoder + /// + /// the node parameters + /// true if can use it, otherwise false + internal static bool CanProcess_Vaapi_Hevc(NodeParameters args) => CanProcess(args, "hevc_vaapi"); + + /// + /// Checks if this flow runner can use VAAPI H.264 encoder + /// + /// the node parameters + /// true if can use it, otherwise false + 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); + } + + /// + /// Tests if the encoding parameters can be executed + /// + /// the node paramterse + /// the location of ffmpeg + /// the encoding parameter to test + /// true if can be processed + 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; + } + } +} diff --git a/VideoNodes/LogicalNodes/DetectBlackBars.cs b/VideoLegacyNodes/LogicalNodes/DetectBlackBars.cs similarity index 100% rename from VideoNodes/LogicalNodes/DetectBlackBars.cs rename to VideoLegacyNodes/LogicalNodes/DetectBlackBars.cs diff --git a/VideoLegacyNodes/Plugin.cs b/VideoLegacyNodes/Plugin.cs new file mode 100644 index 0000000000000000000000000000000000000000..12e08d36ff2b976c10cd02009eb9aa7bada24c39 GIT binary patch literal 762 zcmb7?%Syvg5QhJ?;5!_$s*8perB+2jlu8#Zg4n&OQ37dNn#3aFtE=DSf~OD{a+u8h zpIg4aGCgUfSh+SxOC+a>5~WBJzH9C9g<4>H#9IG)GVNDJQ;SA3y>_j#rLthI=iRKlFC*;1gYR{(ITwYMT=v- zZm>nfCrs*~lzKVO$wzTI|6>b&RH*4*mf?R?+i+yt+pl&FXuY$GJIoT2sgg1poKzQkhJu{QwesJT*iPj)BUReiZR+pvGZ zsqQTBuI{|M2mipFRp<%0CA0v2%f9Oi|JE6BxiMsX$W*6jZ57*VclU4nC-p|BOpq{3 Pk9qz0{d;^?zWVzERmOLU literal 0 HcmV?d00001 diff --git a/VideoLegacyNodes/ResolutionHelper.cs b/VideoLegacyNodes/ResolutionHelper.cs new file mode 100644 index 00000000..14507293 --- /dev/null +++ b/VideoLegacyNodes/ResolutionHelper.cs @@ -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; + } +} diff --git a/VideoLegacyNodes/VideoInfo.cs b/VideoLegacyNodes/VideoInfo.cs new file mode 100644 index 00000000..e1927f29 --- /dev/null +++ b/VideoLegacyNodes/VideoInfo.cs @@ -0,0 +1,113 @@ +namespace FileFlows.VideoNodes +{ + public class VideoInfo + { + public string FileName { get; set; } + /// + /// Gets or sets the bitrate in bytes per second + /// + public float Bitrate { get; set; } + public List VideoStreams { get; set; } = new List(); + public List AudioStreams { get; set; } = new List(); + public List SubtitleStreams { get; set; } = new List(); + + public List Chapters { get; set; } = new List(); + } + + public class VideoFileStream + { + /// + /// The original index of the stream in the overall video + /// + public int Index { get; set; } + /// + /// The index of the specific type + /// + public int TypeIndex { get; set; } + /// + /// The stream title (name) + /// + public string Title { get; set; } = ""; + + /// + /// The bitrate(BPS) of the video stream in bytes per second + /// + public float Bitrate { get; set; } + + /// + /// The codec of the stream + /// + public string Codec { get; set; } = ""; + + public string IndexString { get; set; } + + /// + /// Gets or sets if the stream is HDR + /// + public bool HDR { get; set; } + } + + public class VideoStream : VideoFileStream + { + /// + /// The width of the video stream + /// + public int Width { get; set; } + /// + /// The height of the video stream + /// + public int Height { get; set; } + /// + /// The number of frames per second + /// + public float FramesPerSecond { get; set; } + + /// + /// The duration of the stream + /// + public TimeSpan Duration { get; set; } + } + + public class AudioStream : VideoFileStream + { + /// + /// The language of the stream + /// + public string Language { get; set; } + + /// + /// The channels of the stream + /// + public float Channels { get; set; } + + /// + /// The duration of the stream + /// + public TimeSpan Duration { get; set; } + + /// + /// The sample rate of the audio stream + /// + public int SampleRate { get; set; } + } + + public class SubtitleStream : VideoFileStream + { + /// + /// The language of the stream + /// + public string Language { get; set; } + + /// + /// If this is a forced subtitle + /// + public bool Forced { get; set; } + } + + public class Chapter + { + public string Title { get; set; } + public TimeSpan Start { get; set; } + public TimeSpan End { get; set; } + } +} \ No newline at end of file diff --git a/VideoLegacyNodes/VideoInfoHelper.cs b/VideoLegacyNodes/VideoInfoHelper.cs new file mode 100644 index 00000000..3a186b3e --- /dev/null +++ b/VideoLegacyNodes/VideoInfoHelper.cs @@ -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 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(); + + var rgxChapter = new Regex("Chapter #(.*?)(?=(Chapter #|$))", RegexOptions.Singleline); + var chapters = new List(); + + 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(); } + } + + 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; + } + } +} \ No newline at end of file diff --git a/VideoLegacyNodes/VideoLegacyNodes.csproj b/VideoLegacyNodes/VideoLegacyNodes.csproj new file mode 100644 index 0000000000000000000000000000000000000000..c63cbb86b821c21da1b9cb83b167cd7bb7cbeb70 GIT binary patch literal 2994 zcmcJRUr!TJ5XI-UCVqz-Vxqom)kGymnh*iSXl-oq$(I&NS*UH(7O`Jm{he8sy}P9a zBtGnRyF2&HnKS49`Sbg&wQOo*JGH(Y+T4P*?a0n-%Qoz@rM%PE8P_pi6KmS8b@+)ql+}!qf>e>&UFUT_`d&5Tl=Y;H;R?=sW zXZ@CQ-zGlB&r!aCH1%Gn<_4WjY|}fO*b94YFTEeDqGQC&vq$ijGPlEBKc(PO8 zilgMuqqRy@BV$bUS+49-`2v)@%02WQbNqdJbK1R&FCyLUr`QVHiYj9?B^&j zvp3A=I{wbQb$q2j0)|woyB00yu^uz#u;Wa%UZrH2bI)8>t|_jzs4uv(UG9ObQZk^a z`VzSJ;5_zupn0kY=^d!3Yd`%>kGp1Y@UdEZq=)>HcrB~A#r(gLV+?~PW97A(yG1D1 zxh0AER@9-DavhHHW$SPKvP-L1>LPX@R(&{C*K#3cXkwrhJCI$GW7kD{NTGM!hk|iJ z{0Nm!UE>iW0ySj2O3rju{<|9<6E26cy>g!CCb*k3 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 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 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()); + } + 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; + } + } +} \ No newline at end of file diff --git a/VideoNodes/VideoNodes/FFMPEG.cs b/VideoLegacyNodes/VideoNodes/FFMPEG.cs similarity index 100% rename from VideoNodes/VideoNodes/FFMPEG.cs rename to VideoLegacyNodes/VideoNodes/FFMPEG.cs diff --git a/VideoNodes/VideoNodes/Remux.cs b/VideoLegacyNodes/VideoNodes/Remux.cs similarity index 100% rename from VideoNodes/VideoNodes/Remux.cs rename to VideoLegacyNodes/VideoNodes/Remux.cs diff --git a/VideoNodes/VideoNodes/SubtitleLanguageRemover.cs b/VideoLegacyNodes/VideoNodes/SubtitleLanguageRemover.cs similarity index 100% rename from VideoNodes/VideoNodes/SubtitleLanguageRemover.cs rename to VideoLegacyNodes/VideoNodes/SubtitleLanguageRemover.cs diff --git a/VideoNodes/VideoNodes/SubtitleRemover.cs b/VideoLegacyNodes/VideoNodes/SubtitleRemover.cs similarity index 100% rename from VideoNodes/VideoNodes/SubtitleRemover.cs rename to VideoLegacyNodes/VideoNodes/SubtitleRemover.cs diff --git a/VideoNodes/VideoNodes/VideoEncode.cs b/VideoLegacyNodes/VideoNodes/VideoEncode.cs similarity index 100% rename from VideoNodes/VideoNodes/VideoEncode.cs rename to VideoLegacyNodes/VideoNodes/VideoEncode.cs diff --git a/VideoLegacyNodes/VideoNodes/VideoNode.cs b/VideoLegacyNodes/VideoNodes/VideoNode.cs new file mode 100644 index 00000000..55414b41 --- /dev/null +++ b/VideoLegacyNodes/VideoNodes/VideoNode.cs @@ -0,0 +1,144 @@ +namespace FileFlows.VideoNodes +{ + using FileFlows.Plugin; + + public abstract class VideoNode : Node + { + /// + /// Gets the Node Parameters + /// + protected NodeParameters Args { get; private set; } + + /// + /// Gets if this node is obsolete and should be phased out + /// + public override bool Obsolete => true; + + /// + /// Gets a message to show when the user tries to use this obsolete node + /// + public override string ObsoleteMessage => "This node has been replaced by the FFMPEG Builder version"; + +#if (DEBUG) + /// + /// Used for unit tests + /// + /// the args + public void SetArgs(NodeParameters args) + { + this.Args = args; + } +#endif + + /// + /// Gets the FFMPEG executable location + /// + protected string FFMPEG { get; private set; } + public override string Icon => "fas fa-video"; + + /// + /// Executed before execute, sets ffmpegexe etc + /// + /// the node parametes + /// true if successfully + 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 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; + } + var result = args.Parameters[VIDEO_INFO] as VideoInfo; + if (result == null) + { + args.Logger.WLog("VideoInfo not found for file"); + return null; + } + return result; + } + + + + } +} \ No newline at end of file diff --git a/VideoNodes/VideoNodes/VideoScaler.cs b/VideoLegacyNodes/VideoNodes/VideoScaler.cs similarity index 100% rename from VideoNodes/VideoNodes/VideoScaler.cs rename to VideoLegacyNodes/VideoNodes/VideoScaler.cs diff --git a/VideoNodes/VideoNodes/Video_H265_AC3.cs b/VideoLegacyNodes/VideoNodes/Video_H265_AC3.cs similarity index 100% rename from VideoNodes/VideoNodes/Video_H265_AC3.cs rename to VideoLegacyNodes/VideoNodes/Video_H265_AC3.cs diff --git a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioNormalization.cs b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioNormalization.cs index fd8cc238..662e3b0a 100644 --- a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioNormalization.cs +++ b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioNormalization.cs @@ -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(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 + { + "-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(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; } + } + } diff --git a/VideoNodes/FfmpegBuilderNodes/Metadata/FfmpegBuilderAutoChapters.cs b/VideoNodes/FfmpegBuilderNodes/Metadata/FfmpegBuilderAutoChapters.cs index cdaca6ee..22f5e7a2 100644 --- a/VideoNodes/FfmpegBuilderNodes/Metadata/FfmpegBuilderAutoChapters.cs +++ b/VideoNodes/FfmpegBuilderNodes/Metadata/FfmpegBuilderAutoChapters.cs @@ -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 + { + "-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(); + } + } } } diff --git a/VideoNodes/FfmpegBuilderNodes/Metadata/FfmpegBuilderComskipChapters.cs b/VideoNodes/FfmpegBuilderNodes/Metadata/FfmpegBuilderComskipChapters.cs index a7dcc2e6..e2be690b 100644 --- a/VideoNodes/FfmpegBuilderNodes/Metadata/FfmpegBuilderComskipChapters.cs +++ b/VideoNodes/FfmpegBuilderNodes/Metadata/FfmpegBuilderComskipChapters.cs @@ -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(); + } + + } } diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderCropBlackBars.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderCropBlackBars.cs index 0c56f844..ea48ae3f 100644 --- a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderCropBlackBars.cs +++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderCropBlackBars.cs @@ -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); + + } } \ No newline at end of file diff --git a/VideoNodes/Tests/AudioAddTrackTests.cs b/VideoNodes/Tests/AudioAddTrackTests.cs deleted file mode 100644 index ad98da0f..00000000 --- a/VideoNodes/Tests/AudioAddTrackTests.cs +++ /dev/null @@ -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 \ No newline at end of file diff --git a/VideoNodes/Tests/AudioNormalizationTests.cs b/VideoNodes/Tests/AudioNormalizationTests.cs deleted file mode 100644 index d5a0643a..00000000 --- a/VideoNodes/Tests/AudioNormalizationTests.cs +++ /dev/null @@ -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 \ No newline at end of file diff --git a/VideoNodes/Tests/AudioTrackRemovalTests.cs b/VideoNodes/Tests/AudioTrackRemovalTests.cs deleted file mode 100644 index 71a9d1a7..00000000 --- a/VideoNodes/Tests/AudioTrackRemovalTests.cs +++ /dev/null @@ -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 \ No newline at end of file diff --git a/VideoNodes/Tests/AutoChaptersTests.cs b/VideoNodes/Tests/AutoChaptersTests.cs deleted file mode 100644 index e468c67c..00000000 --- a/VideoNodes/Tests/AutoChaptersTests.cs +++ /dev/null @@ -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 \ No newline at end of file diff --git a/VideoNodes/Tests/DetectBlackBarsTests.cs b/VideoNodes/Tests/DetectBlackBarsTests.cs deleted file mode 100644 index 7452863d..00000000 --- a/VideoNodes/Tests/DetectBlackBarsTests.cs +++ /dev/null @@ -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 \ No newline at end of file diff --git a/VideoNodes/Tests/FFMPEGTests.cs b/VideoNodes/Tests/FFMPEGTests.cs deleted file mode 100644 index 8c476b6c..00000000 --- a/VideoNodes/Tests/FFMPEGTests.cs +++ /dev/null @@ -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 \ No newline at end of file diff --git a/VideoNodes/Tests/SubtitleLanguageRemoverTests.cs b/VideoNodes/Tests/SubtitleLanguageRemoverTests.cs deleted file mode 100644 index a423bd15..00000000 --- a/VideoNodes/Tests/SubtitleLanguageRemoverTests.cs +++ /dev/null @@ -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 \ No newline at end of file diff --git a/VideoNodes/Tests/SubtitleRemoverTests.cs b/VideoNodes/Tests/SubtitleRemoverTests.cs deleted file mode 100644 index ebb4a09e..00000000 --- a/VideoNodes/Tests/SubtitleRemoverTests.cs +++ /dev/null @@ -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 \ No newline at end of file diff --git a/VideoNodes/Tests/VideoEncodeTests.cs b/VideoNodes/Tests/VideoEncodeTests.cs deleted file mode 100644 index a0b44b5b..00000000 --- a/VideoNodes/Tests/VideoEncodeTests.cs +++ /dev/null @@ -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 \ No newline at end of file diff --git a/VideoNodes/Tests/VideoInfoHelperTests.cs b/VideoNodes/Tests/VideoInfoHelperTests.cs index ccb7845e..71f3b11a 100644 --- a/VideoNodes/Tests/VideoInfoHelperTests.cs +++ b/VideoNodes/Tests/VideoInfoHelperTests.cs @@ -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 - { - "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 - { - 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 { "eng" }; - node.OrderedTracks = new List { "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 - { - 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 { "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 - { - 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(); - 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 - { - 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 { "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 - { - 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 { "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); - } diff --git a/VideoNodes/Tests/VideoScalerTests.cs b/VideoNodes/Tests/VideoScalerTests.cs deleted file mode 100644 index 3916b61a..00000000 --- a/VideoNodes/Tests/VideoScalerTests.cs +++ /dev/null @@ -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 - { - 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 - { - 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 \ No newline at end of file diff --git a/VideoNodes/VideoNodes.csproj b/VideoNodes/VideoNodes.csproj index 1a3a36571a4026b496d033cf279ce0dab853f444..d609e9e32b8d11d0a6f2a1cd4b5d3d17db9e07b5 100644 GIT binary patch delta 20 bcmZotXj0hlh>6j3@?)k*M$^r<%tv_uOd|%` delta 20 bcmZotXj0hlh>6i;@?)k*Mw899%tv_uOc4gy