refactored video hw testing

removed obsolete plugins
This commit is contained in:
john
2022-11-04 13:08:29 +13:00
parent ee200bfe3a
commit 767aaac83b
68 changed files with 149 additions and 5166 deletions

View File

@@ -5,8 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

View File

@@ -6,8 +6,8 @@
<Nullable>enable</Nullable>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<PublishSingleFile>true</PublishSingleFile>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

View File

@@ -4,8 +4,8 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>
<Authors>John Andrews</Authors>

View File

@@ -5,8 +5,8 @@
<Nullable>enable</Nullable>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<PublishSingleFile>true</PublishSingleFile>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>
<Authors>John Andrews</Authors>

View File

@@ -4,8 +4,8 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

View File

@@ -5,8 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

View File

@@ -5,8 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

View File

@@ -9,8 +9,8 @@
<PublishTrimmed>true</PublishTrimmed>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<PublishSingleFile>true</PublishSingleFile>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>
<Authors>John Andrews</Authors>

View File

@@ -5,8 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

Binary file not shown.

Binary file not shown.

View File

@@ -13,8 +13,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChecksumNodes", "ChecksumNo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CollectionNodes", "CollectionNodes\CollectionNodes.csproj", "{4211AAE9-0764-4626-B0F2-089A466E002A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicNodes", "MusicNodes\MusicNodes.csproj", "{A7D498A4-5C67-45E9-9D33-20E8F570C25A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EmailNodes", "EmailNodes\EmailNodes.csproj", "{8F2739B8-2BD6-4AAC-A54B-2EE1B4AAF559}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gotify", "Gotify\Gotify.csproj", "{18E7013A-BF85-47FA-806C-934267C7BCA5}"
@@ -29,8 +27,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apprise", "Apprise\Apprise.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageNodes", "ImageNodes\ImageNodes.csproj", "{3C6B9933-B6BC-4C00-9247-71F575AA276B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VideoLegacyNodes", "VideoLegacyNodes\VideoLegacyNodes.csproj", "{4339720B-5061-431F-9080-FC087ACFDB3B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudioNodes", "AudioNodes\AudioNodes.csproj", "{600204C7-94F1-4793-9D02-D836A1B15B60}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComicNodes", "ComicNodes\ComicNodes.csproj", "{45568FCB-00FF-4AEA-AA43-DC569D667B04}"
@@ -61,10 +57,6 @@ Global
{4211AAE9-0764-4626-B0F2-089A466E002A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4211AAE9-0764-4626-B0F2-089A466E002A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4211AAE9-0764-4626-B0F2-089A466E002A}.Release|Any CPU.Build.0 = Release|Any CPU
{A7D498A4-5C67-45E9-9D33-20E8F570C25A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7D498A4-5C67-45E9-9D33-20E8F570C25A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7D498A4-5C67-45E9-9D33-20E8F570C25A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7D498A4-5C67-45E9-9D33-20E8F570C25A}.Release|Any CPU.Build.0 = Release|Any CPU
{8F2739B8-2BD6-4AAC-A54B-2EE1B4AAF559}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F2739B8-2BD6-4AAC-A54B-2EE1B4AAF559}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F2739B8-2BD6-4AAC-A54B-2EE1B4AAF559}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -93,10 +85,6 @@ Global
{3C6B9933-B6BC-4C00-9247-71F575AA276B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C6B9933-B6BC-4C00-9247-71F575AA276B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C6B9933-B6BC-4C00-9247-71F575AA276B}.Release|Any CPU.Build.0 = Release|Any CPU
{4339720B-5061-431F-9080-FC087ACFDB3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4339720B-5061-431F-9080-FC087ACFDB3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4339720B-5061-431F-9080-FC087ACFDB3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4339720B-5061-431F-9080-FC087ACFDB3B}.Release|Any CPU.Build.0 = Release|Any CPU
{600204C7-94F1-4793-9D02-D836A1B15B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{600204C7-94F1-4793-9D02-D836A1B15B60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{600204C7-94F1-4793-9D02-D836A1B15B60}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@@ -5,8 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

View File

@@ -5,8 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

View File

@@ -6,8 +6,8 @@
<Nullable>enable</Nullable>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<PublishSingleFile>true</PublishSingleFile>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

View File

@@ -1,17 +0,0 @@
namespace FileFlows.MusicNodes
{
internal static class ExtensionMethods
{
public static void AddOrUpdate(this Dictionary<string, object> dict, string key, object value)
{
if (dict.ContainsKey(key))
dict[key] = value;
else
dict.Add(key, value);
}
public static string? EmptyAsNull(this string str)
{
return str == string.Empty ? null : str;
}
}
}

View File

@@ -1,66 +0,0 @@
namespace FileFlows.MusicNodes
{
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class MusicFile : MusicNode
{
public override bool Obsolete => true;
public override int Outputs => 1;
public override FlowElementType Type => FlowElementType.Input;
private Dictionary<string, object> _Variables;
public override Dictionary<string, object> Variables => _Variables;
public MusicFile()
{
_Variables = new Dictionary<string, object>()
{
{ "mi.Album", "Album" },
{ "mi.Artist", "Artist" },
{ "mi.ArtistThe", "Artist, The" },
{ "mi.Bitrate", 845 },
{ "mi.Channels", 2 },
{ "mi.Codec", "flac" },
{ "mi.Date", new DateTime(2020, 05, 23) },
{ "mi.Year", 2020 },
{ "mi.Duration", 256 },
{ "mi.Encoder", "FLAC 1.2.1" },
{ "mi.Frequency", 44100 },
{ "mi.Genres", new [] { "Pop", "Rock" } },
{ "mi.Language", "English" },
{ "mi.Title", "Song Title" },
{ "mi.Track", 2 },
{ "mi.Disc", 2 },
{ "mi.TotalDiscs", 2 }
};
}
public override int Execute(NodeParameters args)
{
string ffmpegExe = GetFFMpegExe(args);
if (string.IsNullOrEmpty(ffmpegExe))
return -1;
try
{
if (ReadMusicFileInfo(args, ffmpegExe, args.WorkingFile))
return 1;
var musicInfo = GetMusicInfo(args);
if (string.IsNullOrEmpty(musicInfo.Codec) == false)
args.RecordStatistic("CODEC", musicInfo.Codec);
return 0;
}
catch (Exception ex)
{
args.Logger.ELog("Failed processing MusicFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,21 +0,0 @@
namespace FileFlows.MusicNodes
{
public class MusicInfo
{
public string Language { get; set; }
public int Track { get; set; }
public int Disc { get; set; }
public int TotalDiscs { get; set; }
public string Artist { get; set; }
public string Title { get; set; }
public string Album { get; set; }
public DateTime Date { get; set; }
public string[] Genres { get; set; }
public string Encoder { get; set; }
public long Duration { get; set; }
public long Bitrate { get; set; }
public string Codec { get; set; }
public long Channels { get; set; }
public long Frequency { get; set; }
}
}

View File

@@ -1,328 +0,0 @@
namespace FileFlows.MusicNodes
{
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
public class MusicInfoHelper
{
private string ffMpegExe;
private ILogger Logger;
public MusicInfoHelper(string ffMpegExe, ILogger logger)
{
this.ffMpegExe = ffMpegExe;
this.Logger = logger;
}
public static string GetFFMpegPath(NodeParameters args) => args.GetToolPath("FFMpeg");
public MusicInfo Read(string filename)
{
var mi = new MusicInfo();
if (File.Exists(filename) == false)
{
Logger.ELog("File not found: " + filename);
return mi;
}
if (string.IsNullOrEmpty(ffMpegExe) || File.Exists(ffMpegExe) == false)
{
Logger.ELog("FFMpeg not found: " + (ffMpegExe ?? "not passed in"));
return mi;
}
mi = ReadMetaData(filename);
try
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo();
process.StartInfo.FileName = ffMpegExe;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.Arguments = $"-hide_banner -i \"{filename}\"";
process.Start();
string output = process.StandardError.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (string.IsNullOrEmpty(error) == false && error != "At least one output file must be specified")
{
Logger.ELog("Failed reading ffmpeg info: " + error);
return mi;
}
Logger.ILog("Music Information:" + Environment.NewLine + output);
if(output.IndexOf("Input #0") < 0)
{
Logger.ELog("Failed to read audio information for file");
return mi;
}
if (output.ToLower().Contains("mp3"))
mi.Codec = "mp3";
else if (output.ToLower().Contains("ogg"))
mi.Codec = "ogg";
else if (output.ToLower().Contains("flac"))
mi.Codec = "flac";
else if (output.ToLower().Contains("wav"))
mi.Codec = "wav";
else if (filename.ToLower().EndsWith(".mp3"))
mi.Codec = "mp3";
else if (filename.ToLower().EndsWith(".ogg"))
mi.Codec = "ogg";
else if (filename.ToLower().EndsWith(".flac"))
mi.Codec = "flac";
else if (filename.ToLower().EndsWith(".wav"))
mi.Codec = "wav";
foreach (string line in output.Split('\n'))
{
int colonIndex = line.IndexOf(":");
if(colonIndex < 1)
continue;
string lowLine = line.ToLower().Trim();
if (lowLine.StartsWith("language"))
mi.Language = line.Substring(colonIndex + 1).Trim();
else if (lowLine.StartsWith("track") && lowLine.Contains("total") == false)
{
if (mi.Track < 1)
{
var trackMatch = Regex.Match(line.Substring(colonIndex + 1).Trim(), @"^[\d]+");
if (trackMatch.Success && int.TryParse(trackMatch.Value, out int value))
mi.Track = value;
}
}
else if (lowLine.StartsWith("artist") || lowLine.StartsWith("album_artist"))
{
if (string.IsNullOrWhiteSpace(mi.Artist))
mi.Artist = line.Substring(colonIndex + 1).Trim();
}
else if (lowLine.StartsWith("title") && lowLine.Contains(".jpg") == false)
{
if (string.IsNullOrWhiteSpace(mi.Title))
mi.Title = line.Substring(colonIndex + 1).Trim();
}
else if (lowLine.StartsWith("album"))
{
if (string.IsNullOrWhiteSpace(mi.Album))
mi.Album = line.Substring(colonIndex + 1).Trim();
}
else if (lowLine.StartsWith("disc"))
{
if (int.TryParse(line.Substring(colonIndex + 1).Trim(), out int value))
mi.Disc = value;
}
else if (lowLine.StartsWith("disctotal") || lowLine.StartsWith("totaldiscs"))
{
if (int.TryParse(line.Substring(colonIndex + 1).Trim(), out int value))
mi.TotalDiscs = value;
}
else if (lowLine.StartsWith("date") || lowLine.StartsWith("retail date") || lowLine.StartsWith("retaildate") || lowLine.StartsWith("originaldate") || lowLine.StartsWith("original date"))
{
if (int.TryParse(line.Substring(colonIndex + 1).Trim(), out int value))
{
if(mi.Date < new DateTime(1900, 1, 1))
mi.Date = new DateTime(value, 1, 1);
}
else if(DateTime.TryParse(line.Substring(colonIndex + 1).Trim(), out DateTime dtValue) && dtValue.Year > 1900)
mi.Date = dtValue;
}
else if (lowLine.StartsWith("genre"))
{
if(mi.Genres?.Any() != true)
mi.Genres = line.Substring(colonIndex + 1).Trim().Split(' ');
}
else if (lowLine.StartsWith("encoder"))
mi.Encoder = line.Substring(colonIndex + 1).Trim();
else if (lowLine.StartsWith("duration"))
{
if (mi.Duration < 1)
{
string temp = line.Substring(colonIndex + 1).Trim();
if (temp.IndexOf(",") > 0)
{
temp = temp.Substring(0, temp.IndexOf(","));
if (TimeSpan.TryParse(temp, out TimeSpan value))
mi.Duration = (long)value.TotalSeconds;
}
}
}
if (line.ToLower().IndexOf("bitrate:") > 0)
{
string br = line.Substring(line.ToLower().IndexOf("bitrate:") + "bitrate:".Length).Trim();
if (br.IndexOf(" ") > 0)
{
br = br.Substring(0, br.IndexOf(" "));
if (long.TryParse(br, out long value))
mi.Bitrate = value;
}
}
var match = Regex.Match(line, @"([\d]+) Hz");
if (match.Success)
{
mi.Frequency = int.Parse(match.Groups[1].Value);
}
if (line.IndexOf(" stereo,") > 0)
mi.Channels = 2;
}
}
}
catch (Exception ex)
{
Logger.ELog(ex.Message, ex.StackTrace.ToString());
}
if (string.IsNullOrEmpty(mi.Artist) || string.IsNullOrEmpty(mi.Album) || mi.Track < 1 || string.IsNullOrEmpty(mi.Title))
{
// try parse the file
ParseFileNameInfo(filename, mi);
}
return mi;
}
public MusicInfo ReadMetaData(string file)
{
using var tfile = TagLib.File.Create(file);
MusicInfo info = new MusicInfo();
try
{
info.Title = tfile.Tag.Title;
info.Duration = (long)tfile.Properties.Duration.TotalSeconds;
info.TotalDiscs = Convert.ToInt32(tfile.Tag.DiscCount);
if (info.TotalDiscs < 1)
info.TotalDiscs = 1;
info.Disc = Convert.ToInt32(tfile.Tag.Disc);
if (info.Disc < 1)
info.Disc = 1;
info.Artist = String.Join(", ", tfile.Tag.AlbumArtists);
info.Album = tfile.Tag.Album;
info.Track = Convert.ToInt32(tfile.Tag.Track);
if(tfile.Tag.Year > 1900)
{
info.Date = new DateTime(Convert.ToInt32(tfile.Tag.Year), 1, 1);
}
info.Genres = tfile.Tag.Genres.SelectMany(x => x.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim())).ToArray();
}
catch (Exception) { }
tfile.Dispose();
return info;
}
public void ParseFileNameInfo(string filename, MusicInfo mi)
{
using var tfile = TagLib.File.Create(filename);
try
{
var fileInfo = new FileInfo(filename);
bool dirty = false;
if (mi.Disc < 1)
{
var cdMatch = Regex.Match(filename.Replace("\\", "/"), @"(?<=(/(cd|disc)))[\s]*([\d]+)(?!=(/))", RegexOptions.IgnoreCase);
if (cdMatch.Success && int.TryParse(cdMatch.Value.Trim(), out int disc))
{
dirty = true;
mi.Disc = disc;
tfile.Tag.Disc = Convert.ToUInt32(disc);
}
}
if (mi.Track < 1)
{
var trackMatch = Regex.Match(fileInfo.Name, @"[\-_\s\.]+([\d]{1,2})[\-_\s\.]+");
if (trackMatch.Success)
{
string trackString = trackMatch.Value;
if (int.TryParse(Regex.Match(trackString, @"[\d]+").Value, out int track))
{
mi.Track = track;
tfile.Tag.Track = Convert.ToUInt32(track);
dirty = true;
}
}
}
string album = fileInfo.Directory.Name;
var yearMatch = Regex.Match(album, @"(?<=(\())[\d]{4}(?!=\))");
if (yearMatch.Success)
{
album = album.Replace("(" + yearMatch.Value + ")", "").Trim();
if (mi.Date < new DateTime(1900, 1, 1))
{
if (int.TryParse(yearMatch.Value, out int year))
{
mi.Date = new DateTime(year, 1, 1);
tfile.Tag.Year = Convert.ToUInt32(year);
dirty = true;
}
}
}
if (string.IsNullOrEmpty(mi.Album))
{
mi.Album = album;
if (string.IsNullOrEmpty(album) == false)
{
tfile.Tag.Album = mi.Album;
dirty = true;
}
}
if (string.IsNullOrEmpty(mi.Artist))
{
mi.Artist = fileInfo.Directory.Parent.Name;
if (string.IsNullOrEmpty(mi.Artist) == false)
{
tfile.Tag.AlbumArtists = new[] { mi.Artist };
dirty = true;
}
}
// the title
if (string.IsNullOrEmpty(mi.Title))
{
int titleIndex = fileInfo.Name.LastIndexOf(" - ");
if (titleIndex > 0)
{
mi.Title = fileInfo.Name.Substring(titleIndex + 3);
if (string.IsNullOrEmpty(fileInfo.Extension) == false)
{
mi.Title = mi.Title.Replace(fileInfo.Extension, "");
tfile.Tag.Title = mi.Title;
dirty = true;
}
}
}
if(dirty)
tfile.Save();
}
catch (Exception ex)
{
Logger?.WLog("Failed parsing music info from filename: " + ex.Message + Environment.NewLine + ex.StackTrace);
}
finally
{
tfile.Dispose();
}
}
}
}

View File

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-16"?><Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<PublishSingleFile>true</PublishSingleFile>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>
<Authors>John Andrews</Authors>
<Product>Music Nodes</Product>
<PackageProjectUrl>https://fileflows.com/</PackageProjectUrl>
<Description>OBSOLETE. This plugin has been replaced by the Audio Nodes plugin.</Description>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;CS8618;CS8601;CS8602;CS8603;CS8604;CS8618;CS8625;CS8765;CS8767;</NoWarn>
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;CS8618;CS8601;CS8602;CS8603;CS8604;CS8618;CS8625;CS8765;CS8767;</NoWarn>
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup Condition=" '$(Configuration)' == 'Debug'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
</ItemGroup>
<ItemGroup>
<Content Include="MusicNodes.en.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="TagLibSharp" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="Plugin">
<HintPath>..\FileFlows.Plugin.dll</HintPath>
<SpecificVersion>False</SpecificVersion>
</Reference>
</ItemGroup>
</Project>

View File

@@ -1,83 +0,0 @@
{
"Flow":{
"Parts": {
"MusicFile": {
"Description": "An input music file that has had its Music Information read and can be processed",
"Outputs": {
"1": "Music file from library"
}
},
"AudioFileNormalization": {
"Description": "Normalizes an audio file using two passes of FFMPEGs loudnorm filter",
"Outputs": {
"1": "Audio file normalized and saved to temporary file"
}
},
"ConvertAudio": {
"Description": "Convert a music file to the specified audio codec",
"Outputs": {
"1": "Audio converted and saved to temporary file",
"2": "Audio already in codec, no conversion done"
},
"Fields": {
"Bitrate": "Bitrate",
"Bitrate-Help": "The bitrate for the new file, the higher the bitrate the better the quality but larger the file.",
"Codec": "Codec",
"Codec-Help": "The audio codec to convert the file into.",
"SkipIfCodecMatches": "Skip If Codec Matches",
"SkipIfCodecMatches-Help": "If the existing audio codec matches, this file will not be processed regardless of the bitrate. Otherwise if off, the bitrate must be less than or equal to for it to skip."
}
},
"ConvertToAAC": {
"Description": "Convert a music file to AAC",
"Outputs": {
"1": "Audio converted and saved to temporary file"
},
"Fields": {
"Bitrate": "Bitrate",
"Bitrate-Help": "The bitrate for the new AAC file, the higher the bitrate the better the quality but larger the file. 192 Kbps is the recommended rate."
}
},
"ConvertToFLAC": {
"Description": "Convert a music file to FLAC",
"Outputs": {
"1": "Audio converted and saved to temporary file"
},
"Fields": {
"Bitrate": "Bitrate",
"Bitrate-Help": "The bitrate for the new FLAC file, the higher the bitrate the better the quality but larger the file. 128 Kbps is the recommended rate."
}
},
"ConvertToMP3": {
"Description": "Convert a music file to MP3",
"Outputs": {
"1": "Audio converted and saved to temporary file"
},
"Fields": {
"Bitrate": "Bitrate",
"Bitrate-Help": "The bitrate for the new MP3 file, the higher the bitrate the better the quality but larger the file. 192 Kbps is the recommended rate."
}
},
"ConvertToOGG": {
"Description": "Convert a music file to OGG",
"Outputs": {
"1": "Audio converted and saved to temporary file"
},
"Fields": {
"Bitrate": "Bitrate",
"Bitrate-Help": "The bitrate for the new OGG file, the higher the bitrate the better the quality but larger the file. 128 Kbps is the recommended rate."
}
},
"ConvertToWAV": {
"Description": "Convert a music file to WAV",
"Outputs": {
"1": "Audio converted and saved to temporary file"
},
"Fields": {
"Bitrate": "Bitrate",
"Bitrate-Help": "The bitrate for the new WAV file, the higher the bitrate the better the quality but larger the file. 128 Kbps is the recommended rate."
}
}
}
}
}

View File

@@ -1,116 +0,0 @@
using FileFlows.Plugin;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
namespace FileFlows.MusicNodes;
public class AudioFileNormalization : MusicNode
{
public override bool Obsolete => true;
public override int Inputs => 1;
public override int Outputs => 1;
public override FlowElementType Type => FlowElementType.Process;
public override string Icon => "fas fa-volume-up";
const string LOUDNORM_TARGET = "I=-24:LRA=7:TP=-2.0";
public override int Execute(NodeParameters args)
{
try
{
string ffmpegExe = GetFFMpegExe(args);
if (string.IsNullOrEmpty(ffmpegExe))
return -1;
MusicInfo musicInfo = GetMusicInfo(args);
if (musicInfo == null)
return -1;
List<string> ffArgs = new List<string>();
long sampleRate = musicInfo.Frequency > 0 ? musicInfo.Frequency : 48_000;
string twoPass = DoTwoPass(args, ffmpegExe);
ffArgs.AddRange(new[] { "-i", args.WorkingFile, "-c:a", musicInfo.Codec, "-ar", sampleRate.ToString(), "-af", twoPass });
string extension = new FileInfo(args.WorkingFile).Extension;
if (extension.StartsWith("."))
extension = extension.Substring(1);
string outputFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + "." + extension);
ffArgs.Add(outputFile);
var result = args.Execute(new ExecuteArgs
{
Command = ffmpegExe,
ArgumentList = ffArgs.ToArray()
});
return result.ExitCode == 0 ? 1 : -1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing AudioFile: " + ex.Message);
return -1;
}
}
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public string DoTwoPass(NodeParameters args, string ffmpegExe)
{
//-af loudnorm=I=-24:LRA=7:TP=-2.0"
var result = args.Execute(new ExecuteArgs
{
Command = ffmpegExe,
ArgumentList = new[]
{
"-hide_banner",
"-i", args.WorkingFile,
"-af", "loudnorm=" + LOUDNORM_TARGET + ":print_format=json",
"-f", "null",
"-"
}
});
if(result.ExitCode != 0)
throw new Exception("Failed to prcoess audio track");
string output = result.StandardOutput;
int index = output.LastIndexOf("{");
if (index == -1)
throw new Exception("Failed to detected json in output");
string json = output.Substring(index);
json = json.Substring(0, json.IndexOf("}") + 1);
if (string.IsNullOrEmpty(json))
throw new Exception("Failed to parse TwoPass json");
LoudNormStats stats = JsonSerializer.Deserialize<LoudNormStats>(json);
string ar = $"loudnorm=print_format=summary:linear=true:{LOUDNORM_TARGET}:measured_I={stats.input_i}:measured_LRA={stats.input_lra}:measured_tp={stats.input_tp}:measured_thresh={stats.input_thresh}:offset={stats.target_offset}";
return ar;
}
private class LoudNormStats
{
/*
{
"input_i" : "-7.47",
"input_tp" : "12.33",
"input_lra" : "6.70",
"input_thresh" : "-18.13",
"output_i" : "-24.25",
"output_tp" : "-3.60",
"output_lra" : "5.90",
"output_thresh" : "-34.74",
"normalization_type" : "dynamic",
"target_offset" : "0.25"
}
*/
public string input_i { get; set; }
public string input_tp { get; set; }
public string input_lra { get; set; }
public string input_thresh { get; set; }
public string target_offset { get; set; }
}
}

View File

@@ -1,314 +0,0 @@
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FileFlows.MusicNodes
{
public class ConvertToMP3 : ConvertNode
{
public override bool Obsolete => true;
protected override string Extension => "mp3";
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
protected override List<string> GetArguments()
{
return new List<string>
{
"-c:a",
"mp3",
"-ab",
Bitrate + "k"
};
}
}
public class ConvertToWAV : ConvertNode
{
protected override string Extension => "wav";
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
protected override List<string> GetArguments()
{
return new List<string>
{
"-c:a",
"pcm_s16le",
"-ab",
Bitrate + "k"
};
}
}
public class ConvertToAAC : ConvertNode
{
protected override string Extension => "aac";
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
protected override bool SetId3Tags => true;
protected override List<string> GetArguments()
{
return new List<string>
{
"-c:a",
"aac",
"-ab",
Bitrate + "k"
};
}
}
public class ConvertToOGG: ConvertNode
{
protected override string Extension => "ogg";
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
protected override List<string> GetArguments()
{
return new List<string>
{
"-c:a",
"libvorbis",
"-ab",
Bitrate + "k"
};
}
}
//public class ConvertToFLAC : ConvertNode
//{
// protected override string Extension => "flac";
// public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
// protected override List<string> GetArguments()
// {
// return new List<string>
// {
// "-c:a",
// "flac",
// "-ab",
// Bitrate + "k"
// };
// }
//}
public class ConvertAudio : ConvertNode
{
protected override string Extension => Codec;
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
[Select(nameof(CodecOptions), 0)]
public string Codec { get; set; }
[Boolean(3)]
public bool SkipIfCodecMatches { get; set; }
public override int Outputs => 2;
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 = "MP3", Value = "MP3"},
new ListOption { Label = "OGG", Value = "ogg"},
new ListOption { Label = "WAV", Value = "wav"},
};
}
return _CodecOptions;
}
}
protected override List<string> GetArguments()
{
string codec = Codec switch
{
"ogg" => "libvorbis",
"wav" => "pcm_s16le",
_ => Codec.ToLower()
};
return new List<string>
{
"-c:a",
codec,
"-ab",
Bitrate + "k"
};
}
public override int Execute(NodeParameters args)
{
MusicInfo musicInfo = GetMusicInfo(args);
if (musicInfo == null)
return -1;
if(musicInfo.Codec?.ToLower() == Codec?.ToLower())
{
if (SkipIfCodecMatches)
{
args.Logger?.ILog($"Music file already '{Codec}' at bitrate '{musicInfo.Bitrate}', and set to skip if codec matches");
return 2;
}
if(musicInfo.Bitrate <= Bitrate)
{
args.Logger?.ILog($"Music file already '{Codec}' at bitrate '{musicInfo.Bitrate}'");
return 2;
}
}
return base.Execute(args);
}
}
public abstract class ConvertNode:MusicNode
{
protected abstract string Extension { get; }
protected virtual bool SetId3Tags => false;
public override int Inputs => 1;
public override int Outputs => 1;
protected virtual List<string> GetArguments()
{
return new List<string>
{
"-map_metadata",
"0:0",
"-ab",
Bitrate + "k"
};
}
public override FlowElementType Type => FlowElementType.Process;
[Select(nameof(BitrateOptions), 1)]
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 = "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)
{
string ffmpegExe = GetFFMpegExe(args);
if (string.IsNullOrEmpty(ffmpegExe))
return -1;
//MusicInfo musicInfo = GetMusicInfo(args);
//if (musicInfo == null)
// return -1;
if (Bitrate < 64 || Bitrate > 320)
{
args.Logger?.ILog("Bitrate not set or invalid, setting to 192kbps");
Bitrate = 192;
}
string outputFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + "." + Extension);
var ffArgs = GetArguments();
ffArgs.Insert(0, "-hide_banner");
ffArgs.Insert(1, "-y"); // tells ffmpeg to replace the file if already exists, which it shouldnt but just incase
ffArgs.Insert(2, "-i");
ffArgs.Insert(3, args.WorkingFile);
ffArgs.Insert(4, "-vn"); // disables video
ffArgs.Add(outputFile);
args.Logger?.ILog("FFArgs: " + String.Join(" ", ffArgs.Select(x => x.IndexOf(" ") > 0 ? "\"" + x + "\"" : x).ToArray()));
var result = args.Execute(new ExecuteArgs
{
Command = ffmpegExe,
ArgumentList = ffArgs.ToArray()
});
if(result.ExitCode != 0)
{
args.Logger?.ELog("Invalid exit code detected: " + result.ExitCode);
return -1;
}
//CopyMetaData(outputFile, args.FileName);
args.SetWorkingFile(outputFile);
// update the music file info
if (ReadMusicFileInfo(args, ffmpegExe, args.WorkingFile))
return 1;
return -1;
}
//private void CopyMetaData(string outputFile, string originalFile)
//{
// Track original = new Track(originalFile);
// Track dest = new Track(outputFile);
// dest.Album = original.Album;
// dest.AlbumArtist = original.AlbumArtist;
// dest.Artist = original.Artist;
// dest.Comment = original.Comment;
// dest.Composer= original.Composer;
// dest.Conductor = original.Conductor;
// dest.Copyright = original.Copyright;
// dest.Date = original.Date;
// dest.Description= original.Description;
// dest.DiscNumber= original.DiscNumber;
// dest.DiscTotal = original.DiscTotal;
// if (original.EmbeddedPictures?.Any() == true)
// {
// foreach (var pic in original.EmbeddedPictures)
// dest.EmbeddedPictures.Add(pic);
// }
// dest.Genre= original.Genre;
// dest.Lyrics= original.Lyrics;
// dest.OriginalAlbum= original.OriginalAlbum;
// dest.OriginalArtist = original.OriginalArtist;
// dest.Popularity= original.Popularity;
// dest.Publisher= original.Publisher;
// dest.PublishingDate= original.PublishingDate;
// dest.Title= original.Title;
// dest.TrackNumber= original.TrackNumber;
// dest.TrackTotal= original.TrackTotal;
// dest.Year= original.Year;
// foreach (var key in original.AdditionalFields.Keys)
// {
// if(dest.AdditionalFields.ContainsKey(key))
// dest.AdditionalFields[key] = original.AdditionalFields[key];
// else
// dest.AdditionalFields.Add(key, original.AdditionalFields[key]);
// }
// dest.Save();
//}
}
}

