mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2026-02-13 18:08:59 -06:00
FF-1128 - added video is interlaced flow element
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
|
||||
|
||||
/// <summary>
|
||||
/// Flow element that adds a deinterlace filter to the video stream
|
||||
/// </summary>
|
||||
public class FfmpegBuilderDeinterlace : FfmpegBuilderNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of outputs
|
||||
/// </summary>
|
||||
public override int Outputs => 1;
|
||||
|
||||
/// <summary>
|
||||
/// Get the help URL
|
||||
/// </summary>
|
||||
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/deinterlace";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mode for the deinterlacing
|
||||
/// </summary>
|
||||
[Select(nameof(ModeOptions), 1)]
|
||||
[DefaultValue(1)]
|
||||
public int Mode { get; set; }
|
||||
|
||||
private static List<ListOption> _ModeOptions;
|
||||
/// <summary>
|
||||
/// Gets the mode options
|
||||
/// </summary>
|
||||
public static List<ListOption> ModeOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_ModeOptions == null)
|
||||
{
|
||||
_ModeOptions = new List<ListOption>
|
||||
{
|
||||
new () { Label = "Send Frame", Value = 0 },
|
||||
new () { Label = "Send Field", Value = 1 },
|
||||
new () { Label = "Send Frame No Spatial", Value = 2 },
|
||||
new () { Label = "Send Field No Spatial", Value = 3}
|
||||
};
|
||||
}
|
||||
return _ModeOptions;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the flow element
|
||||
/// </summary>
|
||||
/// <param name="args">the parameters</param>
|
||||
/// <returns>the output to call next</returns>
|
||||
public override int Execute(NodeParameters args)
|
||||
{
|
||||
var videoInfo = GetVideoInfo(args);
|
||||
if (videoInfo == null || videoInfo.VideoStreams?.Any() != true)
|
||||
return -1;
|
||||
|
||||
var vidStream = Model.VideoStreams?.FirstOrDefault(x => x.Deleted == false);
|
||||
if (vidStream == null)
|
||||
{
|
||||
args.Logger.ILog("No video stream found");
|
||||
return 2;
|
||||
}
|
||||
|
||||
vidStream.Filter.Add($"yadif={Mode}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -232,10 +232,10 @@ public class CanUseHardwareEncoding:Node
|
||||
if (DisabledByVariables(args, encodingParams))
|
||||
return false;
|
||||
|
||||
string ffmpeg = args.GetToolPath("FFMpeg");
|
||||
string ffmpeg = args.GetToolPath("FFmpeg");
|
||||
if (string.IsNullOrEmpty(ffmpeg))
|
||||
{
|
||||
args.Logger.ELog("FFMpeg tool not found.");
|
||||
args.Logger.ELog("FFmpeg tool not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
namespace FileFlows.VideoNodes;
|
||||
|
||||
using System.Linq;
|
||||
using System.ComponentModel;
|
||||
using FileFlows.Plugin;
|
||||
using FileFlows.Plugin.Attributes;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Flow element to test if a video has a stream
|
||||
/// </summary>
|
||||
public class VideoHasStream : VideoNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of inputs
|
||||
/// </summary>
|
||||
public override int Inputs => 1;
|
||||
/// <summary>
|
||||
/// Gets the number of outputs
|
||||
/// </summary>
|
||||
public override int Outputs => 2;
|
||||
/// <summary>
|
||||
/// Gets the type of flow element
|
||||
/// </summary>
|
||||
public override FlowElementType Type => FlowElementType.Logic;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the help URL
|
||||
/// </summary>
|
||||
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/logical-nodes/video-has-stream";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of stream to check for
|
||||
/// </summary>
|
||||
[Select(nameof(StreamTypeOptions), 1)]
|
||||
public string Stream { get; set; }
|
||||
|
||||
private static List<ListOption> _StreamTypeOptions;
|
||||
/// <summary>
|
||||
/// Gets the types of streams available to check for
|
||||
/// </summary>
|
||||
public static List<ListOption> StreamTypeOptions
|
||||
{
|
||||
get
|
||||
@@ -35,20 +49,37 @@ public class VideoHasStream : VideoNode
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title to look for
|
||||
/// </summary>
|
||||
[TextVariable(2)]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec to look for
|
||||
/// </summary>
|
||||
[TextVariable(3)]
|
||||
public string Codec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the language to look for
|
||||
/// </summary>
|
||||
[ConditionEquals(nameof(Stream), "Video", inverse: true)]
|
||||
[TextVariable(4)]
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of channels to look for
|
||||
/// </summary>
|
||||
[ConditionEquals(nameof(Stream), "Audio")]
|
||||
[NumberFloat(5)]
|
||||
public float Channels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the flow element
|
||||
/// </summary>
|
||||
/// <param name="args">the arguments</param>
|
||||
/// <returns>the output to call next</returns>
|
||||
public override int Execute(NodeParameters args)
|
||||
{
|
||||
var videoInfo = GetVideoInfo(args);
|
||||
@@ -104,6 +135,12 @@ public class VideoHasStream : VideoNode
|
||||
return found ? 1 : 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a value matches the pattern
|
||||
/// </summary>
|
||||
/// <param name="pattern">the pattern</param>
|
||||
/// <param name="value">the value</param>
|
||||
/// <returns>the result</returns>
|
||||
private MatchResult ValueMatch(string pattern, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
@@ -128,10 +165,22 @@ public class VideoHasStream : VideoNode
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match results
|
||||
/// </summary>
|
||||
private enum MatchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// No Match
|
||||
/// </summary>
|
||||
NoMatch = 0,
|
||||
/// <summary>
|
||||
/// Matched
|
||||
/// </summary>
|
||||
Matched = 1,
|
||||
/// <summary>
|
||||
/// Skipped
|
||||
/// </summary>
|
||||
Skipped = 2
|
||||
}
|
||||
}
|
||||
|
||||
127
VideoNodes/LogicalNodes/VideoIsInterlaced.cs
Normal file
127
VideoNodes/LogicalNodes/VideoIsInterlaced.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
namespace FileFlows.VideoNodes;
|
||||
|
||||
/// <summary>
|
||||
/// Flow element to test if a video is interlaced
|
||||
/// </summary>
|
||||
public class VideoIsInterlaced : VideoNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of inputs
|
||||
/// </summary>
|
||||
public override int Inputs => 1;
|
||||
/// <summary>
|
||||
/// Gets the number of outputs
|
||||
/// </summary>
|
||||
public override int Outputs => 2;
|
||||
/// <summary>
|
||||
/// Gets the type of flow element
|
||||
/// </summary>
|
||||
public override FlowElementType Type => FlowElementType.Logic;
|
||||
/// <summary>
|
||||
/// Gets the icon
|
||||
/// </summary>
|
||||
public override string Icon => "fas fa-question";
|
||||
/// <summary>
|
||||
/// Gets the help URL
|
||||
/// </summary>
|
||||
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/logical-nodes/video-is-interlaced";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the threshold
|
||||
/// </summary>
|
||||
[Range(1, 100)]
|
||||
[Slider(1)]
|
||||
[DefaultValue(10)]
|
||||
public int Threshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the flow element
|
||||
/// </summary>
|
||||
/// <param name="args">the arguments</param>
|
||||
/// <returns>the output to call next</returns>
|
||||
public override int Execute(NodeParameters args)
|
||||
{
|
||||
string ffmpeg = args.GetToolPath("FFmpeg");
|
||||
if (string.IsNullOrEmpty(ffmpeg))
|
||||
{
|
||||
args.Logger?.ELog("FFmpeg tool not found.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
var ffOutput = args.Execute(new()
|
||||
{
|
||||
Command = ffmpeg,
|
||||
Silent = true,
|
||||
ArgumentList = new[]
|
||||
{
|
||||
"-hide_banner",
|
||||
"-i", args.WorkingFile,
|
||||
"-vf", "idet",
|
||||
"-f", "null", "-"
|
||||
}
|
||||
});
|
||||
|
||||
if (string.IsNullOrEmpty(ffOutput.StandardOutput))
|
||||
{
|
||||
args.Logger?.ELog("Failed to get standard output from FFmpeg");
|
||||
return 2;
|
||||
}
|
||||
|
||||
bool isInterlaced = IsVideoInterlaced(args.Logger, ffOutput.StandardOutput, Math.Max(Threshold, 1));
|
||||
args.Logger?.ILog("Is interlaced: " + isInterlaced);
|
||||
return isInterlaced ? 1 : 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a video is interlaced based on FFmpeg output.
|
||||
/// </summary>
|
||||
/// <param name="ffmpegOutput">The FFmpeg output as a string.</param>
|
||||
/// <param name="interlaceThreshold">An optional threshold for the number of interlaced frames to consider.</param>
|
||||
/// <returns>True if the video is interlaced, false if it's not.</returns>
|
||||
public static bool IsVideoInterlaced(ILogger logger, string ffmpegOutput, int interlaceThreshold = 10)
|
||||
{
|
||||
// Define regular expressions to match interlaced and progressive frame counts
|
||||
Regex tffRegex = new Regex(@"TFF:[\s]*(\d+)");
|
||||
Regex bffRegex = new Regex(@"BFF:[\s]*(\d+)");
|
||||
Regex progressiveRegex = new Regex(@"Progressive:[\s]*(\d+)");
|
||||
|
||||
int tffCount = 0;
|
||||
int bffCount = 0;
|
||||
int progressiveCount = 0;
|
||||
|
||||
// Use regular expressions to extract counts of interlaced and progressive frames
|
||||
MatchCollection tffMatches = tffRegex.Matches(ffmpegOutput);
|
||||
MatchCollection bffMatches = bffRegex.Matches(ffmpegOutput);
|
||||
MatchCollection progressiveMatches = progressiveRegex.Matches(ffmpegOutput);
|
||||
|
||||
foreach (Match match in tffMatches)
|
||||
{
|
||||
tffCount += int.Parse(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
foreach (Match match in bffMatches)
|
||||
{
|
||||
bffCount += int.Parse(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
foreach (Match match in progressiveMatches)
|
||||
{
|
||||
progressiveCount += int.Parse(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
// Calculate the total count of interlaced frames
|
||||
int totalInterlacedCount = tffCount + bffCount;
|
||||
|
||||
int totalFrames = totalInterlacedCount + progressiveCount;
|
||||
float percent = ((float)totalInterlacedCount) / totalFrames * 100f;
|
||||
|
||||
logger?.ILog("Interlaced threshold percentage: " + interlaceThreshold);
|
||||
logger?.ILog("Total Interlaced Frames: " + totalInterlacedCount);
|
||||
logger?.ILog("Total Progressive Frames: " + progressiveCount);
|
||||
logger?.ILog("Total Frames: " + totalFrames);
|
||||
logger?.ILog("Interlaced Percent: " + percent);
|
||||
|
||||
// Check if the total count of interlaced frames exceeds the threshold
|
||||
return percent >= interlaceThreshold;
|
||||
}
|
||||
}
|
||||
73
VideoNodes/Tests/VideoIsInterlacedTests.cs
Normal file
73
VideoNodes/Tests/VideoIsInterlacedTests.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
#if(DEBUG)
|
||||
using FileFlows.VideoNodes;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace VideoNodes.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class VideoIsInterlacedTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void NotInterlaced()
|
||||
{
|
||||
var logger = new TestLogger();
|
||||
|
||||
string ffmpegOutput = @"
|
||||
NUMBER_OF_BYTES : 3632640
|
||||
_STATISTICS_WRITING_APP: mkvmerge v60.0.0 ('Are We Copies?') 64-bit
|
||||
_STATISTICS_WRITING_DATE_UTC: 2022-12-07 21:09:16
|
||||
DURATION : 00:00:45.438000000
|
||||
encoder : Lavc60.3.100 pcm_s16le
|
||||
frame= 1 fps=0.0 q=-0.0 size=N/A time=00:00:00.63 bitrate=N/A speed=12.1x
|
||||
frame= 196 fps=0.0 q=-0.0 size=N/A time=00:00:08.83 bitrate=N/A speed=15.9x
|
||||
frame= 403 fps=382 q=-0.0 size=N/A time=00:00:17.50 bitrate=N/A speed=16.6x
|
||||
frame= 607 fps=390 q=-0.0 size=N/A time=00:00:25.98 bitrate=N/A speed=16.7x
|
||||
frame= 807 fps=392 q=-0.0 size=N/A time=00:00:34.33 bitrate=N/A speed=16.7x
|
||||
frame= 1012 fps=395 q=-0.0 size=N/A time=00:00:42.84 bitrate=N/A speed=16.7x
|
||||
frame= 1089 fps=397 q=-0.0 Lsize=N/A time=00:00:45.40 bitrate=N/A speed=16.5x
|
||||
video:510kB audio:25542kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown
|
||||
[Parsed_idet_0 @ 0x5618bf1fba40] Repeated Fields: Neither: 1089 Top: 0 Bottom: 0
|
||||
[Parsed_idet_0 @ 0x5618bf1fba40] Single frame detection: TFF: 0 BFF: 0 Progressive: 374 Undetermined: 715
|
||||
[Parsed_idet_0 @ 0x5618bf1fba40] Multi frame detection: TFF: 0 BFF: 0 Progressive: 1089 Undetermined: 0";
|
||||
|
||||
bool interlaced = VideoIsInterlaced.IsVideoInterlaced(logger, ffmpegOutput, 10);
|
||||
var log = logger.ToString();
|
||||
|
||||
Assert.IsFalse(interlaced);
|
||||
Assert.IsTrue(log.Contains("Total Progressive Frames: " + (374 + 1089)));
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void IsInterlaced()
|
||||
{
|
||||
var logger = new TestLogger();
|
||||
|
||||
string ffmpegOutput = @"
|
||||
NUMBER_OF_BYTES : 3632640
|
||||
_STATISTICS_WRITING_APP: mkvmerge v60.0.0 ('Are We Copies?') 64-bit
|
||||
_STATISTICS_WRITING_DATE_UTC: 2022-12-07 21:09:16
|
||||
DURATION : 00:00:45.438000000
|
||||
encoder : Lavc60.3.100 pcm_s16le
|
||||
frame= 1 fps=0.0 q=-0.0 size=N/A time=00:00:00.63 bitrate=N/A speed=12.1x
|
||||
frame= 196 fps=0.0 q=-0.0 size=N/A time=00:00:08.83 bitrate=N/A speed=15.9x
|
||||
frame= 403 fps=382 q=-0.0 size=N/A time=00:00:17.50 bitrate=N/A speed=16.6x
|
||||
frame= 607 fps=390 q=-0.0 size=N/A time=00:00:25.98 bitrate=N/A speed=16.7x
|
||||
frame= 807 fps=392 q=-0.0 size=N/A time=00:00:34.33 bitrate=N/A speed=16.7x
|
||||
frame= 1012 fps=395 q=-0.0 size=N/A time=00:00:42.84 bitrate=N/A speed=16.7x
|
||||
frame= 1089 fps=397 q=-0.0 Lsize=N/A time=00:00:45.40 bitrate=N/A speed=16.5x
|
||||
video:510kB audio:25542kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown
|
||||
[Parsed_idet_0 @ 0x5618bf1fba40] Repeated Fields: Neither: 1089 Top: 0 Bottom: 0
|
||||
[Parsed_idet_0 @ 0x5618bf1fba40] Single frame detection: TFF: 420 BFF: 0 Progressive: 374 Undetermined: 715
|
||||
[Parsed_idet_0 @ 0x5618bf1fba40] Multi frame detection: TFF: 65 BFF: 0 Progressive: 1089 Undetermined: 0";
|
||||
|
||||
bool interlaced = VideoIsInterlaced.IsVideoInterlaced(logger, ffmpegOutput, 10);
|
||||
var log = logger.ToString();
|
||||
|
||||
Assert.IsTrue(interlaced);
|
||||
Assert.IsTrue(log.Contains("Total Progressive Frames: " + (374 + 1089)));
|
||||
Assert.IsTrue(log.Contains("Total Interlaced Frames: " + (420 + 65)));
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -81,6 +81,16 @@
|
||||
},
|
||||
"Description": "Creates an instance of the FFMPEG Builder which can build a FFMPEG argument to then execute with the FFMPEG Executor."
|
||||
},
|
||||
"FfmpegBuilderDeinterlace": {
|
||||
"Label": "FFMPEG Builder: Deinterlace",
|
||||
"Outputs": {
|
||||
"1": "Added deinterlace filter to video stream"
|
||||
},
|
||||
"Fields": {
|
||||
"Mode": "Mode",
|
||||
"Mode-Help": "The deinterlacing method to use. See help for more information."
|
||||
}
|
||||
},
|
||||
"FfmpegBuilderExecutor": {
|
||||
"Label": "FFMPEG Builder: Executor",
|
||||
"Outputs": {
|
||||
@@ -579,6 +589,18 @@
|
||||
"OnlyTextSubtitles-Help": "If only text subtitles should be extracted, all image based subtitles will be skipped."
|
||||
}
|
||||
},
|
||||
"VideoIsInterlaced": {
|
||||
"Label": "Video Is Interlaced",
|
||||
"Description": "Tests if a video file is interlaced",
|
||||
"Outputs": {
|
||||
"1": "Video is interlaced",
|
||||
"2": "Video is not interlaced"
|
||||
},
|
||||
"Fields": {
|
||||
"Threshold": "Threshold",
|
||||
"Threshold-Help": "The percentage of frames detected as interlaced the video must have to be considered interlaced"
|
||||
}
|
||||
},
|
||||
"VideoHasStream": {
|
||||
"Label": "Video Has Stream",
|
||||
"Description": "Tests if a video file contains a track",
|
||||
|
||||
Reference in New Issue
Block a user