diff --git a/VideoNodes/Tests/AudioAddTrackTests.cs b/VideoNodes/Tests/AudioAddTrackTests.cs new file mode 100644 index 00000000..aa2d7c8a --- /dev/null +++ b/VideoNodes/Tests/AudioAddTrackTests.cs @@ -0,0 +1,46 @@ +#if(DEBUG) + +namespace VideoNodes.Tests +{ + using FileFlows.VideoNodes; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + [TestClass] + public class AudioAddTrackTests + { + [TestMethod] + public void AudioAddTrackTests_Mono_First() + { + const string file = @"D:\videos\unprocessed\The Witcher - S02E05 - Turn Your Back.mkv"; + var logger = new TestLogger(); + var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", logger); + var vii = vi.Read(file); + + const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; + + AudioAddTrack node = new(); + var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty); + args.GetToolPathActual = (string tool) => ffmpeg; + args.TempPath = @"D:\videos\temp"; + + new VideoFile().Execute(args); + node.Bitrate = 128; + node.Channels = 2; + node.Index = 2; + node.Codec = "aac"; + + int output = node.Execute(args); + string log = logger.ToString(); + Assert.AreEqual(1, output); + } + + } +} + + +#endif \ No newline at end of file diff --git a/VideoNodes/VideoNodes.en.json b/VideoNodes/VideoNodes.en.json index 8536d03d..c119a203 100644 --- a/VideoNodes/VideoNodes.en.json +++ b/VideoNodes/VideoNodes.en.json @@ -1,6 +1,21 @@ { "Flow":{ "Parts": { + "AudioAddTrack": { + "Outputs": { + "1": "Audio track adde 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.", + "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" + } + + }, "AudioAdjustVolume": { "Outputs": { "1": "Audio tracks volume was adjusted and saved to temporary file", diff --git a/VideoNodes/VideoNodes/AudioAddTrack.cs b/VideoNodes/VideoNodes/AudioAddTrack.cs new file mode 100644 index 00000000..a5403dbe --- /dev/null +++ b/VideoNodes/VideoNodes/AudioAddTrack.cs @@ -0,0 +1,179 @@ +namespace FileFlows.VideoNodes +{ + using FileFlows.Plugin; + using FileFlows.Plugin.Attributes; + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + + public class AudioAddTrack: EncodingNode + { + public override int Outputs => 1; + + public override string Icon => "fas fa-volume-down"; + + [NumberInt(1)] + [Range(1, 100)] + public int Index { get; set; } + + + [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; + } + } + + [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 = "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; + } + } + + public override int Execute(NodeParameters args) + { + try + { + VideoInfo videoInfo = GetVideoInfo(args); + if (videoInfo == null) + return -1; + + string ffmpegExe = GetFFMpegExe(args); + if (string.IsNullOrEmpty(ffmpegExe)) + return -1; + + List ffArgs = new List + { + "-c", "copy", + "-map", "0:v", + }; + + bool added = false; + int audioIndex = 0; + for(int i = 0; i < videoInfo.AudioStreams.Count; i++) + { + if((i + 1) == Index) + { + ffArgs.AddRange(GetNewAudioTrackParameters(videoInfo, audioIndex)); + added = true; + ++audioIndex; + } + ffArgs.AddRange(new[] + { + "-map", videoInfo.AudioStreams[i].IndexString, + "-c:a:" + audioIndex, "copy" + }); + ++audioIndex; + } + + if(added == false) // incase the index is greater than the number of tracks this file has + ffArgs.AddRange(GetNewAudioTrackParameters(videoInfo, audioIndex)); + + if (videoInfo.SubtitleStreams?.Any() == true) + ffArgs.AddRange(new[] { "-map", "0:s" }); + + if (Index < 2) + { + // this makes the first audio track now the default track + ffArgs.AddRange(new[] { "-disposition:a:0", "default" }); + } + + string extension = new FileInfo(args.WorkingFile).Extension; + if(extension.StartsWith(".")) + extension = extension.Substring(1); + + if (Encode(args, ffmpegExe, ffArgs, extension) == false) + return -1; + + return 1; + } + catch (Exception ex) + { + args.Logger?.ELog("Failed processing VideoFile: " + ex.Message); + return -1; + } + } + + private string[] GetNewAudioTrackParameters(VideoInfo videoInfo, int index) + { + if (Channels == 0) + { + // same as source + return new[] + { + "-map", videoInfo.AudioStreams[0].IndexString, + "-c:a:" + index, Codec, + "-b:a:" + index, Bitrate + "k" + }; + } + else + { + return new[] + { + "-map", videoInfo.AudioStreams[0].IndexString, + "-c:a:" + index, Codec, + "-ac", Channels.ToString(), + "-b:a:" + index, Bitrate + "k" + }; + } + } + } +} diff --git a/VideoNodes/VideoNodes/AudioTrackReorder.cs b/VideoNodes/VideoNodes/AudioTrackReorder.cs index 6d6c102e..18608ed4 100644 --- a/VideoNodes/VideoNodes/AudioTrackReorder.cs +++ b/VideoNodes/VideoNodes/AudioTrackReorder.cs @@ -142,6 +142,6 @@ args.Logger?.ELog("Failed processing VideoFile: " + ex.Message); return -1; } -} + } } }