FF-1338 - adding way to convert based on codec

FF-1337 - added ability to run comskip
This commit is contained in:
John Andrews
2024-02-17 10:48:39 +13:00
parent da15c8b900
commit a58aa49b3e
18 changed files with 865 additions and 292 deletions

View File

@@ -1,4 +1,5 @@
using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
using FileFlows.VideoNodes.Helpers;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
@@ -131,20 +132,49 @@ public class FfmpegBuilderAudioConverter : FfmpegBuilderNode
}
}
[DefaultValue("")]
[Select(nameof(FieldOptions), 5)]
public string Field { get; set; }
[TextVariable(5)]
private static List<ListOption> _FieldOptions;
internal const string FIELD_TITLE = "Title";
internal const string FIELD_LANGUAGE = "Language";
internal const string FIELD_CODEC = "Codec";
public static List<ListOption> FieldOptions
{
get
{
if (_FieldOptions == null)
{
_FieldOptions = new List<ListOption>
{
new() { Label = "Convert All", Value = "" },
new() { Label = "Title", Value = FIELD_TITLE },
new() { Label = "Language", Value = FIELD_LANGUAGE },
new() { Label = "Codec", Value = FIELD_CODEC },
};
}
return _FieldOptions;
}
}
[TextVariable(6)]
[ConditionEquals(nameof(Field), "", true)]
public string Pattern { get; set; }
[Boolean(6)]
public bool NotMatching { get; set; }
[Boolean(7)]
public bool UseLanguageCode { get; set; }
[ConditionEquals(nameof(Field), "", true)]
public bool NotMatching { get; set; }
public override int Execute(NodeParameters args)
{
bool converting = false;
Regex? regex = null;
foreach (var track in Model.AudioStreams)
{
if (track.Deleted)
@@ -153,44 +183,74 @@ public class FfmpegBuilderAudioConverter : FfmpegBuilderNode
continue;
}
if (string.IsNullOrEmpty(this.Pattern))
{
bool convertResult = ConvertTrack(args, track);
if (convertResult)
bool convert = false;
if (string.IsNullOrEmpty(this.Field))
{
convert = true;
}
else
{
string testValue = Field switch
{
args.Logger?.ILog($"Stream {track} will be converted");
converting = true;
FIELD_LANGUAGE => track.Language?.EmptyAsNull() ?? track.Stream?.Language ?? string.Empty,
FIELD_TITLE => track.Title?.EmptyAsNull() ?? track.Stream?.Title ?? string.Empty,
FIELD_CODEC => track.Codec?.EmptyAsNull() ?? track.Stream?.Codec ?? string.Empty,
_ => null
};
if (testValue == null)
{
args.Logger?.ILog("Failed to load test value for stream: " + track);
continue;
}
string pattern = this.Pattern ?? string.Empty;
if (GeneralHelper.IsRegex(pattern))
{
try
{
convert =
new Regex(pattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase).IsMatch(
testValue);
if (NotMatching)
convert = !convert;
}
catch (Exception ex)
{
args.Logger?.WLog("Invalid pattern: " + ex.Message);
continue;
}
}
else if (Field == FIELD_LANGUAGE)
{
args.Logger?.ILog("Matching language using language helper.");
convert = LanguageHelper.AreSame(pattern, testValue);
if (NotMatching)
convert = !convert;
}
else
{
args.Logger?.ILog($"Stream {track} will not be converted");
convert = string.Equals(pattern, testValue, StringComparison.InvariantCultureIgnoreCase);
if (NotMatching)
convert = !convert;
}
}
if (convert == false)
{
args.Logger?.ILog("Stream does not match conditions: " + track);
continue;
}
if (regex == null)
regex = new Regex(this.Pattern, RegexOptions.IgnoreCase);
string str = UseLanguageCode ? track.Stream.Language : track.Stream.Title;
if (string.IsNullOrEmpty(str) == false) // if empty we always use this since we have no info to go on
bool convertResult = ConvertTrack(args, track);
if (convertResult)
{
bool matches = regex.IsMatch(str);
if (NotMatching)
matches = !matches;
if (matches)
{
bool convertResult = ConvertTrack(args, track);
if (convertResult)
{
args.Logger?.ILog($"Stream {track} matches pattern and will be converted");
converting = true;
}
else
{
args.Logger?.ILog($"Stream {track} matches pattern but will not be converted");
}
}
args.Logger?.ILog($"Stream {track} matches pattern and will be converted");
converting = true;
}
else
{
args.Logger?.ILog($"Stream {track} matches pattern but will not be converted");
}
}
return converting ? 1 : 2;

View File

@@ -1,5 +1,6 @@
using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
using System.Text;
using FileFlows.VideoNodes.Helpers;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
@@ -7,6 +8,13 @@ public class FfmpegBuilderComskipChapters : FfmpegBuilderNode
{
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/comskip-chapters";
public override int Outputs => 2;
/// <summary>
/// Gets or sets if comskip should be run if no EDL file is found
/// </summary>
[Boolean(1)]
public bool RunComskipIfNoEdl { get; set; }
public override int Execute(NodeParameters args)
{
@@ -37,7 +45,17 @@ public class FfmpegBuilderComskipChapters : FfmpegBuilderNode
if (edlFile.IsFailed)
{
args.Logger.ILog(edlFile.Error);
return string.Empty;
if (RunComskipIfNoEdl == false)
return string.Empty;
var csResult = ComskipHelper.RunComskip(args, args.FileService.GetLocalPath(args.WorkingFile));
if (csResult.Failed(out string error))
{
args.Logger.ILog(error);
return string.Empty;
}
edlFile = csResult;
args.Logger?.ILog("Created EDL File: " + edlFile);
}
string text = System.IO.File.ReadAllText(edlFile) ?? string.Empty;

View File

@@ -102,6 +102,7 @@ public class FfmpegSubtitleStream : FfmpegStream
Codec,
Title,
IsDefault ? "Default" : null,
Stream?.Forced == true ? "Forced" : null,
Deleted ? "Deleted" : null,
HasChange ? "Changed" : null
}.Where(x => string.IsNullOrWhiteSpace(x) == false));

View File

@@ -0,0 +1,273 @@
// using System.Text.Json;
// using System.Text.Json.Serialization;
//
// namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
//
// /// <summary>
// /// FFmpeg Builder flow element that encodes a video using HDR 10+
// /// </summary>
// public class FfmpegBuilderHdr10 : FfmpegBuilderNode
// {
// /// <inheritdoc />
// public override int Inputs => 1;
//
// /// <inheritdoc />
// public override int Outputs => 2;
//
// /// <inheritdoc />
// public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/hdr-10";
//
// /// <summary>
// /// Gets or sets the quality of the video encode
// /// </summary>
// [Slider(1, inverse: true)]
// [Range(0, 51)]
// [DefaultValue(28)]
// public int Quality { get; set; }
//
// /// <summary>
// /// Gets or sets the speed to encode
// /// </summary>
// [Select(nameof(SpeedOptions), 2)]
// public string Speed { get; set; }
//
// private static List<ListOption> _SpeedOptions;
// /// <summary>
// /// Gets or sets the codec options
// /// </summary>
// public static List<ListOption> SpeedOptions
// {
// get
// {
// if (_SpeedOptions == null)
// {
// _SpeedOptions = new List<ListOption>
// {
// new () { Label = "Very Slow", Value = "veryslow" },
// new () { Label = "Slower", Value = "slower" },
// new () { Label = "Slow", Value = "slow" },
// new () { Label = "Medium", Value = "medium" },
// new () { Label = "Fast", Value = "fast" },
// new () { Label = "Faster", Value = "faster" },
// new () { Label = "Very Fast", Value = "veryfast" },
// new () { Label = "Super Fast", Value = "superfast" },
// new () { Label = "Ultra Fast", Value = "ultrafast" },
// };
// }
// return _SpeedOptions;
// }
// }
//
// /// <inheritdoc />
// public override int Execute(NodeParameters args)
// {
// var ffprobeResult = GetFFprobe(args);
// if (ffprobeResult.Failed(out string ffprobeError))
// {
// args.FailureReason = ffprobeError;
// args.Logger?.ELog(ffprobeError);
// return -1;
// }
//
// var model = GetModel();
// var video = model.VideoStreams.FirstOrDefault();
// if (video == null)
// {
// args.Logger?.WLog("No video stream found in FFmpeg Builder");
// return 2;
// }
//
// string ffprobe = ffprobeResult.Value;
// var sideDataResult = GetColorData(args, ffprobe, args.WorkingFile);
// if (sideDataResult.Failed(out string error))
// {
// args.Logger?.ILog("Failed ot get HDR10 info: " + error);
// return 2;
// }
//
// var sd = sideDataResult.Value;
// string gx = sd.GreenX.Split('/')[0];
// string gy = sd.GreenY.Split('/')[0];
// string bx = sd.BlueX.Split('/')[0];
// string by = sd.BlueY.Split('/')[0];
// string rx = sd.RedX.Split('/')[0];
// string ry = sd.RedY.Split('/')[0];
// string wpx = sd.WhitePointX.Split('/')[0];
// string wpy = sd.WhitePointY.Split('/')[0];
// string minLum = sd.MinLuminance.Split('/')[0];
// string maxLum = sd.MaxLuminance.Split('/')[0];
// string display = $@"G({gx},{gy})B({bx},{by})R({rx},{ry})WP({wpx},{wpy})L({maxLum},{minLum})";
//
// args.Logger?.ILog("Display Information: " + display);
//
// video.EncodingParameters = new()
// {
// "libx265",
// "-x265-params",
// $"hdr-opt=1:repeat-headers=1:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display={display}:max-cll=0,0",
// "-crf",
// Quality.ToString(),
// "-preset",
// Speed,
// "-pix_fmt",
// "yuv420p10le"
// };
// // qsv
// // video.EncodingParameters = new()
// // {
// // "hevc_qsv",
// // "-global_quality",
// // "28",
// // "-colorspace",
// // "bt2020nc",
// // "-master-display",
// // display,
// // "-max-cll",
// // "0,0"
// // };
// // nvenc
// // video.EncodingParameters = new()
// // {
// // "hevc_nvenc",
// // "-rc",
// // "vbr_hq",
// // "-preset",
// // "slow",
// // "-b:v",
// // "10M",
// // "-colorspace",
// // "bt2020nc",
// // "master-display",
// // display,
// // "max-cll",
// // "0,0"
// // };
// video.Codec = "hevc";
//
// return 1;
// }
//
// private Result<SideData> GetColorData(NodeParameters args, string ffprobe, string file)
// {
// var result = args.Execute(new()
// {
// Command = ffprobe,
// ArgumentList = new[]
// {
// "-hide_banner",
// "-loglevel",
// "warning",
// "-select_streams",
// "v",
// "-print_format",
// "json",
// "-show_frames",
// "-read_intervals",
// "%+#1",
// "-show_entries",
// "frame=color_space,color_primaries,color_transfer,side_data_list,pix_fmt",
// "-i",
// file
// }
// });
// if (result.ExitCode != 0)
// return Result<SideData>.Fail("FFprobe failed to get HDR+ information");
//
// RootObject? rootObject;
// try
// {
// string json = result.StandardOutput?.EmptyAsNull() ?? result.Output;
// rootObject = JsonSerializer.Deserialize<RootObject>(json);
// }
// catch (Exception ex)
// {
// return Result<SideData>.Fail("Error parsing FFprobe JSON: " + ex.Message);
// }
//
// var sideData = rootObject?.Frames?.FirstOrDefault()?.SideDataList?.FirstOrDefault();
// if (sideData == null || sideData.BlueX == null)
// return Result<SideData>.Fail("No side data found");
//
// return sideData;
// }
//
// public bool Hdr10MetadataExist(NodeParameters args, string hdr10plusTool, string file)
// {
//
// }
//
// /// <summary>
// /// Represents the side data of the frame.
// /// </summary>
// public class SideData
// {
// [JsonPropertyName("side_data_type")]
// public string SideDataType { get; set; }
//
// [JsonPropertyName("red_x")]
// public string RedX { get; set; }
//
// [JsonPropertyName("red_y")]
// public string RedY { get; set; }
//
// [JsonPropertyName("green_x")]
// public string GreenX { get; set; }
//
// [JsonPropertyName("green_y")]
// public string GreenY { get; set; }
//
// [JsonPropertyName("blue_x")]
// public string BlueX { get; set; }
//
// [JsonPropertyName("blue_y")]
// public string BlueY { get; set; }
//
// [JsonPropertyName("white_point_x")]
// public string WhitePointX { get; set; }
//
// [JsonPropertyName("white_point_y")]
// public string WhitePointY { get; set; }
//
// [JsonPropertyName("min_luminance")]
// public string MinLuminance { get; set; }
//
// [JsonPropertyName("max_luminance")]
// public string MaxLuminance { get; set; }
//
// [JsonPropertyName("max_content")]
// public int MaxContent { get; set; }
//
// [JsonPropertyName("max_average")]
// public int MaxAverage { get; set; }
// }
//
// /// <summary>
// /// Represents a frame.
// /// </summary>
// public class Frame
// {
// [JsonPropertyName("pix_fmt")]
// public string PixFmt { get; set; }
//
// [JsonPropertyName("color_space")]
// public string ColorSpace { get; set; }
//
// [JsonPropertyName("color_primaries")]
// public string ColorPrimaries { get; set; }
//
// [JsonPropertyName("color_transfer")]
// public string ColorTransfer { get; set; }
//
// [JsonPropertyName("side_data_list")]
// public List<SideData> SideDataList { get; set; }
// }
//
// /// <summary>
// /// Represents the root object of the JSON.
// /// </summary>
// public class RootObject
// {
// [JsonPropertyName("frames")]
// public List<Frame> Frames { get; set; }
// }
// }

View File

@@ -0,0 +1,157 @@
using System.IO;
namespace FileFlows.VideoNodes.Helpers;
/// <summary>
/// Helper class for comskip
/// </summary>
public class ComskipHelper
{
/// <summary>
/// Runs comksip against a video file and creates a EDL file
/// </summary>
/// <param name="args">the NodeParameters</param>
/// <param name="file">the video file to run comskip against</param>
/// <returns>the ELD filename</returns>
public static Result<string> RunComskip(NodeParameters args, string file)
{
if (System.IO.File.Exists(file) == false)
return Result<string>.Fail("File does not exist");
try
{
var csIni = GetComskipIniFile(args, file);
var comskip = args.GetToolPath("comskip")?.EmptyAsNull() ?? (OperatingSystem.IsWindows()
? "comskip.exe"
: "comskip");
string edl = FileHelper.ChangeExtension(file, "txt");
var result = args.Execute(new()
{
Command = comskip,
ArgumentList = new[]
{
"--ini=" + csIni,
file
}
});
if (File.Exists(edl) == false)
return Result<string>.Fail("Failed to create EDL file");
string edlContent = File.ReadAllText(edl);
args.Logger?.ILog(new string('-', 30) + "\n" + edlContent + new string('-', 30));
return edl;
}
catch (Exception ex)
{
return Result<string>.Fail("Failed running comskip: " + ex.Message);
}
}
private static string GetComskipIniFile(NodeParameters args, string file)
{
var csIni = args.GetToolPath("comskip.ini");
if (string.IsNullOrWhiteSpace(csIni) == false)
{
if (csIni.IndexOf("\n") > 0)
{
// csini is the contents of the csini, make a file
args.Logger?.ILog("Using comskip.ini variable contents");
var tempFile = FileHelper.Combine(args.TempPath, "comskip.ini");
File.WriteAllText(tempFile, csIni);
return tempFile;
}
args.Logger?.ILog("Using comskip.ini file variable");
return csIni;
}
var path = FileHelper.Combine(FileHelper.GetDirectory(file), "comskip.ini");
if (File.Exists(path))
{
args.Logger?.ILog("Using comskip.ini found with input file.");
return path;
}
args.Logger?.ILog("Using default comskip.ini file");
csIni = FileHelper.Combine(args.TempPath, "comskip.ini");
// create the default ini file
File.WriteAllText(csIni, @"detect_method=111 ;1=black frame, 2=logo, 4=scene change, 8=fuzzy logic, 16=closed captions, 32=aspect ration, 64=silence, 128=cutscenes, 255=all
validate_silence=1 ; Default, set to 0 to force using this clues if selected above.
validate_uniform=1 ; Default, set to 0 to force using this clues (like pure white frames) if blackframe is selected above.
validate_scenechange=1 ; Default, set to 0 to force using this clues if selected above.
verbose=10 ;show a lot of extra info, level 5 is also OK, set to 0 to disable
max_brightness=60 ;frame not black if any pixels checked are greater than this (scale 0 to 255)
test_brightness=40 ;frame not pure black if any pixels checked are greater than this, will check average brightness (scale 0 to 255)
max_avg_brightness=25 ;maximum average brightness for a dim frame to be considered black (scale 0 to 255) 0 means autosetting
max_commercialbreak=600 ;maximum length in seconds to consider a segment a commercial break
min_commercialbreak=25 ;minimum length in seconds to consider a segment a commercial break
max_commercial_size=125 ;maximum time in seconds for a single commercial or multiple commercials if no breaks in between
min_commercial_size=4 ;mimimum time in seconds for a single commercial
min_show_segment_length=125 ; any segment longer than this will be scored towards show.
non_uniformity=500 ; Set to 0 to disable cutpoints based on uniform frames
max_volume=500 ; any frame with sound volume larger than this will not be regarded as black frame
min_silence=12 ; Any deep silence longer than this amount of frames is a possible cutpoint
ticker_tape=0 ; Amount of pixels from bottom to ignore in all processing
logo_at_bottom=0 ; Set to 1 to search only for logo at the lower half of the video, do not combine with subtitle setting
punish=0 ; Compare to average for sum of 1=brightness, 2=uniform 4=volume, 8=silence, 16=schange, set to 0 to disable
punish_threshold=1.3 ; Multiply when amount is above average * punish_threshold
punish_modifier=2 ; When above average * threshold multiply score by this value
intelligent_brightness=0 ; Set to 1 to use a USA specific algorithm to tune some of the settings, not adviced outside the USA
logo_percentile=0.92 ; if more then this amount of logo is found then logo detection will be disabled
logo_threshold=0.75
punish_no_logo=1 ; Default, set to 0 to avoid show segments without logo to be scored towards commercial
aggressive_logo_rejection=0
connect_blocks_with_logo=1 ; set to 1 if you want successive blocks with logo on the transition to be regarded as connected, set to 0 to disable
logo_filter=0 ; set the size of the filter to apply to bad logo detection, 4 seems to be a good value.
cut_on_ar_change=1 ; set to 1 if you want to cut also on aspect ratio changes when logo is present, set to 2 to force cuts on aspect ratio changes. set to 0 to disable
delete_show_after_last_commercial=0 ; set to 1 if you want to delete the last block if its a show and after a commercial
delete_show_before_or_after_current=0 ; set to 1 if you want to delete the previous and the next show in the recording, this can lead to the deletion of trailers of next show
delete_block_after_commercial=0 ;set to max size of block in seconds to be discarded, set to 0 to disable
remove_before=0 ; amount of seconds of show to be removed before ALL commercials
remove_after=0 ; amount of seconds of show to be removed after ALL commercials
shrink_logo=5 ; Reduce the duration of the logo with this amount of seconds
after_logo=0 ; set to number of seconds after logo disappears comskip should start to search for silence to insert an additional cutpoint
padding=0
ms_audio_delay=5
volume_slip=20
max_repair_size=200 ; Will repair maximum 200 missing MPEG frames in the timeline, set to 0 to disable repairing for players that don't use PTS.
disable_heuristics=4 bit pattern for disabling heuristics, adding 1 disables heristics 1, adding 2 disables heristics 2, adding 4 disables heristics 3, 255 disables all heuristics
delete_logo_file=0 ; set to 1 if you want comskip to tidy up after finishing
output_framearray=0 ; create a big excel file for detailed analysis, set to 0 to disable
output_videoredo=0
output_womble=0
output_mls=0 ; set to 1 if you want MPeg Video Wizard bookmark file output
output_cuttermaran=0
output_mpeg2schnitt=0
output_mpgtx=0
output_dvrcut=0
output_zoomplayer_chapter=0
output_zoomplayer_cutlist=0
output_edl=1
output_edlx=0
output_vcf=0
output_bsplayer=0
output_btv=0 ; set to 1 if you want Beyond TV chapter cutlist output
output_projectx=0 ; set to 1 if you want ProjectX cutlist output (Xcl)
output_avisynth=0
output_vdr=0 ; set to 1 if you want XBMC to skipping commercials
output_demux=0 ; set to 1 if you want comskip to demux the mpeg file while scanning
sage_framenumber_bug=0
sage_minute_bug=0
live_tv=0 ; set to 1 if you use parallelprocessing and need the output while recording
live_tv_retries=4 ; change to 16 when using live_tv in BTV, used for mpeg PS and TS
dvrms_live_tv_retries=300 ; only used for dvr_ms
standoff=0 ; change to 8000000 when using live_tv in BTV
cuttermaran_options=""cut=\""true\"" unattended=\""true\"" muxResult=\""false\"" snapToCutPoints=\""true\"" closeApp=\""true\""""
mpeg2schnitt_options=""mpeg2schnitt.exe /S /E /R25 /Z %2 %1""
avisynth_options=""LoadPlugin(\""MPEG2Dec3.dll\"") \nMPEG2Source(\""%s\"")\n""
dvrcut_options=""dvrcut \""%s.dvr-ms\"" \""%s_clean.dvr-ms\"" ""
windowtitle=""Comskip - %s""");
return csIni;
}
}

View File

@@ -12,6 +12,6 @@ public class GeneralHelper
/// <returns>True if the input is a regular expression, otherwise false.</returns>
public static bool IsRegex(string input)
{
return new[] { "?", "|", "^", "$" }.Any(ch => input.Contains(ch));
return new[] { "?", "|", "^", "$", "*" }.Any(ch => input.Contains(ch));
}
}

