Move from fpcalc to ffmpeg

This commit is contained in:
ConfusedPolarBear 2022-06-09 14:07:40 -05:00
parent db1fc45993
commit 3459e3ce4b
12 changed files with 115 additions and 64 deletions

View File

@ -1,14 +1,20 @@
/* These tests require that the host system has a version of FFmpeg installed
* which supports both chromaprint and the "-fp_format raw" flag.
*/
using Xunit;
using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
// TODO: rename
public class TestFPCalc
{
[Fact]
public void TestInstallationCheck()
{
Assert.True(FPCalc.CheckFPCalcInstalled());
Assert.True(FPCalc.CheckFFmpegVersion());
}
[Theory]
@ -43,7 +49,7 @@ public class TestFPCalc
3416816715, 3404331257, 3395844345, 3395836155, 3408464089, 3374975369, 1282036360, 1290457736,
1290400440, 1290314408, 1281925800, 1277727404, 1277792932, 1278785460, 1561962388, 1426698196,
3607924711, 4131892839, 4140215815, 4292259591, 3218515717, 3209938229, 3171964197, 3171956013,
4229117295, 4229312879, 4242407935, 4238016959, 4239987133, 4239990013, 3703060732, 1547188252,
4229117295, 4229312879, 4242407935, 4240114111, 4239987133, 4239990013, 3703060732, 1547188252,
1278748677, 1278748935, 1144662786, 1148854786, 1090388802, 1090388962, 1086260130, 1085940098,
1102709122, 45811586, 44634002, 44596656, 44592544, 1122527648, 1109944736, 1109977504, 1111030243,
1111017762, 1109969186, 1126721826, 1101556002, 1084844322, 1084979506, 1084914450, 1084914449,

View File

@ -15,7 +15,7 @@ public class PluginConfiguration : BasePluginConfiguration
}
/// <summary>
/// Gets or sets a value indicating whether the output of fpcalc should be cached to the filesystem.
/// Gets or sets a value indicating whether the episode's fingerprint should be cached to the filesystem.
/// </summary>
public bool CacheFingerprints { get; set; } = true;

View File

@ -33,8 +33,8 @@
<div class="fieldDescription">
If checked, will store the fingerprints for all subsequently scanned files to disk.
Caching fingerprints avoids having to re-run fpcalc on each file, at the expense of disk
usage.
Caching fingerprints avoids having to re-run chromaprint on each file, at the expense of
disk usage.
</div>
</div>
@ -66,7 +66,8 @@
<p>
Erasing introduction timestamps is only necessary after upgrading the plugin if specifically
requested to do so in the plugin's changelog. After the timestamps are erased, run the Analyze episodes scheduled task to re-analyze all media on the server.
requested to do so in the plugin's changelog. After the timestamps are erased, run the
Analyze episodes scheduled task to re-analyze all media on the server.
</p>
</div>
</form>

View File

@ -3,8 +3,8 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>ConfusedPolarBear.Plugin.IntroSkipper</RootNamespace>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>0.1.0.0</FileVersion>
<AssemblyVersion>0.1.5.0</AssemblyVersion>
<FileVersion>0.1.5.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>

View File

