diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestBlackFrames.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestBlackFrames.cs new file mode 100644 index 0000000..794ecd1 --- /dev/null +++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestBlackFrames.cs @@ -0,0 +1,50 @@ +namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests; + +using System; +using System.Collections.Generic; +using Xunit; + +public class TestBlackFrames +{ + [FactSkipFFmpegTests] + public void TestBlackFrameDetection() + { + var expected = new List(); + expected.AddRange(CreateFrameSequence(2, 3)); + expected.AddRange(CreateFrameSequence(5, 6)); + expected.AddRange(CreateFrameSequence(8, 9.96)); + + var actual = FFmpegWrapper.DetectBlackFrames( + queueFile("rainbow.mp4"), + new TimeRange(0, 10) + ); + + for (var i = 0; i < expected.Count; i++) + { + var (e, a) = (expected[i], actual[i]); + Assert.Equal(e.Percentage, a.Percentage); + Assert.True(Math.Abs(e.Time - a.Time) <= 0.005); + } + } + + private QueuedEpisode queueFile(string path) + { + return new() + { + EpisodeId = Guid.NewGuid(), + Path = "../../../video/" + path + }; + } + + private BlackFrame[] CreateFrameSequence(double start, double end) + { + var frames = new List(); + + for (var i = start; i < end; i += 0.04) + { + frames.Add(new(100, i)); + } + + return frames.ToArray(); + } +} diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/video/rainbow.mp4 b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/video/rainbow.mp4 new file mode 100644 index 0000000..8e01cce Binary files /dev/null and b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/video/rainbow.mp4 differ diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs index ce79fdd..f4789db 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs @@ -77,6 +77,11 @@ public PluginConfiguration() /// public int MaximumEpisodeCreditsDuration { get; set; } = 4; + /// + /// Gets or sets the minimum percentage of a frame that must consist of black pixels before it is considered a black frame. + /// + public int BlackFrameMinimumPercentage { get; set; } = 85; + // ===== Playback settings ===== /// diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/BlackFrame.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/BlackFrame.cs new file mode 100644 index 0000000..df3a957 --- /dev/null +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Data/BlackFrame.cs @@ -0,0 +1,28 @@ +namespace ConfusedPolarBear.Plugin.IntroSkipper; + +/// +/// A frame of video that partially (or entirely) consists of black pixels. +/// +public class BlackFrame +{ + /// + /// Initializes a new instance of the class. + /// + /// Percentage of the frame that is black. + /// Time this frame appears at. + public BlackFrame(int percent, double time) + { + Percentage = percent; + Time = time; + } + + /// + /// Gets or sets the percentage of the frame that is black. + /// + public int Percentage { get; set; } + + /// + /// Gets or sets the time (in seconds) this frame appeared at. + /// + public double Time { get; set; } +} diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs b/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs index 66fc68b..e8ba464 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/FFmpegWrapper.cs @@ -16,16 +16,17 @@ public static class FFmpegWrapper { private static readonly object InvertedIndexCacheLock = new(); - // FFmpeg logs lines similar to the following: - // [silencedetect @ 0x000000000000] silence_start: 12.34 - // [silencedetect @ 0x000000000000] silence_end: 56.123 | silence_duration: 43.783 - /// /// Used with FFmpeg's silencedetect filter to extract the start and end times of silence. /// private static readonly Regex SilenceDetectionExpression = new( "silence_(?start|end): (?