diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoEncode.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoEncode.cs index b084bcf5..838e0812 100644 --- a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoEncode.cs +++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoEncode.cs @@ -85,10 +85,13 @@ public class FfmpegBuilderVideoEncode:FfmpegBuilderNode public override int Execute(NodeParameters args) { var stream = Model.VideoStreams.Where(x => x.Deleted == false).First(); + + stream.EncodingParameters.Clear(); + if (Codec == CODEC_H264 || Codec == CODEC_H264_10BIT) - H264(stream, Codec == CODEC_H264_10BIT); + stream.EncodingParameters.AddRange(H264(args, Codec == CODEC_H264_10BIT, Quality, HardwareEncoding)); else if (Codec == CODEC_H265 || Codec == CODEC_H265_10BIT) - H265(stream, Codec == CODEC_H265_10BIT); + stream.EncodingParameters.AddRange(H265(args, Codec == CODEC_H265_10BIT, Quality, HardwareEncoding)); else { args.Logger?.ILog("Unknown codec: " + Codec); @@ -99,88 +102,106 @@ public class FfmpegBuilderVideoEncode:FfmpegBuilderNode return 1; } - private void H264(FfmpegVideoStream stream, bool tenBit) + internal static IEnumerable GetEncodingParameters(NodeParameters args, string codec, int quality, bool useHardwareEncoder) { - if (HardwareEncoding == false) - H26x_CPU(stream, false); - else if (CanUseHardwareEncoding.CanProcess_Nvidia_H264(Args)) - H26x_Nvidia(stream, false); - else if (CanUseHardwareEncoding.CanProcess_Qsv_H264(Args)) - H26x_Qsv(stream, false); - else if (CanUseHardwareEncoding.CanProcess_Amd_H264(Args)) - H26x_Amd(stream, false); - else if (CanUseHardwareEncoding.CanProcess_Vaapi_H264(Args)) - H26x_Vaapi(stream, false); - else - H26x_CPU(stream, false); + if (codec == CODEC_H264 || codec == CODEC_H264_10BIT) + return H264(args, codec == CODEC_H264_10BIT, quality, useHardwareEncoder).Select(x => x.Replace("{index}", "0")); + else if (codec == CODEC_H265 || codec == CODEC_H265_10BIT) + return H265(args, codec == CODEC_H265_10BIT, quality, useHardwareEncoder).Select(x => x.Replace("{index}", "0")); + throw new Exception("Unsupported codec: " + codec); + + } + + private static IEnumerable H264(NodeParameters args, bool tenBit, int quality, bool useHardwareEncoding) + { + List parameters = new List(); + string[] bit10Filters = null; + string[] non10BitFilters = null; + if (useHardwareEncoding == false) + parameters.AddRange(H26x_CPU(false, quality, out bit10Filters)); + else if (CanUseHardwareEncoding.CanProcess_Nvidia_H264(args)) + parameters.AddRange(H26x_Nvidia(false, quality, out non10BitFilters)); + else if (CanUseHardwareEncoding.CanProcess_Qsv_H264(args)) + parameters.AddRange(H26x_Qsv(false, quality)); + else if (CanUseHardwareEncoding.CanProcess_Amd_H264(args)) + parameters.AddRange(H26x_Amd(false, quality)); + else if (CanUseHardwareEncoding.CanProcess_Vaapi_H264(args)) + parameters.AddRange(H26x_Vaapi(false, quality)); + else + parameters.AddRange(H26x_CPU(false, quality, out bit10Filters)); if (tenBit) - stream.EncodingParameters.AddRange(bit10Filters); + parameters.AddRange(bit10Filters ?? new string[] { "-pix_fmt:v:{index}", "p010le", "-profile:v:{index}", "main10" }); + return parameters; } - private void H265(FfmpegVideoStream stream, bool tenBit) + private static IEnumerable H265(NodeParameters args, bool tenBit, int quality, bool useHardwareEncoding) { // hevc_qsv -load_plugin hevc_hw -pix_fmt p010le -profile:v main10 -global_quality 21 -g 24 -look_ahead 1 -look_ahead_depth 60 - if (HardwareEncoding == false) - H26x_CPU(stream, true); - else if (CanUseHardwareEncoding.CanProcess_Nvidia_Hevc(Args)) - H26x_Nvidia(stream, true); - else if (CanUseHardwareEncoding.CanProcess_Qsv_Hevc(Args)) - H26x_Qsv(stream, true); - else if (CanUseHardwareEncoding.CanProcess_Amd_Hevc(Args)) - H26x_Amd(stream, true); - else if (CanUseHardwareEncoding.CanProcess_Vaapi_Hevc(Args)) - H26x_Vaapi(stream, true); - else - H26x_CPU(stream, true); + List parameters = new List(); + string[] bit10Filters = null; + string[] non10BitFilters = null; + if (useHardwareEncoding == false) + parameters.AddRange(H26x_CPU(true, quality, out bit10Filters)); + else if (CanUseHardwareEncoding.CanProcess_Nvidia_Hevc(args)) + parameters.AddRange(H26x_Nvidia(true, quality, out non10BitFilters)); + else if (CanUseHardwareEncoding.CanProcess_Qsv_Hevc(args)) + parameters.AddRange(H26x_Qsv(true, quality)); + else if (CanUseHardwareEncoding.CanProcess_Amd_Hevc(args)) + parameters.AddRange(H26x_Amd(true, quality)); + else if (CanUseHardwareEncoding.CanProcess_Vaapi_Hevc(args)) + parameters.AddRange(H26x_Vaapi(true, quality)); + else + parameters.AddRange(H26x_CPU(true, quality, out bit10Filters)); if (tenBit) - stream.EncodingParameters.AddRange(bit10Filters); + parameters.AddRange(bit10Filters ?? new string[] { "-pix_fmt:v:{index}", "p010le", "-profile:v:{index}", "main10" }); else if(non10BitFilters?.Any() == true) - stream.EncodingParameters.AddRange(non10BitFilters); + parameters.AddRange(non10BitFilters); + return parameters; } - private void H26x_CPU(FfmpegVideoStream stream, bool h265) + private static IEnumerable H26x_CPU(bool h265, int quality, out string[] bit10Filters) { - stream.EncodingParameters.Clear(); - stream.EncodingParameters.AddRange(new [] - { - h265 ? "libx265" : "libx264", - "-preset", "slow", - "-crf", Quality.ToString() - }); bit10Filters = new[] { "-pix_fmt:v:{index}", "yuv420p10le", "-profile:v:{index}", "main10" }; + return new [] + { + h265 ? "libx265" : "libx264", + "-preset", "slow", + "-crf", quality.ToString() + }; } - private void H26x_Nvidia(FfmpegVideoStream stream, bool h265) + private static IEnumerable H26x_Nvidia(bool h265, int quality, out string[] non10BitFilters) { - stream.EncodingParameters.Clear(); - stream.EncodingParameters.AddRange(new [] + if (h265 == false) + non10BitFilters = new[] { "-pix_fmt:v:{index}", "yuv420p" }; + else + non10BitFilters = null; + + return new [] { h265 ? "hevc_nvenc" : "h264_nvenc", "-rc", "constqp", - "-qp", Quality.ToString(), + "-qp", quality.ToString(), //"-b:v", "0K", // this would do a two-pass... slower "-preset", "p6", // https://www.reddit.com/r/ffmpeg/comments/gg5szi/what_is_spatial_aq_and_temporal_aq_with_nvenc/ "-spatial-aq", "1" - }); - if (h265 == false) { - non10BitFilters = new[] { "-pix_fmt:v:{index}", "yuv420p" }; - } + }; } - private void H26x_Qsv(FfmpegVideoStream stream, bool h265) + private static IEnumerable H26x_Qsv(bool h265, int quality) { //hevc_qsv -load_plugin hevc_hw -pix_fmt p010le -profile:v main10 -global_quality 21 -g 24 -look_ahead 1 -look_ahead_depth 60 - stream.EncodingParameters.Clear(); + var parameters = new List(); if (h265) { - stream.EncodingParameters.AddRange(new[] + parameters.AddRange(new[] { "hevc_qsv", "-load_plugin", "hevc_hw" @@ -188,43 +209,42 @@ public class FfmpegBuilderVideoEncode:FfmpegBuilderNode } else { - stream.EncodingParameters.AddRange(new[] + parameters.AddRange(new[] { "h264_qsv" }); } - stream.EncodingParameters.AddRange(new[] + parameters.AddRange(new[] { - "-global_quality", Quality.ToString(), + "-global_quality", quality.ToString(), "-preset", "slower", }); + return parameters; } - private void H26x_Amd(FfmpegVideoStream stream, bool h265) + private static IEnumerable H26x_Amd(bool h265, int quality) { - stream.EncodingParameters.Clear(); - stream.EncodingParameters.AddRange(new[] + return new[] { h265 ? "hevc_amf" : "h264_amf", - "-qp", Quality.ToString(), + "-qp", quality.ToString(), //"-b:v", "0K", // this would do a two-pass... slower "-preset", "slower", // https://www.reddit.com/r/ffmpeg/comments/gg5szi/what_is_spatial_aq_and_temporal_aq_with_nvenc/ "-spatial-aq", "1" - }); + }; } - private void H26x_Vaapi(FfmpegVideoStream stream, bool h265) + private static IEnumerable H26x_Vaapi(bool h265, int quality) { - stream.EncodingParameters.Clear(); - stream.EncodingParameters.AddRange(new[] + return new[] { h265 ? "hevc_vaapi" : "h264_vaapi", - "-qp", Quality.ToString(), + "-qp", quality.ToString(), //"-b:v", "0K", // this would do a two-pass... slower "-preset", "slower", // https://www.reddit.com/r/ffmpeg/comments/gg5szi/what_is_spatial_aq_and_temporal_aq_with_nvenc/ "-spatial-aq", "1" - }); + }; } } diff --git a/VideoNodes/Tests/AudioToVideoTests.cs b/VideoNodes/Tests/AudioToVideoTests.cs new file mode 100644 index 00000000..b36c99f9 --- /dev/null +++ b/VideoNodes/Tests/AudioToVideoTests.cs @@ -0,0 +1,57 @@ +#if(DEBUG) + +namespace VideoNodes.Tests; + +using FileFlows.VideoNodes; +using FileFlows.VideoNodes.FfmpegBuilderNodes; +using FileFlows.VideoNodes.VideoNodes; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class AudioToVideoTests : TestBase +{ + [TestMethod] + public void AudioToVideo_Waves_h265() + => TestStyle(FfmpegBuilderVideoEncode.CODEC_H264, AudioToVideo.VisualizationStyle.Waves); + + [TestMethod] + public void AudioToVideo_AudioVectorScope_H265() + => TestStyle(FfmpegBuilderVideoEncode.CODEC_H265, AudioToVideo.VisualizationStyle.AudioVectorScope); + + + [TestMethod] + public void AudioToVideo_Spectrum_H265_10Bit() + => TestStyle(FfmpegBuilderVideoEncode.CODEC_H265_10BIT, AudioToVideo.VisualizationStyle.Spectrum); + + + + private void TestStyle(string codec, AudioToVideo.VisualizationStyle style) + { + var logger = new TestLogger(); + string file = @"D:\music\unprocessed\01-billy_joel-movin_out.mp3"; + var vi = new VideoInfoHelper(FfmpegPath, logger); + var vii = vi.Read(file); + + var args = new NodeParameters(file, logger, false, string.Empty); + args.GetToolPathActual = (string tool) => FfmpegPath; + args.TempPath = TempPath; + + + AudioToVideo node = new(); + node.Container = "mkv"; + node.Resolution = "1280x720"; + node.Codec = codec; + node.HardwareEncoding = true; + node.Visualization = style; + if (node.Visualization == AudioToVideo.VisualizationStyle.Waves) + node.Color = "#007bff"; + node.PreExecute(args); + int output = node.Execute(args); + + var log = logger.ToString(); + Assert.AreEqual(1, output); + } +} + + +#endif \ No newline at end of file diff --git a/VideoNodes/VideoNodes.en.json b/VideoNodes/VideoNodes.en.json index 89346140..d0ddf1bb 100644 --- a/VideoNodes/VideoNodes.en.json +++ b/VideoNodes/VideoNodes.en.json @@ -7,6 +7,20 @@ "7": { "1": "7.1" }, "Flow": { "Parts": { + "AudioToVideo": { + "Description": "Converts an audio file into a video file and generates a video based on the audio", + "Fields": { + "Visualisation": "Visualisation", + "Visualisation-Help": "The visualation to use in the generated video. See Help for examples", + "Container": "Container", + "Resolution": "Resolution", + "Codec": "Codec", + "HardwareEncoding": "Hardware Encode", + "HardwareEncoding-Help": "When checked, will test to see if hardware encoders are found on the Processing Node, and if found will use hardware encoding, otherwise will fallback to CPU encoding.", + "Color": "Color", + "Color-Help": "Optional #RRGGBB color code to use for the sound waves, must be in the format #RRGGBB, for example #FF0090" + } + }, "CanUseHardwareEncoding": { "Description": "Checks if the specified hardware encoder is currently available to the Flow.", "Fields": { diff --git a/VideoNodes/VideoNodes/AudioToVideo.cs b/VideoNodes/VideoNodes/AudioToVideo.cs new file mode 100644 index 00000000..2daffcc8 --- /dev/null +++ b/VideoNodes/VideoNodes/AudioToVideo.cs @@ -0,0 +1,161 @@ +using FileFlows.VideoNodes.FfmpegBuilderNodes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FileFlows.VideoNodes.VideoNodes; + +/// +/// Node that converts a audio file into a video file and generates a video based on the audio +/// +public class AudioToVideo : EncodingNode +{ + public override int Outputs => 1; + public override int Inputs => 1; + public override string HelpUrl => "https://docs.fileflows.com/plugins/video-nodes/audio-to-video"; + + public override string Icon => "fas fa-headphones"; + + public enum VisualizationStyle + { + Waves = 1, + AudioVectorScope = 2, + Spectrum = 3 + } + + [DefaultValue(VisualizationStyle.Waves)] + [Select(nameof(VisualizationOptions), 1)] + public VisualizationStyle Visualization { get; set; } + + private static List _VisualisationOptions; + public static List VisualizationOptions + { + get + { + if (_VisualisationOptions == null) + { + _VisualisationOptions = new List + { + new ListOption { Label = "Waves", Value = VisualizationStyle.Waves }, + new ListOption { Label = "Audio Vector Scope", Value = VisualizationStyle.AudioVectorScope }, + new ListOption { Label = "Spectrum", Value = VisualizationStyle.Spectrum }, + }; + } + return _VisualisationOptions; + } + } + + + [DefaultValue("mkv")] + [Select(nameof(ContainerOptions), 2)] + public string Container { get; set; } = string.Empty; + + private static List _ContainerOptions; + public static List ContainerOptions + { + get + { + if (_ContainerOptions == null) + { + _ContainerOptions = new List + { + new ListOption { Label = "MKV", Value = "mkv"}, + new ListOption { Label = "MP4", Value = "mp4"} + }; + } + return _ContainerOptions; + } + } + + [DefaultValue("1280x720")] + [Select(nameof(ResolutionOptions), 3)] + public string Resolution { get; set; } + + private static List _ResolutionOptions; + public static List ResolutionOptions + { + get + { + if (_ResolutionOptions == null) + { + _ResolutionOptions = new List + { + new ListOption { Label = "480p", Value = "640x480"}, + new ListOption { Label = "720p", Value = "1280x720"}, + new ListOption { Label = "1080p", Value = "1920x1080"}, + new ListOption { Label = "4K", Value = "3840x2160"} + }; + } + return _ResolutionOptions; + } + } + + [DefaultValue(FfmpegBuilderVideoEncode.CODEC_H264)] + [Select(nameof(CodecOptions), 4)] + public string Codec { get; set; } + + private static List _CodecOptions; + /// + /// Gets or sets the codec options + /// + public static List CodecOptions + { + get + { + if (_CodecOptions == null) + { + _CodecOptions = new List + { + new () { Label = "H.264", Value = FfmpegBuilderVideoEncode.CODEC_H264 }, + //new () { Label = "H.264 (10-Bit)", Value = FfmpegBuilderVideoEncode.CODEC_H264_10BIT }, + new () { Label = "H.265", Value = FfmpegBuilderVideoEncode.CODEC_H265 }, + new () { Label = "H.265 (10-Bit)", Value = FfmpegBuilderVideoEncode.CODEC_H265_10BIT }, + }; + } + return _CodecOptions; + } + } + + [Boolean(5)] + [DefaultValue(true)] + public bool HardwareEncoding { get; set; } + + [TextVariable(6)] + [DefaultValue("#ff0090")] + [ConditionEquals(nameof(Visualization), VisualizationStyle.Waves)] + public string Color { get; set; } + + public override int Execute(NodeParameters args) + { + List ffArgs = new List(); + var encodingParameters = FfmpegBuilderVideoEncode.GetEncodingParameters(args, this.Codec, 28, HardwareEncoding); + + switch (Visualization) + { + case VisualizationStyle.Waves: + var color = this.Color; + if (Regex.IsMatch(color ?? String.Empty, "#[0-9a-fA-F]{6}") == false) + color = "#ff0090"; // use default colour + ffArgs.AddRange(new[] { "-filter_complex", $"[0:a]showwaves=s={Resolution}:mode=line:s=hd1080:colors={color}[v]" }); + break; + case VisualizationStyle.AudioVectorScope: + ffArgs.AddRange(new[] { "-filter_complex", $"[0:a]avectorscope=s={Resolution}[v]" }); + break; + case VisualizationStyle.Spectrum: + ffArgs.AddRange(new[] { "-filter_complex", $"[0:a]showspectrum=s={Resolution}:mode=separate:color=intensity:slide=1:scale=cbrt[v]" }); + break; + } + ffArgs.AddRange(new[] { "-map", "[v]" }); + ffArgs.Add("-vcodec"); + ffArgs.AddRange(encodingParameters); + ffArgs.AddRange(new[] { "-map", "0:a" }); + if (base.Encode(args, base.FFMPEG, ffArgs, this.Container) == false) + { + args.Logger?.ELog("Failed to encode"); + return -1; + } + return 1; + } +} diff --git a/VideoNodes/VideoNodes/VideoNode.cs b/VideoNodes/VideoNodes/VideoNode.cs index 4ed09442..4d7b9b6d 100644 --- a/VideoNodes/VideoNodes/VideoNode.cs +++ b/VideoNodes/VideoNodes/VideoNode.cs @@ -118,6 +118,8 @@ namespace FileFlows.VideoNodes protected VideoInfo GetVideoInfo(NodeParameters args, bool refreshIfFileChanged = true) { var vi = GetVideoInfoActual(args); + if(vi == null) return null; + if (refreshIfFileChanged == false || vi.FileName == args.FileName) return vi;