@ -51,10 +51,10 @@ public class Entrypoint : IServerEntryPoint
{
FPCalc.Logger = _logger;
// Assert that fpcalc is installed
if (!FPCalc.CheckFPCalcInstalled())
// Assert that ffmpeg with chromaprint is installed
if (!FPCalc.CheckFFmpegVersion())
{
_logger.LogError("fpcalc is not installed on this system - episodes will not be analyzed");
_logger.LogError("ffmpeg with chromaprint is not installed on this system - episodes will not be analyzed");
return Task.CompletedTask;
}

View File

@ -7,10 +7,12 @@ using System.IO;
using System.Text;
using Microsoft.Extensions.Logging;
// TODO: rename
namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// <summary>
/// Wrapper for the fpcalc utility.
/// Wrapper for libchromaprint.
/// </summary>
public static class FPCalc
{
@ -20,16 +22,16 @@ public static class FPCalc
public static ILogger? Logger { get; set; }
/// <summary>
/// Check that the fpcalc utility is installed.
/// Check that the installed version of ffmpeg supports chromaprint.
/// </summary>
/// <returns>true if fpcalc is installed, false on any error.</returns>
public static bool CheckFPCalcInstalled()
/// <returns>true if a compatible version of ffmpeg is installed, false on any error.</returns>
public static bool CheckFFmpegVersion()
{
try
{
var version = GetOutput("-version", 2000).TrimEnd();
Logger?.LogInformation("fpcalc -version: {Version}", version);
return version.StartsWith("fpcalc version", StringComparison.OrdinalIgnoreCase);
var version = Encoding.UTF8.GetString(GetOutput("-version", 2000));
Logger?.LogDebug("ffmpeg version: {Version}", version);
return version.Contains("--enable-chromaprint", StringComparison.OrdinalIgnoreCase);
}
catch
{
@ -44,7 +46,7 @@ public static class FPCalc
/// <returns>Numerical fingerprint points.</returns>
public static ReadOnlyCollection<uint> Fingerprint(QueuedEpisode episode)
{
// Try to load this episode from cache before running fpcalc.
// Try to load this episode from cache before running ffmpeg.
if (LoadCachedFingerprint(episode, out ReadOnlyCollection<uint> cachedFingerprint))
{
Logger?.LogDebug("Fingerprint cache hit on {File}", episode.Path);
@ -53,32 +55,24 @@ public static class FPCalc
Logger?.LogDebug("Fingerprinting {Duration} seconds from {File}", episode.FingerprintDuration, episode.Path);
// FIXME: revisit escaping
var path = "\"" + episode.Path + "\"";
var duration = episode.FingerprintDuration.ToString(CultureInfo.InvariantCulture);
var args = " -raw -length " + duration + " " + path;
var args = string.Format(
CultureInfo.InvariantCulture,
"-i \"{0}\" -to {1} -ac 2 -f chromaprint -fp_format raw -",
episode.Path,
episode.FingerprintDuration);
/* Returns output similar to the following:
* DURATION=123
* FINGERPRINT=123456789,987654321,123456789,987654321,123456789,987654321
*/
var raw = GetOutput(args);
var lines = raw.Split("\n");
if (lines.Length < 2)
// Returns all fingerprint points as raw 32 bit unsigned integers (little endian).
var rawPoints = GetOutput(args);
if (rawPoints.Length == 0 || rawPoints.Length % 4 != 0)
{
Logger?.LogTrace("fpcalc output is {Raw}", raw);
throw new FingerprintException("fpcalc output was malformed");
throw new FingerprintException("chromaprint output was malformed");
}
// Remove the "FINGERPRINT=" prefix and split into an array of numbers.
var fingerprint = lines[1].Substring(12).Split(",");
var results = new List<uint>();
foreach (var rawNumber in fingerprint)
for (var i = 0; i < rawPoints.Length; i += 4)
{
results.Add(Convert.ToUInt32(rawNumber, CultureInfo.InvariantCulture));
var rawPoint = rawPoints.Slice(i, 4);
results.Add(BitConverter.ToUInt32(rawPoint));
}
// Try to cache this fingerprint.
@ -88,23 +82,43 @@ public static class FPCalc
}
/// <summary>
/// Runs fpcalc and returns standard output.
/// Runs ffmpeg and returns standard output.
/// </summary>
/// <param name="args">Arguments to pass to fpcalc.</param>
/// <param name="timeout">Timeout (in seconds) to wait for fpcalc to exit.</param>
private static string GetOutput(string args, int timeout = 60 * 1000)
/// <param name="args">Arguments to pass to ffmpeg.</param>
/// <param name="timeout">Timeout (in seconds) to wait for ffmpeg to exit.</param>
private static ReadOnlySpan<byte> GetOutput(string args, int timeout = 60 * 1000)
{
var info = new ProcessStartInfo("fpcalc", args);
info.CreateNoWindow = true;
info.RedirectStandardOutput = true;
var ffmpegPath = Plugin.Instance?.FFmpegPath ?? "ffmpeg";
var fpcalc = new Process();
fpcalc.StartInfo = info;
var info = new ProcessStartInfo(ffmpegPath, args)
{
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
fpcalc.Start();
fpcalc.WaitForExit(timeout);
var ffmpeg = new Process
{
StartInfo = info
};
return fpcalc.StandardOutput.ReadToEnd();
ffmpeg.Start();
ffmpeg.WaitForExit(timeout);
using (MemoryStream ms = new MemoryStream())
{
var buf = new byte[4096];
var bytesRead = 0;
do
{
bytesRead = ffmpeg.StandardOutput.BaseStream.Read(buf, 0, buf.Length);
ms.Write(buf, 0, bytesRead);
}
while (bytesRead > 0);
return ms.ToArray().AsSpan();
}
}
/// <summary>

View File

@ -5,6 +5,7 @@ using System.IO;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
@ -23,7 +24,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
/// <param name="serverConfiguration">Server configuration manager.</param>
public Plugin(
IApplicationPaths applicationPaths,
IXmlSerializer xmlSerializer,
IServerConfigurationManager serverConfiguration)
: base(applicationPaths, xmlSerializer)
{
_xmlSerializer = xmlSerializer;
@ -37,6 +42,9 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
_introPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "intros.xml");
// Get the path to FFmpeg.
FFmpegPath = serverConfiguration.GetEncodingOptions().EncoderAppPathDisplay;
Intros = new Dictionary<Guid, Intro>();
AnalysisQueue = new Dictionary<Guid, List<QueuedEpisode>>();
Instance = this;
@ -64,6 +72,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// </summary>
public string FingerprintCachePath { get; private set; }
/// <summary>
/// Gets the full path to FFmpeg.
/// </summary>
public string FFmpegPath { get; private set; }
/// <inheritdoc />
public override string Name => "Intro Skipper";

View File

@ -104,6 +104,12 @@ public class FingerprinterTask : IScheduledTask
var queue = Plugin.Instance!.AnalysisQueue;
var totalProcessed = 0;
if (queue.Count == 0)
{
throw new FingerprintException(
"No episodes to analyze: either no show libraries are defined or ffmpeg is not properly installed");
}
// TODO: make configurable
var options = new ParallelOptions();
options.MaxDegreeOfParallelism = 2;

View File

@ -1,18 +1,16 @@
# Intro Skipper (ALPHA)
# Intro Skipper (beta)
<div align="center">
<img alt="Plugin Banner" src="https://raw.githubusercontent.com/ConfusedPolarBear/intro-skipper/master/images/logo.png" />
</div>
Analyzes the audio of television episodes to detect and skip over intros. Currently in alpha.
Analyzes the audio of television episodes to detect and skip over intros. Jellyfin must use a version of `ffmpeg` that has been compiled with `--enable-chromaprint` (`jellyfin-ffmpeg` versions 5.0.1-5 and later will work).
Installing this plugin (along with a modified web interface and `fpcalc`) will result in a skip intro button displaying in the video player, like this:
If you use the custom web interface on your server, you will be able to click a skip button to skip intros, like this:
![Skip intro button](images/skip-button.png)
If you use Jellyfin clients that do not use the web interface provided by the server, the plugin can be configured to automatically skip intros.
This plugin **will not work** without installing `fpcalc`. The easiest way to do this is to follow the steps below.
However, if you use Jellyfin clients that do not use the web interface provided by the server, the plugin can be configured to automatically skip intros.
## Introduction requirements
@ -21,7 +19,8 @@ Show introductions will only be detected if they are:
* Located within the first 25% of an episode, or the first 10 minutes, whichever is smaller
* At least 20 seconds long
## Step 1: Install the modified web interface + fpcalc
## Step 1: Install the modified web interface (optional)
This step is only necessary if you do not use the automatic skip feature.
1. Run the `ghcr.io/confusedpolarbear/jellyfin-intro-skipper` container just as you would any other Jellyfin container
1. If you reuse the configuration data from another container, **make sure to create a backup first**.
2. Follow the plugin installation steps below

10
docker/Dockerfile.ffmpeg5 Normal file
View File

@ -0,0 +1,10 @@
FROM jellyfin/jellyfin:10.8.0-beta3
RUN curl -Lo jellyfin-ffmpeg5.deb \
https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v5.0.1-5/jellyfin-ffmpeg5_5.0.1-5-bullseye_amd64.deb
RUN apt update
RUN apt install -y libxcb-randr0
RUN dpkg -i jellyfin-ffmpeg5.deb
COPY dist/ /jellyfin/jellyfin-web/

View File

@ -4,3 +4,8 @@ Build instructions for the `ghcr.io/confusedpolarbear/jellyfin-intro-skipper` co
2. Run `npm run build:production`
3. Copy the `dist` folder into this folder
4. Run `docker build .`
## `Dockerfile.ffmpeg5` testing instructions
1. Follow steps 1 - 3 above (only needed if you don't use the automatic skip feature)
2. Run `docker build . -t jellyfin-ffmpeg5 -f Dockerfile.ffmpeg5`

View File

@ -2,15 +2,12 @@
## Requirements
* Jellyfin 10.8.0 beta 2 (beta 3 may also work, untested)
* Jellyfin 10.8.0 beta 3
* Compiled [jellyfin-web](https://github.com/ConfusedPolarBear/jellyfin-web/tree/intros) interface with intro skip button
* [chromaprint](https://github.com/acoustid/chromaprint) (only versions 1.4.3 and later have been verified to work)
## Instructions
1. Install the `fpcalc` program
1. On Debian based distributions, this is provided by the `libchromaprint-tools` package
2. Compiled binaries can also be downloaded from the [GitHub repository](https://github.com/acoustid/chromaprint/releases/tag/v1.5.1)
1. Install the latest version of ffmpeg from https://github.com/jellyfin/jellyfin-ffmpeg/releases
2. Download the latest modified web interface from the releases tab and either:
1. Serve the web interface directly from your Jellyfin server, or
2. Serve the web interface using an external web server