Merge branch 'ffmpeg5'

This commit is contained in:
ConfusedPolarBear 2022-06-10 22:20:05 -05:00
commit 70b4f0e034
12 changed files with 102 additions and 72 deletions

View File

@ -1,5 +1,12 @@
# Changelog
## v0.1.5.0 (unreleased)
* Use `ffmpeg` to generate audio fingerprints instead of `fpcalc`
* Requires that the installed version of `ffmpeg`:
* Was compiled with the `--enable-chromaprint` option
* Understands the `-fp_format raw` flag
* `jellyfin-ffmpeg 5.0.1-5` meets both of these requirements
## v0.1.0.0 (2022-06-09)
* Add option to automatically skip intros
* Cache audio fingerprints by default

View File

@ -1,14 +1,18 @@
/* 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;
public class TestFPCalc
public class TestAudioFingerprinting
{
[Fact]
public void TestInstallationCheck()
{
Assert.True(FPCalc.CheckFPCalcInstalled());
Assert.True(Chromaprint.CheckFFmpegVersion());
}
[Theory]
@ -43,7 +47,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,
@ -52,7 +56,7 @@ public class TestFPCalc
3472417825, 3395841056, 3458735136, 3341420624, 1076496560, 1076501168, 1076501136, 1076497024
};
var actual = FPCalc.Fingerprint(queueEpisode("audio/big_buck_bunny_intro.mp3"));
var actual = Chromaprint.Fingerprint(queueEpisode("audio/big_buck_bunny_intro.mp3"));
Assert.Equal(expected, actual);
}

View File

@ -10,9 +10,9 @@ using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper;
/// <summary>
/// Wrapper for the fpcalc utility.
/// Wrapper for libchromaprint.
/// </summary>
public static class FPCalc
public static class Chromaprint
{
/// <summary>
/// Gets or sets the logger.
@ -20,16 +20,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 +44,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 +53,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 for " + episode.Path + " was malformed");
throw new FingerprintException("chromaprint output for " + episode.Path + " 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 +80,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

@ -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

@ -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

@ -105,7 +105,7 @@ public class VisualizationController : ControllerBase
{
if (needle.EpisodeId == id)
{
return FPCalc.Fingerprint(needle);
return Chromaprint.Fingerprint(needle);
}
}
}

View File

@ -49,12 +49,12 @@ public class Entrypoint : IServerEntryPoint
/// <returns>Task.</returns>
public Task RunAsync()
{
FPCalc.Logger = _logger;
Chromaprint.Logger = _logger;
// Assert that fpcalc is installed
if (!FPCalc.CheckFPCalcInstalled())
// Assert that ffmpeg with chromaprint is installed
if (!Chromaprint.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

@ -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

@ -106,7 +106,7 @@ public class FingerprinterTask : IScheduledTask
if (queue.Count == 0)
{
throw new FingerprintException(
"No episodes to analyze: either no show libraries are defined or fpcalc is not properly installed");
"No episodes to analyze: either no show libraries are defined or ffmpeg could not be found");
}
var totalProcessed = 0;
@ -274,8 +274,8 @@ public class FingerprinterTask : IScheduledTask
/// <returns>Intros for the first and second episodes.</returns>
public (Intro Lhs, Intro Rhs) FingerprintEpisodes(QueuedEpisode lhsEpisode, QueuedEpisode rhsEpisode)
{
var lhsFingerprint = FPCalc.Fingerprint(lhsEpisode);
var rhsFingerprint = FPCalc.Fingerprint(rhsEpisode);
var lhsFingerprint = Chromaprint.Fingerprint(lhsEpisode);
var rhsFingerprint = Chromaprint.Fingerprint(rhsEpisode);
// Cache the fingerprints for quicker recall in the second pass (if one is needed).
lock (_fingerprintCacheLock)

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

View File

@ -1,5 +1,3 @@
FROM jellyfin/jellyfin:10.8.0-beta2
RUN apt update && apt install -y libchromaprint-tools
FROM jellyfin/jellyfin:10.8.0
COPY dist/ /jellyfin/jellyfin-web/

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