mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2025-12-31 02:09:30 -06:00
added FFMPEG Builder nodes
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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<ListOption> _CodecOptions;
|
||||
public static List<ListOption> CodecOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_CodecOptions == null)
|
||||
{
|
||||
_CodecOptions = new List<ListOption>
|
||||
{
|
||||
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<ListOption> _ChannelsOptions;
|
||||
public static List<ListOption> ChannelsOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_ChannelsOptions == null)
|
||||
{
|
||||
_ChannelsOptions = new List<ListOption>
|
||||
{
|
||||
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<ListOption> _BitrateOptions;
|
||||
public static List<ListOption> BitrateOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_BitrateOptions == null)
|
||||
{
|
||||
_BitrateOptions = new List<ListOption>
|
||||
{
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs
Normal file
51
VideoNodes/FfmpegBuilderNodes/FfmpegBuilderExecutor.cs
Normal file
@@ -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<string> ffArgs = new List<string>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
VideoNodes/FfmpegBuilderNodes/FfmpegBuilderNode.cs
Normal file
62
VideoNodes/FfmpegBuilderNodes/FfmpegBuilderNode.cs
Normal file
@@ -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<Match>()
|
||||
.Select(x => x.Value.Trim('"'))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
VideoNodes/FfmpegBuilderNodes/FfmpegBuilderStart.cs
Normal file
24
VideoNodes/FfmpegBuilderNodes/FfmpegBuilderStart.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs
Normal file
36
VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs
Normal file
@@ -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<string> _EncodingParameters = new List<string>();
|
||||
public List<string> EncodingParameters
|
||||
{
|
||||
get => _EncodingParameters;
|
||||
set
|
||||
{
|
||||
_EncodingParameters = value ?? new List<string>();
|
||||
}
|
||||
}
|
||||
public override string[] GetParameters(int outputIndex)
|
||||
{
|
||||
if (Deleted)
|
||||
return new string[] { };
|
||||
|
||||
var results = new List<string> { "-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();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
VideoNodes/FfmpegBuilderNodes/Models/FfmpegModel.cs
Normal file
57
VideoNodes/FfmpegBuilderNodes/Models/FfmpegModel.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
namespace FileFlows.VideoNodes.FfmpegBuilderNodes.Models
|
||||
{
|
||||
public class FfmpegModel
|
||||
{
|
||||
private List<FfmpegVideoStream> _VideoStreams = new List<FfmpegVideoStream>();
|
||||
public List<FfmpegVideoStream> VideoStreams
|
||||
{
|
||||
get => _VideoStreams;
|
||||
set => _VideoStreams = value ?? new List<FfmpegVideoStream>();
|
||||
}
|
||||
private List<FfmpegAudioStream> _AudioStreams = new List<FfmpegAudioStream>();
|
||||
public List<FfmpegAudioStream> AudioStreams
|
||||
{
|
||||
get => _AudioStreams;
|
||||
set => _AudioStreams = value ?? new List<FfmpegAudioStream>();
|
||||
}
|
||||
private List<FfmpegSubtitleStream> _SubtitleStreams = new List<FfmpegSubtitleStream>();
|
||||
public List<FfmpegSubtitleStream> SubtitleStreams
|
||||
{
|
||||
get => _SubtitleStreams;
|
||||
set => _SubtitleStreams = value ?? new List<FfmpegSubtitleStream>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
VideoNodes/FfmpegBuilderNodes/Models/FfmpegStream.cs
Normal file
12
VideoNodes/FfmpegBuilderNodes/Models/FfmpegStream.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
22
VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs
Normal file
22
VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs
Normal file
@@ -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<string> { "-map", "0:s:" + outputIndex, "-c:s:" + (Stream.TypeIndex - 1) };
|
||||
//if (EncodingParameters.Any() == false)
|
||||
{
|
||||
results.Add("copy");
|
||||
return results.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs
Normal file
61
VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
namespace FileFlows.VideoNodes.FfmpegBuilderNodes.Models
|
||||
{
|
||||
public class FfmpegVideoStream : FfmpegStream
|
||||
{
|
||||
public VideoStream Stream { get; set; }
|
||||
|
||||
private List<string> _Filter = new List<string>();
|
||||
public List<string> Filter
|
||||
{
|
||||
get => _Filter;
|
||||
set
|
||||
{
|
||||
_Filter = value ?? new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> _EncodingParameters = new List<string>();
|
||||
public List<string> EncodingParameters
|
||||
{
|
||||
get => _EncodingParameters;
|
||||
set
|
||||
{
|
||||
_EncodingParameters = value ?? new List<string>();
|
||||
}
|
||||
}
|
||||
public override bool HasChange => EncodingParameters.Any() || Filter.Any();
|
||||
|
||||
public override string[] GetParameters(int outputIndex)
|
||||
{
|
||||
if (Deleted)
|
||||
return new string[] { };
|
||||
|
||||
var results = new List<string> { "-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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string> SubtitlesToRemove { get; set; }
|
||||
|
||||
private static List<ListOption> _Options;
|
||||
public static List<ListOption> Options
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Options == null)
|
||||
{
|
||||
_Options = new List<ListOption>
|
||||
{
|
||||
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<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderScaler.cs
Normal file
64
VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderScaler.cs
Normal file
@@ -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<ListOption> _ResolutionOptions;
|
||||
public static List<ListOption> ResolutionOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_ResolutionOptions == null)
|
||||
{
|
||||
_ResolutionOptions = new List<ListOption>
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
VideoNodes/GlobalUsings.cs
Normal file
9
VideoNodes/GlobalUsings.cs
Normal file
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
313
VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs
Normal file
313
VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_BasicTests.cs
Normal file
@@ -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
|
||||
@@ -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",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -19,6 +19,11 @@
|
||||
Gets or sets if this library will use fingerprinting to determine if a file already is known
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:FileFlows.Shared.Models.Library.WaitTimeSeconds">
|
||||
<summary>
|
||||
Gets or sets the number of seconds that have to pass between changes to the folder for it to be scanned into the library
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:FileFlows.Shared.Models.Library.ExcludeHidden">
|
||||
<summary>
|
||||
Gets or sets if hidden files and folders should be excluded from the library
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -63,4 +63,6 @@ normalization
|
||||
doesn''t
|
||||
*
|
||||
%
|
||||
eng
|
||||
eng
|
||||
remux
|
||||
Scaler
|
||||
Binary file not shown.
Reference in New Issue
Block a user