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;