View File

@@ -1,110 +0,0 @@
namespace FileFlows.MusicNodes
{
using FileFlows.Plugin;
public abstract class MusicNode : Node
{
public override bool Obsolete => true;
public override string Icon => "fas fa-music";
protected string GetFFMpegExe(NodeParameters args)
{
string ffmpeg = args.GetToolPath("FFMpeg");
if (string.IsNullOrEmpty(ffmpeg))
{
args.Logger.ELog("FFMpeg tool not found.");
return "";
}
var fileInfo = new FileInfo(ffmpeg);
if (fileInfo.Exists == false)
{
args.Logger.ELog("FFMpeg tool configured by ffmpeg file does not exist.");
return "";
}
return fileInfo.FullName;
}
protected string GetFFMpegPath(NodeParameters args)
{
string ffmpeg = args.GetToolPath("FFMpeg");
if (string.IsNullOrEmpty(ffmpeg))
{
args.Logger.ELog("FFMpeg tool not found.");
return "";
}
var fileInfo = new FileInfo(ffmpeg);
if (fileInfo.Exists == false)
{
args.Logger.ELog("FFMpeg tool configured by ffmpeg file does not exist.");
return "";
}
return fileInfo.DirectoryName;
}
private const string MUSIC_INFO = "MusicInfo";
protected void SetMusicInfo(NodeParameters args, MusicInfo musicInfo, Dictionary<string, object> variables)
{
if (args.Parameters.ContainsKey(MUSIC_INFO))
args.Parameters[MUSIC_INFO] = musicInfo;
else
args.Parameters.Add(MUSIC_INFO, musicInfo);
if(musicInfo.Artist.EndsWith(", The"))
variables.AddOrUpdate("mi.Artist", "The " + musicInfo.Artist.Substring(0, musicInfo.Artist.Length - ", The".Length).Trim());
else
variables.AddOrUpdate("mi.Artist", musicInfo.Artist);
if(musicInfo.Artist?.StartsWith("The ") == true)
variables.AddOrUpdate("mi.ArtistThe", musicInfo.Artist.Substring(4).Trim() + ", The");
else
variables.AddOrUpdate("mi.ArtistThe", musicInfo.Artist);
variables.AddOrUpdate("mi.Album", musicInfo.Album);
variables.AddOrUpdate("mi.Bitrate", musicInfo.Bitrate);
variables.AddOrUpdate("mi.Channels", musicInfo.Channels);
variables.AddOrUpdate("mi.Codec", musicInfo.Codec);
variables.AddOrUpdate("mi.Date", musicInfo.Date);
variables.AddOrUpdate("mi.Year", musicInfo.Date.Year);
variables.AddOrUpdate("mi.Duration", musicInfo.Duration);
variables.AddOrUpdate("mi.Encoder", musicInfo.Encoder);
variables.AddOrUpdate("mi.Frequency", musicInfo.Frequency);
variables.AddOrUpdate("mi.Genres", musicInfo.Genres);
variables.AddOrUpdate("mi.Language", musicInfo.Language);
variables.AddOrUpdate("mi.Title", musicInfo.Title);
variables.AddOrUpdate("mi.Track", musicInfo.Track);
variables.AddOrUpdate("mi.Disc", musicInfo.Disc < 1 ? 1 : musicInfo.Disc);
variables.AddOrUpdate("mi.TotalDiscs", musicInfo.TotalDiscs < 1 ? 1 : musicInfo.TotalDiscs);
args.UpdateVariables(variables);
}
protected MusicInfo GetMusicInfo(NodeParameters args)
{
if (args.Parameters.ContainsKey(MUSIC_INFO) == false)
{
args.Logger.WLog("No codec information loaded, use a 'Music File' node first");
return null;
}
var result = args.Parameters[MUSIC_INFO] as MusicInfo;
if (result == null)
{
args.Logger.WLog("MusicInfo not found for file");
return null;
}
return result;
}
protected bool ReadMusicFileInfo(NodeParameters args, string ffmpegExe, string filename)
{
var musicInfo = new MusicInfoHelper(ffmpegExe, args.Logger).Read(filename);
if (musicInfo.Duration == 0)
{
args.Logger?.ILog("Failed to load music information.");
return false;
}
SetMusicInfo(args, musicInfo, Variables);
return true;
}
}
}

View File

@@ -1,15 +0,0 @@
namespace FileFlows.MusicNodes;
using System.ComponentModel.DataAnnotations;
using FileFlows.Plugin.Attributes;
public class Plugin : FileFlows.Plugin.IPlugin
{
public Guid Uid => new Guid("d84fbd06-f0e3-4827-8de0-6b0ef20dd883");
public string Name => "Music Nodes (Obsolete)";
public string MinimumVersion => "1.0.4.2019";
public void Init()
{
}
}

View File

@@ -1,82 +0,0 @@
#if(DEBUG)
namespace FileFlows.MusicNodes.Tests;
using FileFlows.MusicNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class AudioFileNormalizationTests
{
[TestMethod]
public void AudioFileNormalization_Mp3()
{
const string file = @"D:\music\unprocessed\01-billy_joel-movin_out.mp3";
AudioFileNormalization node = new ();
var logger = new TestLogger();
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
new MusicFile().Execute(args); // need to read the music info and set it
int output = node.Execute(args);
string log = logger.ToString();
Assert.AreEqual(1, output);
}
[TestMethod]
public void AudioFileNormalization_Bulk()
{
foreach (var file in Directory.GetFiles(@"d:\music\unprocessed"))
{
AudioFileNormalization node = new();
var logger = new TestLogger();
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
new MusicFile().Execute(args); // need to read the music info and set it
int output = node.Execute(args);
string log = logger.ToString();
Assert.AreEqual(1, output);
}
}
[TestMethod]
public void AudioFileNormalization_ConvertFlacToMp3()
{
const string file = @"D:\music\flacs\03-billy_joel-dont_ask_me_why.flac";
var logger = new TestLogger();
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
new MusicFile().Execute(args); // need to read the music info and set it
ConvertToMP3 convertNode = new();
int output = convertNode.Execute(args);
AudioFileNormalization normalNode = new();
output = normalNode.Execute(args);
string log = logger.ToString();
Assert.AreEqual(1, output);
}
}
#endif

