From 3459e3ce4b49dd2b7564a36e5810a1f2dd045d34 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Thu, 9 Jun 2022 14:07:40 -0500
Subject: [PATCH] Move from fpcalc to ffmpeg
---
.../TestAudioFingerprinting.cs | 10 +-
.../Configuration/PluginConfiguration.cs | 2 +-
.../Configuration/configPage.html | 7 +-
...nfusedPolarBear.Plugin.IntroSkipper.csproj | 4 +-
.../Entrypoint.cs | 6 +-
.../FPCalc.cs | 94 +++++++++++--------
.../Plugin.cs | 15 ++-
.../ScheduledTasks/FingerprinterTask.cs | 6 ++
README.md | 13 ++-
docker/Dockerfile.ffmpeg5 | 10 ++
docker/README.md | 5 +
docs/native.md | 7 +-
12 files changed, 115 insertions(+), 64 deletions(-)
create mode 100644 docker/Dockerfile.ffmpeg5
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
index 72279ac..fbf6076 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestAudioFingerprinting.cs
@@ -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,
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
index 6fb7e49..9b3c9cf 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/PluginConfiguration.cs
@@ -15,7 +15,7 @@ public class PluginConfiguration : BasePluginConfiguration
}
///
- /// 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.
///
public bool CacheFingerprints { get; set; } = true;
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
index 7e00e3b..ca14011 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
@@ -33,8 +33,8 @@
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.
@@ -66,7 +66,8 @@
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.
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj b/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj
index 5a7b391..16990a2 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj
@@ -3,8 +3,8 @@
net6.0
ConfusedPolarBear.Plugin.IntroSkipper
- 0.1.0.0
- 0.1.0.0
+ 0.1.5.0
+ 0.1.5.0
true
true
enable
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs
index 952cc46..c3df4f1 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Entrypoint.cs
@@ -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;
}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/FPCalc.cs b/ConfusedPolarBear.Plugin.IntroSkipper/FPCalc.cs
index af80747..7deba7c 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/FPCalc.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/FPCalc.cs
@@ -7,10 +7,12 @@ using System.IO;
using System.Text;
using Microsoft.Extensions.Logging;
+// TODO: rename
+
namespace ConfusedPolarBear.Plugin.IntroSkipper;
///
-/// Wrapper for the fpcalc utility.
+/// Wrapper for libchromaprint.
///
public static class FPCalc
{
@@ -20,16 +22,16 @@ public static class FPCalc
public static ILogger? Logger { get; set; }
///
- /// Check that the fpcalc utility is installed.
+ /// Check that the installed version of ffmpeg supports chromaprint.
///
- /// true if fpcalc is installed, false on any error.
- public static bool CheckFPCalcInstalled()
+ /// true if a compatible version of ffmpeg is installed, false on any error.
+ 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
/// Numerical fingerprint points.
public static ReadOnlyCollection 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 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();
- 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
}
///
- /// Runs fpcalc and returns standard output.
+ /// Runs ffmpeg and returns standard output.
///
- /// Arguments to pass to fpcalc.
- /// Timeout (in seconds) to wait for fpcalc to exit.
- private static string GetOutput(string args, int timeout = 60 * 1000)
+ /// Arguments to pass to ffmpeg.
+ /// Timeout (in seconds) to wait for ffmpeg to exit.
+ private static ReadOnlySpan 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();
+ }
}
///
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
index 6dde104..e069363 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
@@ -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, IHasWebPages
///
/// Instance of the interface.
/// Instance of the interface.
- public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ /// Server configuration manager.
+ public Plugin(
+ IApplicationPaths applicationPaths,
+ IXmlSerializer xmlSerializer,
+ IServerConfigurationManager serverConfiguration)
: base(applicationPaths, xmlSerializer)
{
_xmlSerializer = xmlSerializer;
@@ -37,6 +42,9 @@ public class Plugin : BasePlugin, IHasWebPages
_introPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "intros.xml");
+ // Get the path to FFmpeg.
+ FFmpegPath = serverConfiguration.GetEncodingOptions().EncoderAppPathDisplay;
+
Intros = new Dictionary();
AnalysisQueue = new Dictionary>();
Instance = this;
@@ -64,6 +72,11 @@ public class Plugin : BasePlugin, IHasWebPages
///
public string FingerprintCachePath { get; private set; }
+ ///
+ /// Gets the full path to FFmpeg.
+ ///
+ public string FFmpegPath { get; private set; }
+
///
public override string Name => "Intro Skipper";
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs
index 11bc351..c674822 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs
@@ -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;
diff --git a/README.md b/README.md
index b8b7a0e..6dc62cc 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,16 @@
-# Intro Skipper (ALPHA)
+# Intro Skipper (beta)
-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
diff --git a/docker/Dockerfile.ffmpeg5 b/docker/Dockerfile.ffmpeg5
new file mode 100644
index 0000000..36358c1
--- /dev/null
+++ b/docker/Dockerfile.ffmpeg5
@@ -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/
diff --git a/docker/README.md b/docker/README.md
index eaa5124..11f158e 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -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`
diff --git a/docs/native.md b/docs/native.md
index ae09f4b..07307fd 100644
--- a/docs/native.md
+++ b/docs/native.md
@@ -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