From b0fc1ccea4911a38c2b3da5810bc63c442757497 Mon Sep 17 00:00:00 2001 From: John Andrews Date: Mon, 25 Apr 2022 12:56:41 +1200 Subject: [PATCH 1/7] Added support for hardware decoding in ffmpeg builder executor --- .../FfmpegBuilderExecutor.cs | 43 +++++++++++++++++++ VideoNodes/VideoNodes.en.json | 6 ++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs index 6d748cc3..0e9f1411 100644 --- a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs @@ -10,12 +10,22 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes public override int Outputs => 2; public override FlowElementType Type => FlowElementType.BuildEnd; + public override bool NoEditorOnAdd => true; + + [DefaultValue(true)] + [Boolean(1)] + public bool HardwareDecoding { get; set; } + public override int Execute(NodeParameters args) { this.Init(args); var model = this.Model; List ffArgs = new List(); ffArgs.AddRange(new[] { "-strict", "-2" }); // allow experimental stuff + if (HardwareDecoding) + { + ffArgs.AddRange(GetHardwareDecodingArgs()); + } bool hasChange = false; int actualIndex = 0; int currentType = 0; @@ -70,5 +80,38 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes return 1; } + + internal string[] GetHardwareDecodingArgs() + { + string testFile = Path.Combine(args.TempPath, Guid.NewGuid() + ".hwtest.mkv"); + foreach(var hw in new [] { "cuda", "dxva2","qsv","d3d11va","opencl" }) + { + // ffmpeg -y -hwaccel qsvf -f lavfi -i color=color=red -frames:v 10 test.mkv + try + { + var result = args.Execute(new ExecuteArgs + { + ArgumentList = new[] + { + "-y", + "-hwaccel", hw, + "-f", "lavfi", + "-i", "color=color=red", + "-frames:v", "10", + testFile + } + }); + if (result.ExitCode == 0) + { + args.Logger?.ILog("Supported hardware decoding detected: " + hw); + return new[] { "-hwaccel", hw }; + } + } + catch (Exception) { } + } + + args.Logger?.ILog("No hardware decoding availble"); + return new string[] { }; + } } } diff --git a/VideoNodes/VideoNodes.en.json b/VideoNodes/VideoNodes.en.json index 901e82d7..8d968fee 100644 --- a/VideoNodes/VideoNodes.en.json +++ b/VideoNodes/VideoNodes.en.json @@ -157,7 +157,11 @@ "1": "FFMPEG Builder ran successfully and created new temporary file", "2": "No changes detected in FFMPEG Builder, file not created" }, - "Description": "Executes a FFMPEG Builder command created by other FFMPEG Builder nodes." + "Description": "Executes a FFMPEG Builder command created by other FFMPEG Builder nodes.", + "Fields": { + "HardwareDecoding": "Hardware Decoding", + "HardwareDecoding-Help": "If the executor should attempt to use hardware decoding. If not available the execution will proceed just without hardware decoding enabled." + } }, "FfmpegBuilderAudioAddTrack": { "Label": "FFMPEG Builder: Audio Add Track", From 47ca14e2597a1b645455185b9ca0f8a77bb51096 Mon Sep 17 00:00:00 2001 From: John Andrews Date: Mon, 25 Apr 2022 13:06:52 +1200 Subject: [PATCH 2/7] fixing hardware decoding and added unit test --- .../FfmpegBuilderExecutor.cs | 13 ++-- .../FfmpegBuilder_BasicTests.cs | 37 ++++++++++ .../FfmpegBuilder_MetadataTests.cs | 70 +++++++++---------- 3 files changed, 80 insertions(+), 40 deletions(-) diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs index 0e9f1411..36b3e326 100644 --- a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs @@ -22,10 +22,6 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes var model = this.Model; List ffArgs = new List(); ffArgs.AddRange(new[] { "-strict", "-2" }); // allow experimental stuff - if (HardwareDecoding) - { - ffArgs.AddRange(GetHardwareDecodingArgs()); - } bool hasChange = false; int actualIndex = 0; int currentType = 0; @@ -66,7 +62,13 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes else model.InputFiles[0] = args.WorkingFile; - foreach(string file in model.InputFiles) + + if (HardwareDecoding) + { + startArgs.AddRange(GetHardwareDecodingArgs()); + } + + foreach (string file in model.InputFiles) { startArgs.Add("-i"); startArgs.Add(file); @@ -91,6 +93,7 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes { var result = args.Execute(new ExecuteArgs { + Command = ffmpegExe, ArgumentList = new[] { "-y", diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs index 95f38714..b500103f 100644 --- a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs +++ b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs @@ -776,6 +776,43 @@ namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests string log = logger.ToString(); Assert.AreEqual(1, result); } + + + [TestMethod] + public void FfmpegBuilder_HardwareDecoding() + { + const string file = @"D:\videos\unprocessed\basic.mkv"; + var logger = new TestLogger(); + const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; + var vi = new VideoInfoHelper(ffmpeg, logger); + var vii = vi.Read(file); + var args = new NodeParameters(file, logger, false, string.Empty); + args.GetToolPathActual = (string tool) => ffmpeg; + args.TempPath = @"D:\videos\temp"; + args.Parameters.Add("VideoInfo", vii); + + + FfmpegBuilderStart ffStart = new(); + Assert.AreEqual(1, ffStart.Execute(args)); + + FfmpegBuilderVideoCodec ffEncode = new(); + ffEncode.VideoCodec = "h264"; + ffEncode.Execute(args); + + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "ac3"; + ffAddAudio.Index = 0; + ffAddAudio.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + ffExecutor.HardwareDecoding = true; + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + } } diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_MetadataTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_MetadataTests.cs index d60d9efa..99fca048 100644 --- a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_MetadataTests.cs +++ b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_MetadataTests.cs @@ -1,44 +1,44 @@ -#if(DEBUG) +//#if(DEBUG) -using FileFlows.VideoNodes.FfmpegBuilderNodes; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using VideoNodes.Tests; +//using FileFlows.VideoNodes.FfmpegBuilderNodes; +//using Microsoft.VisualStudio.TestTools.UnitTesting; +//using VideoNodes.Tests; -namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests -{ - [TestClass] - public class FfmpegBuilder_MetadataTests - { - [TestMethod] - public void FfmpegBuilder_MetadataJson() - { - const string file = @"D:\videos\unprocessed\basic.mkv"; - var logger = new TestLogger(); - const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; - var vi = new VideoInfoHelper(ffmpeg, logger); - var vii = vi.Read(file); - var args = new NodeParameters(file, logger, false, string.Empty); - VideoMetadata md = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(@"D:\videos\metadata.json")); - args.Variables.Add("VideoMetadata", md); - args.GetToolPathActual = (string tool) => ffmpeg; - args.TempPath = @"D:\videos\temp"; - args.Parameters.Add("VideoInfo", vii); +//namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests +//{ +// [TestClass] +// public class FfmpegBuilder_MetadataTests +// { +// [TestMethod] +// public void FfmpegBuilder_MetadataJson() +// { +// const string file = @"D:\videos\unprocessed\basic.mkv"; +// var logger = new TestLogger(); +// const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; +// var vi = new VideoInfoHelper(ffmpeg, logger); +// var vii = vi.Read(file); +// var args = new NodeParameters(file, logger, false, string.Empty); +// VideoMetadata md = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(@"D:\videos\metadata.json")); +// args.Variables.Add("VideoMetadata", md); +// args.GetToolPathActual = (string tool) => ffmpeg; +// args.TempPath = @"D:\videos\temp"; +// args.Parameters.Add("VideoInfo", vii); - FfmpegBuilderStart ffStart = new (); - Assert.AreEqual(1, ffStart.Execute(args)); +// FfmpegBuilderStart ffStart = new (); +// Assert.AreEqual(1, ffStart.Execute(args)); - FfmpegBuilderVideoMetadata ffMetadata = new(); - Assert.AreEqual(1, ffMetadata.Execute(args)); +// FfmpegBuilderVideoMetadata ffMetadata = new(); +// Assert.AreEqual(1, ffMetadata.Execute(args)); - FfmpegBuilderExecutor ffExecutor = new(); - int result = ffExecutor.Execute(args); +// FfmpegBuilderExecutor ffExecutor = new(); +// int result = ffExecutor.Execute(args); - string log = logger.ToString(); - Assert.AreEqual(1, result); - } - } -} +// string log = logger.ToString(); +// Assert.AreEqual(1, result); +// } +// } +//} -#endif \ No newline at end of file +//#endif \ No newline at end of file From c75ac43da55a7cb83724dd1640f75cf80705c091 Mon Sep 17 00:00:00 2001 From: John Andrews Date: Mon, 25 Apr 2022 14:51:53 +1200 Subject: [PATCH 3/7] fixed spacing in file --- VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs index 36b3e326..0f0ec6d5 100644 --- a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs @@ -86,7 +86,7 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes internal string[] GetHardwareDecodingArgs() { string testFile = Path.Combine(args.TempPath, Guid.NewGuid() + ".hwtest.mkv"); - foreach(var hw in new [] { "cuda", "dxva2","qsv","d3d11va","opencl" }) + foreach(var hw in new [] { "cuda", "dxva2", "qsv", "d3d11va", "opencl" }) { // ffmpeg -y -hwaccel qsvf -f lavfi -i color=color=red -frames:v 10 test.mkv try From 00745eae04dde12309344e2a61699676e4265614 Mon Sep 17 00:00:00 2001 From: John Andrews Date: Mon, 25 Apr 2022 17:07:42 +1200 Subject: [PATCH 4/7] FFMPEG Builder: Add Audio Track now will reuse and copy a stream if a matching one is found --- .../Audio/FfmpegBuilderAudioAddTrack.cs | 91 ++++++++-- .../FfmpegBuilder_AddAudioTests.cs | 169 ++++++++++++++++++ 2 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_AddAudioTests.cs diff --git a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioAddTrack.cs b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioAddTrack.cs index 288f958f..53b8ca73 100644 --- a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioAddTrack.cs +++ b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioAddTrack.cs @@ -97,27 +97,32 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode var audio = new FfmpegAudioStream(); -#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - var bestAudio = Model.AudioStreams.Where(x => System.Text.Json.JsonSerializer.Serialize(x.Stream).ToLower().Contains("commentary") == false) -#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - .OrderBy(x => + var bestAudio = GetBestAudioTrack(args, Model.AudioStreams.Select(x => x.Stream)); + if (bestAudio == null) { - if (Language != string.Empty) - { - args.Logger?.ILog("Language: " + x.Stream.Language, x); - if (string.IsNullOrEmpty(x.Stream.Language)) - return 50; // no language specified - if (x.Stream.Language?.ToLower() != Language) - return 100; // low priority not the desired language - } - return 0; - }) - .ThenByDescending(x => x.Stream.Channels) - .ThenBy(x => x.Index) - .FirstOrDefault(); - audio.Stream = bestAudio.Stream; + args.Logger.WLog("No source audio track found"); + return -1; + } - audio.EncodingParameters.AddRange(GetNewAudioTrackParameters("0:a:" + (bestAudio.Stream.TypeIndex))); + audio.Stream = bestAudio; + + bool directCopy = false; + if(bestAudio.Codec.ToLower() == this.Codec.ToLower()) + { + if(this.Channels == 0 || this.Channels == bestAudio.Channels) + { + directCopy = true; + } + } + + if (directCopy) + { + args.Logger?.ILog($"Source audio is already in appropriate format, just copying that track: {bestAudio.IndexString}, Channels: {bestAudio.Channels}, Codec: {bestAudio.Codec}"); + } + else + { + audio.EncodingParameters.AddRange(GetNewAudioTrackParameters("0:a:" + (bestAudio.TypeIndex))); + } if (Index > Model.AudioStreams.Count - 1) Model.AudioStreams.Add(audio); else @@ -126,6 +131,54 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode return 1; } + internal AudioStream GetBestAudioTrack(NodeParameters args, IEnumerable streams) + { +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + var bestAudio = streams.Where(x => System.Text.Json.JsonSerializer.Serialize(x).ToLower().Contains("commentary") == false) +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + .OrderBy(x => + { + if (Language != string.Empty) + { + args.Logger?.ILog("Language: " + x.Language, x); + if (string.IsNullOrEmpty(x.Language)) + return 50; // no language specified + if (x.Language.ToLower() != Language) + return 100; // low priority not the desired language + } + return 0; + }) + .ThenByDescending(x => { + if(this.Channels == 2) + { + if (x.Channels == 2) + return 1_000_000_000; + // compare codecs + if (x.Codec?.ToLower() == this.Codec?.ToLower()) + return 1_000_000; + } + if(this.Channels == 1) + { + if (x.Channels == 1) + return 1_000_000_000; + if (x.Channels <= 2.1f) + return 5_000_000; + if (x.Codec?.ToLower() == this.Codec?.ToLower()) + return 1_000_000; + } + + // now we want best channels, but to prefer matching codec + if (x.Codec?.ToLower() == this.Codec?.ToLower()) + { + return 1_000 + x.Channels; + } + return x.Channels; + }) + .ThenBy(x => x.Index) + .FirstOrDefault(); + return bestAudio; + } + private string[] GetNewAudioTrackParameters(string source) { diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_AddAudioTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_AddAudioTests.cs new file mode 100644 index 00000000..fa21a374 --- /dev/null +++ b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_AddAudioTests.cs @@ -0,0 +1,169 @@ +#if(DEBUG) + +using FileFlows.VideoNodes.FfmpegBuilderNodes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using VideoNodes.Tests; + +namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests; + +[TestClass] +public class FfmpegBuilder_AddAudioTests +{ + VideoInfo vii; + NodeParameters args; + private void Prepare() + { + const string file = @"D:\videos\unprocessed\basic.mkv"; + var logger = new TestLogger(); + const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; + var vi = new VideoInfoHelper(ffmpeg, logger); + vii = vi.Read(file); + vii.AudioStreams = new List + { + new AudioStream + { + Index = 2, + IndexString = "0:a:0", + Language = "en", + Codec = "AC3", + Channels = 5.1f + }, + new AudioStream + { + Index = 3, + IndexString = "0:a:1", + Language = "en", + Codec = "AAC", + Channels = 2 + }, + new AudioStream + { + Index = 4, + IndexString = "0:a:3", + Language = "en", + Codec = "AAC", + Channels = 2 + }, + new AudioStream + { + Index = 5, + IndexString = "0:a:4", + Language = "en", + Codec = "AAC", + Channels = 5.1f + } + }; + args = new NodeParameters(file, logger, false, string.Empty); + args.GetToolPathActual = (string tool) => ffmpeg; + args.TempPath = @"D:\videos\temp"; + args.Parameters.Add("VideoInfo", vii); + + + FfmpegBuilderStart ffStart = new(); + Assert.AreEqual(1, ffStart.Execute(args)); + } + + [TestMethod] + public void FfmpegBuilder_AddAudio_AacStereo() + { + Prepare(); + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "aac"; + ffAddAudio.Channels = 2; + var best = ffAddAudio.GetBestAudioTrack(args, vii.AudioStreams); + + Assert.IsNotNull(best); + + Assert.AreEqual(3, best.Index); + Assert.AreEqual("AAC", best.Codec); + Assert.AreEqual(2f, best.Channels); + } + + [TestMethod] + public void FfmpegBuilder_AddAudio_AacSameAsSource() + { + Prepare(); + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "aac"; + ffAddAudio.Channels = 0; + var best = ffAddAudio.GetBestAudioTrack(args, vii.AudioStreams); + + Assert.IsNotNull(best); + + Assert.AreEqual(5, best.Index); + Assert.AreEqual("AAC", best.Codec); + Assert.AreEqual(5.1f, best.Channels); + } + + [TestMethod] + public void FfmpegBuilder_AddAudio_Ac3SameAsSource() + { + Prepare(); + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "ac3"; + ffAddAudio.Channels = 0; + ffAddAudio.Index = 1; + var best = ffAddAudio.GetBestAudioTrack(args, vii.AudioStreams); + + Assert.IsNotNull(best); + + Assert.AreEqual(2, best.Index); + Assert.AreEqual("AC3", best.Codec); + Assert.AreEqual(5.1f, best.Channels); + } + + [TestMethod] + public void FfmpegBuilder_AddAudio_DtsSame() + { + Prepare(); + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "dts"; + ffAddAudio.Channels = 0; + var best = ffAddAudio.GetBestAudioTrack(args, vii.AudioStreams); + + Assert.IsNotNull(best); + + Assert.AreEqual(2, best.Index); + Assert.AreEqual("AC3", best.Codec); + Assert.AreEqual(5.1f, best.Channels); + } + + [TestMethod] + public void FfmpegBuilder_AddAudio_DtsStereo() + { + Prepare(); + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "dts"; + ffAddAudio.Channels = 2; + var best = ffAddAudio.GetBestAudioTrack(args, vii.AudioStreams); + + Assert.IsNotNull(best); + + Assert.AreEqual(3, best.Index); + Assert.AreEqual("AAC", best.Codec); + Assert.AreEqual(2f, best.Channels); + } + [TestMethod] + public void FfmpegBuilder_AddAudio_DtsMono() + { + Prepare(); + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "dts"; + ffAddAudio.Channels = 1; + var best = ffAddAudio.GetBestAudioTrack(args, vii.AudioStreams); + + Assert.IsNotNull(best); + + Assert.AreEqual(3, best.Index); + Assert.AreEqual("AAC", best.Codec); + Assert.AreEqual(2f, best.Channels); + } +} + +#endif \ No newline at end of file From b5812009a1513b1ad50d5c326f03c4047e20739d Mon Sep 17 00:00:00 2001 From: John Andrews Date: Tue, 26 Apr 2022 21:37:12 +1200 Subject: [PATCH 5/7] Added FFMPEG Builder Video Bitrate. Added ability to adjust probe size --- .../FfmpegBuilderExecutor.cs | 3 + .../Models/FfmpegVideoStream.cs | 16 ++- .../Video/FfmpegBuilderVideoBitrate.cs | 54 ++++++++++ VideoNodes/InputNodes/VideoFile.cs | 9 ++ .../FfmpegBuilder_AddAudioTests.cs | 1 + .../FfmpegBuilder_BasicTests.cs | 101 ++++++++++++++++++ VideoNodes/VideoInfoHelper.cs | 53 +++++++-- VideoNodes/VideoNodes.en.json | 17 +++ VideoNodes/VideoNodes/EncodingNode.cs | 2 +- 9 files changed, 243 insertions(+), 13 deletions(-) create mode 100644 VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoBitrate.cs diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs index 0f0ec6d5..343ea890 100644 --- a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs @@ -62,6 +62,9 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes else model.InputFiles[0] = args.WorkingFile; + startArgs.AddRange(new[] { + "-probesize", VideoInfoHelper.ProbeSize + "M" + }); if (HardwareDecoding) { diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs index d10ab753..774f0f68 100644 --- a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs @@ -50,7 +50,17 @@ _OptionalEncodingParameters = value ?? new List(); } } - public override bool HasChange => EncodingParameters.Any() || Filter.Any(); + private List _AdditionalParameters = new List(); + public List AdditionalParameters + { + get => _AdditionalParameters; + set + { + _AdditionalParameters = value ?? new List(); + } + } + public override bool HasChange => EncodingParameters.Any() || Filter.Any() || AdditionalParameters.Any(); + public override string[] GetParameters(int outputIndex) { @@ -58,7 +68,7 @@ return new string[] { }; var results = new List { "-map", "0:v:" + outputIndex }; - if (Filter.Any() == false && EncodingParameters.Any() == false) + if (Filter.Any() == false && EncodingParameters.Any() == false && AdditionalParameters.Any() == false) { results.Add("-c:v:" + Stream.TypeIndex); results.Add("copy"); @@ -78,6 +88,8 @@ // we need to set this codec since a filter will be applied, so we cant copy it. //results.Add("copy"); } + if (AdditionalParameters.Any()) + results.AddRange(AdditionalParameters.Select(x => x.Replace("{index}", outputIndex.ToString()))); if (Filter.Any() || OptionalFilter.Any()) { diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoBitrate.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoBitrate.cs new file mode 100644 index 00000000..00e63549 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoBitrate.cs @@ -0,0 +1,54 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes; + +public class FfmpegBuilderVideoBitrate : FfmpegBuilderNode +{ + public override string HelpUrl => "https://github.com/revenz/FileFlows/wiki/FFMPEG-Builder:-Video-Bitrate"; + + /// + /// Gets or sets the bitrate in K + /// + [NumberInt(1)] + [DefaultValue(3000)] + public float Bitrate { get; set; } + + + public override int Execute(NodeParameters args) + { + base.Init(args); + + var video = Model.VideoStreams?.Where(x => x.Deleted == false)?.FirstOrDefault(); + if (video?.Stream == null) + { + args.Logger?.ELog("No video stream found"); + return -1; + } + + + + if(Bitrate < 0) + { + args.Logger?.ELog("Minimum birate not set"); + return -1; + } + int minimum = (int)(Bitrate * 0.75f); + int maximum = (int)(Bitrate * 1.25f); + + float currentBitrate = (int)(video.Stream.Bitrate / 1024f); + if(currentBitrate <= 0) + { + // need to work it out + currentBitrate = (float)(args.WorkingFileSize / video.Stream.Duration.TotalSeconds); + // rough estimate of 75% of the file is video + currentBitrate *= 0.75f; + } + video.AdditionalParameters.AddRange(new[] + { + "-b:v:{index}", Bitrate + "k", + "-minrate", minimum + "k", + "-maxrate", maximum + "k", + "-bufsize", currentBitrate + "k" + }); + + return 1; + } +} diff --git a/VideoNodes/InputNodes/VideoFile.cs b/VideoNodes/InputNodes/VideoFile.cs index fa9f823b..b203a3e9 100644 --- a/VideoNodes/InputNodes/VideoFile.cs +++ b/VideoNodes/InputNodes/VideoFile.cs @@ -9,6 +9,13 @@ namespace FileFlows.VideoNodes public override int Outputs => 1; public override FlowElementType Type => FlowElementType.Input; + public override bool NoEditorOnAdd => true; + + [DefaultValue(25)] + [NumberInt(1)] + [Range(5, 1000)] + public int ProbeSize { get; set; } + private Dictionary _Variables; public override Dictionary Variables => _Variables; public VideoFile() @@ -48,6 +55,8 @@ namespace FileFlows.VideoNodes if (string.IsNullOrEmpty(ffmpegExe)) return -1; + VideoInfoHelper.ProbeSize = this.ProbeSize; + try { diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_AddAudioTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_AddAudioTests.cs index fa21a374..4371582a 100644 --- a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_AddAudioTests.cs +++ b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_AddAudioTests.cs @@ -148,6 +148,7 @@ public class FfmpegBuilder_AddAudioTests Assert.AreEqual("AAC", best.Codec); Assert.AreEqual(2f, best.Channels); } + [TestMethod] public void FfmpegBuilder_AddAudio_DtsMono() { diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs index b500103f..db982ae4 100644 --- a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs +++ b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs @@ -813,6 +813,107 @@ namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests Assert.AreEqual(1, result); } + + [TestMethod] + public void FfmpegBuilder_VideoBitrate() + { + const string file = @"D:\videos\unprocessed\basic.mkv"; + var logger = new TestLogger(); + const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; + var vi = new VideoInfoHelper(ffmpeg, logger); + var vii = vi.Read(file); + var args = new NodeParameters(file, logger, false, string.Empty); + args.GetToolPathActual = (string tool) => ffmpeg; + args.TempPath = @"D:\videos\temp"; + args.Parameters.Add("VideoInfo", vii); + + + FfmpegBuilderStart ffStart = new(); + Assert.AreEqual(1, ffStart.Execute(args)); + + FfmpegBuilderVideoBitrate ffBitrate = new(); + ffBitrate.Bitrate = 1_000; + ffBitrate.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + ffExecutor.HardwareDecoding = true; + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + [TestMethod] + public void FfmpegBuilder_VideoCodecAndBitrate() + { + const string file = @"D:\videos\unprocessed\basic.mkv"; + var logger = new TestLogger(); + const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; + var vi = new VideoInfoHelper(ffmpeg, logger); + var vii = vi.Read(file); + var args = new NodeParameters(file, logger, false, string.Empty); + args.GetToolPathActual = (string tool) => ffmpeg; + args.TempPath = @"D:\videos\temp"; + args.Parameters.Add("VideoInfo", vii); + + + FfmpegBuilderStart ffStart = new(); + Assert.AreEqual(1, ffStart.Execute(args)); + + FfmpegBuilderVideoCodec ffEncode = new(); + ffEncode.VideoCodec = "h264"; + ffEncode.Force = true; + ffEncode.Execute(args); + + FfmpegBuilderVideoBitrate ffBitrate = new(); + ffBitrate.Bitrate = 1_000; + ffBitrate.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + ffExecutor.HardwareDecoding = true; + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + [TestMethod] + public void FfmpegBuilder_FF43() + { + const string file = @"D:\videos\testfiles\ff-43.ts"; + var logger = new TestLogger(); + const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; + var vi = new VideoInfoHelper(ffmpeg, logger); + var vii = vi.Read(file); + var args = new NodeParameters(file, logger, false, string.Empty); + args.GetToolPathActual = (string tool) => ffmpeg; + args.TempPath = @"D:\videos\temp"; + args.Parameters.Add("VideoInfo", vii); + + + FfmpegBuilderStart ffStart = new(); + Assert.AreEqual(1, ffStart.Execute(args)); + + FfmpegBuilderAudioTrackRemover ffRemoveAudio = new(); + ffRemoveAudio.RemoveAll = true; + ffRemoveAudio.Execute(args); + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "ac3"; + ffAddAudio.Index = 0; + ffAddAudio.Execute(args); + + FfmpegBuilderAudioAddTrack ffAddAudio2 = new(); + ffAddAudio2.Codec = "aac"; + ffAddAudio2.Index = 1; + ffAddAudio2.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } } } diff --git a/VideoNodes/VideoInfoHelper.cs b/VideoNodes/VideoInfoHelper.cs index c3f78a52..3a186b3e 100644 --- a/VideoNodes/VideoInfoHelper.cs +++ b/VideoNodes/VideoInfoHelper.cs @@ -16,6 +16,21 @@ namespace FileFlows.VideoNodes 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; @@ -48,9 +63,16 @@ namespace FileFlows.VideoNodes process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.StartInfo.CreateNoWindow = true; - process.StartInfo.ArgumentList.Add("-hide_banner"); - process.StartInfo.ArgumentList.Add("-i"); - process.StartInfo.ArgumentList.Add(filename); + 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(); @@ -243,14 +265,25 @@ namespace FileFlows.VideoNodes 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() ?? ""; - //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]+)?")) + if (info.IndexOf("0 channels") >= 0) { - audio.Channels = float.Parse(Regex.Match(parts[2], @"^[\d]+(\.[\d]+)?").Value); + 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); diff --git a/VideoNodes/VideoNodes.en.json b/VideoNodes/VideoNodes.en.json index 8d968fee..91970bcd 100644 --- a/VideoNodes/VideoNodes.en.json +++ b/VideoNodes/VideoNodes.en.json @@ -119,6 +119,11 @@ "Description": "An input video file that has had its VideoInformation read and can be processed", "Outputs": { "1": "Video file from library" + }, + "Fields": { + "ProbeSize": "Probe Size", + "ProbeSize-Suffix": "MB", + "ProbeSize-Help": "The probe size to use in FFMPEG when executing." } }, "DetectBlackBars": { @@ -373,6 +378,18 @@ "1": "FFMPEG Builder video streams set to encode in 10 Bit" } }, + "FfmpegBuilderVideoBitrate": { + "Label": "FFMPEG Builder: Video Bitrate", + "Description": "Sets FFMPEG Builder to encode the video given the bitrate", + "Outputs": { + "1": "FFMPEG Builder video streams updated" + }, + "Fields": { + "Bitrate": "Bitrate", + "Bitrate-Suffix": "KB", + "Bitrate-Help": "The target bitrate of the video in kilobytes" + } + }, "FfmpegBuilderVideoCodec": { "Label": "FFMPEG Builder: Video Codec", "Description": "Sets FFMPEG Builder to encode the video streams in the specified codec", diff --git a/VideoNodes/VideoNodes/EncodingNode.cs b/VideoNodes/VideoNodes/EncodingNode.cs index 33a9c1e6..512c677e 100644 --- a/VideoNodes/VideoNodes/EncodingNode.cs +++ b/VideoNodes/VideoNodes/EncodingNode.cs @@ -185,7 +185,7 @@ namespace FileFlows.VideoNodes }).Result; if (cmd.ExitCode != 0 || string.IsNullOrWhiteSpace(cmd.Output) == false) { - args.Logger?.WLog($"Cant prcoess '{encodingParams}': {cmd.Output ?? ""}"); + args.Logger?.WLog($"Cant process '{encodingParams}': {cmd.Output ?? ""}"); return false; } return true; From aa1933a4243f7b6b32343118e45db48234074767 Mon Sep 17 00:00:00 2001 From: John Andrews Date: Thu, 28 Apr 2022 13:39:25 +1200 Subject: [PATCH 6/7] increaed version number of plugins --- build/buildplugins.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/buildplugins.ps1 b/build/buildplugins.ps1 index 0dd0788e..fcfd5a01 100644 --- a/build/buildplugins.ps1 +++ b/build/buildplugins.ps1 @@ -23,7 +23,7 @@ $output = $output | Resolve-Path Remove-Item Builds -Recurse -ErrorAction SilentlyContinue $revision = (git rev-list --count --first-parent HEAD) -join "`n" -$version = "0.5.3.$revision" +$version = "0.5.4.$revision" $json = "[`n" From 7a9f5b8be30394e5120b8bda424889dc1b0e50ad Mon Sep 17 00:00:00 2001 From: John Andrews Date: Wed, 4 May 2022 19:05:18 +1200 Subject: [PATCH 7/7] updated hevc_qsv default parameters --- VideoNodes/VideoNodes/EncodingNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VideoNodes/VideoNodes/EncodingNode.cs b/VideoNodes/VideoNodes/EncodingNode.cs index 512c677e..3230aa58 100644 --- a/VideoNodes/VideoNodes/EncodingNode.cs +++ b/VideoNodes/VideoNodes/EncodingNode.cs @@ -101,7 +101,7 @@ namespace FileFlows.VideoNodes if(vidparams.ToLower() == "hevc" || vidparams.ToLower() == "h265") { // try find best hevc encoder - foreach(string vidparam in new [] { "hevc_nvenc -preset hq", "hevc_qsv -load_plugin hevc_hw", "hevc_amf", "hevc_vaapi" }) + foreach(string vidparam in new [] { "hevc_nvenc -preset hq", "hevc_qsv -global_quality 28 -load_plugin hevc_hw", "hevc_amf", "hevc_vaapi" }) { bool canProcess = CanProcessEncoder(ffmpeg, vidparam); if (canProcess)