diff --git a/FileFlows.Plugin.dll b/FileFlows.Plugin.dll index 111075da..f3efaf01 100644 Binary files a/FileFlows.Plugin.dll and b/FileFlows.Plugin.dll differ diff --git a/FileFlows.Plugin.pdb b/FileFlows.Plugin.pdb index a8cbfced..341340c6 100644 Binary files a/FileFlows.Plugin.pdb and b/FileFlows.Plugin.pdb differ diff --git a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioAddTrack.cs b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioAddTrack.cs new file mode 100644 index 00000000..46a88d34 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioAddTrack.cs @@ -0,0 +1,173 @@ +using FileFlows.VideoNodes.FfmpegBuilderNodes.Models; + +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderAudioAddTrack: FfmpegBuilderNode + { + public override string Icon => "fas fa-volume-off"; + + [NumberInt(1)] + [Range(1, 100)] + [DefaultValue(2)] + public int Index { get; set; } + + + [DefaultValue("aac")] + [Select(nameof(CodecOptions), 1)] + public string Codec { get; set; } + + private static List _CodecOptions; + public static List CodecOptions + { + get + { + if (_CodecOptions == null) + { + _CodecOptions = new List + { + new ListOption { Label = "AAC", Value = "aac"}, + new ListOption { Label = "AC3", Value = "ac3"}, + new ListOption { Label = "EAC3", Value = "eac3" }, + new ListOption { Label = "MP3", Value = "mp3"}, + }; + } + return _CodecOptions; + } + } + + [DefaultValue(2f)] + [Select(nameof(ChannelsOptions), 2)] + public float Channels { get; set; } + + private static List _ChannelsOptions; + public static List ChannelsOptions + { + get + { + if (_ChannelsOptions == null) + { + _ChannelsOptions = new List + { + new ListOption { Label = "Same as source", Value = 0}, + new ListOption { Label = "Mono", Value = 1f}, + new ListOption { Label = "Stereo", Value = 2f} + }; + } + return _ChannelsOptions; + } + } + + [Select(nameof(BitrateOptions), 3)] + public int Bitrate { get; set; } + + private static List _BitrateOptions; + public static List BitrateOptions + { + get + { + if (_BitrateOptions == null) + { + _BitrateOptions = new List + { + new ListOption { Label = "Automatic", Value = 0}, + new ListOption { Label = "64 Kbps", Value = 64}, + new ListOption { Label = "96 Kbps", Value = 96}, + new ListOption { Label = "128 Kbps", Value = 128}, + new ListOption { Label = "160 Kbps", Value = 160}, + new ListOption { Label = "192 Kbps", Value = 192}, + new ListOption { Label = "224 Kbps", Value = 224}, + new ListOption { Label = "256 Kbps", Value = 256}, + new ListOption { Label = "288 Kbps", Value = 288}, + new ListOption { Label = "320 Kbps", Value = 320}, + }; + } + return _BitrateOptions; + } + } + + [DefaultValue("eng")] + [TextVariable(4)] + public string Language { get; set; } + + public override int Execute(NodeParameters args) + { + base.Init(args); + + var audio = new FfmpegAudioStream(); + audio.Stream = Model.AudioStreams[0].Stream; + +#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 => + { + 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.EncodingParameters.AddRange(GetNewAudioTrackParameters("0:a:" + (bestAudio.Stream.TypeIndex - 1))); + if (Index > Model.AudioStreams.Count) + Model.AudioStreams.Add(audio); + else + Model.AudioStreams.Insert(Math.Max(0, Index - 1), audio); + + return 1; + } + + + private string[] GetNewAudioTrackParameters(string source) + { + if (Channels == 0) + { + // same as source + if (Bitrate == 0) + { + return new[] + { + "-map", source, + "-c:a:{index}", + Codec + }; + } + return new[] + { + "-map", source, + "-c:a:{index}", + Codec, + "-b:a:{index}", Bitrate + "k" + }; + } + else + { + if (Bitrate == 0) + { + return new[] + { + "-map", source, + "-c:a:{index}", + Codec, + "-ac", Channels.ToString() + }; + } + return new[] + { + "-map", source, + "-c:a:{index}", + Codec, + "-ac", Channels.ToString(), + "-b:a:{index}", Bitrate + "k" + }; + } + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioTrackRemover.cs b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioTrackRemover.cs new file mode 100644 index 00000000..81e55524 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioTrackRemover.cs @@ -0,0 +1,54 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderAudioTrackRemover: FfmpegBuilderNode + { + public override string Icon => "fas fa-volume-off"; + + public override int Outputs => 2; + + [Boolean(1)] + public bool RemoveAll { get; set; } + + + [TextVariable(2)] + [ConditionEquals(nameof(RemoveAll), false)] + public string Pattern { get; set; } + + [Boolean(3)] + [ConditionEquals(nameof(RemoveAll), false)] + public bool NotMatching { get; set; } + + [Boolean(4)] + [ConditionEquals(nameof(RemoveAll), false)] + public bool UseLanguageCode { get; set; } + + public override int Execute(NodeParameters args) + { + this.Init(args); + bool removing = false; + var regex = new Regex(this.Pattern, RegexOptions.IgnoreCase); + foreach(var audio in Model.AudioStreams) + { + if (RemoveAll) + { + audio.Deleted = true; + removing = true; + continue; + } + string str = UseLanguageCode ? audio.Stream.Language : audio.Stream.Title; + if (string.IsNullOrEmpty(str) == false) // if empty we always use this since we have no info to go on + { + bool matches = regex.IsMatch(str); + if (NotMatching) + matches = !matches; + if (matches) + { + audio.Deleted = true; + removing = true; + } + } + } + return removing ? 1 : 2; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs new file mode 100644 index 00000000..3adbf07d --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs @@ -0,0 +1,51 @@ +using FileFlows.Plugin; +using FileFlows.VideoNodes.FfmpegBuilderNodes.Models; + +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderExecutor: FfmpegBuilderNode + { + public override string Icon => "far fa-file-video"; + public override int Inputs => 1; + public override int Outputs => 2; + public override FlowElementType Type => FlowElementType.BuildEnd; + + public override int Execute(NodeParameters args) + { + this.Init(args); + var model = this.Model; + List ffArgs = new List(); + bool hasChange = false; + int actualIndex = 0; + int currentType = 0; + foreach (var item in model.VideoStreams.Select((x, index) => (stream: (FfmpegStream)x, index, type: 1)).Union( + model.AudioStreams.Select((x, index) => (stream: (FfmpegStream)x, index, type: 2))).Union( + model.SubtitleStreams.Select((x, index) => (stream: (FfmpegStream)x, index, type: 3)))) + { + if (item.stream.Deleted) + { + hasChange = true; + continue; + } + if (currentType != item.type) + { + actualIndex = 0; + currentType = item.type; + } + ffArgs.AddRange(item.stream.GetParameters(actualIndex)); + hasChange |= item.stream.HasChange; + ++actualIndex; + } + + if (hasChange == false && (string.IsNullOrWhiteSpace(model.Extension) || args.WorkingFile.ToLower().EndsWith("." + model.Extension.ToLower()))) + return 2; // nothing to do + + string extension = model.Extension?.EmptyAsNull() ?? "mkv"; + + if (Encode(args, ffmpegExe, ffArgs, extension) == false) + return -1; + + return 1; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderNode.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderNode.cs new file mode 100644 index 00000000..cc6d81e2 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderNode.cs @@ -0,0 +1,62 @@ +using FileFlows.Plugin; +using FileFlows.VideoNodes.FfmpegBuilderNodes.Models; + +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public abstract class FfmpegBuilderNode: EncodingNode + { + private const string MODEL_KEY = "FFMPEG_BUILDER_MODEL"; + protected string ffmpegExe; + + public override int Inputs => 1; + public override int Outputs => 1; + public override string Icon => "far fa-file-video"; + public override FlowElementType Type => FlowElementType.BuildPart; + + protected void Init(NodeParameters args) + { + this.args = args; + this.ffmpegExe = GetFFMpegExe(args); + if (string.IsNullOrEmpty(ffmpegExe)) + throw new Exception("FFMPEG not found"); + + if (Model == null) + throw new Exception("FFMPEG Builder Model not set, use the \"FFMPEG Builder Start\" node to first"); + } + + public override int Execute(NodeParameters args) + { + return 1; + } + + protected FfmpegModel Model + { + get + { + if (args.Variables.ContainsKey(MODEL_KEY)) + return args.Variables[MODEL_KEY] as FfmpegModel; + return null; + } + set + { + if (args.Variables.ContainsKey(MODEL_KEY)) + { + if (value == null) + args.Variables.Remove(MODEL_KEY); + else + args.Variables[MODEL_KEY] = value; + } + else if(value != null) + args.Variables.Add(MODEL_KEY, value); + } + } + + protected string[] SplitCommand(string cmd) + { + return Regex.Matches(cmd, @"[\""].+?[\""]|[^ ]+") + .Cast() + .Select(x => x.Value.Trim('"')) + .ToArray(); + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderStart.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderStart.cs new file mode 100644 index 00000000..93326014 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderStart.cs @@ -0,0 +1,24 @@ +using FileFlows.Plugin; + +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderStart: FfmpegBuilderNode + { + public override int Inputs => 1; + public override int Outputs => 1; + public override string Icon => "far fa-file-video"; + public override FlowElementType Type => FlowElementType.BuildStart; + + public override int Execute(NodeParameters args) + { + this.args = args; + VideoInfo videoInfo = GetVideoInfo(args); + if (videoInfo == null) + return -1; + + this.Model = Models.FfmpegModel.CreateModel(videoInfo); + this.Init(args); + return 1; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs new file mode 100644 index 00000000..1e1ca200 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs @@ -0,0 +1,36 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes.Models +{ + public class FfmpegAudioStream:FfmpegStream + { + public AudioStream Stream { get; set; } + public override bool HasChange => false; + + private List _EncodingParameters = new List(); + public List EncodingParameters + { + get => _EncodingParameters; + set + { + _EncodingParameters = value ?? new List(); + } + } + public override string[] GetParameters(int outputIndex) + { + if (Deleted) + return new string[] { }; + + var results = new List { "-map", "0:a:" + (Stream.TypeIndex - 1), "-c:a:" + outputIndex }; + if (EncodingParameters.Any() == false) + { + results.Add("copy"); + return results.ToArray(); + } + if (EncodingParameters[0] == "-map") + results.Clear(); + + results.AddRange(EncodingParameters.Select(x => x.Replace("{index}", outputIndex.ToString()))); + + return results.ToArray(); + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegModel.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegModel.cs new file mode 100644 index 00000000..59a9218c --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegModel.cs @@ -0,0 +1,57 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes.Models +{ + public class FfmpegModel + { + private List _VideoStreams = new List(); + public List VideoStreams + { + get => _VideoStreams; + set => _VideoStreams = value ?? new List(); + } + private List _AudioStreams = new List(); + public List AudioStreams + { + get => _AudioStreams; + set => _AudioStreams = value ?? new List(); + } + private List _SubtitleStreams = new List(); + public List SubtitleStreams + { + get => _SubtitleStreams; + set => _SubtitleStreams = value ?? new List(); + } + + public string Extension { get; set; } + + + internal static FfmpegModel CreateModel(VideoInfo info) + { + var model = new FfmpegModel(); + foreach (var item in info.VideoStreams.Select((stream, index) => (stream, index))) + { + model.VideoStreams.Add(new FfmpegVideoStream + { + Index = item.index, + Stream = item.stream, + }); + } + foreach (var item in info.AudioStreams.Select((stream, index) => (stream, index))) + { + model.AudioStreams.Add(new FfmpegAudioStream + { + Index = item.index, + Stream = item.stream, + }); + } + foreach (var item in info.SubtitleStreams.Select((stream, index) => (stream, index))) + { + model.SubtitleStreams.Add(new FfmpegSubtitleStream + { + Index = item.index, + Stream = item.stream, + }); + } + return model; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegStream.cs new file mode 100644 index 00000000..cf469ba4 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegStream.cs @@ -0,0 +1,12 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes.Models +{ + public abstract class FfmpegStream + { + public bool Deleted { get; set; } + public int Index { get; set; } + + public abstract bool HasChange { get; } + + public abstract string[] GetParameters(int outputIndex); + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs new file mode 100644 index 00000000..e05cb3f3 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs @@ -0,0 +1,22 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes.Models +{ + public class FfmpegSubtitleStream : FfmpegStream + { + public SubtitleStream Stream { get; set; } + + public override bool HasChange => false; + + public override string[] GetParameters(int outputIndex) + { + if (Deleted) + return new string[] { }; + + var results = new List { "-map", "0:s:" + outputIndex, "-c:s:" + (Stream.TypeIndex - 1) }; + //if (EncodingParameters.Any() == false) + { + results.Add("copy"); + return results.ToArray(); + } + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs new file mode 100644 index 00000000..f915afd4 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs @@ -0,0 +1,61 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes.Models +{ + public class FfmpegVideoStream : FfmpegStream + { + public VideoStream Stream { get; set; } + + private List _Filter = new List(); + public List Filter + { + get => _Filter; + set + { + _Filter = value ?? new List(); + } + } + + private List _EncodingParameters = new List(); + public List EncodingParameters + { + get => _EncodingParameters; + set + { + _EncodingParameters = value ?? new List(); + } + } + public override bool HasChange => EncodingParameters.Any() || Filter.Any(); + + public override string[] GetParameters(int outputIndex) + { + if (Deleted) + return new string[] { }; + + var results = new List { "-map", "0:v:" + outputIndex }; + if (Filter.Any() == false && EncodingParameters.Any() == false) + { + results.Add("-c:v:" + Stream.TypeIndex); + results.Add("copy"); + return results.ToArray(); + } + + if (EncodingParameters.Any()) + { + results.Add("-c:v:" + Stream.TypeIndex); + results.AddRange(EncodingParameters.Select(x => x.Replace("{index}", outputIndex.ToString()))); + } + else + { + // we need to set this codec since a filter will be applied, so we cant copy it. + //results.Add("copy"); + } + + if (Filter.Any()) + { + results.Add("-vf"); + results.Add(String.Join(", ", Filter)); + } + + return results.ToArray(); + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Subtitle/FfmpegBuilderSubtitleFormatRemover.cs b/VideoNodes/FfmpegBuilderNodes/Subtitle/FfmpegBuilderSubtitleFormatRemover.cs new file mode 100644 index 00000000..e8c1f6c2 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Subtitle/FfmpegBuilderSubtitleFormatRemover.cs @@ -0,0 +1,77 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderSubtitleFormatRemover : FfmpegBuilderNode + { + public override string Icon => "fas fa-comment"; + public override int Outputs => 2; + + [Boolean(1)] + public bool RemoveAll { get; set; } + + [Checklist(nameof(Options), 2)] + public List SubtitlesToRemove { get; set; } + + private static List _Options; + public static List Options + { + get + { + if (_Options == null) + { + _Options = new List + { + new ListOption { Value = "mov_text", Label = "3GPP Timed Text subtitle"}, + new ListOption { Value = "ssa", Label = "ASS (Advanced SubStation Alpha) subtitle (codec ass)"}, + new ListOption { Value = "ass", Label = "ASS (Advanced SubStation Alpha) subtitle"}, + new ListOption { Value = "xsub", Label = "DivX subtitles (XSUB)" }, + new ListOption { Value = "dvbsub", Label = "DVB subtitles (codec dvb_subtitle)"}, + new ListOption { Value = "dvdsub", Label = "DVD subtitles (codec dvd_subtitle)"}, + new ListOption { Value = "dvb_teletext", Label = "DVB/Teletext Format"}, + new ListOption { Value = "text", Label = "Raw text subtitle"}, + new ListOption { Value = "subrip", Label = "SubRip subtitle"}, + new ListOption { Value = "srt", Label = "SubRip subtitle (codec subrip)"}, + new ListOption { Value = "ttml", Label = "TTML subtitle"}, + new ListOption { Value = "webvtt", Label = "WebVTT subtitle"}, + }; + } + return _Options; + } + } + + + public override int Execute(NodeParameters args) + { + this.Init(args); + + if (RemoveAll) + { + if (Model.SubtitleStreams.Any() == false) + return 2; + foreach (var stream in Model.SubtitleStreams) + stream.Deleted = true; + return 1; + } + + + var removeCodecs = SubtitlesToRemove?.Where(x => string.IsNullOrWhiteSpace(x) == false)?.Select(x => x.ToLower())?.ToList() ?? new List(); + + if (removeCodecs.Count == 0) + return 2; // nothing to remove + + + bool removing = false; + foreach (var sub in Model.SubtitleStreams) + { + args.Logger?.ILog("Subtitle found: " + sub.Stream.Codec + ", " + sub.Stream.Title); + if (removeCodecs.Contains(sub.Stream.Codec.ToLower())) + { + sub.Deleted = true; + removing = true; + continue; + } + } + + return removing ? 1 : 2; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Subtitle/FfmpegBuilderSubtitleTrackRemover.cs b/VideoNodes/FfmpegBuilderNodes/Subtitle/FfmpegBuilderSubtitleTrackRemover.cs new file mode 100644 index 00000000..db73f971 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Subtitle/FfmpegBuilderSubtitleTrackRemover.cs @@ -0,0 +1,42 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderSubtitleTrackRemover : FfmpegBuilderNode + { + public override string Icon => "fas fa-comment"; + + public override int Outputs => 2; + + + [TextVariable(1)] + public string Pattern { get; set; } + + [Boolean(2)] + public bool NotMatching { get; set; } + + [Boolean(3)] + public bool UseLanguageCode { get; set; } + + public override int Execute(NodeParameters args) + { + this.Init(args); + bool removing = false; + var regex = new Regex(this.Pattern, RegexOptions.IgnoreCase); + foreach(var stream in Model.SubtitleStreams) + { + string str = UseLanguageCode ? stream.Stream.Language : stream.Stream.Title; + if (string.IsNullOrEmpty(str) == false) // if empty we always use this since we have no info to go on + { + bool matches = regex.IsMatch(str); + if (NotMatching) + matches = !matches; + if (matches) + { + stream.Deleted = true; + removing = true; + } + } + } + return removing ? 1 : 2; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Subtitle/FfmpegBuilderUnsupportedMP4Subtitles.cs b/VideoNodes/FfmpegBuilderNodes/Subtitle/FfmpegBuilderUnsupportedMP4Subtitles.cs new file mode 100644 index 00000000..dbcf75b6 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Subtitle/FfmpegBuilderUnsupportedMP4Subtitles.cs @@ -0,0 +1,25 @@ +//namespace FileFlows.VideoNodes.FfmpegBuilderNodes +//{ +// public class FfmpegBuilderUnsupportedMP4Subtitles : FfmpegBuilderNode +// { +// public override string Icon => "fas fa-comment"; + +// public override int Outputs => 2; + +// public override int Execute(NodeParameters args) +// { +// this.Init(args); +// bool removing = false; +// string[] unsupported = new[] { "" }; +// foreach (var stream in Model.SubtitleStreams) +// { +// if (unsupported.Contains(stream.Stream.Codec?.ToLower())) +// { +// stream.Deleted = true; +// removing = true; +// } +// } +// return removing ? 1 : 2; +// } +// } +//} diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderCropBlackBars.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderCropBlackBars.cs new file mode 100644 index 00000000..bf9e1cbf --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderCropBlackBars.cs @@ -0,0 +1,37 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode + { + [NumberInt(1)] + public int CroppingThreshold { get; set; } + public override int Outputs => 2; + public override int Execute(NodeParameters args) + { + base.Init(args); + + string ffmpeg = GetFFMpegExe(args); + if (string.IsNullOrEmpty(ffmpeg)) + return -1; + + var videoInfo = GetVideoInfo(args); + if (videoInfo == null || videoInfo.VideoStreams?.Any() != true) + return -1; + + + string crop = DetectBlackBars.Detect(ffmpeg, videoInfo, args, this.CroppingThreshold); + if (string.IsNullOrWhiteSpace(crop)) + return 2; + + //var parts = crop.Split(':'); + ////parts[2] = "iw-" + parts[2]; + ////parts[3] = "ih-" + parts[3]; + //crop = String.Join(":", parts.Take(2)); + + args.Logger?.ILog("Black bars detected, crop: " + crop); + + var video = Model.VideoStreams[0]; + video.Filter.AddRange(new[] { "crop=" + crop }); + return 1; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderRemuxToMP4.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderRemuxToMP4.cs new file mode 100644 index 00000000..5fc00f23 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderRemuxToMP4.cs @@ -0,0 +1,12 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderRemuxToMP4: FfmpegBuilderNode + { + public override int Execute(NodeParameters args) + { + base.Init(args); + this.Model.Extension = "mp4"; + return 1; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderRemuxToMkv.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderRemuxToMkv.cs new file mode 100644 index 00000000..9b7844cf --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderRemuxToMkv.cs @@ -0,0 +1,12 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderRemuxToMkv: FfmpegBuilderNode + { + public override int Execute(NodeParameters args) + { + base.Init(args); + this.Model.Extension = "mkv"; + return 1; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderScaler.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderScaler.cs new file mode 100644 index 00000000..562967ae --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderScaler.cs @@ -0,0 +1,64 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderScaler : FfmpegBuilderNode + { + [Boolean(2)] + public bool Force { get; set; } + + + [Select(nameof(ResolutionOptions), 1)] + public string Resolution { get; set; } + + + private static List _ResolutionOptions; + public static List ResolutionOptions + { + get + { + if (_ResolutionOptions == null) + { + _ResolutionOptions = new List + { + // we use -2 here so the width is divisible by 2 and automatically scaled to + // the appropriate height, if we forced the height it could be stretched + new ListOption { Value = "640:-2", Label = "480P"}, + new ListOption { Value = "1280:-2", Label = "720P"}, + new ListOption { Value = "1920:-2", Label = "1080P"}, + new ListOption { Value = "3840:-2", Label = "4K" } + }; + } + return _ResolutionOptions; + } + } + public override int Outputs => 2; + public override int Execute(NodeParameters args) + { + base.Init(args); + + string ffmpeg = GetFFMpegExe(args); + if (string.IsNullOrEmpty(ffmpeg)) + return -1; + + var videoInfo = GetVideoInfo(args); + if (videoInfo == null || videoInfo.VideoStreams?.Any() != true) + return -1; + + if (Force == false) + { + var resolution = ResolutionHelper.GetResolution(videoInfo); + if (resolution == ResolutionHelper.Resolution.r1080p && Resolution.StartsWith("1920")) + return 2; + else if (resolution == ResolutionHelper.Resolution.r4k && Resolution.StartsWith("3840")) + return 2; + else if (resolution == ResolutionHelper.Resolution.r720p && Resolution.StartsWith("1280")) + return 2; + else if (resolution == ResolutionHelper.Resolution.r480p && Resolution.StartsWith("640")) + return 2; + } + + Model.VideoStreams[0].Filter.AddRange(new[] { $"scale={Resolution}:flags=lanczos" }); + + return 1; + } + } +} diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoEncode.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoEncode.cs new file mode 100644 index 00000000..afdc685a --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderVideoEncode.cs @@ -0,0 +1,62 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes +{ + public class FfmpegBuilderVideoEncode:FfmpegBuilderNode + { + public override int Outputs => 2; + + [DefaultValue("hevc")] + [TextVariable(1)] + public string VideoCodec { get; set; } + + [DefaultValue("hevc_nvenc -preset hq -crf 23")] + [TextVariable(2)] + public string VideoCodecParameters { get; set; } + + [Boolean(3)] + public bool Force { get; set; } + + public override int Execute(NodeParameters args) + { + base.Init(args); + + string codec = args.ReplaceVariables(VideoCodec ?? string.Empty); + string parameters = args.ReplaceVariables(VideoCodecParameters ?? codec); + + if (string.IsNullOrWhiteSpace(parameters)) + return 1; // nothing to do + + parameters = CheckVideoCodec(ffmpegExe, parameters); + + bool encoding = false; + foreach (var stream in Model.VideoStreams) + { + if(Force == false) + { + if (IsSameVideoCodec(stream.Stream.Codec, this.VideoCodec)) + continue; + } + stream.EncodingParameters.Clear(); + stream.EncodingParameters.AddRange(SplitCommand(parameters)); + encoding = true; + } + return encoding ? 1 : 2; + } + + 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; + } + } + } +} diff --git a/VideoNodes/GlobalUsings.cs b/VideoNodes/GlobalUsings.cs new file mode 100644 index 00000000..8c2a7e49 --- /dev/null +++ b/VideoNodes/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/VideoNodes/LogicalNodes/DetectBlackBars.cs b/VideoNodes/LogicalNodes/DetectBlackBars.cs index 75654e64..6c532cba 100644 --- a/VideoNodes/LogicalNodes/DetectBlackBars.cs +++ b/VideoNodes/LogicalNodes/DetectBlackBars.cs @@ -40,20 +40,8 @@ namespace FileFlows.VideoNodes if (videoInfo == null || videoInfo.VideoStreams?.Any() != true) return -1; - int vidWidth = videoInfo.VideoStreams[0].Width; - int vidHeight = videoInfo.VideoStreams[0].Height; - if (vidWidth < 1) - { - args.Logger?.ELog("Failed to find video width"); - return -1; - } - if (vidHeight < 1) - { - args.Logger?.ELog("Failed to find video height"); - return -1; - } - string crop = Execute(ffmpeg, args.WorkingFile, args, vidWidth, vidHeight); + string crop = Detect(ffmpeg, videoInfo, args, this.CroppingThreshold); if (crop == string.Empty) return 2; @@ -66,7 +54,24 @@ namespace FileFlows.VideoNodes return 1; } - public string Execute(string ffplay, string file, NodeParameters args, int vidWidth, int vidHeight) + public static 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); + } + + public static string Execute(string ffplay, string file, NodeParameters args, int vidWidth, int vidHeight, int threshold) { try { @@ -74,7 +79,7 @@ namespace FileFlows.VideoNodes int y = int.MaxValue; int width = 0; int height = 0; - foreach (int ss in new int[] { 60, 120, 240, 360 }) // check at multiple times + foreach (int ss in new int[] { 60, 100, 240, 360 }) // check at multiple times { using (var process = new Process()) { @@ -127,13 +132,13 @@ namespace FileFlows.VideoNodes if (y == int.MaxValue) y = 0; - if (CroppingThreshold < 0) - CroppingThreshold = 0; + if (threshold < 0) + threshold = 0; args.Logger?.DLog($"Video dimensions: {vidWidth}x{vidHeight}"); - var willCrop = TestAboveThreshold(vidWidth, vidHeight, width, height, CroppingThreshold); - args.Logger?.ILog($"Crop detection, x:{x}, y:{y}, width: {width}, height: {height}, total:{willCrop.diff}, threshold:{CroppingThreshold}, above threshold: {willCrop}"); + 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; } diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs new file mode 100644 index 00000000..c7851974 --- /dev/null +++ b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs @@ -0,0 +1,313 @@ +#if(DEBUG) + +using FileFlows.VideoNodes.FfmpegBuilderNodes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using VideoNodes.Tests; + +namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests +{ + [TestClass] + public class FfmpegBuilder_BasicTests + { + [TestMethod] + public void FfmpegBuilder_AddAc3Aac() + { + 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)); + + FfmpegBuilderVideoEncode ffEncode = new (); + ffEncode.VideoCodec = "h264"; + ffEncode.Execute(args); + + + FfmpegBuilderAudioAddTrack ffAddAudio = new (); + ffAddAudio.Codec = "ac3"; + ffAddAudio.Index = 1; + ffAddAudio.Execute(args); + + FfmpegBuilderAudioAddTrack ffAddAudio2 = new(); + ffAddAudio2.Codec = "aac"; + ffAddAudio2.Index = 2; + ffAddAudio2.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + [TestMethod] + public void FfmpegBuilder_AddAc3AacMp4NoSubs() + { + 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)); + + FfmpegBuilderVideoEncode ffEncode = new(); + ffEncode.VideoCodec = "h264"; + ffEncode.Execute(args); + + FfmpegBuilderRemuxToMP4 ffMp4 = new(); + ffMp4.Execute(args); + + + FfmpegBuilderSubtitleFormatRemover ffSubRemover = new(); + ffSubRemover.RemoveAll = true; + ffSubRemover.Execute(args); + + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "ac3"; + ffAddAudio.Index = 1; + ffAddAudio.Execute(args); + + FfmpegBuilderAudioAddTrack ffAddAudio2 = new(); + ffAddAudio2.Codec = "aac"; + ffAddAudio2.Index = 2; + ffAddAudio2.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + [TestMethod] + public void FfmpegBuilder_AddAc3AacMp4NoSubs_BlackBars() + { + const string file = @"D:\videos\unprocessed\blackbars.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)); + + FfmpegBuilderVideoEncode ffEncode = new(); + ffEncode.VideoCodec = "h265"; + ffEncode.Execute(args); + + FfmpegBuilderRemuxToMP4 ffMp4 = new(); + ffMp4.Execute(args); + + FfmpegBuilderCropBlackBars ffCropBlackBars = new(); + ffCropBlackBars.CroppingThreshold = 10; + ffCropBlackBars.Execute(args); + + FfmpegBuilderSubtitleFormatRemover ffSubRemover = new(); + ffSubRemover.RemoveAll = true; + ffSubRemover.Execute(args); + + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "ac3"; + ffAddAudio.Index = 1; + ffAddAudio.Execute(args); + + FfmpegBuilderAudioAddTrack ffAddAudio2 = new(); + ffAddAudio2.Codec = "aac"; + ffAddAudio2.Index = 2; + ffAddAudio2.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + [TestMethod] + public void FfmpegBuilder_AddAc3AacMp4NoSubs_BlackBars_Scaled480p() + { + const string file = @"D:\videos\unprocessed\blackbars.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)); + + FfmpegBuilderVideoEncode ffEncode = new(); + ffEncode.VideoCodec = "h265"; + ffEncode.Execute(args); + + FfmpegBuilderRemuxToMP4 ffMp4 = new(); + ffMp4.Execute(args); + + FfmpegBuilderCropBlackBars ffCropBlackBars = new(); + ffCropBlackBars.CroppingThreshold = 10; + ffCropBlackBars.Execute(args); + + FfmpegBuilderScaler ffScaler = new(); + ffScaler.Resolution = "640:-2"; + ffScaler.Execute(args); + + FfmpegBuilderSubtitleFormatRemover ffSubRemover = new(); + ffSubRemover.RemoveAll = true; + ffSubRemover.Execute(args); + + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "ac3"; + ffAddAudio.Index = 1; + ffAddAudio.Execute(args); + + FfmpegBuilderAudioAddTrack ffAddAudio2 = new(); + ffAddAudio2.Codec = "aac"; + ffAddAudio2.Index = 2; + ffAddAudio2.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + + [TestMethod] + public void FfmpegBuilder_AddAc3AacMp4NoSubs_BlackBars_Scaled4k() + { + const string file = @"D:\videos\unprocessed\blackbars.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)); + + FfmpegBuilderVideoEncode ffEncode = new(); + ffEncode.VideoCodec = "h265"; + ffEncode.Execute(args); + + FfmpegBuilderRemuxToMP4 ffMp4 = new(); + ffMp4.Execute(args); + + FfmpegBuilderCropBlackBars ffCropBlackBars = new(); + ffCropBlackBars.CroppingThreshold = 10; + ffCropBlackBars.Execute(args); + + FfmpegBuilderScaler ffScaler = new(); + ffScaler.Resolution = "3840:-2"; + ffScaler.Execute(args); + + FfmpegBuilderSubtitleFormatRemover ffSubRemover = new(); + ffSubRemover.RemoveAll = true; + ffSubRemover.Execute(args); + + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "ac3"; + ffAddAudio.Index = 1; + ffAddAudio.Execute(args); + + FfmpegBuilderAudioAddTrack ffAddAudio2 = new(); + ffAddAudio2.Codec = "aac"; + ffAddAudio2.Index = 2; + ffAddAudio2.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + + [TestMethod] + public void FfmpegBuilder_AddAc3AacMp4NoSubs_BlackBars_Scaled480p2() + { + 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)); + + FfmpegBuilderVideoEncode ffEncode = new(); + ffEncode.VideoCodec = "h265"; + ffEncode.Execute(args); + + FfmpegBuilderRemuxToMP4 ffMp4 = new(); + ffMp4.Execute(args); + + FfmpegBuilderCropBlackBars ffCropBlackBars = new(); + ffCropBlackBars.CroppingThreshold = 10; + ffCropBlackBars.Execute(args); + + FfmpegBuilderScaler ffScaler = new(); + ffScaler.Resolution = "640:-2"; + ffScaler.Execute(args); + + FfmpegBuilderSubtitleFormatRemover ffSubRemover = new(); + ffSubRemover.RemoveAll = true; + ffSubRemover.Execute(args); + + + FfmpegBuilderAudioAddTrack ffAddAudio = new(); + ffAddAudio.Codec = "ac3"; + ffAddAudio.Index = 1; + ffAddAudio.Execute(args); + + FfmpegBuilderAudioAddTrack ffAddAudio2 = new(); + ffAddAudio2.Codec = "aac"; + ffAddAudio2.Index = 2; + ffAddAudio2.Execute(args); + + FfmpegBuilderExecutor ffExecutor = new(); + int result = ffExecutor.Execute(args); + + string log = logger.ToString(); + Assert.AreEqual(1, result); + } + } +} + +#endif \ No newline at end of file diff --git a/VideoNodes/VideoNodes.en.json b/VideoNodes/VideoNodes.en.json index 8e21dfa5..05dfcdc5 100644 --- a/VideoNodes/VideoNodes.en.json +++ b/VideoNodes/VideoNodes.en.json @@ -5,7 +5,7 @@ "Outputs": { "1": "Audio track added and saved to temporary file" }, - "Description": "Adds a new audio track to ta 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.", + "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. 1 based, so to insert the new audio track as the first track set this to 1.", @@ -75,7 +75,7 @@ } }, "AudioTrackSetLanguage": { - "Label": "Audio: Set Language", + "Label": "Audio Set Language", "Outputs": { "1": "Audio tracks updated to new temporary file", "2": "Audio tracks NOT updated" @@ -144,6 +144,140 @@ "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." } }, + "FfmpegBuilderStart": { + "Label": "FFMPEG Builder: Start", + "Outputs": { + "1": "FFMPEG Builder created and ready to add FFMPEG Builder nodes to" + }, + "Description": "Creates an instance of the FFMPEG Builder which can build a FFMPEG argument to then execute with the FFMPEG Executor." + }, + "FfmpegBuilderExecutor": { + "Label": "FFMPEG Builder: Executor", + "Outputs": { + "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." + }, + "FfmpegBuilderAudioAddTrack": { + "Label": "FFMPEG Builder: Audio Add Track", + "Outputs": { + "1": "Added audio track to FFMPEG Builder" + }, + "Description": "Adds a new audio track to FFMPEG Builder which will be added once the builder is executed.", + "Fields": { + "Index": "Index", + "Index-Help": "The index where to insert the new audio track. 1 based, so to insert the new audio track as the first track set this to 1.", + "Channels": "Channels", + "Channels-Help": "The number of channels to convert this audio track to.", + "Bitrate": "Bitrate", + "Bitrate-Help": "Bitrate of the new audio track", + "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" + } + }, + "FfmpegBuilderAudioTrackRemover": { + "Label": "FFMPEG Builder: Audio Track Remover", + "Outputs": { + "1": "Audio tracks set to remove", + "2": "Audio tracks NOT set to 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": { + "RemoveAll": "Remove All", + "RemoveAll-Help": "Remove all current tracks from the output file. You can use Add Audio Track afterwards to add tracks of specific codecs", + "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" + } + }, + "FfmpegBuilderSubtitleFormatRemover": { + "Label": "FFMPEG Builder: Subtitle Format Remover", + "Description": "Removes subtitles from a video file if found.", + "Outputs": { + "1": "Subtitles marked for removal in FFMPEG Builder", + "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" + } + }, + "FfmpegBuilderSubtitleTrackRemover": { + "Label": "FFMPEG Builder: Subtitle Track Remover", + "Outputs": { + "1": "Subtitles marked for removal in FFMPEG Builder", + "2": "No subtitles to remove" + }, + "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", + "UseLanguageCode": "Use Language Code", + "UseLanguageCode-Help": "If the language code of the audio track should be used instead of the title" + } + }, + "FfmpegBuilderCropBlackBars": { + "Label": "FFMPEG Builder: Crop Black Bars", + "Description": "Updated FFMPEG Builder to crop black bars if detected", + "Outputs": { + "1": "FFMPEG Builder updated to crop black bars", + "2": "No black bars detected, not cropping" + }, + "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." + } + }, + "FfmpegBuilderRemuxToMkv": { + "Label": "FFMPEG Builder: Remux to MKV", + "Descritption": "Remuxes a video file into a MKV container.", + "Outputs": { + "1": "FFMPEG Builder set to remux to MKV" + } + }, + "FfmpegBuilderRemuxToMP4": { + "Label": "FFMPEG Builder: Remux to MP4", + "Descritption": "Remuxes a video file into a MP4 container.", + "Outputs": { + "1": "FFMPEG Builder set to remux to MP4" + } + }, + "FfmpegBuilderScaler": { + "Label": "FFMPEG Builder: Video Scaler", + "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": "FFMPEG Builder scale filter added", + "2": "Video was already in/near the scaled resolution" + }, + "Fields": { + "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" + } + }, + "FfmpegBuilderVideoEncode": { + "Label": "FFMPEG Builder: Video Encode", + "Description": "Encodes video streams in the specified codec", + "Outputs": { + "1": "FFMPEG Builder video streams set to encode", + "2": "Video already in target codec, will not re-encode" + }, + "Fields": { + "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.", + "Force": "Force Encode", + "Force-Help": "Will force a encode of the video even if it is already in the target Video Codec" + } + }, "RemuxToMKV": { "Descritption": "Remuxes a video file into a MKV container. All streams will be copied to the new container", "Outputs": { @@ -214,7 +348,6 @@ "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", diff --git a/build/utils/PluginInfoGenerator/FileFlows.Plugin.dll b/build/utils/PluginInfoGenerator/FileFlows.Plugin.dll index c59b1137..07202f7d 100644 Binary files a/build/utils/PluginInfoGenerator/FileFlows.Plugin.dll and b/build/utils/PluginInfoGenerator/FileFlows.Plugin.dll differ diff --git a/build/utils/PluginInfoGenerator/FileFlows.Plugin.pdb b/build/utils/PluginInfoGenerator/FileFlows.Plugin.pdb index 4857400e..4f87f723 100644 Binary files a/build/utils/PluginInfoGenerator/FileFlows.Plugin.pdb and b/build/utils/PluginInfoGenerator/FileFlows.Plugin.pdb differ diff --git a/build/utils/PluginInfoGenerator/FileFlows.ServerShared.dll b/build/utils/PluginInfoGenerator/FileFlows.ServerShared.dll index 9f5180ac..dfc86557 100644 Binary files a/build/utils/PluginInfoGenerator/FileFlows.ServerShared.dll and b/build/utils/PluginInfoGenerator/FileFlows.ServerShared.dll differ diff --git a/build/utils/PluginInfoGenerator/FileFlows.ServerShared.pdb b/build/utils/PluginInfoGenerator/FileFlows.ServerShared.pdb index 67138b71..02019162 100644 Binary files a/build/utils/PluginInfoGenerator/FileFlows.ServerShared.pdb and b/build/utils/PluginInfoGenerator/FileFlows.ServerShared.pdb differ diff --git a/build/utils/PluginInfoGenerator/FileFlows.Shared.dll b/build/utils/PluginInfoGenerator/FileFlows.Shared.dll index 6c451020..5fe58097 100644 Binary files a/build/utils/PluginInfoGenerator/FileFlows.Shared.dll and b/build/utils/PluginInfoGenerator/FileFlows.Shared.dll differ diff --git a/build/utils/PluginInfoGenerator/FileFlows.Shared.pdb b/build/utils/PluginInfoGenerator/FileFlows.Shared.pdb index 9c3bb71e..e76acc2c 100644 Binary files a/build/utils/PluginInfoGenerator/FileFlows.Shared.pdb and b/build/utils/PluginInfoGenerator/FileFlows.Shared.pdb differ diff --git a/build/utils/PluginInfoGenerator/FileFlows.Shared.xml b/build/utils/PluginInfoGenerator/FileFlows.Shared.xml index 160d3562..0fffa020 100644 --- a/build/utils/PluginInfoGenerator/FileFlows.Shared.xml +++ b/build/utils/PluginInfoGenerator/FileFlows.Shared.xml @@ -19,6 +19,11 @@ Gets or sets if this library will use fingerprinting to determine if a file already is known + + + Gets or sets the number of seconds that have to pass between changes to the folder for it to be scanned into the library + + Gets or sets if hidden files and folders should be excluded from the library diff --git a/build/utils/PluginInfoGenerator/PluginInfoGenerator.dll b/build/utils/PluginInfoGenerator/PluginInfoGenerator.dll index 6a545092..87b73d5b 100644 Binary files a/build/utils/PluginInfoGenerator/PluginInfoGenerator.dll and b/build/utils/PluginInfoGenerator/PluginInfoGenerator.dll differ diff --git a/build/utils/PluginInfoGenerator/PluginInfoGenerator.pdb b/build/utils/PluginInfoGenerator/PluginInfoGenerator.pdb index a0420b17..72af4d4f 100644 Binary files a/build/utils/PluginInfoGenerator/PluginInfoGenerator.pdb and b/build/utils/PluginInfoGenerator/PluginInfoGenerator.pdb differ diff --git a/build/utils/spellcheck/ignoredwords.txt b/build/utils/spellcheck/ignoredwords.txt index b9c6b882..d4f6ec69 100644 --- a/build/utils/spellcheck/ignoredwords.txt +++ b/build/utils/spellcheck/ignoredwords.txt @@ -63,4 +63,6 @@ normalization doesn''t * % -eng \ No newline at end of file +eng +remux +Scaler \ No newline at end of file diff --git a/ref/FileFlows.Plugin.dll b/ref/FileFlows.Plugin.dll index 68e74035..99314939 100644 Binary files a/ref/FileFlows.Plugin.dll and b/ref/FileFlows.Plugin.dll differ