View File

@@ -1,153 +0,0 @@
#if(DEBUG)
namespace FileFlows.MusicNodes.Tests
{
using FileFlows.MusicNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class ConvertTests
{
[TestMethod]
public void Convert_FlacToAac()
{
const string file = @"D:\music\unprocessed\01-billy_joel-you_may_be_right.flac";
ConvertToAAC node = new ();
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
new MusicFile().Execute(args); // need to read the music info and set it
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
[TestMethod]
public void Convert_FlacToMp3()
{
const string file = @"D:\music\unprocessed\01-billy_joel-you_may_be_right.flac";
ConvertToMP3 node = new();
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
new MusicFile().Execute(args); // need to read the music info and set it
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
[TestMethod]
public void Convert_Mp3ToWAV()
{
const string file = @"D:\music\unprocessed\04-billy_joel-scenes_from_an_italian_restaurant-b2125758.mp3";
ConvertToWAV node = new();
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
new MusicFile().Execute(args); // need to read the music info and set it
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
[TestMethod]
public void Convert_Mp3ToOgg()
{
const string file = @"D:\music\unprocessed\04-billy_joel-scenes_from_an_italian_restaurant-b2125758.mp3";
ConvertToOGG node = new();
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
new MusicFile().Execute(args); // need to read the music info and set it
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
[TestMethod]
public void Convert_AacToMp3()
{
const string file = @"D:\music\temp\37f315a0-4afc-4a72-a0b4-eb7eb681b9b3.aac";
ConvertToMP3 node = new();
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
new MusicFile().Execute(args); // need to read the music info and set it
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
[TestMethod]
public void Convert_Mp3_AlreadyMp3()
{
const string file = @"D:\videos\music\13-the_cranberries-why.mp3";
ConvertAudio node = new();
node.SkipIfCodecMatches = true;
node.Codec = "mp3";
node.Bitrate = 192;
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
new MusicFile().Execute(args); // need to read the music info and set it
int output = node.Execute(args);
Assert.AreEqual(2, output);
}
[TestMethod]
public void Convert_VideoToMp3()
{
const string file = @"D:\videos\testfiles\basic.mkv";
ConvertToMP3 node = new();
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
//new MusicFile().Execute(args); // need to read the music info and set it
node.PreExecute(args);
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
[TestMethod]
public void Convert_VideoToAac()
{
const string file = @"D:\videos\testfiles\basic.mkv";
ConvertToAAC node = new();
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
args.TempPath = @"D:\music\temp";
//new MusicFile().Execute(args); // need to read the music info and set it
node.PreExecute(args);
int output = node.Execute(args);
Assert.AreEqual(1, output);
}
}
}
#endif

View File

@@ -1,92 +0,0 @@
#if(DEBUG)
namespace FileFlows.MusicNodes.Tests
{
using FileFlows.MusicNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[TestClass]
public class MusicInfoTests
{
[TestMethod]
public void MusicInfo_SplitTrack()
{
const string file = @"\\oracle\music\The Cranberries\No Need To Argue\The Cranberries - No Need To Argue - 00 - I Don't Need (Demo).mp3";
const string ffmpegExe = @"C:\utils\ffmpeg\ffmpeg.exe";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => ffmpegExe;
args.TempPath = @"D:\music\temp";
var musicInfo = new MusicInfoHelper(ffmpegExe, args.Logger).Read(args.WorkingFile);
Assert.AreEqual(9, musicInfo.Track);
}
[TestMethod]
public void MusicInfo_NormalTrack()
{
const string file = @"\\oracle\music\Taylor Swift\Speak Now\Taylor Swift - Speak Now - 08 - Never Grow Up.mp3";
const string ffmpegExe = @"C:\utils\ffmpeg\ffmpeg.exe";
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
args.GetToolPathActual = (string tool) => ffmpegExe;
args.TempPath = @"D:\music\temp";
var musicInfo = new MusicInfoHelper(ffmpegExe, args.Logger).Read(args.WorkingFile);
Assert.AreEqual(8, musicInfo.Track);
}
[TestMethod]
public void MusicInfo_GetMetaData()
{
const string ffmpegExe = @"C:\utils\ffmpeg\ffmpeg.exe";
var logger = new TestLogger();
foreach (string file in Directory.GetFiles(@"D:\videos\music"))
{
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => ffmpegExe;
// laod the variables
Assert.AreEqual(1, new MusicFile().Execute(args));
var mi = new MusicInfoHelper(ffmpegExe, args.Logger).Read(args.WorkingFile);
string folder = args.ReplaceVariables("{mi.ArtistThe} ({mi.Year})");
Assert.AreEqual($"{mi.Artist} ({mi.Date.Year})", folder);
string fname = args.ReplaceVariables("{mi.Artist} - {mi.Album} - {mi.Track:##} - {mi.Title}");
Assert.AreEqual($"{mi.Artist} - {mi.Track.ToString("00")} - {mi.Title}", fname);
}
}
[TestMethod]
public void MusicInfo_FileNameMetadata()
{
const string ffmpegExe = @"C:\utils\ffmpeg\ffmpeg.exe";
var logger = new TestLogger();
string file = @"\\jor-el\music\Meat Loaf\Bat out of Hell II- Back Into Hell… (1993)\Meat Loaf - Bat out of Hell II- Back Into Hell… - 03 - Id Do Anything for Love (but I Wont Do That).flac";
var mi = new MusicInfo();
new MusicInfoHelper(ffmpegExe, logger).ParseFileNameInfo(file, mi);
Assert.AreEqual("Meat Loaf", mi.Artist);
Assert.AreEqual("Bat out of Hell II- Back Into Hell…", mi.Album);
Assert.AreEqual(1993, mi.Date.Year);
Assert.AreEqual("Id Do Anything for Love (but I Wont Do That)", mi.Title);
Assert.AreEqual(3, mi.Track);
}
}
}
#endif

View File

@@ -1,61 +0,0 @@
#if(DEBUG)
namespace FileFlows.MusicNodes.Tests
{
using FileFlows.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
internal class TestLogger : ILogger
{
private List<string> Messages = new List<string>();
public void DLog(params object[] args) => Log("DBUG", args);
public void ELog(params object[] args) => Log("ERRR", args);
public void ILog(params object[] args) => Log("INFO", args);
public void WLog(params object[] args) => Log("WARN", args);
private void Log(string type, object[] args)
{
if (args == null || args.Length == 0)
return;
string message = type + " -> " +
string.Join(", ", args.Select(x =>
x == null ? "null" :
x.GetType().IsPrimitive || x is string ? x.ToString() :
System.Text.Json.JsonSerializer.Serialize(x)));
Messages.Add(message);
}
public bool Contains(string message)
{
if (string.IsNullOrWhiteSpace(message))
return false;
string log = string.Join(Environment.NewLine, Messages);
return log.Contains(message);
}
public override string ToString()
{
return String.Join(Environment.NewLine, this.Messages.ToArray());
}
public string GetTail(int length = 50)
{
if (length <= 0)
length = 50;
if (Messages.Count <= length)
return string.Join(Environment.NewLine, Messages);
return string.Join(Environment.NewLine, Messages.TakeLast(length));
}
}
}
#endif

View File

@@ -5,8 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<FileVersion>1.0.6.495</FileVersion>
<ProductVersion>1.0.6.495</ProductVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>

View File

@@ -1,64 +0,0 @@
namespace FileFlows.VideoNodes
{
using System.Linq;
using System.Text.RegularExpressions;
internal static class ExtensionMethods
{
public static void AddOrUpdate(this Dictionary<string, object> dict, string key, object value) {
if (dict.ContainsKey(key))
dict[key] = value;
else
dict.Add(key, value);
}
public static string? EmptyAsNull(this string str)
{
return str == string.Empty ? null : str;
}
public static bool TryMatch(this Regex regex, string input, out Match match)
{
match = regex.Match(input);
return match.Success;
}
public static IEnumerable<string> SplitCommandLine(this string commandLine)
{
bool inQuotes = false;
return commandLine.Split(c =>
{
if (c == '\"')
inQuotes = !inQuotes;
return !inQuotes && c == ' ';
})
.Select(arg => arg.Trim().TrimMatchingQuotes('\"'))
.Where(arg => !string.IsNullOrEmpty(arg));
}
public static IEnumerable<string> Split(this string str,
Func<char, bool> controller)
{
int nextPiece = 0;
for (int c = 0; c < str.Length; c++)
{
if (controller(str[c]))
{
yield return str.Substring(nextPiece, c - nextPiece);
nextPiece = c + 1;
}
}
yield return str.Substring(nextPiece);
}
public static string TrimMatchingQuotes(this string input, char quote)
{
if ((input.Length >= 2) &&
(input[0] == quote) && (input[input.Length - 1] == quote))
return input.Substring(1, input.Length - 2);
return input;
}
}
}

View File

@@ -1,235 +0,0 @@
namespace FileFlows.VideoNodes
{
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
public class FFMpegEncoder
{
private string ffMpegExe;
private ILogger Logger;
StringBuilder outputBuilder, errorBuilder;
TaskCompletionSource<bool> outputCloseEvent, errorCloseEvent;
private Regex rgxTime = new Regex(@"(?<=(time=))([\d]+:?)+\.[\d]+");
public delegate void TimeEvent(TimeSpan time);
public event TimeEvent AtTime;
private Process process;
public FFMpegEncoder(string ffMpegExe, ILogger logger)
{
this.ffMpegExe = ffMpegExe;
this.Logger = logger;
}
public (bool successs, string output) Encode(string input, string output, List<string> arguments, bool dontAddInputFile = false, bool dontAddOutputFile = false)
{
arguments ??= new List<string> ();
// -y means it will overwrite a file if output already exists
if (dontAddInputFile == false) {
arguments.Insert(0, "-i");
arguments.Insert(1, input);
arguments.Insert(2, "-y");
}
if (dontAddOutputFile == false)
{
if (arguments.Last() != "-")
arguments.Add(output);
else
Logger.ILog("Last argument '-' skipping adding output file");
}
string argsString = String.Join(" ", arguments.Select(x => x.IndexOf(" ") > 0 ? "\"" + x + "\"" : x));
Logger.ILog(new string('=', ("FFMpeg.Arguments: " + argsString).Length));
Logger.ILog("FFMpeg.Arguments: " + argsString);
Logger.ILog(new string('=', ("FFMpeg.Arguments: " + argsString).Length));
var task = ExecuteShellCommand(ffMpegExe, arguments, 0);
task.Wait();
Logger.ILog("Exit Code: " + task.Result.ExitCode);
return (task.Result.ExitCode == 0, task.Result.Output); // exitcode 0 means it was successful
}
internal void Cancel()
{
try
{
if (this.process != null)
{
this.process.Kill();
this.process = null;
}
}
catch (Exception) { }
}
public async Task<ProcessResult> ExecuteShellCommand(string command, List<string> arguments, int timeout = 0)
{
var result = new ProcessResult();
using (var process = new Process())
{
this.process = process;
process.StartInfo.FileName = command;
if (arguments?.Any() == true)
{
foreach (string arg in arguments)
process.StartInfo.ArgumentList.Add(arg);
}
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
outputBuilder = new StringBuilder();
outputCloseEvent = new TaskCompletionSource<bool>();
process.OutputDataReceived += OnOutputDataReceived;
errorBuilder = new StringBuilder();
errorCloseEvent = new TaskCompletionSource<bool>();
process.ErrorDataReceived += OnErrorDataReceived;
bool isStarted;
try
{
isStarted = process.Start();
}
catch (Exception error)
{
// Usually it occurs when an executable file is not found or is not executable
result.Completed = true;
result.ExitCode = -1;
result.Output = error.Message;
isStarted = false;
}
if (isStarted)
{
// Reads the output stream first and then waits because deadlocks are possible
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// Creates task to wait for process exit using timeout
var waitForExit = WaitForExitAsync(process, timeout);
// Create task to wait for process exit and closing all output streams
var processTask = Task.WhenAll(waitForExit, outputCloseEvent.Task, errorCloseEvent.Task);
// Waits process completion and then checks it was not completed by timeout
if (
(
(timeout > 0 && await Task.WhenAny(Task.Delay(timeout), processTask) == processTask) ||
(timeout == 0 && await Task.WhenAny(processTask) == processTask)
)
&& waitForExit.Result)
{
result.Completed = true;
result.ExitCode = process.ExitCode;
result.Output = $"{outputBuilder}{errorBuilder}";
}
else
{
try
{
// Kill hung process
process.Kill();
}
catch
{
}
}
}
}
process = null;
return result;
}
public void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
{
// The output stream has been closed i.e. the process has terminated
if (e.Data == null)
{
outputCloseEvent.SetResult(true);
}
else
{
if (e.Data.Contains("Skipping NAL unit"))
return; // just slighlty ignore these
if (rgxTime.IsMatch(e.Data))
{
var timeString = rgxTime.Match(e.Data).Value;
var ts = TimeSpan.Parse(timeString);
Logger.DLog("TimeSpan Detected: " + ts);
if (AtTime != null)
AtTime.Invoke(ts);
}
Logger.ILog(e.Data);
outputBuilder.AppendLine(e.Data);
}
}
public void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
{
// The error stream has been closed i.e. the process has terminated
if (e.Data == null)
{
errorCloseEvent.SetResult(true);
}
else if (e.Data.ToLower().Contains("failed") || e.Data.Contains("No capable devices found") || e.Data.ToLower().Contains("error"))
{
Logger.ELog(e.Data);
errorBuilder.AppendLine(e.Data);
}
else if (e.Data.Contains("Skipping NAL unit"))
{
return; // just slighlty ignore these
}
else
{
if (rgxTime.IsMatch(e.Data))
{
var timeString = rgxTime.Match(e.Data).Value;
var ts = TimeSpan.Parse(timeString);
if (AtTime != null)
AtTime.Invoke(ts);
}
Logger.ILog(e.Data);
outputBuilder.AppendLine(e.Data);
}
}
private static Task<bool> WaitForExitAsync(Process process, int timeout)
{
if (timeout > 0)
return Task.Run(() => process.WaitForExit(timeout));
return Task.Run(() =>
{
process.WaitForExit();
return Task.FromResult<bool>(true);
});
}
public struct ProcessResult
{
public bool Completed;
public int? ExitCode;
public string Output;
}
}
}

View File

@@ -1,9 +0,0 @@
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;

View File

@@ -1,124 +0,0 @@
namespace FileFlows.VideoNodes;
class CanUseHardwareEncodingChecker
{
public enum HardwareEncoder
{
Nvidia_H264 = 1,
Amd_H264 = 2,
Qsv_H264 = 3,
Vaapi_H264 = 4,
Nvidia_Hevc = 11,
Amd_Hevc = 12,
Qsv_Hevc = 13,
Vaapi_Hevc = 14,
}
/// <summary>
/// Checks if this flow runner can use NVIDIA HEVC encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Nvidia_Hevc(NodeParameters args) => CanProcess(args, "hevc_nvenc");
/// <summary>
/// Checks if this flow runner can use NVIDIA H.264 encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Nvidia_H264(NodeParameters args) => CanProcess(args, "h264_nvenc");
/// <summary>
/// Checks if this flow runner can use AND HEVC encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Amd_Hevc(NodeParameters args) => CanProcess(args, "hevc_amf");
/// <summary>
/// Checks if this flow runner can use AND H.264 encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Amd_H264(NodeParameters args) => CanProcess(args, "h264_amf");
/// <summary>
/// Checks if this flow runner can use Intels QSV HEVC encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Qsv_Hevc(NodeParameters args) => CanProcess(args, "hevc_qsv -global_quality 28 -load_plugin hevc_hw");
/// <summary>
/// Checks if this flow runner can use Intels QSV H.264 encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Qsv_H264(NodeParameters args) => CanProcess(args, "h264_qsv");
/// <summary>
/// Checks if this flow runner can use VAAPI HEVC encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Vaapi_Hevc(NodeParameters args) => CanProcess(args, "hevc_vaapi");
/// <summary>
/// Checks if this flow runner can use VAAPI H.264 encoder
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Vaapi_H264(NodeParameters args) => CanProcess(args, "h264_vaapi");
private static bool CanProcess(NodeParameters args, string encodingParams)
{
string ffmpeg = args.GetToolPath("FFMpeg");
if (string.IsNullOrEmpty(ffmpeg))
{
args.Logger.ELog("FFMpeg tool not found.");
return false;
}
return CanProcess(args, ffmpeg, encodingParams);
}
/// <summary>
/// Tests if the encoding parameters can be executed
/// </summary>
/// <param name="args">the node paramterse</param>
/// <param name="ffmpeg">the location of ffmpeg</param>
/// <param name="encodingParams">the encoding parameter to test</param>
/// <returns>true if can be processed</returns>
internal static bool CanProcess(NodeParameters args, string ffmpeg, string encodingParams)
{
bool can = CanExecute();
if (can == false && encodingParams?.Contains("amf") == true)
{
// AMD/AMF has a issue where it reports false at first but then passes
// https://github.com/revenz/FileFlows/issues/106
Thread.Sleep(2000);
can = CanExecute();
}
return can;
bool CanExecute()
{
string cmdArgs = $"-loglevel error -f lavfi -i color=black:s=1080x1080 -vframes 1 -an -c:v {encodingParams} -f null -\"";
var cmd = args.Process.ExecuteShellCommand(new ExecuteArgs
{
Command = ffmpeg,
Arguments = cmdArgs,
Silent = true
}).Result;
if (cmd.ExitCode != 0 || string.IsNullOrWhiteSpace(cmd.Output) == false)
{
args.Logger?.WLog($"Cant process '{encodingParams}': {cmd.Output ?? ""}");
return false;
}
return true;
}
}
}

View File

@@ -1,157 +0,0 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class DetectBlackBars : VideoNode
{
public override int Outputs => 2;
public override int Inputs => 1;
public override FlowElementType Type => FlowElementType.Logic;
public override string HelpUrl => "https://docs.fileflows.com/plugins/video-nodes/logical-nodes/detect-black-bars";
public override string Icon => "fas fa-film";
internal const string CROP_KEY = "VideoCrop";
private Dictionary<string, object> _Variables;
public override Dictionary<string, object> Variables => _Variables;
[NumberInt(1)]
public int CroppingThreshold { get; set; }
public DetectBlackBars()
{
_Variables = new Dictionary<string, object>()
{
{ CROP_KEY, "1920:1000:0:40" }
};
}
public override int Execute(NodeParameters args)
{
var videoInfo = GetVideoInfo(args);
if (videoInfo == null || videoInfo.VideoStreams?.Any() != true)
return -1;
string crop = Detect(FFMPEG, videoInfo, args, this.CroppingThreshold);
if (crop == string.Empty)
return 2;
args.Logger?.ILog("Black bars detected, crop: " + crop);
args.UpdateVariables(new Dictionary<string, object>
{
{ CROP_KEY, crop }
});
return 1;
}
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
{
int x = int.MaxValue;
int y = int.MaxValue;
int width = 0;
int height = 0;
foreach (int ss in new int[] { 60, 120, 240, 360 }) // check at multiple times
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo();
process.StartInfo.FileName = ffplay;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.Arguments = $" -ss {ss} -i \"{file}\" -hide_banner -vframes 25 -vf cropdetect -f null -";
args.Logger?.DLog("Executing ffmpeg " + process.StartInfo.Arguments);
process.Start();
string output = process.StandardError.ReadToEnd();
Console.WriteLine(output);
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
var dimMatch = Regex.Match(output, @"Stream #[\d]+:[\d]+(.*?)Video:(.*?)([\d]+)x([\d]+)", RegexOptions.Multiline);
if (dimMatch.Success == false)
{
args.Logger?.WLog("Can't find dimensions for video");
continue;
}
var matches = Regex.Matches(output, @"(?<=(crop=))([\d]+:){3}[\d]+");
foreach (Match match in matches)
{
int[] parts = match.Value.Split(':').Select(x => int.Parse(x)).ToArray();
x = Math.Min(x, parts[2]);
y = Math.Min(y, parts[3]);
width = Math.Max(width, parts[0]);
height = Math.Max(height, parts[1]);
}
}
}
if (width == 0 || height == 0)
{
args.Logger?.WLog("Width/Height not detected: " + width + "x" + height);
return String.Empty;
}
if(x == 0 && y == 0)
{
// nothing to do
return String.Empty;
}
if (x == int.MaxValue)
x = 0;
if (y == int.MaxValue)
y = 0;
if (threshold < 0)
threshold = 0;
args.Logger?.DLog($"Video dimensions: {vidWidth}x{vidHeight}");
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;
}
catch (Exception)
{
return string.Empty;
}
}
public static (bool crop, int diff) TestAboveThreshold(int vidWidth, int vidHeight, int detectedWidth, int detectedHeight, int threshold)
{
int diff = (vidWidth - detectedWidth) + (vidHeight - detectedHeight);
return (diff > threshold, diff);
}
}
}

View File

@@ -1,48 +0,0 @@
namespace FileFlows.VideoNodes
{
using System.Linq;
using System.ComponentModel;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System.ComponentModel.DataAnnotations;
public class VideoCodec : VideoNode
{
public override string ObsoleteMessage => "This node has been combined into 'Video Has Track'";
public override int Inputs => 1;
public override int Outputs => 2;
public override FlowElementType Type => FlowElementType.Logic;
public override string HelpUrl => "https://docs.fileflows.com/plugins/video-nodes/logical-nodes/video-codec";
[StringArray(1)]
[Required]
public string[] Codecs { get; set; }
public override int Execute(NodeParameters args)
{
var videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
var codec = videoInfo.VideoStreams.FirstOrDefault(x => Codecs.Contains(x.Codec.ToLower()));
if (codec != null)
{
args.Logger.ILog($"Matching video codec found[{codec.Index}]: {codec.Codec}");
return 1;
}
var acodec = videoInfo.AudioStreams.FirstOrDefault(x => Codecs.Contains(x.Codec.ToLower()));
if (acodec != null)
{
args.Logger.ILog($"Matching audio codec found[{acodec.Index}]: {acodec.Codec}, language: {acodec.Language}");
return 1;
}
// not found, execute 2nd outputacodec
return 2;
}
}
}

View File

@@ -1,15 +0,0 @@
namespace FileFlows.VideoNodes;
using System.ComponentModel.DataAnnotations;
using FileFlows.Plugin.Attributes;
public class Plugin : FileFlows.Plugin.IPlugin
{
public Guid Uid => new Guid("881b486b-4b38-4e66-b39e-fbc0fc9deee0");
public string Name => "Video Nodes";
public string MinimumVersion => "1.0.4.2019";
public void Init()
{
}
}

View File

@@ -1,43 +0,0 @@
namespace FileFlows.VideoNodes
{
internal class ResolutionHelper
{
public enum Resolution
{
Unknown,
r480p,
r720p,
r1080p,
r4k,
}
public static Resolution GetResolution(VideoInfo videoInfo)
{
var video = videoInfo?.VideoStreams?.FirstOrDefault();
if (video == null)
return Resolution.Unknown;
return GetResolution(video.Width, video.Height);
}
public static Resolution GetResolution(int width, int height)
{
// so if the video is in portait mode, we test the height as if it were the width
int w = Math.Max(width, height);
int h = Math.Min(width, height);
if (Between(w, 1860, 1980))
return Resolution.r1080p;
else if (Between(w, 3780, 3900))
return Resolution.r4k;
else if (Between(w, 1220, 1340))
return Resolution.r720p;
else if (Between(w, 600, 700))
return Resolution.r480p;
return Resolution.Unknown;
}
private static bool Between(int value, int lower, int max) => value >= lower && value <= max;
}
}

View File

@@ -1,113 +0,0 @@
namespace FileFlows.VideoNodes
{
public class VideoInfo
{
public string FileName { get; set; }
/// <summary>
/// Gets or sets the bitrate in bytes per second
/// </summary>
public float Bitrate { get; set; }
public List<VideoStream> VideoStreams { get; set; } = new List<VideoStream>();
public List<AudioStream> AudioStreams { get; set; } = new List<AudioStream>();
public List<SubtitleStream> SubtitleStreams { get; set; } = new List<SubtitleStream>();
public List<Chapter> Chapters { get; set; } = new List<Chapter>();
}
public class VideoFileStream
{
/// <summary>
/// The original index of the stream in the overall video
/// </summary>
public int Index { get; set; }
/// <summary>
/// The index of the specific type
/// </summary>
public int TypeIndex { get; set; }
/// <summary>
/// The stream title (name)
/// </summary>
public string Title { get; set; } = "";
/// <summary>
/// The bitrate(BPS) of the video stream in bytes per second
/// </summary>
public float Bitrate { get; set; }
/// <summary>
/// The codec of the stream
/// </summary>
public string Codec { get; set; } = "";
public string IndexString { get; set; }
/// <summary>
/// Gets or sets if the stream is HDR
/// </summary>
public bool HDR { get; set; }
}
public class VideoStream : VideoFileStream
{
/// <summary>
/// The width of the video stream
/// </summary>
public int Width { get; set; }
/// <summary>
/// The height of the video stream
/// </summary>
public int Height { get; set; }
/// <summary>
/// The number of frames per second
/// </summary>
public float FramesPerSecond { get; set; }
/// <summary>
/// The duration of the stream
/// </summary>
public TimeSpan Duration { get; set; }
}
public class AudioStream : VideoFileStream
{
/// <summary>
/// The language of the stream
/// </summary>
public string Language { get; set; }
/// <summary>
/// The channels of the stream
/// </summary>
public float Channels { get; set; }
/// <summary>
/// The duration of the stream
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// The sample rate of the audio stream
/// </summary>
public int SampleRate { get; set; }
}
public class SubtitleStream : VideoFileStream
{
/// <summary>
/// The language of the stream
/// </summary>
public string Language { get; set; }
/// <summary>
/// If this is a forced subtitle
/// </summary>
public bool Forced { get; set; }
}
public class Chapter
{
public string Title { get; set; }
public TimeSpan Start { get; set; }
public TimeSpan End { get; set; }
}
}

View File

@@ -1,322 +0,0 @@
namespace FileFlows.VideoNodes
{
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
public class VideoInfoHelper
{
private string ffMpegExe;
private ILogger Logger;
static Regex rgxTitle = new Regex(@"(?<=((^[\s]+title[\s]+:[\s])))(.*?)$", RegexOptions.Multiline);
static Regex rgxDuration = new Regex(@"(?<=((^[\s]+DURATION(\-[\w]+)?[\s]+:[\s])))([\d]+:?)+\.[\d]{1,7}", RegexOptions.Multiline);
static Regex rgxDuration2 = new Regex(@"(?<=((^[\s]+Duration:[\s])))([\d]+:?)+\.[\d]{1,7}", RegexOptions.Multiline);
static Regex rgxAudioSampleRate = new Regex(@"(?<=((,|\s)))[\d]+(?=([\s]?hz))", RegexOptions.IgnoreCase);
static int _ProbeSize = 25;
internal static int ProbeSize
{
get => _ProbeSize;
set
{
if (value < 5)
_ProbeSize = 5;
else if (value > 1000)
_ProbeSize = 1000;
else
_ProbeSize = value;
}
}
public VideoInfoHelper(string ffMpegExe, ILogger logger)
{
this.ffMpegExe = ffMpegExe;
this.Logger = logger;
}
public static string GetFFMpegPath(NodeParameters args) => args.GetToolPath("FFMpeg");
public VideoInfo Read(string filename)
{
var vi = new VideoInfo();
vi.FileName = filename;
if (File.Exists(filename) == false)
{
Logger.ELog("File not found: " + filename);
return vi;
}
if (string.IsNullOrEmpty(ffMpegExe) || File.Exists(ffMpegExe) == false)
{
Logger.ELog("FFMpeg not found: " + (ffMpegExe ?? "not passed in"));
return vi;
}
try
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo();
process.StartInfo.FileName = ffMpegExe;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
foreach (var arg in new[]
{
"-hide_banner",
"-probesize", ProbeSize + "M",
"-i",
filename,
})
{
process.StartInfo.ArgumentList.Add(arg);
}
process.Start();
string output = process.StandardError.ReadToEnd();
output = output.Replace("At least one output file must be specified", string.Empty).Trim();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (string.IsNullOrEmpty(error) == false && error != "At least one output file must be specified")
{
Logger.ELog("Failed reading ffmpeg info: " + error);
return vi;
}
Logger.ILog("Video Information:" + Environment.NewLine + output);
vi = ParseOutput(Logger, output);
}
}
catch (Exception ex)
{
Logger.ELog(ex.Message, ex.StackTrace.ToString());
}
return vi;
}
public static VideoInfo ParseOutput(ILogger logger, string output)
{
var vi = new VideoInfo();
var rgxStreams = new Regex(@"Stream\s#[\d]+:[\d]+(.*?)(?=(Stream\s#[\d]|$))", RegexOptions.Singleline);
var streamMatches = rgxStreams.Matches(output);
int streamIndex = 0;
// get a rough estimate, bitrate: 346 kb/s
var rgxBitrate = new Regex(@"(?<=(bitrate: ))[\d\.]+(?!=( kb/s))");
var brMatch = rgxBitrate.Match(output);
if (brMatch.Success)
{
vi.Bitrate = float.Parse(brMatch.Value) * 1_000; // to convert to b/s
}
vi.Chapters = ParseChapters(output);
int subtitleIndex = 0;
int videoIndex = 0;
int audioIndex = 0;
foreach (Match sm in streamMatches)
{
if (sm.Value.Contains(" Video: "))
{
var vs = ParseVideoStream(logger, sm.Value, output);
if (vs != null)
{
vs.Index = streamIndex;
vs.TypeIndex = videoIndex;
var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
if (match.Success)
vs.IndexString = match.Value;
vi.VideoStreams.Add(vs);
}
++videoIndex;
}
else if (sm.Value.Contains(" Audio: "))
{
var audio = ParseAudioStream(sm.Value);
if (audio != null)
{
audio.TypeIndex = audioIndex;
audio.Index = streamIndex;
var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
if (match.Success)
audio.IndexString = match.Value;
vi.AudioStreams.Add(audio);
}
++audioIndex;
}
else if (sm.Value.Contains(" Subtitle: "))
{
var sub = ParseSubtitleStream(sm.Value);
if (sub != null)
{
sub.Index = streamIndex;
sub.TypeIndex = subtitleIndex;
var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
if (match.Success)
sub.IndexString = match.Value;
vi.SubtitleStreams.Add(sub);
}
++subtitleIndex;
}
++streamIndex;
}
return vi;
}
private static List<Chapter> ParseChapters(string output)
{
try
{
var rgxChatpers = new Regex("(?<=(Chapters:))(.*?)(?=(Stream))", RegexOptions.Singleline);
string strChapters;
if (rgxChatpers.TryMatch(output, out Match matchChapters))
strChapters = matchChapters.Value.Trim();
else
return new List<Chapter>();
var rgxChapter = new Regex("Chapter #(.*?)(?=(Chapter #|$))", RegexOptions.Singleline);
var chapters = new List<Chapter>();
var rgxTitle = new Regex(@"title[\s]*:[\s]*(.*?)$");
var rgxStart = new Regex(@"(?<=(start[\s]))[\d]+\.[\d]+");
var rgxEnd = new Regex(@"(?<=(end[\s]))[\d]+\.[\d]+");
foreach (Match match in rgxChapter.Matches(strChapters))
{
try
{
Chapter chapter = new Chapter();
if (rgxTitle.TryMatch(match.Value.Trim(), out Match title))
chapter.Title = title.Groups[1].Value;
if (rgxStart.TryMatch(match.Value, out Match start))
{
double startSeconds = double.Parse(start.Value);
chapter.Start = TimeSpan.FromSeconds(startSeconds);
}
if (rgxEnd.TryMatch(match.Value, out Match end))
{
double endSeconds = double.Parse(end.Value);
chapter.End = TimeSpan.FromSeconds(endSeconds);
}
if (chapter.Start > TimeSpan.Zero || chapter.End > TimeSpan.Zero)
{
chapters.Add(chapter);
}
}
catch (Exception ) { }
}
return chapters;
}catch (Exception) { return new List<Chapter>(); }
}
public static VideoStream ParseVideoStream(ILogger logger, string info, string fullOutput)
{
// Stream #0:0(eng): Video: h264 (High), yuv420p(tv, bt709/unknown/unknown, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 23.98 fps, 23.98 tbr, 1k tbn (default)
string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
VideoStream vs = new VideoStream();
vs.Codec = line.Substring(line.IndexOf("Video: ") + "Video: ".Length).Replace(",", "").Trim().Split(' ').First().ToLower();
var dimensions = Regex.Match(line, @"([\d]{3,})x([\d]{3,})");
if (int.TryParse(dimensions.Groups[1].Value, out int width))
vs.Width = width;
if (int.TryParse(dimensions.Groups[2].Value, out int height))
vs.Height = height;
if (Regex.IsMatch(line, @"([\d]+(\.[\d]+)?)\sfps") && float.TryParse(Regex.Match(line, @"([\d]+(\.[\d]+)?)\sfps").Groups[1].Value, out float fps))
vs.FramesPerSecond = fps;
var rgxBps = new Regex(@"(?<=((BPS(\-[\w]+)?[\s]*:[\s])))([\d]+)");
if (rgxBps.IsMatch(info) && float.TryParse(rgxBps.Match(info).Value, out float bps))
vs.Bitrate = bps;
if (rgxDuration.IsMatch(info) && TimeSpan.TryParse(rgxDuration.Match(info).Value, out TimeSpan duration) && duration.TotalSeconds > 0)
{
vs.Duration = duration;
logger?.ILog("Video stream duration: " + vs.Duration);
}
else if (rgxDuration2.IsMatch(fullOutput) && TimeSpan.TryParse(rgxDuration2.Match(fullOutput).Value, out TimeSpan duration2) && duration2.TotalSeconds > 0)
{
vs.Duration = duration2;
logger?.ILog("Video stream duration: " + vs.Duration);
}
else
{
logger?.ILog("Failed to read duration for VideoStream: " + info);
}
// As per https://video.stackexchange.com/a/33827
// "HDR is only the new transfer function" (PQ or HLG)
vs.HDR = info.Contains("arib-std-b67") || info.Contains("smpte2084");
return vs;
}
public static AudioStream ParseAudioStream(string info)
{
// Stream #0:1(eng): Audio: dts (DTS), 48000 Hz, stereo, fltp, 1536 kb/s (default)
string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
var parts = line.Split(",").Select(x => x?.Trim() ?? "").ToArray();
AudioStream audio = new AudioStream();
audio.Title = "";
// this isnt type index, this is overall index
audio.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value) - 1;
audio.Codec = parts[0].Substring(parts[0].IndexOf("Audio: ") + "Audio: ".Length).Trim().Split(' ').First().ToLower() ?? "";
audio.Language = Regex.Match(line, @"(?<=(Stream\s#[\d]+:[\d]+)\()[^\)]+").Value?.ToLower() ?? "";
if (info.IndexOf("0 channels") >= 0)
{
audio.Channels = 0;
}
else
{
try
{
//Logger.ILog("codec: " + vs.Codec);
if (parts[2] == "stereo")
audio.Channels = 2;
else if (parts[2] == "mono")
audio.Channels = 1;
else if (Regex.IsMatch(parts[2], @"^[\d]+(\.[\d]+)?"))
{
audio.Channels = float.Parse(Regex.Match(parts[2], @"^[\d]+(\.[\d]+)?").Value);
}
}
catch (Exception) { }
}
var match = rgxAudioSampleRate.Match(info);
if (match.Success)
audio.SampleRate = int.Parse(match.Value);
if (rgxTitle.IsMatch(info))
audio.Title = rgxTitle.Match(info).Value.Trim();
if (rgxDuration.IsMatch(info))
audio.Duration = TimeSpan.Parse(rgxDuration.Match(info).Value);
return audio;
}
public static SubtitleStream ParseSubtitleStream(string info)
{
// Stream #0:1(eng): Audio: dts (DTS), 48000 Hz, stereo, fltp, 1536 kb/s (default)
string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
var parts = line.Split(",").Select(x => x?.Trim() ?? "").ToArray();
SubtitleStream sub = new SubtitleStream();
sub.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value);
sub.Codec = line.Substring(line.IndexOf("Subtitle: ") + "Subtitle: ".Length).Trim().Split(' ').First().ToLower();
sub.Language = Regex.Match(line, @"(?<=(Stream\s#[\d]+:[\d]+)\()[^\)]+").Value?.ToLower() ?? "";
if (rgxTitle.IsMatch(info))
sub.Title = rgxTitle.Match(info).Value.Trim();
sub.Forced = info.ToLower().Contains("forced");
return sub;
}
}
}

