diff --git a/Apprise/Apprise.csproj b/Apprise/Apprise.csproj index 4596b27b..17902ed7 100644 Binary files a/Apprise/Apprise.csproj and b/Apprise/Apprise.csproj differ diff --git a/BasicNodes/BasicNodes.csproj b/BasicNodes/BasicNodes.csproj index 541afdb9..5e01a027 100644 Binary files a/BasicNodes/BasicNodes.csproj and b/BasicNodes/BasicNodes.csproj differ diff --git a/ChecksumNodes/ChecksumNodes.csproj b/ChecksumNodes/ChecksumNodes.csproj index ddd2c5ad..62d072ee 100644 Binary files a/ChecksumNodes/ChecksumNodes.csproj and b/ChecksumNodes/ChecksumNodes.csproj differ diff --git a/CollectionNodes/CollectionNodes.csproj b/CollectionNodes/CollectionNodes.csproj index 9db42547..65453ef1 100644 Binary files a/CollectionNodes/CollectionNodes.csproj and b/CollectionNodes/CollectionNodes.csproj differ diff --git a/DiscordNodes/DiscordNodes.csproj b/DiscordNodes/DiscordNodes.csproj index 729f591c..7954babf 100644 Binary files a/DiscordNodes/DiscordNodes.csproj and b/DiscordNodes/DiscordNodes.csproj differ diff --git a/EmailNodes/EmailNodes.csproj b/EmailNodes/EmailNodes.csproj index 46dab284..679fc1b1 100644 Binary files a/EmailNodes/EmailNodes.csproj and b/EmailNodes/EmailNodes.csproj differ diff --git a/Emby/Emby.csproj b/Emby/Emby.csproj index 4969388d..39ac1bff 100644 Binary files a/Emby/Emby.csproj and b/Emby/Emby.csproj differ 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 4a159fd7..b68c3f5d 100644 Binary files a/FileFlows.Plugin.dll and b/FileFlows.Plugin.dll differ diff --git a/FileFlows.Plugin.pdb b/FileFlows.Plugin.pdb index af193c62..3be45027 100644 Binary files a/FileFlows.Plugin.pdb and b/FileFlows.Plugin.pdb differ diff --git a/FileFlowsPlugins.sln b/FileFlowsPlugins.sln index ff9eeb7b..0969713d 100644 --- a/FileFlowsPlugins.sln +++ b/FileFlowsPlugins.sln @@ -27,7 +27,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby", "Emby\Emby.csproj", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apprise", "Apprise\Apprise.csproj", "{CA750701-C4CF-482F-B5F3-A40E188F3E14}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageNodes", "ImageNodes\ImageNodes.csproj", "{3C6B9933-B6BC-4C00-9247-71F575AA276B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageNodes", "ImageNodes\ImageNodes.csproj", "{3C6B9933-B6BC-4C00-9247-71F575AA276B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoLegacyNodes", "VideoLegacyNodes\VideoLegacyNodes.csproj", "{4339720B-5061-431F-9080-FC087ACFDB3B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,6 +89,10 @@ Global {3C6B9933-B6BC-4C00-9247-71F575AA276B}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C6B9933-B6BC-4C00-9247-71F575AA276B}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C6B9933-B6BC-4C00-9247-71F575AA276B}.Release|Any CPU.Build.0 = Release|Any CPU + {4339720B-5061-431F-9080-FC087ACFDB3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4339720B-5061-431F-9080-FC087ACFDB3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4339720B-5061-431F-9080-FC087ACFDB3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4339720B-5061-431F-9080-FC087ACFDB3B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Gotify/Gotify.csproj b/Gotify/Gotify.csproj index 3bca8b26..177fe48d 100644 Binary files a/Gotify/Gotify.csproj and b/Gotify/Gotify.csproj differ diff --git a/ImageNodes/ImageNodes.csproj b/ImageNodes/ImageNodes.csproj index d789e8af..87b6dd3d 100644 Binary files a/ImageNodes/ImageNodes.csproj and b/ImageNodes/ImageNodes.csproj differ diff --git a/MetaNodes/MetaNodes.csproj b/MetaNodes/MetaNodes.csproj index deabd4bd..a0b48439 100644 Binary files a/MetaNodes/MetaNodes.csproj and b/MetaNodes/MetaNodes.csproj differ diff --git a/MusicNodes/MusicNodes.csproj b/MusicNodes/MusicNodes.csproj index 392fc0fc..66bfedfe 100644 Binary files a/MusicNodes/MusicNodes.csproj and b/MusicNodes/MusicNodes.csproj differ diff --git a/Plex/Plex.csproj b/Plex/Plex.csproj index f78449dd..6073a967 100644 Binary files a/Plex/Plex.csproj and b/Plex/Plex.csproj differ diff --git a/VideoLegacyNodes/ExtensionMethods.cs b/VideoLegacyNodes/ExtensionMethods.cs new file mode 100644 index 00000000..560e3432 --- /dev/null +++ b/VideoLegacyNodes/ExtensionMethods.cs @@ -0,0 +1,64 @@ +namespace FileFlows.VideoNodes +{ + using System.Linq; + using System.Text.RegularExpressions; + + internal static class ExtensionMethods + { + public static void AddOrUpdate(this Dictionary 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 00000000..12e08d36 Binary files /dev/null and b/VideoLegacyNodes/Plugin.cs differ 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 00000000..c63cbb86 Binary files /dev/null and b/VideoLegacyNodes/VideoLegacyNodes.csproj differ diff --git a/VideoLegacyNodes/VideoLegacyNodes.en.json b/VideoLegacyNodes/VideoLegacyNodes.en.json new file mode 100644 index 00000000..6dcc77bd --- /dev/null +++ b/VideoLegacyNodes/VideoLegacyNodes.en.json @@ -0,0 +1,254 @@ +{ + "H": { + "264": "H.264", + "265": "H.265" + }, + "5": { "1": "5.1" }, + "7": { "1": "7.1" }, + "Flow": { + "Parts": { + "AudioAddTrack": { + "Outputs": { + "1": "Audio track added and saved to temporary file" + }, + "Description": "Adds a new audio track to the video file, all other audio tracks will remain. This will use the first audio track of the file as the source audio track to convert.", + "Fields": { + "Index": "Index", + "Index-Help": "The index where to insert the new audio track. 0 based, so to insert the new audio track as the first track set this to 0.", + "Channels": "Channels", + "Channels-Help": "The number of channels to convert this audio track to.", + "Bitrate": "Bitrate", + "Bitrate-Help": "Bitrate of the new audio track" + } + + }, + "AudioAdjustVolume": { + "Outputs": { + "1": "Audio tracks volume was adjusted and saved to temporary file", + "2": "Audio tracks were not adjusted" + }, + "Description": "Adjusts audio tracks volume in a video file using FFMPEG", + "Fields": { + "VolumePercent": "Volume Percent", + "VolumePercent-Help": "The percent of the adjusted volume.\n100 means no adjustment\n50 means half volume\n0 means muted" + } + }, + "AudioNormalization": { + "Outputs": { + "1": "Audio tracks were normalized and saved to temporary file", + "2": "No audio tracks were found to be normalized" + }, + "Description": "Normalizes all audio tracks in a video file using FFMPEGs loudnorm filter", + "Fields": { + "AllAudio": "All Audio Tracks", + "AllAudio-Help": "If all audio tracks should be normalized or if just the first track should be", + "TwoPass": "Two Pass", + "TwoPass-Help": "If the audio tracks should use two pass normalization. This improves the normalization but increases the processing time.", + "Pattern": "Pattern", + "Pattern-Help": "An optional regular expression to filter out audio tracks to normalize. Will match against the title, codec and language", + "NotMatching": "Not Matching", + "NotMatching-Help": "If the pattern should be used to exclude audio tracks from normalization, otherwise if the audio track matches they will be normalized." + } + }, + "AudioTrackRemover": { + "Outputs": { + "1": "Audio tracks were removed", + "2": "Audio tracks were NOT removed" + }, + "Description": "Allows you to remove audio tracks based on either their title or their language codes.\n\nAny title (or language code if set to \"Use Language Code\") that is blank will NOT be removed regardless of the pattern.", + "Fields": { + "Pattern": "Pattern", + "Pattern-Help": "A regular expression to match against, eg \"commentary\" to remove commentary tracks", + "NotMatching": "Not Matching", + "NotMatching-Help": "If audio tracks NOT matching the pattern should be removed", + "UseLanguageCode": "Use Language Code", + "UseLanguageCode-Help": "If the language code of the audio track should be used instead of the title" + } + }, + "AudioTrackReorder": { + "Outputs": { + "1": "Audio tracks re-ordered in new temporary file", + "2": "Audio tracks NOT re-ordered" + }, + "Description": "Allows you to reorder audio tracks in the preferred order.\n\nEnter the languages/audio codecs/channels in the order you want. Any not listed will be ordered after the ones entered in their original order.\nIf there are multiple tracks with same language/codec/channels, they will be ordered first by the order you entered, then in their original order.\n\nOutput 1: Tracks were reordered\nOutput 2: Tracks did not need reordering", + "Fields": { + "OrderedTracks": "Ordered Audio Codecs", + "OrderedTracks-Help": "The order of audio codecs to the audio tracks by. This is done after the languages (if any)", + "Languages": "Languages", + "Languages-Help": "The order of languages to sort the audio tracks by. This sorting is done before the codec.", + "Channels": "Channels", + "Channels-Help": "The order of audio channels to sort the audio tracks by. This sorting is done before languages.\nFor example \"5.1\",\"7.1\",\"6.2\",\"2\"" + } + }, + "AudioTrackSetLanguage": { + "Label": "Audio Set Language", + "Outputs": { + "1": "Audio tracks updated to new temporary file", + "2": "Audio tracks NOT updated" + }, + "Description": "Allows you to set the language for any audio tracks that have no language set. If the audio track does have a language set, it will be skipped.\n\nOutput 1: Audio Tracks were updated\nOutput 2: No audio tracks were needing to be updated", + "Fields": { + "Language": "Language", + "Language-Help": "The ISO 639-2 language code to use. \nhttps://en.wikipedia.org/wiki/List_of_ISO_639-2_codes" + } + }, + "AutoChapters": { + "Description": "Automatically detect scene changes in the video to generate chapters.", + "Outputs": { + "1": "Chapters generated and saved to temporary file", + "2": "No chapters detected or video already had chapters" + }, + "Fields": { + "MinimumLength": "Minimum Length", + "MinimumLength-Suffix": "seconds", + "MinimumLength-Help": "The minimum length of a chapter in seconds", + "Percent": "Percent", + "Percent-Suffix": "%", + "Percent-Help": "The threshold percentage value to use for scene detection changes. A good value is 45%" + } + }, + "ComskipChapters": { + "Description": "Uses a comskip EDL file and will create chapters given that EDL comskip file.", + "Outputs": { + "1": "Commercials chapters created, saved to temporary file", + "2": "No commercials detected" + } + }, + "DetectBlackBars": { + "Description": "Processes a video file and scans for black bars in the video.\n\nIf found a parameter \"VideoCrop\" will be added.\n\nOutput 1: Black bars detected\nOutput 2: Not detected", + "Outputs": { + "1": "Black bars detected", + "2": "No black bars detected" + }, + "Fields": { + "CroppingThreshold": "Threshold", + "CroppingThreshold-Help": "The amount of pixels that must be greater than to crop. E.g. if there's only 5 pixels detected as black space, you may consider this too small to crop." + } + }, + "FFMPEG": { + "Description": "The node lets you run any FFMPEG command you like. Giving you full control over what it can do.\n\nFor more information refer to the FFMPEG documentation", + "Outputs": { + "1": "Video processed" + }, + "Fields": { + "Extension": "Extension", + "Extension-Help": "The file extension to use on the newly created file", + "CommandLine": "Command Line", + "CommandLine-Help": "The command line to run with FFMPEG.\n'{WorkingFile}': the working file of the flow\n'{Output}': The output file that will be passed as the last parameter to FFMPEG including the extension defined above." + } + }, + "RemuxToMKV": { + "Descritption": "Remuxes a video file into a MKV container. All streams will be copied to the new container", + "Outputs": { + "1": "File remuxed to temporary file", + "2": "File was already in a MKV container" + }, + "Fields": { + "Force": "Force", + "Force-Help": "If the file should be always remuxed into a MKV container even when it already is in a MKV container.\ni.e. a new temporary file will always be created." + } + }, + "RemuxToMP4": { + "Descritption": "Remuxes a video file into a MP4 container. All streams will be copied to the new container", + "Outputs": { + "1": "File remuxed to temporary file", + "2": "File was already in a MP4 container" + }, + "Fields": { + "Force": "Force", + "Force-Help": "If the file should be always remuxed into a MP4 container even when it already is in a MP4 container.\ni.e. a new temporary file will always be created." + } + }, + "SubtitleRemover": { + "Description": "Removes subtitles from a video file if found.\n\nOutput 1: Subtitles were removed\nOutput 2: No subtitles found that needed to be removed", + "Outputs": { + "1": "Subtitles removed in new temporary file", + "2": "No subtitles to remove" + }, + "Fields": { + "SubtitlesToRemove": "Subtitles To Remove", + "RemoveAll": "Remove All", + "RemoveAll-Help": "When checked, all subtitles will be removed from the file, otherwise only those selected below will be" + } + }, + "SubtitleLanguageRemover": { + "Outputs": { + "1": "Subtitles were removed", + "2": "Subtitles were NOT removed" + }, + "Description": "Allows you to remove subtitles based on either their title or their language codes.\n\nAny language (or title if set to \"Use Title\") that is blank will NOT be removed regardless of the pattern.", + "Fields": { + "Pattern": "Pattern", + "Pattern-Help": "A regular expression to match against, eg \"eng\" to remove English tracks", + "NotMatching": "Not Matching", + "NotMatching-Help": "If subtitles NOT matching the pattern should be removed", + "UseTitle": "Use Title", + "UseTitle-Help": "If the title of the subtitle should be used for matching instead of the language" + } + }, + "VideoCodec": { + "Description": "This node will check the codecs in the input file, and trigger when matched.\n\nOutput 1: Matches\nOutput 2: Does not match", + "Fields": { + "Codecs": "Codecs", + "Codecs-Help": "Enter a list of case insensitive video or audio codecs.\nEg hevc, h265, mpeg4, ac3" + } + }, + "VideoEncode": { + "Description": "A generic video encoding node, this lets you customize how to encode a video file using ffmpeg.\n\nOutput 1: Video was processed\nOutput 2: No processing required", + "Outputs": { + "1": "Video re-encoded to temporary file", + "2": "Video not re-encoded" + }, + "Fields": { + "Extension": "Extension", + "Extension-Help": "The file extension to use on the newly created file.", + "VideoCodec": "Video Codec", + "VideoCodec-Help": "The video codec the video should be in, for example hevc, h264.\nIf left empty all original video tracks will be copied.", + "VideoCodecParameters": "Video Codec Parameters", + "VideoCodecParameters-Help": "The parameters to use to encode the video, eg. \"hevc_nvenc -preset hq -crf 23\" to encode into hevc using the HQ preset a constant rate factor of 23 and using NVIDIA hardware acceleration.", + "AudioCodec": "Audio Codec", + "AudioCodec-Help": "The audio codec to encode the video with.\nIf left empty all original audio tracks will be copied.", + "Language": "Language", + "Language-Help": "Optional ISO 639-2 language code to use. Will attempt to find an audio track with this language code if not the best audio track will be used.\nhttps://en.wikipedia.org/wiki/List_of_ISO_639-2_codes" + } + }, + "VideoHasStream": { + "Description": "Tests if a video file contains a stream", + "Outputs": { + "1": "Contains the matching stream", + "2": "Does not contain the matching stream" + }, + "Fields": { + "Stream": "Type", + "Stream-Help": "The type of stream to look for", + "Title": "Title", + "Title-Help": "A regular expression used to test the stream title", + "Codec": "Codec", + "Codec-Help": "A regular expression used to test the stream codec", + "Language": "Language", + "Language-Help": "A regular expression used to test the stream language", + "Channels": "Channels", + "Channels-Help": "The number of channels to test for. Set to 0 to ignore this check" + } + }, + "VideoScaler": { + "Description": "This allows you to scale a video to the specified dimensions. It will retain the aspect ratio of the video so if the video was 1920x1000 it would scale to 1280x668 if you select 720P.", + "Outputs": { + "1": "Video rescaled to temporary file", + "2": "Video was already in/near the scaled resolution" + }, + "Fields": { + "VideoCodec": "Video Codec", + "Language-Help": "The video codec to encode the scaled video in", + "Extension": "Extension", + "Extension-Help": "The file extension to use on the newly created file", + "Force": "Force", + "Force-Help": "When checked the video will be force scaled even if the working file is already in this resolution (or near this resolution).", + "Resolution": "Resolution", + "VideoCodecParameters": "Video Codec Parameters", + "VideoCodecParameters-Help": "The parameters to use to encode the video, eg. \"hevc_nvenc -preset hq -crf 23\" to encode into hevc using the HQ preset a constant rate factor of 23 and using NVIDIA hardware acceleration." + } + } + } + } +} \ No newline at end of file diff --git a/VideoNodes/VideoNodes/AudioAddTrack.cs b/VideoLegacyNodes/VideoNodes/AudioAddTrack.cs similarity index 100% rename from VideoNodes/VideoNodes/AudioAddTrack.cs rename to VideoLegacyNodes/VideoNodes/AudioAddTrack.cs diff --git a/VideoNodes/VideoNodes/AudioAdjustVolume.cs b/VideoLegacyNodes/VideoNodes/AudioAdjustVolume.cs similarity index 100% rename from VideoNodes/VideoNodes/AudioAdjustVolume.cs rename to VideoLegacyNodes/VideoNodes/AudioAdjustVolume.cs diff --git a/VideoNodes/VideoNodes/AudioNormalization.cs b/VideoLegacyNodes/VideoNodes/AudioNormalization.cs similarity index 100% rename from VideoNodes/VideoNodes/AudioNormalization.cs rename to VideoLegacyNodes/VideoNodes/AudioNormalization.cs diff --git a/VideoNodes/VideoNodes/AudioTrackRemover.cs b/VideoLegacyNodes/VideoNodes/AudioTrackRemover.cs similarity index 100% rename from VideoNodes/VideoNodes/AudioTrackRemover.cs rename to VideoLegacyNodes/VideoNodes/AudioTrackRemover.cs diff --git a/VideoNodes/VideoNodes/AudioTrackReorder.cs b/VideoLegacyNodes/VideoNodes/AudioTrackReorder.cs similarity index 100% rename from VideoNodes/VideoNodes/AudioTrackReorder.cs rename to VideoLegacyNodes/VideoNodes/AudioTrackReorder.cs diff --git a/VideoNodes/VideoNodes/AudioTrackSetLanguage.cs b/VideoLegacyNodes/VideoNodes/AudioTrackSetLanguage.cs similarity index 100% rename from VideoNodes/VideoNodes/AudioTrackSetLanguage.cs rename to VideoLegacyNodes/VideoNodes/AudioTrackSetLanguage.cs diff --git a/VideoNodes/VideoNodes/AutoChapters.cs b/VideoLegacyNodes/VideoNodes/AutoChapters.cs similarity index 100% rename from VideoNodes/VideoNodes/AutoChapters.cs rename to VideoLegacyNodes/VideoNodes/AutoChapters.cs diff --git a/VideoNodes/VideoNodes/ComskipChapters.cs b/VideoLegacyNodes/VideoNodes/ComskipChapters.cs similarity index 100% rename from VideoNodes/VideoNodes/ComskipChapters.cs rename to VideoLegacyNodes/VideoNodes/ComskipChapters.cs diff --git a/VideoLegacyNodes/VideoNodes/EncodingNode.cs b/VideoLegacyNodes/VideoNodes/EncodingNode.cs new file mode 100644 index 00000000..9fd48e6c --- /dev/null +++ b/VideoLegacyNodes/VideoNodes/EncodingNode.cs @@ -0,0 +1,236 @@ +namespace FileFlows.VideoNodes +{ + using System.ComponentModel; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using FileFlows.Plugin; + using FileFlows.Plugin.Attributes; + + public abstract class EncodingNode : VideoNode + { + public override int Outputs => 2; + public override int Inputs => 1; + public override FlowElementType Type => FlowElementType.Process; + + protected TimeSpan TotalTime; + + private FFMpegEncoder Encoder; + + public bool Encode(NodeParameters args, string ffmpegExe, List 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 1a3a3657..d609e9e3 100644 Binary files a/VideoNodes/VideoNodes.csproj and b/VideoNodes/VideoNodes.csproj differ