View File

@@ -57,9 +57,27 @@ public class FfmpegBuilder_AudioConverterTests: TestBase
Language = "en",
Codec = "AAC",
Channels = 5.1f
},
new AudioStream
{
Index = 6,
TypeIndex = 4,
IndexString = "0:a:4",
Language = "en",
Codec = "eac3",
Channels = 5.1f
},
new AudioStream
{
Index = 7,
TypeIndex = 5,
IndexString = "0:a:5",
Language = "en",
Codec = "ac3",
Channels = 5.1f
}
};
args = new NodeParameters(file, logger, false, string.Empty, null);
args = new NodeParameters(file, logger, false, string.Empty, new LocalFileService());
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
args.Parameters.Add("VideoInfo", vii);
@@ -77,8 +95,8 @@ public class FfmpegBuilder_AudioConverterTests: TestBase
FfmpegBuilderAudioConverter ffAudioConvert = new();
ffAudioConvert.Codec = "aac";
ffAudioConvert.Pattern = "fre";
ffAudioConvert.UseLanguageCode = true;
ffAudioConvert.Pattern = "fre";;
ffAudioConvert.Field = FfmpegBuilderAudioConverter.FIELD_LANGUAGE;
ffAudioConvert.PreExecute(args);
int result = ffAudioConvert.Execute(args);
Assert.AreEqual(2, result);
@@ -92,7 +110,7 @@ public class FfmpegBuilder_AudioConverterTests: TestBase
FfmpegBuilderAudioConverter ffAudioConvert = new();
ffAudioConvert.Codec = "ac3";
ffAudioConvert.Pattern = "fre";
ffAudioConvert.UseLanguageCode = true;
ffAudioConvert.Field = FfmpegBuilderAudioConverter.FIELD_LANGUAGE;
ffAudioConvert.PreExecute(args);
int result = ffAudioConvert.Execute(args);
Assert.AreEqual(1, result);
@@ -136,11 +154,11 @@ public class FfmpegBuilder_AudioConverterTests: TestBase
ffAudioConvert.PreExecute(args);
int result = ffAudioConvert.Execute(args);
Assert.AreEqual(1, result);
var model = args.Variables["FFMPEG_BUILDER_MODEL"] as FfmpegModel;
var model = args.Variables[FfmpegBuilderNode.MODEL_KEY] as FfmpegModel;
Assert.IsNotNull(model);
var audio = model.AudioStreams[2];
Assert.AreEqual("-map", audio.EncodingParameters[0]);
Assert.AreEqual("0:a:2", audio.EncodingParameters[1]);
Assert.AreEqual("0:a:{sourceTypeIndex}", audio.EncodingParameters[1]);
Assert.AreEqual("-c:a:{index}", audio.EncodingParameters[2]);
Assert.AreEqual("ac3", audio.EncodingParameters[3]);
Assert.AreEqual("-ac:a:{index}", audio.EncodingParameters[4]);
@@ -148,91 +166,6 @@ public class FfmpegBuilder_AudioConverterTests: TestBase
Assert.AreEqual("-b:a:{index}", audio.EncodingParameters[6]);
Assert.AreEqual("384k", audio.EncodingParameters[7]);
}
//[TestMethod]
//public void FfmpegBuilder_AudioConverter_AacSameAsSource()
//{
// Prepare();
// FfmpegBuilderAudioConverter ffAudioConvert = new();
// ffAudioConvert.Codec = "aac";
// ffAudioConvert.Channels = 0;
// var best = ffAudioConvert.GetBestAudioTrack(args, vii.AudioStreams);
// Assert.IsNotNull(best);
// Assert.AreEqual(5, best.Index);
// Assert.AreEqual("AAC", best.Codec);
// Assert.AreEqual(5.1f, best.Channels);
//}
//[TestMethod]
//public void FfmpegBuilder_AudioConverter_Ac3SameAsSource()
//{
// Prepare();
// FfmpegBuilderAudioConverter ffAudioConvert = new();
// ffAudioConvert.Codec = "ac3";
// ffAudioConvert.Channels = 0;
// ffAudioConvert.Index = 1;
// var best = ffAudioConvert.GetBestAudioTrack(args, vii.AudioStreams);
// Assert.IsNotNull(best);
// Assert.AreEqual(2, best.Index);
// Assert.AreEqual("AC3", best.Codec);
// Assert.AreEqual(5.1f, best.Channels);
//}
//[TestMethod]
//public void FfmpegBuilder_AudioConverter_DtsSame()
//{
// Prepare();
// FfmpegBuilderAudioConverter ffAudioConvert = new();
// ffAudioConvert.Codec = "dts";
// ffAudioConvert.Channels = 0;
// var best = ffAudioConvert.GetBestAudioTrack(args, vii.AudioStreams);
// Assert.IsNotNull(best);
// Assert.AreEqual(2, best.Index);
// Assert.AreEqual("AC3", best.Codec);
// Assert.AreEqual(5.1f, best.Channels);
//}
//[TestMethod]
//public void FfmpegBuilder_AudioConverter_DtsStereo()
//{
// Prepare();
// FfmpegBuilderAudioConverter ffAudioConvert = new();
// ffAudioConvert.Codec = "dts";
// ffAudioConvert.Channels = 2;
// var best = ffAudioConvert.GetBestAudioTrack(args, vii.AudioStreams);
// Assert.IsNotNull(best);
// Assert.AreEqual(3, best.Index);
// Assert.AreEqual("AAC", best.Codec);
// Assert.AreEqual(2f, best.Channels);
//}
//[TestMethod]
//public void FfmpegBuilder_AudioConverter_DtsMono()
//{
// Prepare();
// FfmpegBuilderAudioConverter ffAudioConvert = new();
// ffAudioConvert.Codec = "dts";
// ffAudioConvert.Channels = 1;
// var best = ffAudioConvert.GetBestAudioTrack(args, vii.AudioStreams);
// Assert.IsNotNull(best);
// Assert.AreEqual(3, best.Index);
// Assert.AreEqual("AAC", best.Codec);
// Assert.AreEqual(2f, best.Channels);
//}
[TestMethod]
public void FfmpegBuilder_AudioConverter_Opus_All()
@@ -241,7 +174,7 @@ public class FfmpegBuilder_AudioConverterTests: TestBase
var logger = new TestLogger();
var vi = new VideoInfoHelper(FfmpegPath, logger);
var vii = vi.Read(file);
var args = new NodeParameters(file, logger, false, string.Empty, null);
var args = new NodeParameters(file, logger, false, string.Empty, new LocalFileService());
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
args.Parameters.Add("VideoInfo", vii);
@@ -267,6 +200,32 @@ public class FfmpegBuilder_AudioConverterTests: TestBase
var newInfo = vi.Read(args.WorkingFile);
Assert.AreEqual("opus", newInfo.AudioStreams[0].Codec);
}
[TestMethod]
public void FfmpegBuilder_AudioConverter_Ac3ToOpus()
{
Prepare();
FfmpegBuilderAudioConverter ffAudioConvert = new();
ffAudioConvert.Codec = "opus";
ffAudioConvert.Field = FfmpegBuilderAudioConverter.FIELD_CODEC;
ffAudioConvert.Pattern = "ac3";
ffAudioConvert.PreExecute(args);
int result = ffAudioConvert.Execute(args);
Assert.AreEqual(1, result);
var model = args.Variables[FfmpegBuilderNode.MODEL_KEY] as FfmpegModel;
Assert.IsNotNull(model);
var firstAc3 = model.AudioStreams[0];
var eac3 = model.AudioStreams[4];
var secondAc3 = model.AudioStreams[5];
Assert.AreEqual(firstAc3.ToString(), "0 / en / opus / 5.1 / Changed");
Assert.AreEqual(secondAc3.ToString(), "5 / en / opus / 5.1 / Changed");
Assert.AreEqual(eac3.ToString(), "4 / en / eac3 / 5.1");
}
}
#endif