View File

@@ -1,44 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<PublishSingleFile>true</PublishSingleFile>
<FileVersion>1.0.4.189</FileVersion>
<ProductVersion>1.0.4.189</ProductVersion>
<PublishTrimmed>true</PublishTrimmed>
<Company>FileFlows</Company>
<Authors>John Andrews</Authors>
<Product>Video Legacy Nodes</Product>
<PackageProjectUrl>https://fileflows.com/</PackageProjectUrl>
<Description>Legacy Video Nodes that are now obsolete and have been replaced.</Description>
<RootNamespace>FileFlows.VideoNodes</RootNamespace>
</PropertyGroup>
<ItemGroup>
<None Remove="VideoLegacyNodes.en.json" />
</ItemGroup>
<ItemGroup>
<Content Include="VideoLegacyNodes.en.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup Condition=" '$(Configuration)' == 'Debug'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
</ItemGroup>
<ItemGroup>
<Reference Include="Plugin">
<HintPath>..\FileFlows.Plugin.dll</HintPath>
<SpecificVersion>False</SpecificVersion>
</Reference>
</ItemGroup>
</Project>

View File

@@ -1,254 +0,0 @@
{
"H": {
"264": "H.264",
"265": "H.265"
},
"5": { "1": "5.1" },
"7": { "1": "7.1" },
"Flow": {
"Parts": {
"AudioAddTrack": {
"Outputs": {
"1": "Audio track added and saved to temporary file"
},
"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. 0 based, so to insert the new audio track as the first track set this to 0.",
"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",
"2": "Audio tracks were not adjusted"
},
"Description": "Adjusts audio tracks volume in a video file using FFMPEG",
"Fields": {
"VolumePercent": "Volume Percent",
"VolumePercent-Help": "The percent of the adjusted volume.\n100 means no adjustment\n50 means half volume\n0 means muted"
}
},
"AudioNormalization": {
"Outputs": {
"1": "Audio tracks were normalized and saved to temporary file",
"2": "No audio tracks were found to be normalized"
},
"Description": "Normalizes all audio tracks in a video file using FFMPEGs loudnorm filter",
"Fields": {
"AllAudio": "All Audio Tracks",
"AllAudio-Help": "If all audio tracks should be normalized or if just the first track should be",
"TwoPass": "Two Pass",
"TwoPass-Help": "If the audio tracks should use two pass normalization. This improves the normalization but increases the processing time.",
"Pattern": "Pattern",
"Pattern-Help": "An optional regular expression to filter out audio tracks to normalize. Will match against the title, codec and language",
"NotMatching": "Not Matching",
"NotMatching-Help": "If the pattern should be used to exclude audio tracks from normalization, otherwise if the audio track matches they will be normalized."
}
},
"AudioTrackRemover": {
"Outputs": {
"1": "Audio tracks were removed",
"2": "Audio tracks were NOT 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": {
"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"
}
},
"AudioTrackReorder": {
"Outputs": {
"1": "Audio tracks re-ordered in new temporary file",
"2": "Audio tracks NOT re-ordered"
},
"Description": "Allows you to reorder audio tracks in the preferred order.\n\nEnter the languages/audio codecs/channels in the order you want. Any not listed will be ordered after the ones entered in their original order.\nIf there are multiple tracks with same language/codec/channels, they will be ordered first by the order you entered, then in their original order.\n\nOutput 1: Tracks were reordered\nOutput 2: Tracks did not need reordering",
"Fields": {
"OrderedTracks": "Ordered Audio Codecs",
"OrderedTracks-Help": "The order of audio codecs to the audio tracks by. This is done after the languages (if any)",
"Languages": "Languages",
"Languages-Help": "The order of languages to sort the audio tracks by. This sorting is done before the codec.",
"Channels": "Channels",
"Channels-Help": "The order of audio channels to sort the audio tracks by. This sorting is done before languages.\nFor example \"5.1\",\"7.1\",\"6.2\",\"2\""
}
},
"AudioTrackSetLanguage": {
"Label": "Audio Set Language",
"Outputs": {
"1": "Audio tracks updated to new temporary file",
"2": "Audio tracks NOT updated"
},
"Description": "Allows you to set the language for any audio tracks that have no language set. If the audio track does have a language set, it will be skipped.\n\nOutput 1: Audio Tracks were updated\nOutput 2: No audio tracks were needing to be updated",
"Fields": {
"Language": "Language",
"Language-Help": "The [ISO 639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code to use."
}
},
"AutoChapters": {
"Description": "Automatically detect scene changes in the video to generate chapters.",
"Outputs": {
"1": "Chapters generated and saved to temporary file",
"2": "No chapters detected or video already had chapters"
},
"Fields": {
"MinimumLength": "Minimum Length",
"MinimumLength-Suffix": "seconds",
"MinimumLength-Help": "The minimum length of a chapter in seconds",
"Percent": "Percent",
"Percent-Suffix": "%",
"Percent-Help": "The threshold percentage value to use for scene detection changes. A good value is 45%"
}
},
"ComskipChapters": {
"Description": "Uses a comskip EDL file and will create chapters given that EDL comskip file.",
"Outputs": {
"1": "Commercials chapters created, saved to temporary file",
"2": "No commercials detected"
}
},
"DetectBlackBars": {
"Description": "Processes a video file and scans for black bars in the video.\n\nIf found a parameter \"VideoCrop\" will be added.\n\nOutput 1: Black bars detected\nOutput 2: Not detected",
"Outputs": {
"1": "Black bars detected",
"2": "No black bars detected"
},
"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."
}
},
"FFMPEG": {
"Description": "The node lets you run any FFMPEG command you like. Giving you full control over what it can do.\n\nFor more information refer to the FFMPEG documentation",
"Outputs": {
"1": "Video processed"
},
"Fields": {
"Extension": "Extension",
"Extension-Help": "The file extension to use on the newly created file",
"CommandLine": "Command Line",
"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."
}
},
"RemuxToMKV": {
"Descritption": "Remuxes a video file into a MKV container. All streams will be copied to the new container",
"Outputs": {
"1": "File remuxed to temporary file",
"2": "File was already in a MKV container"
},
"Fields": {
"Force": "Force",
"Force-Help": "If the file should be always remuxed into a MKV container even when it already is in a MKV container.\ni.e. a new temporary file will always be created."
}
},
"RemuxToMP4": {
"Descritption": "Remuxes a video file into a MP4 container. All streams will be copied to the new container",
"Outputs": {
"1": "File remuxed to temporary file",
"2": "File was already in a MP4 container"
},
"Fields": {
"Force": "Force",
"Force-Help": "If the file should be always remuxed into a MP4 container even when it already is in a MP4 container.\ni.e. a new temporary file will always be created."
}
},
"SubtitleRemover": {
"Description": "Removes subtitles from a video file if found.\n\nOutput 1: Subtitles were removed\nOutput 2: No subtitles found that needed to be removed",
"Outputs": {
"1": "Subtitles removed in new temporary file",
"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"
}
},
"SubtitleLanguageRemover": {
"Outputs": {
"1": "Subtitles were removed",
"2": "Subtitles were NOT removed"
},
"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",
"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",
"Fields": {
"Codecs": "Codecs",
"Codecs-Help": "Enter a list of case insensitive video or audio codecs.\nEg hevc, h265, mpeg4, ac3"
}
},
"VideoEncode": {
"Description": "A generic video encoding node, this lets you customize how to encode a video file using ffmpeg.\n\nOutput 1: Video was processed\nOutput 2: No processing required",
"Outputs": {
"1": "Video re-encoded to temporary file",
"2": "Video not re-encoded"
},
"Fields": {
"Extension": "Extension",
"Extension-Help": "The file extension to use on the newly created file.",
"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.",
"AudioCodec": "Audio Codec",
"AudioCodec-Help": "The audio codec to encode the video with.\nIf left empty all original audio tracks will be copied.",
"Language": "Language",
"Language-Help": "Optional [ISO 639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code to use. Will attempt to find an audio track with this language code if not the best audio track will be used."
}
},
"VideoHasStream": {
"Description": "Tests if a video file contains a stream",
"Outputs": {
"1": "Contains the matching stream",
"2": "Does not contain the matching stream"
},
"Fields": {
"Stream": "Type",
"Stream-Help": "The type of stream to look for",
"Title": "Title",
"Title-Help": "A regular expression used to test the stream title",
"Codec": "Codec",
"Codec-Help": "A regular expression used to test the stream codec",
"Language": "Language",
"Language-Help": "A regular expression used to test the stream language",
"Channels": "Channels",
"Channels-Help": "The number of channels to test for. Set to 0 to ignore this check"
}
},
"VideoScaler": {
"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": "Video rescaled to temporary file",
"2": "Video was already in/near the scaled resolution"
},
"Fields": {
"VideoCodec": "Video Codec",
"Language-Help": "The video codec to encode the scaled video in",
"Extension": "Extension",
"Extension-Help": "The file extension to use on the newly created file",
"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",
"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."
}
}
}
}
}

