Add blackframe API

This commit is contained in:
ConfusedPolarBear 2022-11-01 00:53:56 -05:00
parent af89e5f2b4
commit 8ee400f1f1
5 changed files with 154 additions and 9 deletions

View File

@ -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<BlackFrame>();
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<BlackFrame>();
for (var i = start; i < end; i += 0.04)
{
frames.Add(new(100, i));
}
return frames.ToArray();
}
}

View File

@ -77,6 +77,11 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary> /// </summary>
public int MaximumEpisodeCreditsDuration { get; set; } = 4; public int MaximumEpisodeCreditsDuration { get; set; } = 4;
/// <summary>
/// Gets or sets the minimum percentage of a frame that must consist of black pixels before it is considered a black frame.
/// </summary>
public int BlackFrameMinimumPercentage { get; set; } = 85;
// ===== Playback settings ===== // ===== Playback settings =====
/// <summary> /// <summary>

View File

@ -0,0 +1,28 @@
namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// <summary>
/// A frame of video that partially (or entirely) consists of black pixels.
/// </summary>
public class BlackFrame
{
/// <summary>
/// Initializes a new instance of the <see cref="BlackFrame"/> class.
/// </summary>
/// <param name="percent">Percentage of the frame that is black.</param>
/// <param name="time">Time this frame appears at.</param>
public BlackFrame(int percent, double time)
{
Percentage = percent;
Time = time;
}
/// <summary>
/// Gets or sets the percentage of the frame that is black.
/// </summary>
public int Percentage { get; set; }
/// <summary>
/// Gets or sets the time (in seconds) this frame appeared at.
/// </summary>
public double Time { get; set; }
}

View File

@ -16,16 +16,17 @@ public static class FFmpegWrapper
{ {
private static readonly object InvertedIndexCacheLock = new(); 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
/// <summary> /// <summary>
/// Used with FFmpeg's silencedetect filter to extract the start and end times of silence. /// Used with FFmpeg's silencedetect filter to extract the start and end times of silence.
/// </summary> /// </summary>
private static readonly Regex SilenceDetectionExpression = new( private static readonly Regex SilenceDetectionExpression = new(
"silence_(?<type>start|end): (?<time>[0-9\\.]+)"); "silence_(?<type>start|end): (?<time>[0-9\\.]+)");
/// <summary>
/// Used with FFmpeg's blackframe filter to extract the time and percentage of black pixels.
/// </summary>
private static readonly Regex BlackFrameRegex = new("(pblack|t):[0-9.]+");
/// <summary> /// <summary>
/// Gets or sets the logger. /// Gets or sets the logger.
/// </summary> /// </summary>
@ -190,7 +191,12 @@ public static class FFmpegWrapper
var currentRange = new TimeRange(); var currentRange = new TimeRange();
var silenceRanges = new List<TimeRange>(); var silenceRanges = new List<TimeRange>();
// Each match will have a type (either "start" or "end") and a timecode (a double). /* Each match will have a type (either "start" or "end") and a timecode (a double).
*
* Sample output:
* [silencedetect @ 0x000000000000] silence_start: 12.34
* [silencedetect @ 0x000000000000] silence_end: 56.123 | silence_duration: 43.783
*/
var raw = Encoding.UTF8.GetString(GetOutput(args, cacheKey, true)); var raw = Encoding.UTF8.GetString(GetOutput(args, cacheKey, true));
foreach (Match match in SilenceDetectionExpression.Matches(raw)) foreach (Match match in SilenceDetectionExpression.Matches(raw))
{ {
@ -211,6 +217,61 @@ public static class FFmpegWrapper
return silenceRanges.ToArray(); return silenceRanges.ToArray();
} }
/// <summary>
/// Finds the location of all black frames in a media file within a time range.
/// </summary>
/// <param name="episode">Media file to analyze.</param>
/// <param name="range">Time range to search.</param>
/// <returns>Array of frames that are at least 50% black.</returns>
public static BlackFrame[] DetectBlackFrames(QueuedEpisode episode, TimeRange range)
{
// Seek to the start of the time range and find frames that are at least 50% black.
var args = string.Format(
CultureInfo.InvariantCulture,
"-ss {0} -i \"{1}\" -to {2} -an -dn -sn -vf \"blackframe=amount=50\" -f null -",
range.Start,
episode.Path,
range.End - range.Start);
// Cache the results to GUID-blackframes-v1-START-END.
var cacheKey = string.Format(
CultureInfo.InvariantCulture,
"{0}-blackframes-v1-{1}-{2}",
episode.EpisodeId.ToString("N"),
range.Start,
range.End);
var blackFrames = new List<BlackFrame>();
/* Run the blackframe filter.
*
* Sample output:
* [Parsed_blackframe_0 @ 0x0000000] frame:1 pblack:99 pts:43 t:0.043000 type:B last_keyframe:0
* [Parsed_blackframe_0 @ 0x0000000] frame:2 pblack:99 pts:85 t:0.085000 type:B last_keyframe:0
*/
var raw = Encoding.UTF8.GetString(GetOutput(args, cacheKey, true));
foreach (var line in raw.Split('\n'))
{
var matches = BlackFrameRegex.Matches(line);
if (matches.Count != 2)
{
continue;
}
var (strPercent, strTime) = (
matches[0].Value.Split(':')[1],
matches[1].Value.Split(':')[1]
);
blackFrames.Add(new(
Convert.ToInt32(strPercent, CultureInfo.InvariantCulture),
Convert.ToDouble(strTime, CultureInfo.InvariantCulture)
));
}
return blackFrames.ToArray();
}
/// <summary> /// <summary>
/// Gets Chromaprint debugging logs. /// Gets Chromaprint debugging logs.
/// </summary> /// </summary>
@ -296,10 +357,11 @@ public static class FFmpegWrapper
{ {
var ffmpegPath = Plugin.Instance?.FFmpegPath ?? "ffmpeg"; var ffmpegPath = Plugin.Instance?.FFmpegPath ?? "ffmpeg";
// The silencedetect filter outputs silence timestamps at the info log level. // The silencedetect and blackframe filters output data at the info log level.
var logLevel = args.Contains("silencedetect", StringComparison.OrdinalIgnoreCase) ? var useInfoLevel = args.Contains("silencedetect", StringComparison.OrdinalIgnoreCase) ||
"info" : args.Contains("blackframe", StringComparison.OrdinalIgnoreCase);
"warning";
var logLevel = useInfoLevel ? "info" : "warning";
var cacheOutput = var cacheOutput =
(Plugin.Instance?.Configuration.CacheFingerprints ?? false) && (Plugin.Instance?.Configuration.CacheFingerprints ?? false) &&