namespace ConfusedPolarBear.Plugin.IntroSkipper;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging;
///
/// Media file analyzer used to detect end credits that consist of text overlaid on a black background.
/// Bisects the end of the video file to perform an efficient search.
///
public class BlackFrameAnalyzer : IMediaFileAnalyzer
{
private readonly TimeSpan _maximumError = new(0, 0, 4);
private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
/// Logger.
public BlackFrameAnalyzer(ILogger logger)
{
_logger = logger;
}
///
public ReadOnlyCollection AnalyzeMediaFiles(
ReadOnlyCollection analysisQueue,
AnalysisMode mode,
CancellationToken cancellationToken)
{
if (mode != AnalysisMode.Credits)
{
throw new NotImplementedException("mode must equal Credits");
}
var creditTimes = new Dictionary();
foreach (var episode in analysisQueue)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
var intro = AnalyzeMediaFile(
episode,
mode,
Plugin.Instance!.Configuration.BlackFrameMinimumPercentage);
if (intro is null)
{
continue;
}
creditTimes[episode.EpisodeId] = intro;
}
Plugin.Instance!.UpdateTimestamps(creditTimes, mode);
return analysisQueue
.Where(x => !creditTimes.ContainsKey(x.EpisodeId))
.ToList()
.AsReadOnly();
}
///
/// Analyzes an individual media file. Only public because of unit tests.
///
/// Media file to analyze.
/// Analysis mode.
/// Percentage of the frame that must be black.
/// Credits timestamp.
public Intro? AnalyzeMediaFile(QueuedEpisode episode, AnalysisMode mode, int minimum)
{
var config = Plugin.Instance?.Configuration ?? new Configuration.PluginConfiguration();
// Start by analyzing the last N minutes of the file.
var start = TimeSpan.FromSeconds(config.MaximumEpisodeCreditsDuration);
var end = TimeSpan.FromSeconds(config.MinimumCreditsDuration);
var firstFrameTime = 0.0;
// Continue bisecting the end of the file until the range that contains the first black
// frame is smaller than the maximum permitted error.
while (start - end > _maximumError)
{
// Analyze the middle two seconds from the current bisected range
var midpoint = (start + end) / 2;
var scanTime = episode.Duration - midpoint.TotalSeconds;
var tr = new TimeRange(scanTime, scanTime + 2);
_logger.LogTrace(
"{Episode}, dur {Duration}, bisect [{BStart}, {BEnd}], time [{Start}, {End}]",
episode.Name,
episode.Duration,
start,
end,
tr.Start,
tr.End);
var frames = FFmpegWrapper.DetectBlackFrames(episode, tr, minimum);
_logger.LogTrace(
"{Episode} at {Start} has {Count} black frames",
episode.Name,
tr.Start,
frames.Length);
if (frames.Length == 0)
{
// Since no black frames were found, slide the range closer to the end
start = midpoint;
}
else
{
// Some black frames were found, slide the range closer to the start
end = midpoint;
firstFrameTime = frames[0].Time + scanTime;
}
}
if (firstFrameTime > 0)
{
return new(episode.EpisodeId, new TimeRange(firstFrameTime, episode.Duration));
}
return null;
}
}