View File

@@ -1,197 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.ComponentModel;
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(0, 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;
}
}
public override int Execute(NodeParameters args)
{
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
List<string> ffArgs = new List<string>
{
"-c", "copy",
"-map", "0:v",
};
bool added = false;
int audioIndex = 0;
for(int i = 0; i < videoInfo.AudioStreams.Count; i++)
{
if(i == 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, FFMPEG, 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
if(Bitrate == 0)
{
return new[]
{
"-map", videoInfo.AudioStreams[0].IndexString,
"-c:a:" + index, Codec
};
}
return new[]
{
"-map", videoInfo.AudioStreams[0].IndexString,
"-c:a:" + index, Codec,
"-b:a:" + index, Bitrate + "k"
};
}
else
{
if (Bitrate == 0)
{
return new[]
{
"-map", videoInfo.AudioStreams[0].IndexString,
"-c:a:" + index, Codec,
"-ac", Channels.ToString()
};
}
return new[]
{
"-map", videoInfo.AudioStreams[0].IndexString,
"-c:a:" + index, Codec,
"-ac", Channels.ToString(),
"-b:a:" + index, Bitrate + "k"
};
}
}
}
}

View File

@@ -1,74 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
public class AudioAdjustVolume: EncodingNode
{
public override int Outputs => 2;
public override string Icon => "fas fa-volume-up";
[NumberInt(1)]
[Range(0, 1000)]
public int VolumePercent { get; set; }
public override int Execute(NodeParameters args)
{
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
if (videoInfo.AudioStreams?.Any() != true)
{
args.Logger?.ILog("No audio streams detected");
return 2;
}
if(VolumePercent == 100)
{
args.Logger?.ILog("Volume percent set to 100, no adjustment necessary");
return 2;
}
List<string> ffArgs = new List<string>
{
"-c", "copy",
"-map", "0:v",
};
float volume = this.VolumePercent / 100f;
foreach (var audio in videoInfo.AudioStreams)
{
ffArgs.AddRange(new[] { "-map", $"0:a:{audio.TypeIndex}", "-filter:a", $"volume={volume.ToString(".0######")}" });
}
if (videoInfo.SubtitleStreams?.Any() == true)
ffArgs.AddRange(new[] { "-map", "0:s" });
string extension = new FileInfo(args.WorkingFile).Extension;
if(extension.StartsWith("."))
extension = extension.Substring(1);
if (Encode(args, FFMPEG, ffArgs, extension) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,187 +0,0 @@
namespace FileFlows.VideoNodes;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
public class AudioNormalization: EncodingNode
{
public override int Outputs => 2;
public override string Icon => "fas fa-volume-up";
[Boolean(1)]
public bool AllAudio { get; set; }
[Boolean(2)]
public bool TwoPass { get; set; }
[TextVariable(3)]
public string Pattern { get; set; }
[Boolean(4)]
public bool NotMatching { get; set; }
internal const string LOUDNORM_TARGET = "I=-24:LRA=7:TP=-2.0";
public override int Execute(NodeParameters args)
{
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
if (videoInfo.AudioStreams?.Any() != true)
{
args.Logger?.ILog("No audio streams detected");
return 2;
}
List<string> ffArgs = new List<string>();
ffArgs.AddRange(new[] { "-strict", "-2" }); // allow experimental stuff
ffArgs.AddRange(new[] { "-c", "copy" });
if (videoInfo.VideoStreams?.Any() == true)
ffArgs.AddRange(new[] { "-map", "0:v" });
List<int> tracksToNormalize = new ();
for (int j = 0; j < videoInfo.AudioStreams.Count;j++)
{
var audio = videoInfo.AudioStreams[j];
if(string.IsNullOrEmpty(Pattern) == false)
{
string audioString = audio.Title + ":" + audio.Language + ":" + audio.Codec;
args.Logger?.ILog($"Audio Track [{j}] test string: {audioString}");
bool match = new Regex(Pattern, RegexOptions.IgnoreCase).IsMatch(audioString);
if (NotMatching)
match = !match;
if (match == false)
{
ffArgs.AddRange(new[] { "-map", $"0:a:{j}" });
continue;
}
}
if (AllAudio || j == 0)
{
if (TwoPass)
{
string twoPass = DoTwoPass(this, args, FFMPEG, j);
ffArgs.AddRange(new[] { "-map", $"0:a:{j}", "-c:a:" + j, audio.Codec, "-filter:a:" + j, twoPass });
}
else
{
ffArgs.AddRange(new[] { "-map", $"0:a:{j}", "-c:a:" + j, audio.Codec, "-filter:a:" + j, $"loudnorm={LOUDNORM_TARGET}" });
}
tracksToNormalize.Add(j);
}
else
{
ffArgs.AddRange(new[] { "-map", $"0:a:{j}" });
}
}
if (tracksToNormalize.Any() == false)
{
args.Logger?.ILog("No audio streams to normalize");
return 2;
}
foreach (int i in tracksToNormalize)
args.Logger?.ILog($"Normalizing track [{i}]: {videoInfo.AudioStreams[i].Title};{videoInfo.AudioStreams[i].Language};{videoInfo.AudioStreams[i].Codec};");
if (videoInfo.SubtitleStreams?.Any() == true)
ffArgs.AddRange(new[] { "-map", "0:s" });
string extension = new FileInfo(args.WorkingFile).Extension;
if (extension.StartsWith("."))
extension = extension.Substring(1);
if (Encode(args, FFMPEG, ffArgs, extension) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize<FileFlows.VideoNodes.AudioNormalization.LoudNormStats>(string, System.Text.Json.JsonSerializerOptions?)")]
public static string DoTwoPass(EncodingNode node, NodeParameters args, string ffmpegExe, int audioIndex)
{
//-af loudnorm=I=-24:LRA=7:TP=-2.0"
string output;
var result = node.Encode(args, ffmpegExe, new List<string>
{
"-hide_banner",
"-i", args.WorkingFile,
"-strict", "-2", // allow experimental stuff
"-map", "0:a:" + audioIndex,
"-af", "loudnorm=" + LOUDNORM_TARGET + ":print_format=json",
"-f", "null",
"-"
}, out output, updateWorkingFile: false, dontAddInputFile: true);
if (result == false)
throw new Exception("Failed to prcoess audio track");
int index = output.LastIndexOf("{");
if (index == -1)
throw new Exception("Failed to detected json in output");
string json = output.Substring(index);
json = json.Substring(0, json.IndexOf("}") + 1);
if (string.IsNullOrEmpty(json))
throw new Exception("Failed to parse TwoPass json");
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
LoudNormStats stats = JsonSerializer.Deserialize<LoudNormStats>(json);
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
if (stats.input_i == "-inf" || stats.input_lra == "-inf" || stats.input_tp == "-inf" || stats.input_thresh == "-inf" || stats.target_offset == "-inf")
{
args.Logger?.WLog("-inf detected in loud norm two pass, falling back to single pass loud norm");
return $"loudnorm={LOUDNORM_TARGET}";
}
string ar = $"loudnorm=print_format=summary:linear=true:{LOUDNORM_TARGET}:measured_I={stats.input_i}:measured_LRA={stats.input_lra}:measured_tp={stats.input_tp}:measured_thresh={stats.input_thresh}:offset={stats.target_offset}";
return ar;
}
private class LoudNormStats
{
/*
{
"input_i" : "-7.47",
"input_tp" : "12.33",
"input_lra" : "6.70",
"input_thresh" : "-18.13",
"output_i" : "-24.25",
"output_tp" : "-3.60",
"output_lra" : "5.90",
"output_thresh" : "-34.74",
"normalization_type" : "dynamic",
"target_offset" : "0.25"
}
*/
public string input_i { get; set; }
public string input_tp { get; set; }
public string input_lra { get; set; }
public string input_thresh { get; set; }
public string target_offset { get; set; }
}
}

View File

@@ -1,85 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
public class AudioTrackRemover : EncodingNode
{
public override int Outputs => 2;
public override string Icon => "fas fa-volume-off";
[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)
{
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
List<string> ffArgs = new List<string>
{
"-c", "copy",
"-map", "0:v",
};
bool removing = false;
var regex = new Regex(this.Pattern, RegexOptions.IgnoreCase);
for(int i=0;i< videoInfo.AudioStreams.Count;i++)
{
var audio = videoInfo.AudioStreams[i];
string str = UseLanguageCode ? audio.Language : audio.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)
{
removing = true;
continue;
}
}
ffArgs.AddRange(new[] { "-map", "0:a:" + i });
}
if(removing == false)
{
args.Logger.ILog("Nothing found to remove");
return 2;
}
if (videoInfo.SubtitleStreams?.Any() == true)
ffArgs.AddRange(new[] { "-map", "0:s" });
string extension = new FileInfo(args.WorkingFile).Extension;
if(extension.StartsWith("."))
extension = extension.Substring(1);
if (Encode(args, FFMPEG, ffArgs, extension) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,141 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class AudioTrackReorder: EncodingNode
{
public override int Outputs => 2;
public override string Icon => "fas fa-volume-off";
[StringArray(1)]
public List<string> Languages { get; set; }
[StringArray(2)]
public List<string> OrderedTracks { get; set; }
[StringArray(3)]
public List<string> Channels { get; set; }
public List<AudioStream> Reorder(List<AudioStream> input)
{
Languages ??= new List<string>();
OrderedTracks ??= new List<string>();
Channels ??= new List<string>();
List<float> actualChannels = Channels.Select(x =>
{
if (float.TryParse(x, out float value))
return value;
return -1f;
}).Where(x => x > 0f).ToList();
if (Languages.Any() == false && OrderedTracks.Any() == false && actualChannels.Any() == false)
return input; // nothing to do
Languages.Reverse();
OrderedTracks.Reverse();
actualChannels.Reverse();
const int base_number = 1_000_000_000;
int count = base_number;
var debug = new StringBuilder();
var data = input.OrderBy(x =>
{
int langIndex = Languages.IndexOf(x.Language?.ToLower() ?? String.Empty);
int codecIndex = OrderedTracks.IndexOf(x.Codec?.ToLower() ?? String.Empty);
int channelIndex = actualChannels.IndexOf(x.Channels);
int result = base_number;
if (langIndex >= 0)
{
result -= ((langIndex + 1) * 10_000_000);
}
if(codecIndex >= 0)
{
result -= ((codecIndex + 1) * 100_000);
}
if(channelIndex >= 0)
{
result -= ((channelIndex + 1) * 1_000);
}
if (result == base_number)
result = ++count;
return result;
}).ToList();
return data;
}
public bool AreSame(List<AudioStream> original, List<AudioStream> reordered)
{
for (int i = 0; i < reordered.Count; i++)
{
if (reordered[i] != original[i])
{
return false;
}
}
return true;
}
public override int Execute(NodeParameters args)
{
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
List<string> ffArgs = new List<string>
{
"-c", "copy",
"-map", "0:v",
};
OrderedTracks = OrderedTracks?.Select(x => x.ToLower())?.ToList() ?? new ();
var reordered = Reorder(videoInfo.AudioStreams);
bool same = AreSame(videoInfo.AudioStreams, reordered);
if(same)
{
args.Logger?.ILog("No audio tracks need reordering");
return 2;
}
foreach (var audio in reordered)
{
ffArgs.AddRange(new[] { "-map", audio.IndexString });
}
if (videoInfo.SubtitleStreams?.Any() == true)
ffArgs.AddRange(new[] { "-map", "0:s" });
// 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, FFMPEG, ffArgs, extension) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,65 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public class AudioTrackSetLanguage : EncodingNode
{
public override int Outputs => 2;
public override string Icon => "fas fa-comment-dots";
[Required]
[Text(1)]
public string Language { get; set; }
public override int Execute(NodeParameters args)
{
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
List<string> ffArgs = new List<string>();
int index = 0;
foreach(var at in videoInfo.AudioStreams)
{
if (string.IsNullOrEmpty(at.Language))
{
ffArgs.AddRange(new[] { $"-metadata:s:a:{index}", $"language={Language.ToLower()}" });
}
++index;
}
if (ffArgs.Count == 0)
return 2; // nothing to do
ffArgs.Insert(0, "-map");
ffArgs.Insert(1, "0");
ffArgs.Insert(2, "-c");
ffArgs.Insert(3, "copy");
string extension = new FileInfo(args.WorkingFile).Extension;
if(extension.StartsWith("."))
extension = extension.Substring(1);
args.Logger?.DLog("Working file: " + args.WorkingFile);
args.Logger?.DLog("Extension: " + extension);
if (Encode(args, FFMPEG, ffArgs, extension) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,125 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
public class AutoChapters: EncodingNode
{
public override int Outputs => 2;
[NumberInt(1)]
[DefaultValue(60)]
public int MinimumLength { get; set; } = 60;
[NumberInt(2)]
[DefaultValue(45)]
public int Percent { get; set; } = 45;
public override int Execute(NodeParameters args)
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
if (videoInfo.Chapters?.Count > 3)
{
args.Logger.ILog(videoInfo.Chapters.Count + " chapters already detected in file");
return 2;
}
string tempMetaDataFile = GenerateMetaDataFile(this, args, videoInfo, FFMPEG, this.Percent, this.MinimumLength);
if (string.IsNullOrEmpty(tempMetaDataFile))
return 2;
string[] ffArgs = new[] { "-i", tempMetaDataFile, "-map_metadata", "1", "-codec", "copy", "-max_muxing_queue_size", "1024" };
if (Encode(args, FFMPEG, ffArgs.ToList()))
{
args.Logger?.ILog($"Adding chapters to file");
return 1;
}
args.Logger?.ELog("Processing failed");
return -1;
}
internal static string GenerateMetaDataFile(EncodingNode node, NodeParameters args, VideoInfo videoInfo, string ffmpegExe, int percent, int minimumLength)
{
string output;
var result = node.Encode(args, ffmpegExe, new List<string>
{
"-hide_banner",
"-i", args.WorkingFile,
"-filter:v", $"select='gt(scene,{percent / 100f})',showinfo",
"-f", "null",
"-"
}, out output, updateWorkingFile: false, dontAddInputFile: true);
if (result == false)
{
args.Logger?.WLog("Failed to detect scenes");
return string.Empty;
}
if (minimumLength < 30)
{
args.Logger?.ILog("Mimium length set to invalid number, defaulting to 60 seconds");
minimumLength = 60;
}
else
{
args.Logger?.ILog($"Minimum length of chapter {minimumLength} seconds");
}
StringBuilder metadata = new StringBuilder();
metadata.AppendLine(";FFMETADATA1");
metadata.AppendLine("");
int chapter = 0;
TimeSpan previous = TimeSpan.Zero;
foreach (Match match in Regex.Matches(output, @"(?<=(pts_time:))[\d]+\.[\d]+"))
{
TimeSpan time = TimeSpan.FromSeconds(double.Parse(match.Value));
if (Math.Abs((time - previous).TotalSeconds) < minimumLength)
continue;
AddChapter(previous, time);
previous = time;
}
var totalTime = TimeSpan.FromSeconds(videoInfo.VideoStreams[0].Duration.TotalSeconds);
if (Math.Abs((totalTime - previous).TotalSeconds) > minimumLength)
AddChapter(previous, totalTime);
if (chapter == 0)
{
args.Logger?.ILog("Failed to detect any scene changes");
return string.Empty;
}
string tempMetaDataFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + ".txt");
File.WriteAllText(tempMetaDataFile, metadata.ToString());
return tempMetaDataFile;
void AddChapter(TimeSpan start, TimeSpan end)
{
metadata.AppendLine("[CHAPTER]");
metadata.AppendLine("TIMEBASE=1/1000");
metadata.AppendLine("START=" + ((int)(start.TotalSeconds * 1000)));
metadata.AppendLine("END=" + ((int)(end.TotalSeconds * 1000)));
metadata.AppendLine("title=Chapter " + (++chapter));
metadata.AppendLine();
}
}
}
}

View File

@@ -1,99 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
public class ComskipChapters : EncodingNode
{
public override int Outputs => 2;
public override int Execute(NodeParameters args)
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
string tempMetaDataFile = GenerateMetaDataFile(args, videoInfo);
if (string.IsNullOrEmpty(tempMetaDataFile))
return 2;
string[] ffArgs = new[] { "-i", tempMetaDataFile, "-map_metadata", "1", "-codec", "copy", "-max_muxing_queue_size", "1024" };
if (Encode(args, FFMPEG, ffArgs.ToList()))
{
args.Logger?.ILog($"Added chapters to file");
return 1;
}
args.Logger?.ELog("Processing failed");
return -1;
}
internal static string GenerateMetaDataFile(NodeParameters args, VideoInfo videoInfo)
{
float totalTime = (float)videoInfo.VideoStreams[0].Duration.TotalSeconds;
string edlFile = args.WorkingFile.Substring(0, args.WorkingFile.LastIndexOf(".") + 1) + "edl";
if (File.Exists(edlFile) == false)
edlFile = args.WorkingFile.Substring(0, args.WorkingFile.LastIndexOf(".") + 1) + "edl";
if (File.Exists(edlFile) == false)
{
args.Logger?.ILog("No EDL file found for file");
return string.Empty;
}
string text = File.ReadAllText(edlFile) ?? string.Empty;
float last = 0;
StringBuilder metadata = new StringBuilder();
metadata.AppendLine(";FFMETADATA1");
metadata.AppendLine("");
int chapter = 0;
foreach (string line in text.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries))
{
// 93526.47 93650.13 0
string[] parts = line.Split(new[] { " ", "\t" }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
continue;
float start = 0;
float end = 0;
if (float.TryParse(parts[0], out start) == false || float.TryParse(parts[1], out end) == false)
continue;
if (start < last)
continue;
AddChapter(last, start);
last = end;
}
if (chapter == 0)
{
args.Logger?.ILog("No ads found in edl file");
return string.Empty;
}
AddChapter(last, totalTime);
string tempMetaDataFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + ".txt");
File.WriteAllText(tempMetaDataFile, metadata.ToString());
return tempMetaDataFile;
void AddChapter(float start, float end)
{
metadata.AppendLine("[CHAPTER]");
metadata.AppendLine("TIMEBASE=1/1000");
metadata.AppendLine("START=" + ((int)(start * 1000)));
metadata.AppendLine("END=" + ((int)(end * 1000)));
metadata.AppendLine("title=Chapter " + (++chapter));
metadata.AppendLine();
}
}
}
}

View File

@@ -1,239 +0,0 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public abstract class EncodingNode : VideoNode
{
public override int Outputs => 2;
public override int Inputs => 1;
public override FlowElementType Type => FlowElementType.Process;
protected TimeSpan TotalTime;
private FFMpegEncoder Encoder;
public bool Encode(NodeParameters args, string ffmpegExe, List<string> ffmpegParameters, string extension = "mkv", string outputFile = "", bool updateWorkingFile = true, bool dontAddInputFile = false, bool dontAddOutputFile = false)
{
string output;
return Encode(args, ffmpegExe, ffmpegParameters, out output, extension, outputFile, updateWorkingFile, dontAddInputFile: dontAddInputFile, dontAddOutputFile: dontAddOutputFile);
}
public bool Encode(NodeParameters args, string ffmpegExe, List<string> ffmpegParameters, out string output, string extension = "mkv", string outputFile = "", bool updateWorkingFile = true, bool dontAddInputFile = false, bool dontAddOutputFile = false)
{
if (string.IsNullOrEmpty(extension))
extension = "mkv";
Encoder = new FFMpegEncoder(ffmpegExe, args.Logger);
Encoder.AtTime += AtTimeEvent;
if (string.IsNullOrEmpty(outputFile))
outputFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + "." + extension);
if (TotalTime.TotalMilliseconds == 0)
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo != null)
{
TotalTime = videoInfo.VideoStreams[0].Duration;
args.Logger.ILog("### Total Time: " + TotalTime);
}
}
var success = Encoder.Encode(args.WorkingFile, outputFile, ffmpegParameters, dontAddInputFile: dontAddInputFile, dontAddOutputFile: dontAddOutputFile);
args.Logger.ILog("Encoding successful: " + success.successs);
if (success.successs && updateWorkingFile)
{
args.SetWorkingFile(outputFile);
// get the new video info
var videoInfo = new VideoInfoHelper(ffmpegExe, args.Logger).Read(outputFile);
SetVideoInfo(args, videoInfo, this.Variables ?? new Dictionary<string, object>());
}
Encoder.AtTime -= AtTimeEvent;
Encoder = null;
output = success.output;
return success.successs;
}
public override Task Cancel()
{
if (Encoder != null)
Encoder.Cancel();
return base.Cancel();
}
void AtTimeEvent(TimeSpan time)
{
if (TotalTime.TotalMilliseconds == 0)
{
//Args?.Logger?.DLog("Can't report time progress as total time is 0");
return;
}
float percent = (float)((time.TotalMilliseconds / TotalTime.TotalMilliseconds) * 100);
if(Args?.PartPercentageUpdate != null)
Args.PartPercentageUpdate(percent);
}
public string CheckVideoCodec(string ffmpeg, string vidparams)
{
if (string.IsNullOrEmpty(vidparams))
return string.Empty;
var splitParams = vidparams.ToLower().Split(" ");
if(splitParams.Contains("hevc") || splitParams.Contains("h265"))
{
// try find best hevc encoder
foreach(string vidparam in new [] { "hevc_nvenc -preset hq", "hevc_qsv -global_quality 28 -load_plugin hevc_hw", "hevc_amf", "hevc_vaapi" })
{
bool canProcess = CanUseHardwareEncodingChecker.CanProcess(Args, ffmpeg, vidparam);
if (canProcess)
return vidparam;
}
return "libx265";
}
if (splitParams.Contains("h264"))
{
// try find best hevc encoder
foreach (string vidparam in new[] { "h264_nvenc", "h264_qsv", "h264_amf", "h264_vaapi" })
{
bool canProcess = CanUseHardwareEncodingChecker.CanProcess(Args, ffmpeg, vidparam);
if (canProcess)
return vidparam;
}
return "libx264";
}
// removed in FF-137
//if (vidparams.ToLower().Contains("hevc_nvenc"))
//{
// // nvidia h265 encoding, check can
// bool canProcess = CanUseHardwareEncodingChecker.CanProcess(Args, ffmpeg, vidparams);
// if (canProcess == false)
// {
// // change to cpu encoding
// Args.Logger?.ILog("Can't encode using hevc_nvenc, falling back to CPU encoding H265 (libx265)");
// return "libx265";
// }
// return vidparams;
//}
//else if (vidparams.ToLower().Contains("h264_nvenc"))
//{
// // nvidia h264 encoding, check can
// bool canProcess = CanUseHardwareEncodingChecker.CanProcess(Args, ffmpeg, vidparams);
// if (canProcess == false)
// {
// // change to cpu encoding
// Args.Logger?.ILog("Can't encode using h264_nvenc, falling back to CPU encoding H264 (libx264)");
// return "libx264";
// }
// return vidparams;
//}
//else if (vidparams.ToLower().Contains("hevc_qsv"))
//{
// // nvidia h265 encoding, check can
// bool canProcess = CanUseHardwareEncodingChecker.CanProcess(Args, ffmpeg, vidparams);
// if (canProcess == false)
// {
// // change to cpu encoding
// Args.Logger?.ILog("Can't encode using hevc_qsv, falling back to CPU encoding H265 (libx265)");
// return "libx265";
// }
// return vidparams;
//}
//else if (vidparams.ToLower().Contains("h264_qsv"))
//{
// // nvidia h264 encoding, check can
// bool canProcess = CanUseHardwareEncodingChecker.CanProcess(Args, ffmpeg, vidparams);
// if (canProcess == false)
// {
// // change to cpu encoding
// Args.Logger?.ILog("Can't encode using h264_qsv, falling back to CPU encoding H264 (libx264)");
// return "libx264";
// }
// return vidparams;
//}
return vidparams;
}
public bool HasNvidiaCard(string ffmpeg)
{
try
{
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
var cmd = Args.Process.ExecuteShellCommand(new ExecuteArgs
{
Command = "wmic",
Arguments = "path win32_VideoController get name"
}).Result;
if (cmd.ExitCode == 0)
{
// it worked
if (cmd.Output?.ToLower()?.Contains("nvidia") == false)
return false;
}
}
else
{
// linux, crude method, look for nvidia in the /dev dir
var dir = new DirectoryInfo("/dev");
if (dir.Exists == false)
return false;
bool dev = dir.GetDirectories().Any(x => x.Name.ToLower().Contains("nvidia"));
if (dev == false)
return false;
}
// check cuda in ffmpeg itself
var result = Args.Process.ExecuteShellCommand(new ExecuteArgs
{
Command = ffmpeg,
Arguments = "-hide_banner -init_hw_device list"
}).Result;
return result.Output?.Contains("cuda") == true;
}
catch (Exception ex)
{
Args.Logger?.ELog("Failed to detect NVIDIA card: " + ex.Message + Environment.NewLine + ex.StackTrace);
return false;
}
}
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;
}
}
protected bool SupportsSubtitles(NodeParameters args, VideoInfo videoInfo, string extension)
{
if (videoInfo?.SubtitleStreams?.Any() != true)
return false;
bool mov_text = videoInfo.SubtitleStreams.Any(x => x.Codec == "mov_text");
// if mov_text and going to mkv, we can't convert these subtitles
if (mov_text && extension?.ToLower()?.EndsWith("mkv") == true)
return false;
return true;
//if (Regex.IsMatch(container ?? string.Empty, "(mp(e)?(g)?4)|avi|divx|xvid", RegexOptions.IgnoreCase))
// return false;
//return true;
}
}
}