View File

@@ -0,0 +1,51 @@
// #if(DEBUG)
//
// using FileFlows.VideoNodes.FfmpegBuilderNodes;
// using Microsoft.VisualStudio.TestTools.UnitTesting;
// using VideoNodes.Tests;
// using System.IO;
//
// namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests;
//
// [TestClass]
// public class FfmpegBuilder_HdrTests: TestBase
// {
// [TestMethod]
// public void Encode()
// {
// string file = Path.Combine(TestPath, "HDR10Plus_PA_DTSX.mkv");
// if (File.Exists(file) == false)
// throw new FileNotFoundException(file);
//
// var logger = new TestLogger();
// string ffmpeg = FfmpegPath;
// var vi = new VideoInfoHelper(ffmpeg, logger);
// var vii = vi.Read(file);
// var args = new NodeParameters(file, logger, false, string.Empty, new LocalFileService());
// args.GetToolPathActual = (string tool) =>
// {
// if (tool.ToLowerInvariant() == "ffprobe")
// return "/usr/local/bin/ffprobe";
// return ffmpeg;
// };
// args.TempPath = TempPath;
// args.Parameters.Add("VideoInfo", vii);
//
//
// FfmpegBuilderStart ffStart = new();
// ffStart.PreExecute(args);
// Assert.AreEqual(1, ffStart.Execute(args));
//
// FfmpegBuilderHdr10 hdr10 = new();
// hdr10.PreExecute(args);
// hdr10.Execute(args);
//
// FfmpegBuilderExecutor ffExecutor = new();
// ffExecutor.PreExecute(args);
// int result = ffExecutor.Execute(args);
// string log = logger.ToString();
// Assert.AreEqual(1, result);
// }
// }
//
// #endif

