mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2026-05-12 00:28:55 -05:00
FF-1731: Track selector
This commit is contained in:
@@ -52,7 +52,7 @@ public class Matches : Node
|
||||
value = varValue;
|
||||
else
|
||||
value = args.ReplaceVariables(match.Key, stripMissing: true);
|
||||
string strValue = value?.ToString() ?? string.Empty;
|
||||
string strValue = value?.ToString()?.Trim() ?? string.Empty;
|
||||
|
||||
if (GeneralHelper.IsRegex(match.Value))
|
||||
{
|
||||
@@ -76,12 +76,9 @@ public class Matches : Node
|
||||
return output;
|
||||
}
|
||||
|
||||
if (match.Value == strValue)
|
||||
{
|
||||
args.Logger?.ILog($"Match found '{match.Value}' = {strValue}");
|
||||
if (args.StringHelper.Matches(match.Value, match.Value))
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
if (args.MathHelper.IsMathOperation(match.Value))
|
||||
{
|
||||
if (args.MathHelper.IsTrue(match.Value, strValue))
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Globalization;
|
||||
using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
|
||||
|
||||
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
|
||||
@@ -8,7 +6,7 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
|
||||
/// <summary>
|
||||
/// FFmpeg Builder: Add Audio Track
|
||||
/// </summary>
|
||||
public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
|
||||
public class FfmpegBuilderAudioAddTrack : TrackSelectorFlowElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the icon for this flow element
|
||||
@@ -18,10 +16,13 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
|
||||
/// Gets the help URL for this flow element
|
||||
/// </summary>
|
||||
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/audio-add-track";
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index to insert this track
|
||||
/// </summary>
|
||||
[NumberInt(1)]
|
||||
[NumberInt(3)]
|
||||
[Range(0, 100)]
|
||||
[DefaultValue(1)]
|
||||
public int Index { get; set; }
|
||||
@@ -29,7 +30,7 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
|
||||
/// Gets or sets the codec to to use
|
||||
/// </summary>
|
||||
[DefaultValue("aac")]
|
||||
[Select(nameof(CodecOptions), 1)]
|
||||
[Select(nameof(CodecOptions), 4)]
|
||||
public string Codec { get; set; }
|
||||
|
||||
private static List<ListOption> _CodecOptions;
|
||||
@@ -59,7 +60,7 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
|
||||
/// Gets or sets the audio channels for the new track
|
||||
/// </summary>
|
||||
[DefaultValue(2f)]
|
||||
[Select(nameof(ChannelsOptions), 2)]
|
||||
[Select(nameof(ChannelsOptions), 5)]
|
||||
public float Channels { get; set; }
|
||||
|
||||
private static List<ListOption> _ChannelsOptions;
|
||||
@@ -87,13 +88,13 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
|
||||
/// <summary>
|
||||
/// Gets or sets the bitrate
|
||||
/// </summary>
|
||||
[Select(nameof(BitrateOptions), 3)]
|
||||
[Select(nameof(BitrateOptions), 6)]
|
||||
public int Bitrate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if the bitrate specified should be per channel
|
||||
/// </summary>
|
||||
[Boolean(4)]
|
||||
[Boolean(7)]
|
||||
public bool BitratePerChannel { get; set; }
|
||||
|
||||
private static List<ListOption> _BitrateOptions;
|
||||
@@ -125,7 +126,7 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
|
||||
/// Gets or sets the sample rate
|
||||
/// </summary>
|
||||
[DefaultValue(0)]
|
||||
[Select(nameof(SampleRateOptions), 5)]
|
||||
[Select(nameof(SampleRateOptions), 8)]
|
||||
public int SampleRate { get; set; }
|
||||
|
||||
private static List<ListOption> _SampleRateOptions;
|
||||
@@ -158,17 +159,17 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
|
||||
/// Gets or sets the language of the new track
|
||||
/// </summary>
|
||||
[DefaultValue("eng")]
|
||||
[TextVariable(6)]
|
||||
[TextVariable(9)]
|
||||
public string Language { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets if the title of the new track should be removed
|
||||
/// </summary>
|
||||
[Boolean(7)]
|
||||
[Boolean(10)]
|
||||
public bool RemoveTitle { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the title of the new track
|
||||
/// </summary>
|
||||
[TextVariable(8)]
|
||||
[TextVariable(11)]
|
||||
[ConditionEquals(nameof(RemoveTitle), false)]
|
||||
public string NewTitle { get; set; }
|
||||
|
||||
@@ -186,21 +187,21 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
|
||||
}
|
||||
var audio = new FfmpegAudioStream();
|
||||
|
||||
var bestAudio = GetBestAudioTrack(args, Model.AudioStreams.Select(x => x.Stream));
|
||||
if (bestAudio == null)
|
||||
var sourceAudio = GetSourceTrack<AudioStream>() ?? GetBestAudioTrack(args, Model.AudioStreams.Select(x => x.Stream));
|
||||
if (sourceAudio == null)
|
||||
{
|
||||
args.Logger.ELog("No source audio track found");
|
||||
args.FailureReason = "No source audio track found";
|
||||
return -1;
|
||||
}
|
||||
|
||||
audio.Stream = bestAudio;
|
||||
audio.Stream = sourceAudio;
|
||||
audio.Channels = audio.Stream.Channels;
|
||||
|
||||
bool directCopy = false;
|
||||
if(bestAudio.Codec.ToLower() == this.Codec.ToLower())
|
||||
if(sourceAudio.Codec.ToLower() == this.Codec.ToLower())
|
||||
{
|
||||
if((Channels == 0 || Math.Abs(Channels - bestAudio.Channels) < 0.05f) && Bitrate <= 2)
|
||||
if((Channels == 0 || Math.Abs(Channels - sourceAudio.Channels) < 0.05f) && Bitrate <= 2)
|
||||
{
|
||||
directCopy = true;
|
||||
}
|
||||
@@ -208,8 +209,8 @@ public class FfmpegBuilderAudioAddTrack : FfmpegBuilderNode
|
||||
|
||||
if (directCopy)
|
||||
{
|
||||
audio.Codec = bestAudio.Codec;
|
||||
args.Logger?.ILog($"Source audio is already in appropriate format, just copying that track: {bestAudio.IndexString}, Channels: {bestAudio.Channels}, Codec: {bestAudio.Codec}");
|
||||
audio.Codec = sourceAudio.Codec;
|
||||
args.Logger?.ILog($"Source audio is already in appropriate format, just copying that track: {sourceAudio.IndexString}, Channels: {sourceAudio.Channels}, Codec: {sourceAudio.Codec}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
using FileFlows.VideoNodes.Helpers;
|
||||
|
||||
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
|
||||
|
||||
/// <summary>
|
||||
/// Flow element that has a track selector
|
||||
/// </summary>
|
||||
public abstract class TrackSelectorFlowElement : FfmpegBuilderNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets replacements to replace
|
||||
/// </summary>
|
||||
[Select(nameof(CustomTrackSelectionOptions), 1)]
|
||||
[Required]
|
||||
public bool CustomTrackSelection { get; set; }
|
||||
|
||||
private static List<ListOption> _CustomTrackSelection;
|
||||
/// <summary>
|
||||
/// Gets the codec options
|
||||
/// </summary>
|
||||
public static List<ListOption> CustomTrackSelectionOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_CustomTrackSelection == null)
|
||||
{
|
||||
_CustomTrackSelection = new List<ListOption>
|
||||
{
|
||||
new () { Label = "Automatic", Value = false },
|
||||
new () { Label = "Custom", Value = true},
|
||||
};
|
||||
}
|
||||
return _CustomTrackSelection;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the track selection options
|
||||
/// </summary>
|
||||
[KeyValue(2, optionsProperty: nameof(TrackSelectionOptionsOptions), allowDuplicates: true)]
|
||||
[ConditionEquals(nameof(CustomTrackSelection), true)]
|
||||
[Required]
|
||||
public List<KeyValuePair<string, string>> TrackSelectionOptions { get; set; }
|
||||
|
||||
private static List<ListOption> _TrackSelectionOptionsOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the stream type options
|
||||
/// </summary>
|
||||
public static List<ListOption> TrackSelectionOptionsOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_TrackSelectionOptionsOptions == null)
|
||||
{
|
||||
_TrackSelectionOptionsOptions = new List<ListOption>
|
||||
{
|
||||
new() { Label = "Channels", Value = "Channels" },
|
||||
new() { Label = "Codec", Value = "Codec" },
|
||||
new() { Label = "Language", Value = "Language" },
|
||||
new() { Label = "Title", Value = "Title" }
|
||||
};
|
||||
}
|
||||
|
||||
return _TrackSelectionOptionsOptions;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source audio from the parameters
|
||||
/// </summary>
|
||||
/// <returns>the source audio</returns>
|
||||
internal T? GetSourceTrack<T>() where T : VideoFileStream
|
||||
{
|
||||
if (CustomTrackSelection == false || TrackSelectionOptions?.Any() != true)
|
||||
{
|
||||
Args?.Logger?.ILog("Not using custom track selection");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Model?.AudioStreams?.Any() != true)
|
||||
return null;
|
||||
|
||||
Args?.Logger?.ILog("Using custom track selection");
|
||||
|
||||
List<VideoFileStream> possible = new();
|
||||
List<VideoFileStream>? sourceStreams = typeof(T).Name switch
|
||||
{
|
||||
nameof(VideoStream) => Model.VideoStreams.Select(x => (VideoFileStream)x.Stream).Distinct().ToList(),
|
||||
nameof(AudioStream) => Model.AudioStreams.Select(x => (VideoFileStream)x.Stream).Distinct().ToList(),
|
||||
nameof(SubtitleStream) => Model.SubtitleStreams.Select(x => (VideoFileStream)x.Stream).Distinct().ToList(),
|
||||
_ => null
|
||||
};
|
||||
if (sourceStreams?.Any() != true)
|
||||
{
|
||||
Args?.Logger?.ILog("No source streams found");
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var stream in sourceStreams)
|
||||
{
|
||||
bool matches = true;
|
||||
foreach (var kv in TrackSelectionOptions)
|
||||
{
|
||||
var key = kv.Key?.ToLowerInvariant() ?? string.Empty;
|
||||
var kvValue = Args.ReplaceVariables(kv.Value?.Replace("{orig}", "{OriginalLanguage}") ?? string.Empty, stripMissing: true);
|
||||
switch (key)
|
||||
{
|
||||
case "language":
|
||||
if (LanguageMatches(stream, kv.Value))
|
||||
Args?.Logger?.ILog($"Language Matches '{stream}' = {kv.Value}");
|
||||
else
|
||||
{
|
||||
Args?.Logger?.ILog($"Language does not match '{stream}' = {kv.Value}");
|
||||
matches = false;
|
||||
}
|
||||
break;
|
||||
case "codec":
|
||||
if(Args.StringHelper.Matches(kv.Value, stream.Codec))
|
||||
Args?.Logger?.ILog($"Stream '{stream}' Codec '{stream.Codec}' matches '{kv.Value}'");
|
||||
else
|
||||
{
|
||||
Args?.Logger?.ILog($"Stream '{stream}' Codec '{stream.Codec}' does not match {kv.Value}");
|
||||
matches = false;
|
||||
}
|
||||
break;
|
||||
case "title":
|
||||
if(Args.StringHelper.Matches(kv.Value, stream.Title))
|
||||
Args?.Logger?.ILog($"Stream '{stream}' Title '{stream.Codec}' does match '{kv.Value}'");
|
||||
else
|
||||
{
|
||||
Args?.Logger?.ILog($"Stream '{stream}' Title '{stream.Codec}' does not match '{kv.Value}'");
|
||||
matches = false;
|
||||
}
|
||||
break;
|
||||
case "channels":
|
||||
if (ChannelsMatches(stream, kv.Value))
|
||||
Args?.Logger?.ILog($"Channels Matches '{stream}' = {kv.Value}");
|
||||
else
|
||||
{
|
||||
Args?.Logger?.ILog($"Channels does not match '{stream}' = {kv.Value}");
|
||||
matches = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (matches == false)
|
||||
break;
|
||||
}
|
||||
|
||||
if (matches == false)
|
||||
{
|
||||
Args?.Logger?.ILog($"Track '{stream}' not suitable");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Args?.Logger?.ILog($"Track '{stream}' suitable");
|
||||
possible.Add(stream);
|
||||
}
|
||||
|
||||
Args?.Logger?.ILog("Possible streams found: " + possible.Count);
|
||||
foreach (var stream in possible)
|
||||
Args?.Logger.ILog(" - " + stream);
|
||||
|
||||
return possible.FirstOrDefault() as T;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the channels matches
|
||||
/// </summary>
|
||||
/// <param name="value">the value to check</param>
|
||||
/// <returns>true if channels matches, otherwise false</returns>
|
||||
private bool ChannelsMatches(VideoFileStream stream, string value)
|
||||
{
|
||||
if (stream is AudioStream audio == false)
|
||||
{
|
||||
Args?.Logger?.WLog("Not an audio stream, cannot test Channels");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Args?.MathHelper.IsMathOperation(value) == true)
|
||||
{
|
||||
Args?.Logger?.DLog($"Is Math operation '{value}' comparing '{audio.Channels}'");
|
||||
return Args.MathHelper.IsTrue(value, audio.Channels);
|
||||
}
|
||||
|
||||
if (double.TryParse(value, out var dblValue) == false)
|
||||
{
|
||||
Args?.Logger?.WLog($"Failed to parse '{value}' as a double");
|
||||
return false;
|
||||
}
|
||||
|
||||
return Math.Abs(audio.Channels - dblValue) < 0.05f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the language matches
|
||||
/// </summary>
|
||||
/// <param name="stream">the stream to check</param>
|
||||
/// <param name="value">the value to check</param>
|
||||
/// <returns>true if language matches, otherwise false</returns>
|
||||
private bool LanguageMatches(VideoFileStream stream, string value)
|
||||
{
|
||||
if (value.StartsWith('='))
|
||||
value = value[1..];
|
||||
if (stream is AudioStream audio)
|
||||
return LanguageHelper.AreSame(audio.Language, value);
|
||||
if (stream is SubtitleStream sub)
|
||||
return LanguageHelper.AreSame(sub.Language, value);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,14 @@ using VideoNodes.Tests;
|
||||
namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests;
|
||||
|
||||
[TestClass]
|
||||
public class FfmpegBuilder_AddAudioTests
|
||||
public class FfmpegBuilder_AddAudioTests : TestBase
|
||||
{
|
||||
VideoInfo vii;
|
||||
NodeParameters args;
|
||||
private void Prepare()
|
||||
{
|
||||
const string file = @"D:\videos\unprocessed\basic.mkv";
|
||||
var logger = new TestLogger();
|
||||
const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
var vi = new VideoInfoHelper(ffmpeg, logger);
|
||||
vii = vi.Read(file);
|
||||
var vi = new VideoInfoHelper(FfmpegPath, Logger);
|
||||
vii = vi.Read(TestFile_BasicMkv);
|
||||
vii.AudioStreams = new List<AudioStream>
|
||||
{
|
||||
new AudioStream
|
||||
@@ -54,9 +51,9 @@ public class FfmpegBuilder_AddAudioTests
|
||||
Channels = 5.1f
|
||||
}
|
||||
};
|
||||
args = new NodeParameters(file, logger, false, string.Empty, null);
|
||||
args.GetToolPathActual = (string tool) => ffmpeg;
|
||||
args.TempPath = @"D:\videos\temp";
|
||||
args = new NodeParameters(TestFile_BasicMkv, Logger, false, string.Empty, new LocalFileService());
|
||||
args.GetToolPathActual = (string tool) => FfmpegPath;
|
||||
args.TempPath = TempPath;
|
||||
args.Parameters.Add("VideoInfo", vii);
|
||||
|
||||
|
||||
@@ -65,6 +62,27 @@ public class FfmpegBuilder_AddAudioTests
|
||||
Assert.AreEqual(1, ffStart.Execute(args));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FfmpegBuilder_AddAudio_GetSource_1()
|
||||
{
|
||||
Prepare();
|
||||
FfmpegBuilderAudioAddTrack ffAddAudio = new();
|
||||
ffAddAudio.CustomTrackSelection = true;
|
||||
ffAddAudio.TrackSelectionOptions = new()
|
||||
{
|
||||
new("Codec", "AC*"),
|
||||
new("Language", "English"),
|
||||
new("Codec", "!aac"),
|
||||
};
|
||||
ffAddAudio.PreExecute(args);
|
||||
var source = ffAddAudio.GetSourceTrack<AudioStream>();
|
||||
Assert.IsNotNull(source);
|
||||
Logger.ILog("Source Track: " + source);
|
||||
Assert.AreEqual("en", source.Language);
|
||||
Assert.AreEqual("AC3", source.Codec);
|
||||
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FfmpegBuilder_AddAudio_AacStereo()
|
||||
{
|
||||
|
||||
@@ -161,6 +161,10 @@
|
||||
},
|
||||
"Description": "Adds a new audio track to FFMPEG Builder which will be added once the builder is executed.",
|
||||
"Fields": {
|
||||
"CustomTrackSelection": "Source Track Selection",
|
||||
"TrackSelectionOptions": "Track Selection",
|
||||
"TrackSelectionOptionsKey": "Property",
|
||||
"TrackSelectionOptionsValue": "Value",
|
||||
"Index": "Index",
|
||||
"Index-Help": "The index where to insert the new audio track. 0 based, so to insert the new audio track as the first track set this to 0.",
|
||||
"Channels": "Channels",
|
||||
|
||||
Reference in New Issue
Block a user