View File

@@ -1,71 +0,0 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class FFMPEG : EncodingNode
{
public override int Outputs => 1;
[DefaultValue("-i {WorkingFile} {Output}")]
[TextArea(1)]
[Required]
public string CommandLine { get; set; }
[DefaultValue("mkv")]
[Text(2)]
[Required]
public string Extension { get; set; }
public override string Icon => "far fa-file-video";
public List<string> GetFFMPEGArgs(NodeParameters args, string outputFile)
{
string cmdLine = args.ReplaceVariables(CommandLine);
List<string> ffArgs = cmdLine.SplitCommandLine().Select(x =>
{
if (x.ToLower() == "{workingfile}") return args.WorkingFile;
if (x.ToLower() == "{output}") return outputFile;
return x;
}).ToList();
return ffArgs;
}
public override int Execute(NodeParameters args)
{
if (string.IsNullOrEmpty(CommandLine))
{
args.Logger.ELog("Command Line not set");
return -1;
}
try
{
if (string.IsNullOrEmpty(Extension))
Extension = "mkv";
string outputFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + "." + Extension);
var ffArgs = GetFFMPEGArgs(args, outputFile);
if (Encode(args, FFMPEG, ffArgs, updateWorkingFile: false, dontAddInputFile: true, dontAddOutputFile: true) == false)
return -1;
if (File.Exists(outputFile))
{
args.Logger?.ILog("Output file exists, updating working file: " + outputFile);
args.SetWorkingFile(outputFile);
}
return 1;
}
catch (Exception ex)
{
args.Logger.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,60 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
public class RemuxToMKV: EncodingNode
{
public override string Icon => "far fa-file-video";
[Boolean(1)]
public bool Force { get; set; }
public override int Execute(NodeParameters args)
{
if (Force == false && args.WorkingFile?.ToLower()?.EndsWith(".mkv") == true)
return 2;
try
{
if (Encode(args, FFMPEG, new List<string> { "-c", "copy", "-map", "0" }, "mkv") == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
public class RemuxToMP4 : EncodingNode
{
public override string Icon => "far fa-file-video";
[Boolean(1)]
public bool Force { get; set; }
public override int Execute(NodeParameters args)
{
if (Force == false && args.WorkingFile?.ToLower()?.EndsWith(".mp4") == true)
return 2;
try
{
if (Encode(args, FFMPEG, new List<string> { "-c", "copy", "-map", "0" }, "mp4") == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,85 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
public class SubtitleLanguageRemover: EncodingNode
{
public override int Outputs => 2;
public override string Icon => "fas fa-comment";
[TextVariable(1)]
public string Pattern { get; set; }
[Boolean(2)]
public bool NotMatching { get; set; }
[Boolean(3)]
public bool UseTitle { get; set; }
public override int Execute(NodeParameters args)
{
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
List<string> ffArgs = new List<string>()
{
"-map", "0:v",
"-map", "0:a",
};
bool removing = false;
var regex = new Regex(this.Pattern, RegexOptions.IgnoreCase);
for (int i = 0; i < videoInfo.SubtitleStreams.Count; i++)
{
var sub = videoInfo.SubtitleStreams[i];
string str = UseTitle ? sub.Title : sub.Language;
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)
{
removing = true;
continue;
}
}
ffArgs.AddRange(new[] { "-map", "0:s:" + i });
}
if(removing == false)
{
// nothing to remove
return 2;
}
ffArgs.AddRange(new[] { "-c", "copy" });
string extension = new FileInfo(args.WorkingFile).Extension;
if(extension.StartsWith("."))
extension = extension.Substring(1);
if (Encode(args, FFMPEG, ffArgs, extension) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,118 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
public class SubtitleRemover: EncodingNode
{
public override int Outputs => 2;
public override string Icon => "fas fa-comment";
[Boolean(1)]
public bool RemoveAll { get; set; }
[Checklist(nameof(Options), 2)]
[ConditionEquals(nameof(RemoveAll), false)]
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 = "mov_text", Label = "TX3G (mov_text)"},
new ListOption { Value = "webvtt", Label = "WebVTT subtitle"},
};
}
return _Options;
}
}
public override int Execute(NodeParameters args)
{
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
List<string> ffArgs = new List<string>()
{
"-map", "0:v",
"-map", "0:a",
};
bool foundBadSubtitle = false;
if (RemoveAll == false)
{
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
foreach (var sub in videoInfo.SubtitleStreams)
{
args.Logger?.ILog("Subtitle found: " + sub.Codec + ", " + sub.Title);
if (removeCodecs.Contains(sub.Codec.ToLower()))
{
foundBadSubtitle = true;
continue;
}
ffArgs.AddRange(new[] { "-map", sub.IndexString });
}
}
else
{
foundBadSubtitle = videoInfo.SubtitleStreams?.Any() == true;
}
if(foundBadSubtitle == false)
{
// nothing to remove
return 2;
}
ffArgs.AddRange(new[] { "-c", "copy" });
string extension = new FileInfo(args.WorkingFile).Extension;
if(extension.StartsWith("."))
extension = extension.Substring(1);
if (Encode(args, FFMPEG, ffArgs, extension) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,172 +0,0 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class VideoEncode : EncodingNode
{
[DefaultValue("hevc")]
[TextVariable(1)]
public string VideoCodec { get; set; }
[DefaultValue("hevc")]
[TextVariable(2)]
public string VideoCodecParameters { get; set; }
[DefaultValue("ac3")]
[TextVariable(3)]
public string AudioCodec { get; set; }
[DefaultValue("eng")]
[TextVariable(4)]
public string Language { get; set; }
[DefaultValue("mkv")]
[TextVariable(5)]
public string Extension { get; set; }
public override string Icon => "far fa-file-video";
public override int Execute(NodeParameters args)
{
if (VideoCodec == "COPY")
VideoCodec = string.Empty;
if (AudioCodec == "COPY")
AudioCodec = string.Empty;
if (string.IsNullOrEmpty(VideoCodec) && string.IsNullOrEmpty(AudioCodec))
{
args.Logger?.ELog("Video codec or Audio codec must be set");
return -1;
}
if (string.IsNullOrWhiteSpace(VideoCodecParameters))
VideoCodecParameters = VideoCodec;
VideoCodec = args.ReplaceVariables(VideoCodec ?? string.Empty);
VideoCodecParameters = args.ReplaceVariables(VideoCodecParameters ?? string.Empty);
AudioCodec = args.ReplaceVariables(AudioCodec ?? string.Empty);
Language = args.ReplaceVariables(Language);
VideoCodec = VideoCodec.ToLower();
AudioCodec = AudioCodec.ToLower();
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
Language = Language?.ToLower() ?? "";
// ffmpeg is one based for stream index, so video should be 1, audio should be 2
string encodeVideoParameters = string.Empty, encodeAudioParameters = string.Empty;
const string copyVideoStream = "-map 0:v -c:v copy";
const string copyAudioStream = "-map 0:a -c:a copy";
if (string.IsNullOrEmpty(VideoCodec) == false)
{
var videoIsRightCodec = videoInfo.VideoStreams.FirstOrDefault(x => IsSameVideoCodec(x.Codec ?? string.Empty, VideoCodec));
var videoTrack = videoIsRightCodec ?? videoInfo.VideoStreams[0];
args.Logger?.ILog("Video: ", videoTrack);
string crop = (args.Variables.ContainsKey(DetectBlackBars.CROP_KEY) ? args.Variables[DetectBlackBars.CROP_KEY] as string : string.Empty) ?? string.Empty;
if (crop != string.Empty)
crop = " -vf crop=" + crop;
if (videoIsRightCodec == null || crop != string.Empty)
{
string codecParameters = CheckVideoCodec(FFMPEG, VideoCodecParameters);
encodeVideoParameters = $"-map 0:v:0 -c:v {codecParameters} {crop}";
}
Extension = args.ReplaceVariables(Extension)?.EmptyAsNull() ?? "mkv";
}
else if(string.IsNullOrEmpty(Extension) == false)
{
// vidoe is being copied so use the same extension
Extension = new FileInfo(args.WorkingFile).Extension;
if(Extension.StartsWith("."))
Extension = Extension.Substring(1);
}
if (string.IsNullOrEmpty(AudioCodec) == false)
{
var bestAudio = videoInfo.AudioStreams.Where(x => System.Text.Json.JsonSerializer.Serialize(x).ToLower().Contains("commentary") == false)
.OrderBy(x =>
{
if (Language != string.Empty)
{
args.Logger?.ILog("Language: " + x.Language, x);
if (string.IsNullOrEmpty(x.Language))
return 50; // no language specified
if (x.Language?.ToLower() != Language)
return 100; // low priority not the desired language
}
return 0;
})
.ThenByDescending(x => x.Channels)
.ThenBy(x => x.Index)
.FirstOrDefault();
bool audioRightCodec = bestAudio?.Codec?.ToLower() == AudioCodec && videoInfo.AudioStreams[0] == bestAudio;
args.Logger?.ILog("Best Audio: ", bestAudio == null ? "null" : (object)bestAudio);
if (audioRightCodec == false)
encodeAudioParameters = $"-map 0:{bestAudio!.Index} -c:a {AudioCodec}";
else if(videoInfo.AudioStreams.Count > 1)
encodeAudioParameters = $"-map 0:{bestAudio!.Index} -c:a copy";
}
if(string.IsNullOrEmpty(encodeVideoParameters) && string.IsNullOrEmpty(encodeAudioParameters))
{
args.Logger?.ILog("Video and Audio does not need to be reencoded");
return 2;
}
List<string> ffArgs = new List<string>();
ffArgs.AddRange((encodeVideoParameters?.EmptyAsNull() ?? copyVideoStream).Split(" ").Where(x => string.IsNullOrEmpty(x.Trim()) == false).Select(x => x.Trim()).ToArray());
ffArgs.AddRange((encodeAudioParameters?.EmptyAsNull() ?? copyAudioStream).Split(" ").Where(x => string.IsNullOrEmpty(x.Trim()) == false).Select(x => x.Trim()).ToArray());
TotalTime = videoInfo.VideoStreams[0].Duration;
args.Logger.ILog("### Total Time: " + TotalTime);
if (videoInfo?.SubtitleStreams?.Any() == true)
{
if (SupportsSubtitles(args, videoInfo, Extension))
{
if (Language != string.Empty)
ffArgs.AddRange(new[] { "-map", $"0:s:m:language:{Language}?", "-c:s", "copy" });
else
ffArgs.AddRange(new[] { "-map", "0:s?", "-c:s", "copy" });
}
else
{
args.Logger?.WLog("Unsupported subtitle for target container, subtitles will be removed.");
}
}
if (Encode(args, FFMPEG, ffArgs, Extension) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message);
return -1;
}
}
}
}

View File

@@ -1,164 +0,0 @@
namespace FileFlows.VideoNodes
{
using FileFlows.Plugin;
using System.Text.Json;
public abstract class VideoNode : Node
{
/// <summary>
/// Gets the Node Parameters
/// </summary>
protected NodeParameters Args { get; private set; }
/// <summary>
/// Gets if this node is obsolete and should be phased out
/// </summary>
public override bool Obsolete => true;
/// <summary>
/// Gets a message to show when the user tries to use this obsolete node
/// </summary>
public override string ObsoleteMessage => "This node has been replaced by the FFMPEG Builder version";
#if (DEBUG)
/// <summary>
/// Used for unit tests
/// </summary>
/// <param name="args">the args</param>
public void SetArgs(NodeParameters args)
{
this.Args = args;
}
#endif
/// <summary>
/// Gets the FFMPEG executable location
/// </summary>
protected string FFMPEG { get; private set; }
public override string Icon => "fas fa-video";
/// <summary>
/// Executed before execute, sets ffmpegexe etc
/// </summary>
/// <param name="args">the node parametes</param>
/// <returns>true if successfully</returns>
public override bool PreExecute(NodeParameters args)
{
this.Args = args;
this.FFMPEG = GetFFMpegExe();
return string.IsNullOrEmpty(this.FFMPEG) == false;
}
private string GetFFMpegExe()
{
string ffmpeg = Args.GetToolPath("FFMpeg");
if (string.IsNullOrEmpty(ffmpeg))
{
Args.Logger.ELog("FFMpeg tool not found.");
return "";
}
var fileInfo = new FileInfo(ffmpeg);
if (fileInfo.Exists == false)
{
Args.Logger.ELog("FFMpeg tool configured by ffmpeg.exe file does not exist.");
return "";
}
return fileInfo.FullName;
}
// protected string GetFFMpegPath(NodeParameters args)
// {
// string ffmpeg = args.GetToolPath("FFMpeg");
// if (string.IsNullOrEmpty(ffmpeg))
// {
// args.Logger.ELog("FFMpeg tool not found.");
// return "";
// }
// var fileInfo = new FileInfo(ffmpeg);
// if (fileInfo.Exists == false)
// {
// args.Logger.ELog("FFMpeg tool configured by ffmpeg.exe file does not exist.");
// return "";
// }
// return fileInfo.DirectoryName;
// }
private const string VIDEO_INFO = "VideoInfo";
protected void SetVideoInfo(NodeParameters args, VideoInfo videoInfo, Dictionary<string, object> variables)
{
if (videoInfo.VideoStreams?.Any() == false)
return;
if (args.Parameters.ContainsKey(VIDEO_INFO))
args.Parameters[VIDEO_INFO] = videoInfo;
else
args.Parameters.Add(VIDEO_INFO, videoInfo);
variables.AddOrUpdate("vi.VideoInfo", videoInfo);
variables.AddOrUpdate("vi.Width", videoInfo.VideoStreams[0].Width);
variables.AddOrUpdate("vi.Height", videoInfo.VideoStreams[0].Height);
variables.AddOrUpdate("vi.Duration", videoInfo.VideoStreams[0].Duration.TotalSeconds);
variables.AddOrUpdate("vi.Video.Codec", videoInfo.VideoStreams[0].Codec);
if (videoInfo.AudioStreams?.Any() == true)
{
variables.AddOrUpdate("vi.Audio.Codec", videoInfo.AudioStreams[0].Codec?.EmptyAsNull());
variables.AddOrUpdate("vi.Audio.Channels", videoInfo.AudioStreams[0].Channels > 0 ? (object)videoInfo.AudioStreams[0].Channels : null);
variables.AddOrUpdate("vi.Audio.Language", videoInfo.AudioStreams[0].Language?.EmptyAsNull());
variables.AddOrUpdate("vi.Audio.Codecs", string.Join(", ", videoInfo.AudioStreams.Select(x => x.Codec).Where(x => string.IsNullOrEmpty(x) == false)));
variables.AddOrUpdate("vi.Audio.Languages", string.Join(", ", videoInfo.AudioStreams.Select(x => x.Language).Where(x => string.IsNullOrEmpty(x) == false)));
}
var resolution = ResolutionHelper.GetResolution(videoInfo.VideoStreams[0].Width, videoInfo.VideoStreams[0].Height);
if(resolution == ResolutionHelper.Resolution.r1080p)
variables.AddOrUpdate("vi.Resolution", "1080p");
else if (resolution == ResolutionHelper.Resolution.r4k)
variables.AddOrUpdate("vi.Resolution", "4K");
else if (resolution == ResolutionHelper.Resolution.r720p)
variables.AddOrUpdate("vi.Resolution", "720p");
else if (resolution == ResolutionHelper.Resolution.r480p)
variables.AddOrUpdate("vi.Resolution", "480p");
else if (videoInfo.VideoStreams[0].Width < 900 && videoInfo.VideoStreams[0].Height < 800)
variables.AddOrUpdate("vi.Resolution", "SD");
else
variables.AddOrUpdate("vi.Resolution", videoInfo.VideoStreams[0].Width + "x" + videoInfo.VideoStreams[0].Height);
args.UpdateVariables(variables);
}
protected VideoInfo GetVideoInfo(NodeParameters args)
{
if (args.Parameters.ContainsKey(VIDEO_INFO) == false)
{
args.Logger.WLog("No codec information loaded, use a 'VideoFile' node first");
return null;
}
if(args.Parameters[VIDEO_INFO] == null)
{
args.Logger.WLog("VideoInfo not found for file");
return null;
}
var result = args.Parameters[VIDEO_INFO] as VideoInfo;
if (result != null)
return result;
// may be from non Legacy VideoNodes
try
{
string json = JsonSerializer.Serialize(args.Parameters[VIDEO_INFO]);
var vi = JsonSerializer.Deserialize<VideoInfo>(json);
if (vi == null)
throw new Exception("Failed to deserailize object");
return vi;
}
catch(Exception ex)
{
args.Logger.WLog("VideoInfo could not be deserialized: " + ex.Message);
return null;
}
}
}
}

View File

@@ -1,142 +0,0 @@
namespace FileFlows.VideoNodes
{
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
public class VideoScaler: EncodingNode
{
public override string Icon => "fas fa-search-plus";
public override int Outputs => 2; // this node always re-encodes
[Select(nameof(CodecOptions), 1)]
public string VideoCodec { get; set; }
[Required]
[TextVariable(2)]
[ConditionEquals(nameof(VideoCodec), "Custom")]
public string VideoCodecParameters { get; set; }
private static List<ListOption> _CodecOptions;
public static List<ListOption> CodecOptions
{
get
{
if (_CodecOptions == null)
{
_CodecOptions = new List<ListOption>
{
new ListOption { Label = "Automatic", Value = "###GROUP###"},
new ListOption { Value = "h264", Label = "H264"},
new ListOption { Value = "h265", Label = "H265"},
new ListOption { Label = "CPU Encoding", Value = "###GROUP###"},
new ListOption { Value = "libx264", Label = "H264 (CPU)"},
new ListOption { Value = "libx265", Label = "H265 (CPU)"},
new ListOption { Label = "NVIDIA Hardware Encoding", Value = "###GROUP###"},
new ListOption { Value = "h264_nvenc", Label = "H264 (NVIDIA)"},
new ListOption { Value = "hevc_nvenc -preset hq -crf 23", Label = "H265 (NVIDIA)"},
new ListOption { Label = "Intel Hardware Encoding", Value = "###GROUP###"},
new ListOption { Value = "h264_qsv", Label = "H264 (Intel)"},
new ListOption { Value = "hevc_qsv", Label = "H265 (Intel)"},
new ListOption { Label = "Custom", Value = "###GROUP###"},
new ListOption { Value = "Custom", Label = "Custom"},
};
}
return _CodecOptions;
}
}
[DefaultValue("mkv")]
[TextVariable(4)]
public string Extension { get; set; }
[Boolean(5)]
public bool Force { get; set; }
[Select(nameof(ResolutionOptions), 3)]
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 Execute(NodeParameters args)
{
Extension = args.ReplaceVariables(Extension)?.EmptyAsNull() ?? "mkv";
try
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
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;
}
List<string> ffArgs = new List<string>()
{
"-vf", $"scale={Resolution}:flags=lanczos",
"-c:v"
};
string codec = VideoCodec == "Custom" && string.IsNullOrWhiteSpace(VideoCodecParameters) == false ?
VideoCodecParameters : CheckVideoCodec(FFMPEG, VideoCodec);
foreach (string c in codec.Split(" "))
{
if (string.IsNullOrWhiteSpace(c.Trim()))
continue;
ffArgs.Add(c.Trim());
}
if (Encode(args, FFMPEG, ffArgs, Extension) == false)
return -1;
return 1;
}
catch (Exception ex)
{
args.Logger?.ELog("Failed processing VideoFile: " + ex.Message + Environment.NewLine + ex.StackTrace);
return -1;
}
}
}
}

View File

@@ -1,140 +0,0 @@
//namespace FileFlows.VideoNodes
//{
// using System.ComponentModel;
// using System.Text.RegularExpressions;
// using FileFlows.Plugin;
// using FileFlows.Plugin.Attributes;
// public class Video_H265_AC3 : EncodingNode
// {
// [DefaultValue("eng")]
// [Text(1)]
// public string Language { get; set; }
// [DefaultValue(21)]
// [NumberInt(2)]
// public int Crf { get; set; }
// [DefaultValue(true)]
// [Boolean(3)]
// public bool NvidiaEncoding { get; set; }
// [DefaultValue(0)]
// [NumberInt(4)]
// public int Threads { get; set; }
// [DefaultValue(false)]
// [Boolean(5)]
// public bool NormalizeAudio { get; set; }
// [DefaultValue(false)]
// [Boolean(6)]
// public bool ForceRencode { get; set; }
// public override string Icon => "far fa-file-video";
// public override int Execute(NodeParameters args)
// {
// this.args = args;
// try
// {
// VideoInfo videoInfo = GetVideoInfo(args);
// if (videoInfo == null)
// return -1;
// Language = Language?.ToLower() ?? "";
// // ffmpeg is one based for stream index, so video should be 1, audio should be 2
// var videoH265 = videoInfo.VideoStreams.FirstOrDefault(x => Regex.IsMatch(x.Codec ?? "", @"^(hevc|h(\.)?265)$", RegexOptions.IgnoreCase));
// var videoTrack = videoH265 ?? videoInfo.VideoStreams[0];
// args.Logger.ILog("Video: ", videoTrack);
// var bestAudio = videoInfo.AudioStreams.Where(x => System.Text.Json.JsonSerializer.Serialize(x).ToLower().Contains("commentary") == false)
// .OrderBy(x =>
// {
// if (Language != string.Empty)
// {
// args.Logger.ILog("Language: " + x.Language, x);
// if (string.IsNullOrEmpty(x.Language))
// return 50; // no language specified
// if (x.Language?.ToLower() != Language)
// return 100; // low priority not the desired language
// }
// return 0;
// })
// .ThenByDescending(x => x.Channels)
// //.ThenBy(x => x.CodecName.ToLower() == "ac3" ? 0 : 1) // if we do this we can get commentary tracks...
// .ThenBy(x => x.Index)
// .FirstOrDefault();
// bool firstAc3 = bestAudio?.Codec?.ToLower() == "ac3" && videoInfo.AudioStreams[0] == bestAudio;
// args.Logger.ILog("Best Audio: ", bestAudio == null ? (object)"null" : (object)bestAudio);
// string crop = args.GetParameter<string>(DetectBlackBars.CROP_KEY) ?? "";
// if (crop != string.Empty)
// crop = " -vf crop=" + crop;
// if (ForceRencode == false && firstAc3 == true && videoH265 != null)
// {
// if (crop == string.Empty)
// {
// args.Logger.DLog("File is hevc with the first audio track being AC3");
// return 2;
// }
// else
// {
// args.Logger.ILog("Video is hevc and ac3 but needs to be cropped");
// }
// }
// string ffmpegExe = GetFFMpegExe(args);
// if (string.IsNullOrEmpty(ffmpegExe))
// return -1;
// List<string> ffArgs = new List<string>();
// if (NvidiaEncoding == false && Threads > 0)
// ffArgs.AddRange(new[] { "-threads", Math.Min(Threads, 16).ToString() });
// if (videoH265 == null || crop != string.Empty)
// ffArgs.AddRange(new[] { "-map", "0:v:0", "-c:v", NvidiaEncoding ? "hevc_nvenc - preset hq" : "libx265")} -crf " + (Crf > 0 ? Crf : 21) + crop);
// "
// else
// ffArgs.Add($"-map 0:v:0 -c:v copy");
// TotalTime = videoInfo.VideoStreams[0].Duration;
// if (NormalizeAudio)
// {
// int sampleRate = bestAudio.SampleRate > 0 ? bestAudio.SampleRate : 48_000;
// ffArgs.Add($"-map 0:{bestAudio.Index} -c:a ac3 -ar {sampleRate} -af loudnorm=I=-24:LRA=7:TP=-2.0");
// }
// else if (bestAudio.Codec.ToLower() != "ac3")
// ffArgs.Add($"-map 0:{bestAudio.Index} -c:a ac3");
// else
// ffArgs.Add($"-map 0:{bestAudio.Index} -c:a copy");
// if (Language != string.Empty)
// ffArgs.Add($"-map 0:s:m:language:{Language}? -c:s copy");
// else
// ffArgs.Add($"-map 0:s? -c:s copy");
// if (Encode(args, ffmpegExe, ffArgs) == false)
// return -1;
// return 1;
// }
// catch (Exception ex)
// {
// args.Logger.ELog("Failed processing VideoFile: " + ex.Message);
// return -1;
// }
// }
// }
//}

View File

@@ -1,6 +1,7 @@
using FileFlows.Plugin;
using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
using System.Runtime.InteropServices;
using FileFlows.VideoNodes.Helpers;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes
{
@@ -120,6 +121,12 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes
{
startArgs.AddRange(GetHardwareDecodingArgs());
}
if (ffArgs.Any(x => x.Contains("vaapi") && Helpers.VaapiHelper.VaapiLinux))
{
startArgs.Add("-vaapi_device");
startArgs.Add(VaapiHelper.VaapiRenderDevice);
}
foreach (var file in model.InputFiles)
{
@@ -198,7 +205,7 @@ namespace FileFlows.VideoNodes.FfmpegBuilderNodes
{
if (hw == null)
continue;
if (CanUseHardwareEncoding.DisabledByVariables(Args, string.Join(" ", hw)))
if (CanUseHardwareEncoding.DisabledByVariables(Args, hw))
continue;
try
{

View File

@@ -0,0 +1,11 @@
namespace FileFlows.VideoNodes.Helpers;
/// <summary>
/// Helper for Vaapi
/// </summary>
class VaapiHelper
{
internal static bool VaapiLinux => OperatingSystem.IsLinux() && File.Exists(VaapiRenderDevice);
internal const string VaapiRenderDevice = "/dev/dri/renderD128";
}

View File

@@ -1,4 +1,6 @@
namespace FileFlows.VideoNodes;
using FileFlows.VideoNodes.Helpers;
namespace FileFlows.VideoNodes;
/// <summary>
/// Node for checking if Flow Runner has access to hardware
@@ -139,7 +141,7 @@ public class CanUseHardwareEncoding:Node
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>true if can use it, otherwise false</returns>
internal static bool CanProcess_Qsv_Hevc(NodeParameters args) => CanProcess(args, "hevc_qsv -global_quality 28 -load_plugin hevc_hw");
internal static bool CanProcess_Qsv_Hevc(NodeParameters args) => CanProcess(args, "hevc_qsv", "-global_quality", "28", "-load_plugin", "hevc_hw");
/// <summary>
/// Checks if this flow runner can use Intels QSV H.264 encoder
@@ -169,34 +171,34 @@ public class CanUseHardwareEncoding:Node
/// <param name="args">the node parameters</param>
/// <param name="parameters">the parameters to check</param>
/// <returns>if a encoder/decoder has been disabled by a variable</returns>
internal static bool DisabledByVariables(NodeParameters args, string parameters)
internal static bool DisabledByVariables(NodeParameters args, string[] parameters)
{
if (parameters.ToLower().Contains("nvenc"))
if (parameters.Any(x => x.ToLower().Contains("nvenc")))
{
if (args.GetVariable("NoNvidia") as bool? == true)
return true;
if (args.GetVariable("NoNVIDIA") as bool? == true)
return true;
}
else if (parameters.ToLower().Contains("qsv"))
else if (parameters.Any(x => x.ToLower().Contains("qsv")))
{
if (args.GetVariable("NoQSV") as bool? == true)
return true;
}
else if (parameters.ToLower().Contains("vaapi"))
else if (parameters.Any(x => x.ToLower().Contains("vaapi")))
{
if (args.GetVariable("NoVAAPI") as bool? == true)
return true;
}
else if (parameters.ToLower().Contains("amf"))
else if (parameters.Any(x => x.ToLower().Contains("amf")))
{
if (args.GetVariable("NoAMF") as bool? == true)
return true;
if (args.GetVariable("NoAMD") as bool? == true)
return true;
}
else if (parameters.ToLower().Contains("videotoolbox"))
else if (parameters.Any(x => x.ToLower().Contains("videotoolbox")))
{
if (args.GetVariable("NoVideoToolbox") as bool? == true)
return true;
@@ -204,7 +206,7 @@ public class CanUseHardwareEncoding:Node
return false;
}
private static bool CanProcess(NodeParameters args, string encodingParams)
private static bool CanProcess(NodeParameters args, params string[] encodingParams)
{
if (DisabledByVariables(args, encodingParams))
return false;
@@ -226,7 +228,7 @@ public class CanUseHardwareEncoding:Node
/// <param name="ffmpeg">the location of ffmpeg</param>
/// <param name="encodingParams">the encoding parameter to test</param>
/// <returns>true if can be processed</returns>
internal static bool CanProcess(NodeParameters args, string ffmpeg, string encodingParams)
internal static bool CanProcess(NodeParameters args, string ffmpeg, string[] encodingParams)
{
bool can = CanExecute();
if (can == false && encodingParams?.Contains("amf") == true)
@@ -240,16 +242,49 @@ public class CanUseHardwareEncoding:Node
bool CanExecute()
{
string cmdArgs = $"-loglevel error -f lavfi -i color=black:s=1080x1080 -vframes 1 -an -c:v {encodingParams} -f null -\"";
bool vaapi = encodingParams.Any(x => x.Contains("vaapi")) && VaapiHelper.VaapiLinux;
List<string> arguments = encodingParams.ToList();
if (vaapi)
arguments.AddRange(new [] { "-vf", "'format=nv12,hwupload'", "-strict", "-2"});
arguments.InsertRange(0, new []
{
"-loglevel",
"error",
"-f",
"lavfi",
"-i",
"color=black:s=1080x1080",
"-vframes",
"1",
"-an",
"-c:v"
});
if (vaapi)
{
arguments.InsertRange(0,
new[] { "-fflags", "+genpts", "-vaapi_device", VaapiHelper.VaapiRenderDevice });
arguments.Add(Path.Combine(args.TempPath, Guid.NewGuid() + ".mkv"));
}
else
{
arguments.AddRange(new []
{
"-f",
"null",
"-"
});
}
var cmd = args.Process.ExecuteShellCommand(new ExecuteArgs
{
Command = ffmpeg,
Arguments = cmdArgs,
ArgumentList = arguments.ToArray(),
Silent = true
}).Result;
if (cmd.ExitCode != 0 || string.IsNullOrWhiteSpace(cmd.Output) == false)
{
args.Logger?.WLog($"Cant process '{encodingParams}': {cmd.Output ?? ""}");
string asStr = string.Join(" ", arguments.Select(x => x.Contains(" ") ? "\"" + x + "\"" : x));
args.Logger?.WLog($"Cant process '{ffmpeg} {asStr}': {cmd.Output ?? ""}");
return false;
}
return true;

View File

@@ -109,6 +109,39 @@ public class FfmpegBuilder_MetadataTests: TestBase
string log = logger.ToString();
Assert.AreEqual(1, result);
}
[TestMethod]
public void FfmpegBuilder_Metadata_Remover_BitrateFromConvetted()
{
string file = TestFile_BasicMkv;
var logger = new TestLogger();
var vi = new VideoInfoHelper(FfmpegPath, logger);
var vii = vi.Read(file);
var args = new NodeParameters(file, logger, false, string.Empty);
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
args.Parameters.Add("VideoInfo", vii);
FfmpegBuilderStart ffStart = new();
Assert.IsTrue(ffStart.PreExecute(args));
Assert.AreEqual(1, ffStart.Execute(args));
FfmpegBuilderVideoEncode ffEncode = new();
ffEncode.Codec = "h265";
ffEncode.Quality = 30;
ffEncode.HardwareEncoding = false;
ffEncode.PreExecute(args);
ffEncode.Execute(args);
FfmpegBuilderExecutor ffExecutor = new();
ffExecutor.PreExecute(args);
int result = ffExecutor.Execute(args);
string log = logger.ToString();
Assert.AreEqual(1, result);
}
}
#endif

View File

@@ -1,5 +1,6 @@
#if(DEBUG)
using System.Runtime.InteropServices;
using FileFlows.VideoNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text.Json;
@@ -12,6 +13,9 @@ public abstract class TestBase
public string TestPath { get; private set; }
public string TempPath { get; private set; }
public string FfmpegPath { get; private set; }
public readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
[TestInitialize]
public void TestInitialize()
@@ -24,9 +28,15 @@ public abstract class TestBase
{
LoadSettings("../../../test.settings.json");
}
this.TestPath = this.TestPath?.EmptyAsNull() ?? @"d:\videos\testfiles";
this.TempPath = this.TempPath?.EmptyAsNull() ?? @"d:\videos\temp";
this.FfmpegPath = this.FfmpegPath?.EmptyAsNull() ?? @"C:\utils\ffmpeg\ffmpeg.exe";
this.TestPath = this.TestPath?.EmptyAsNull() ?? (IsLinux ? "~/src/ff-files/test-files" : @"d:\videos\testfiles");
this.TempPath = this.TempPath?.EmptyAsNull() ?? (IsLinux ? "~/src/ff-files/temp" : @"d:\videos\temp");
this.FfmpegPath = this.FfmpegPath?.EmptyAsNull() ?? (IsLinux ? "/usr/bin/ffmpeg" : @"C:\utils\ffmpeg\ffmpeg.exe");
this.TestPath = this.TestPath.Replace("~/", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/");
this.TempPath = this.TempPath.Replace("~/", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/");
this.FfmpegPath = this.FfmpegPath.Replace("~/", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/");
if (Directory.Exists(this.TempPath) == false)
Directory.CreateDirectory(this.TempPath);
}
@@ -35,6 +45,9 @@ public abstract class TestBase
{
try
{
if (File.Exists(filename) == false)
return;
string json = File.ReadAllText(filename);
var settings = JsonSerializer.Deserialize<TestSettings>(json);
this.TestPath = settings.TestPath;

View File

@@ -42,6 +42,10 @@ namespace FileFlows.VideoNodes
public static string GetFFMpegPath(NodeParameters args) => args.GetToolPath("FFMpeg");
public VideoInfo Read(string filename)
{
#if(DEBUG) // UNIT TESTING
filename = filename.Replace("~/", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/");
#endif
var vi = new VideoInfo();
vi.FileName = filename;
if (File.Exists(filename) == false)

Binary file not shown.

View File

@@ -88,10 +88,10 @@ namespace FileFlows.VideoNodes
// try find best hevc encoder
foreach(string vidparam in new [] { "hevc_nvenc -preset hq", "hevc_qsv -global_quality 28 -load_plugin hevc_hw", "hevc_amf", "hevc_vaapi" })
{
if (CanUseHardwareEncoding.DisabledByVariables(Args, vidparam))
if (CanUseHardwareEncoding.DisabledByVariables(Args, vidparam.Split(' ')))
continue;
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, vidparam);
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, vidparam.Split(' '));
if (canProcess)
return vidparam;
}
@@ -102,7 +102,7 @@ namespace FileFlows.VideoNodes
// try find best hevc encoder
foreach (string vidparam in new[] { "h264_nvenc", "h264_qsv", "h264_amf", "h264_vaapi" })
{
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, vidparam);
bool canProcess = CanUseHardwareEncoding.CanProcess(Args, ffmpeg, new [] { vidparam });
if (canProcess)
return vidparam;
}

View File

@@ -1,5 +0,0 @@
{
"TestPath": "C:\\videos\\testfiles",
"TempPath": "C:\\videos\\temp",
"FfmpegPath": "C:\\Utils\\ffmpeg\\ffmpeg.exe"
}