View File

@@ -74,6 +74,7 @@ public abstract class TestBase
protected string TestFile_DefaultIsForcedSub => Path.Combine(TestPath, "sub-default-is-forced.mkv");
protected string TestFile_TwoPassNegInifinity => Path.Combine(TestPath, "audio_normal_neg_infinity.mkv");
protected string TestFile_4k_h264mov => Path.Combine(TestPath, "4k_h264.mov");
protected string TestFile_4k_h264mkv => Path.Combine(TestPath, "4k_h264.mkv");
protected string TestFile_50_mbps_hd_h264 => Path.Combine(TestPath, "50-mbps-hd-h264.mkv");
protected string TestFile_120_mbps_4k_uhd_hevc_10bit => Path.Combine(TestPath, "120-mbps-4k-uhd-hevc-10bit.mkv");

View File

@@ -411,7 +411,7 @@ public class VideoInfoHelper
if (rgxTitle.IsMatch(info))
sub.Title = rgxTitle.Match(info).Value.Trim();
sub.Forced = info.ToLower().Contains("forced");
sub.Forced = info.ToLower().Contains("(forced)");
return sub;
}

View File

@@ -61,6 +61,10 @@
"Outputs": {
"1": "Commercials removed, saved to temporary file",
"2": "No commercials detected"
},
"Fields": {
"RunComskipIfNoEdl": "Run Comskip",
"RunComskipIfNoEdl-Help":"Run comskip against the file if no comskip (EDL) file is found."
}
},
"VideoFile": {
@@ -181,12 +185,12 @@
"Bitrate-Help": "Bitrate of the audio track",
"Codec": "Codec",
"Codec-Help": "The codec to use to encode the audio",
"Field": "Field",
"Field-Help": "The field to match the pattern against. Leave the pattern empty to match anything with no value set.",
"Pattern": "Pattern",
"Pattern-Help": "A regular expression to match against, eg \"commentary\" to match commentary tracks",
"Pattern-Help": "A string or regular expression to match against, eg \"commentary\" to match commentary exactly or \".*commentary.*\" to match commentary anywhere in the string",
"NotMatching": "Not Matching",
"NotMatching-Help": "If audio tracks NOT matching the pattern should be converted",
"UseLanguageCode": "Use Language Code",
"UseLanguageCode-Help": "If the language code of the track should be used instead of the title"
"NotMatching-Help": "If audio tracks NOT matching the pattern should be converted"
}
},
"FfmpegBuilderAudioNormalization": {
@@ -287,6 +291,10 @@
"Outputs": {
"1": "Commercials chapters created, added to FFMPEG Builder",
"2": "No commercials detected"
},
"Fields": {
"RunComskipIfNoEdl": "Run Comskip",
"RunComskipIfNoEdl-Help":"Run comskip against the file if no comskip (EDL) file is found."
}
},
"FfmpegBuilderCustomParameters": {

View File

@@ -1,197 +1,224 @@
namespace FileFlows.VideoNodes
using FileFlows.VideoNodes.Helpers;
namespace FileFlows.VideoNodes;
public class ComskipRemoveAds: EncodingNode
{
using FileFlows.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
/// <inheritdoc />
public override int Outputs => 2;
/// <inheritdoc />
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/comskip-remove-ads";
public class ComskipRemoveAds: EncodingNode
/// <summary>
/// Gets or sets if comskip should be run if no EDL file is found
/// </summary>
[Boolean(1)]
public bool RunComskipIfNoEdl { get; set; }
private string GetEdlFile(NodeParameters args)
{
public override int Outputs => 2;
private string GetEdlFile(NodeParameters args)
string edlFile = args.WorkingFile.Substring(0, args.WorkingFile.LastIndexOf(".", StringComparison.Ordinal) + 1) +
"edl";
if (args.FileService.FileIsLocal(edlFile))
{
string edlFile = args.WorkingFile.Substring(0, args.WorkingFile.LastIndexOf(".", StringComparison.Ordinal) + 1) +
"edl";
if (args.FileService.FileIsLocal(edlFile))
{
if (System.IO.File.Exists(edlFile))
return edlFile;
return args.WorkingFile.Substring(0,
args.WorkingFile.LastIndexOf(".", StringComparison.Ordinal) + 1) + "edl";
}
if (args.FileService.FileExists(edlFile).Is(true) == false)
{
edlFile = args.WorkingFile.Substring(0,
args.WorkingFile.LastIndexOf(".", StringComparison.Ordinal) + 1) + "edl";
if (args.FileService.FileExists(edlFile).Is(true) == false)
return string.Empty;
}
if (System.IO.File.Exists(edlFile))
return edlFile;
var result = args.FileService.GetLocalPath(edlFile);
if (result.IsFailed)
{
args.Logger.ELog("Failed to download edl file locally: " + result.Error);
return null;
}
return result;
return args.WorkingFile.Substring(0,
args.WorkingFile.LastIndexOf(".", StringComparison.Ordinal) + 1) + "edl";
}
if (args.FileService.FileExists(edlFile).Is(true) == false)
{
edlFile = args.WorkingFile.Substring(0,
args.WorkingFile.LastIndexOf(".", StringComparison.Ordinal) + 1) + "edl";
if (args.FileService.FileExists(edlFile).Is(true) == false)
return string.Empty;
}
var result = args.FileService.GetLocalPath(edlFile);
if (result.IsFailed)
{
args.Logger.ELog("Failed to download edl file locally: " + result.Error);
return null;
}
public override int Execute(NodeParameters args)
return result;
}
public override int Execute(NodeParameters args)
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
var localFile = args.FileService.GetLocalPath(args.WorkingFile);
if (localFile.Failed(out string error))
{
VideoInfo videoInfo = GetVideoInfo(args);
if (videoInfo == null)
return -1;
float totalTime = (float)videoInfo.VideoStreams[0].Duration.TotalSeconds;
args.Logger?.WLog("Failed to get file locally: " + error);
return -1;
}
if (localFile != args.WorkingFile)
args.SetWorkingFile(localFile);
float totalTime = (float)videoInfo.VideoStreams[0].Duration.TotalSeconds;
string edlFile = GetEdlFile(args);
string edlFile = GetEdlFile(args);
if (string.IsNullOrWhiteSpace(edlFile) || System.IO.File.Exists(edlFile) == false)
if (string.IsNullOrWhiteSpace(edlFile) || System.IO.File.Exists(edlFile) == false)
{
if (RunComskipIfNoEdl)
{
args.Logger?.ILog("No edl file found, attempting to run comskip");
var runComskipResult = ComskipHelper.RunComskip(args, localFile);
if (runComskipResult.Failed(out string csError))
{
args.Logger?.ELog(csError);
return 2;
}
edlFile = runComskipResult;
}
else
{
args.Logger?.ILog("No EDL file found for file");
return 2;
}
}
string text = System.IO.File.ReadAllText(edlFile) ?? string.Empty;
float last = -1;
List<BreakPoint> breakPoints = new List<BreakPoint>();
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;
string text = System.IO.File.ReadAllText(edlFile) ?? string.Empty;
float last = -1;
List<BreakPoint> breakPoints = new List<BreakPoint>();
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;
if (start < last)
continue;
BreakPoint bp = new BreakPoint();
bp.Start = start;
bp.End = end;
breakPoints.Add(bp);
}
BreakPoint bp = new BreakPoint();
bp.Start = start;
bp.End = end;
breakPoints.Add(bp);
}
if(breakPoints.Any() == false)
{
args.Logger?.ILog("No break points detected in file");
return 2;
}
if(breakPoints.Any() == false)
{
args.Logger?.ILog("No break points detected in file");
return 2;
}
List<string> segments = new List<string>();
List<string> segments = new List<string>();
float segStart = 0;
string extension = args.WorkingFile.Substring(args.WorkingFile.LastIndexOf(".") + 1);
string segmentPrefix = System.IO.Path.Combine(args.TempPath, Guid.NewGuid().ToString())+"_";
int count = 0;
List<string> segmentsInfo = new List<string>();
foreach (BreakPoint bp in breakPoints)
{
if (EncodeSegment(segStart, bp.Start) == false)
{
args.Logger?.ELog("Failed to create segment: " + count);
return 2;
}
segStart = bp.End;
}
// add the end
if (EncodeSegment(segStart, totalTime) == false)
float segStart = 0;
string extension = args.WorkingFile[(args.WorkingFile.LastIndexOf(".", StringComparison.Ordinal) + 1)..];
string segmentPrefix = System.IO.Path.Combine(args.TempPath, Guid.NewGuid().ToString())+"_";
int count = 0;
List<string> segmentsInfo = new List<string>();
foreach (BreakPoint bp in breakPoints)
{
if (EncodeSegment(segStart, bp.Start) == false)
{
args.Logger?.ELog("Failed to create segment: " + count);
return 2;
}
segStart = bp.End;
}
// add the end
if (EncodeSegment(segStart, totalTime) == false)
{
args.Logger?.ELog("Failed to create segment: " + count);
return 2;
}
// stitch file back together
string concatList = segmentPrefix + "concatlist.txt";
string concatListContents = String.Join(Environment.NewLine, segments.Select(x => $"file '{x}'"));
System.IO.File.WriteAllText(concatList, concatListContents);
// stitch file back together
string concatList = segmentPrefix + "concatlist.txt";
string concatListContents = String.Join(Environment.NewLine, segments.Select(x => $"file '{x}'"));
System.IO.File.WriteAllText(concatList, concatListContents);
args.Logger?.ILog("====================================================");
foreach (var str in segmentsInfo)
args.Logger?.ILog(str);
args.Logger?.ILog("Concat list:");
foreach (var line in concatListContents.Split(new String[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries))
args.Logger?.ILog("====================================================");
foreach (var str in segmentsInfo)
args.Logger?.ILog(str);
args.Logger?.ILog("Concat list:");
foreach (var line in concatListContents.Split(new String[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries))
{
args.Logger?.ILog(line);
}
args.Logger?.ILog("====================================================");
List<string> ffArgs = new List<string>
{
"-f", "concat",
"-safe", "0",
"-i", concatList,
"-c", "copy"
};
bool concatResult = Encode(args, FFMPEG, ffArgs, dontAddInputFile: true, extension: extension);
foreach(string segment in segments.Union(new[] { concatList }))
{
try
{
args.Logger?.ILog(line);
System.IO.File.Delete(segment);
}
args.Logger?.ILog("====================================================");
catch (Exception) { }
}
if (concatResult)
return 1;
args.Logger?.ELog("Failed to stitch file back together");
return 2;
bool EncodeSegment(float start, float end)
{
string segment = segmentPrefix + (++count).ToString("D2") + "." + extension;
float duration = end - start;
if (duration < 30)
{
args.Logger?.ILog("Segment is less than 30 seconds, skipping");
return true;
}
List<string> ffArgs = new List<string>
{
"-f", "concat",
"-safe", "0",
"-i", concatList,
"-ss", start.ToString(),
"-t", duration.ToString(),
"-c", "copy"
};
bool concatResult = Encode(args, FFMPEG, ffArgs, dontAddInputFile: true, extension: extension);
foreach(string segment in segments.Union(new[] { concatList }))
if (Encode(args, FFMPEG, ffArgs, outputFile: segment, updateWorkingFile: false))
{
try
{
System.IO.File.Delete(segment);
}
catch (Exception) { }
segments.Add(segment);
segmentsInfo.Add(DebugString(start, end));
return true;
}
if (concatResult)
return 1;
args.Logger?.ELog("Failed to stitch file back together");
return 2;
bool EncodeSegment(float start, float end)
{
string segment = segmentPrefix + (++count).ToString("D2") + "." + extension;
float duration = end - start;
if (duration < 30)
{
args.Logger?.ILog("Segment is less than 30 seconds, skipping");
return true;
}
List<string> ffArgs = new List<string>
{
"-ss", start.ToString(),
"-t", duration.ToString(),
"-c", "copy"
};
if (Encode(args, FFMPEG, ffArgs, outputFile: segment, updateWorkingFile: false))
{
segments.Add(segment);
segmentsInfo.Add(DebugString(start, end));
return true;
}
return false;
}
}
private string DebugString(float start, float end)
{
var tsStart = new TimeSpan((long)start * TimeSpan.TicksPerSecond);
var tsEnd= new TimeSpan((long)end * TimeSpan.TicksPerSecond);
return "Segment: " + tsStart.ToString(@"mm\:ss") + " to " + tsEnd.ToString(@"mm\:ss");
}
private class BreakPoint
{
public float Start { get; set; }
public float End { get; set; }
public float Duration => End - Start;
return false;
}
}
private string DebugString(float start, float end)
{
var tsStart = new TimeSpan((long)start * TimeSpan.TicksPerSecond);
var tsEnd= new TimeSpan((long)end * TimeSpan.TicksPerSecond);
return "Segment: " + tsStart.ToString(@"mm\:ss") + " to " + tsEnd.ToString(@"mm\:ss");
}
private class BreakPoint
{
public float Start { get; set; }
public float End { get; set; }
public float Duration => End - Start;
}
}

View File

@@ -57,6 +57,24 @@ namespace FileFlows.VideoNodes
}
return fileInfo.FullName;
}
/// <summary>
/// Gets the FFprobe location
/// </summary>
/// <param name="args">the node parameters</param>
/// <returns>the FFprobe location</returns>
protected Result<string> GetFFprobe(NodeParameters args)
{
string ffmpeg = args.GetToolPath("FFprobe");
if (string.IsNullOrEmpty(ffmpeg))
return Result<string>.Fail("FFprobe tool not found.");
var fileInfo = new System.IO.FileInfo(ffmpeg);
if (fileInfo.Exists == false)
return Result<string>.Fail("FFprobe tool configured by ffmpeg file does not exist.");
return fileInfo.FullName;
}
// protected string GetFFMpegPath(NodeParameters args)
// {
// string ffmpeg = args.GetToolPath("FFMpeg");