feat(audio): align a fetched manifest to the local file before storing

The signature had a producer and a reader but no consumer, so nothing
ever fingerprinted anything. `ManifestAligner` runs on the fetch path,
before the windows are stored.

A local alignment supersedes the server's offset. The server has never
seen this file — its offset is a runtime-difference inference at best,
while the local comparison is against the media the windows will actually
be drawn over. It also needs no round trip, so no signature leaves the
instance. This is what jRay's spec already meant by matching being a
consumer concern: the server never rewrites a manifest, so one stored
manifest serves every trim of the same cut.

The offset has two terms and only one is in the server's pseudocode. Both
windows are centred on their own file's midpoint, so unequal runtimes
start them at different absolute times; a release with 40 s of extra head
material recovers 20 s from the slide and 20 s from the anchor
difference. Using the slide alone is wrong by half the runtime difference
on every shifted release.

Degradation, never failure. Signatures off, no manifest signature, media
under the window, a `v2:` producer, a missing binary, a decode error —
each applies the server's offset rather than refusing, because a
signature is an enhancement to cut matching and must never break a fetch.

"Un-comparable" and "does not match" are kept distinct, which a test
caught: `Compare` returns null for both, and conflating them would report
a 90-second extra as content disagreeing with its own manifest. A genuine
disagreement is stored anyway — the audio may legitimately differ, a
different language track being the obvious case — and surfaced as a
caveat that outranks the tier's, since it is the stronger statement.

The applied offset, score, slide and the local file's own signature are
written beside the truth file: the offset is otherwise unrecoverable once
the windows are shifted, and the stored signature lets a later fetch
align without decoding again. Provenance is never injected into the truth
file, so the bytes served back stay the producer's (JR-004).

`docs/audio-alignment.md` documents the mechanism end to end.

TRACES: JR-047 | SR-003
This commit is contained in:
2026-07-31 16:52:21 +02:00
parent e8ce779ad3
commit 1e247c4c7d
12 changed files with 981 additions and 29 deletions
@@ -0,0 +1,214 @@
using System;
using System.IO;
using System.Text.Json;
using Jellyfin.Plugin.JRay.Configuration;
using Jellyfin.Plugin.JRay.Models;
using Jellyfin.Plugin.JRay.Services;
using Xunit;
namespace Jellyfin.Plugin.JRay.Tests;
/// <summary>
/// JR-047 — a fetched manifest is aligned against the local file before its
/// windows are stored, and how that was decided is recorded.
///
/// The claim under test is that **the local alignment supersedes the server's
/// offset**. The server has never seen this file; its offset can only be a
/// runtime-difference inference, while a local alignment compares the manifest's
/// own signature against the media the windows will be drawn over.
///
/// The counterweight is that a signature must never break a fetch. Every way the
/// local path can fail — switched off, no manifest signature, short media, a
/// failed decode, or two signatures that do not match — has to fall back to the
/// server's offset rather than refusing. Both halves are asserted here.
///
/// `Resolve` is the decision, split out from the decode so it can be driven
/// without FFmpeg or a media file.
///
/// TRACES: UT-053, UT-054, UT-055, UT-056, UT-057 | JR-047
/// </summary>
public class ManifestAlignerTests
{
private const double FeatureRuntime = 7200.0;
private const double ServerOffset = 3.5;
private static readonly string FixtureDir =
Path.Combine(AppContext.BaseDirectory, "fixtures", "audio");
private static readonly Lazy<string> GoldenSignature = new(() =>
JsonDocument.Parse(File.ReadAllText(Path.Combine(FixtureDir, "jray_audio_v1_golden.json")))
.RootElement.GetProperty("signature").GetString()!);
private static readonly Lazy<byte[]> GoldenFrames = new(() =>
AudioSignatureMatcher.TryParseFrames(GoldenSignature.Value)!);
// UT-053
[Fact]
public void ALocalAlignment_SupersedesTheServersOffset()
{
// Same cut, sampled 100 frames apart — a differently trimmed release.
// The server offered 3.5 s from a runtime comparison; the local audio
// says otherwise, and the local answer is the one that gets applied.
const int Shift = 100;
var source = GoldenFrames.Value;
var alignment = ManifestAligner.Resolve(
Signature(source, 44, 1000),
Signature(source, 144, 1000),
FeatureRuntime,
FeatureRuntime,
MatchTier.Runtime,
ServerOffset);
Assert.Equal(AlignmentSource.Local, alignment.Source);
Assert.Equal(MatchTier.Audio, alignment.Tier);
Assert.Equal(Shift, alignment.OffsetFrames);
Assert.Equal(1.0, alignment.Score!.Value);
Assert.Equal(Shift * AudioSignatureMatcher.FrameSeconds, alignment.OffsetSec, 9);
// The server's claim is kept rather than overwritten: the applied offset
// is otherwise unrecoverable once the windows are shifted, and the two
// disagreeing is exactly what someone debugging would need to see.
Assert.Equal(ServerOffset, alignment.ServerOffsetSec);
Assert.Equal(MatchTier.Runtime, alignment.ServerTier);
}
// UT-054
[Fact]
public void TheLocalSignature_IsRecorded_SoALaterFetchNeedNotDecodeAgain()
{
// The decode is the expensive half and the reason signatures are opt-in.
// Keeping the local one beside the truth file is what lets a second
// manifest be aligned for free.
var alignment = ManifestAligner.Resolve(
GoldenSignature.Value,
GoldenSignature.Value,
FeatureRuntime,
FeatureRuntime,
MatchTier.Runtime,
ServerOffset);
Assert.Equal(GoldenSignature.Value, alignment.LocalSignature);
Assert.Equal(AlignmentSource.Local, alignment.Source);
Assert.Equal(0.0, alignment.OffsetSec);
}
// UT-055
[Fact]
public void EveryUnavailableLocalPath_FallsBackToTheServer_RatherThanRefusing()
{
// A signature is an enhancement to cut matching. A missing one costs a
// tier; it must never be able to break a fetch, so each of these stores
// the manifest on the server's terms.
var cases = new (string? Local, string? Manifest, string Why)[]
{
(null, GoldenSignature.Value, "signatures off, or the decode failed"),
(GoldenSignature.Value, null, "the manifest carried no signature"),
(null, null, "neither side has one"),
};
foreach (var (local, manifest, why) in cases)
{
var alignment = ManifestAligner.Resolve(
local, manifest, FeatureRuntime, FeatureRuntime, MatchTier.Runtime, ServerOffset);
Assert.Equal(AlignmentSource.Server, alignment.Source);
Assert.Equal(ServerOffset, alignment.OffsetSec);
Assert.Equal(MatchTier.Runtime, alignment.Tier);
Assert.Null(alignment.Score);
Assert.Null(alignment.OffsetFrames);
}
// Short media is the same fallback, reached through JR-044 rather than
// through a missing string: both sides have a perfectly valid signature
// and it is still the runtime that decides.
var shortMedia = ManifestAligner.Resolve(
GoldenSignature.Value,
GoldenSignature.Value,
119.0,
FeatureRuntime,
MatchTier.Runtime,
ServerOffset);
Assert.Equal(AlignmentSource.Server, shortMedia.Source);
Assert.Equal(ServerOffset, shortMedia.OffsetSec);
// A `v2:` signature from a future producer is *un-comparable*, not a
// mismatch (JR-045). Reporting it as one would tell the user their audio
// disagrees with the manifest when all that happened is the producer
// moved ahead of this build.
var futureProducer = ManifestAligner.Resolve(
GoldenSignature.Value,
"v2:" + GoldenSignature.Value[AudioSignature.VersionPrefix.Length..],
FeatureRuntime,
FeatureRuntime,
MatchTier.Runtime,
ServerOffset);
Assert.Equal(AlignmentSource.Server, futureProducer.Source);
Assert.Equal(ServerOffset, futureProducer.OffsetSec);
}
// UT-056
[Fact]
public void TwoSignaturesThatDoNotMatch_AreRecorded_ButStillDoNotBreakTheFetch()
{
// The strongest available hint that a manifest describes different
// content. It is not treated as a failure — the audio may legitimately
// differ, a different language track being the obvious case — but it is
// not discarded either.
var alignment = ManifestAligner.Resolve(
GoldenSignature.Value,
PseudoRandomSignature(1288, seed: 4242),
FeatureRuntime,
FeatureRuntime,
MatchTier.Runtime,
ServerOffset);
Assert.Equal(AlignmentSource.LocalMismatch, alignment.Source);
// The manifest is still stored, on the server's terms.
Assert.Equal(ServerOffset, alignment.OffsetSec);
Assert.Equal(MatchTier.Runtime, alignment.Tier);
}
// UT-057
[Fact]
public void AMismatchOutranksTheTier_InTheCaveatShownToTheUser()
{
// A `runtime`-tier match normally needs no caveat at all, so without
// this the strongest warning available would be the one never shown.
var mismatch = new TruthAlignment { Source = AlignmentSource.LocalMismatch };
var caveat = ManifestConverter.DescribeCaveat(MatchTier.Runtime, 0.0, mismatch);
Assert.NotNull(caveat);
Assert.Contains("does not match", caveat, StringComparison.Ordinal);
// It outranks `loose` too, which would otherwise have claimed the slot
// with the weaker statement — that the runtimes differ, not the audio.
var looseCaveat = ManifestConverter.DescribeCaveat(MatchTier.Loose, 0.0, mismatch);
Assert.Contains("does not match", looseCaveat!, StringComparison.Ordinal);
// A clean local alignment keeps the existing behaviour: a shift is
// explained, an aligned match needs nothing.
var local = new TruthAlignment { Source = AlignmentSource.Local };
Assert.Contains("shifted by", ManifestConverter.DescribeCaveat(MatchTier.Audio, 9.29, local)!, StringComparison.Ordinal);
Assert.Null(ManifestConverter.DescribeCaveat(MatchTier.Audio, 0.0, local));
}
private static string Signature(byte[] source, int start, int count)
=> AudioSignature.VersionPrefix + Convert.ToBase64String(source, start, count);
private static string PseudoRandomSignature(int frames, int seed)
{
var bytes = new byte[frames];
var state = (uint)seed;
for (var i = 0; i < frames; i++)
{
state = (state * 1664525u) + 1013904223u;
bytes[i] = (byte)((((state >> 16) % AudioSignature.NumBands) << 2) | ((state >> 8) & 0x03));
}
return AudioSignature.VersionPrefix + Convert.ToBase64String(bytes);
}
}