Die Untertitel kamen vier Sekunden zu spät. Nicht an ihnen lag es: der
Server meldete dem Spieler einen Anfang, der vor dem lag, an dem der
Strom wirklich begann, und genau diese Differenz landet eins zu eins im
Versatz.
Die Schlüsselbildsuche las mit `-read_intervals` bis eine halbe Sekunde
hinter den Wunsch. ffprobe bricht am Ende eines Fensters auf Paketebene
ab — das letzte Schlüsselbild darin kam regelmässig gar nicht mehr
heraus, und die Suche gab das davor zurück. Bei Bildgruppen von vier
Sekunden sind das vier Sekunden.
Nachgemessen gegen die Stelle, an der ffmpeg tatsächlich anfängt, an
einer Datei mit Schlüsselbildern alle vier Sekunden:
Wunsch Suche (0,5 s) ffmpeg Abweichung
52,0 48 52,0 −4,0
73,3 68 72,0 −4,0
100,2 96 100,0 −4,0
Mit zehn Sekunden Nachlauf lag dieselbe Messung bei allen sieben Werten
auf 0,000. Was über den Wunsch hinausgelesen wird, verwirft die
Filterung ohnehin.
Die Prüfung vergleicht nicht gegen eine erwartete Zahl, sondern gegen
ffmpeg selbst — mit dem alten Wert fällt sie um.
Dazu der Filmteil der Abspannerkennung: `Abspannkante` als reine
Rechnung mit elf Prüfungen, `Bildabtaster` über ffmpeg, Marken auch an
Filmen, Knöpfe zum Suchen und Verwerfen auf der Filmseite. An einer
erzeugten Datei trifft die Kante auf vier Sekunden genau.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
160 lines
5.3 KiB
C#
160 lines
5.3 KiB
C#
using Flaro.Core.Media;
|
|
|
|
namespace Flaro.Core.Tests;
|
|
|
|
/// <summary>Wo der Abspann eines Films anfängt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Mit erfundenen Messreihen statt mit Filmen.</b> Geprüft wird das
|
|
/// Beurteilen, nicht ob ffmpeg rechnen kann. Die Zahlen stammen aus einer
|
|
/// Messung an einem erzeugten Film: Handlung 125, Abspann 18.
|
|
/// </remarks>
|
|
public class AbspannkanteTests
|
|
{
|
|
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
|
|
|
|
/// <summary>Eine Reihe von Messpunkten im Abstand von zehn Sekunden.</summary>
|
|
///
|
|
/// <param name="abspannAb">Ab wann dunkel. <c>null</c> heisst: nie.</param>
|
|
private static List<Bildprobe> Reihe(double von, double bis, double? abspannAb)
|
|
{
|
|
var proben = new List<Bildprobe>();
|
|
for (var t = von; t <= bis; t += 10)
|
|
proben.Add(new Bildprobe(t, abspannAb is { } ab && t >= ab ? 18 : 125));
|
|
return proben;
|
|
}
|
|
|
|
[Test]
|
|
public void Eine_dunkle_Strecke_bis_zum_Ende_ist_der_Abspann()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: 420);
|
|
|
|
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(420));
|
|
}
|
|
|
|
/// <summary>Ein Film, der hell endet, hat keinen erkennbaren Abspann.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Namen über laufendem Bild — Marvel, Pixar, jede zweite Komödie. Kein
|
|
/// Fund ist hier die richtige Antwort: ein geratener Knopf schnitte
|
|
/// Handlung weg.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Ohne_dunkles_Ende_kein_Fund()
|
|
{
|
|
Assert.That(Abspannkante.Suchen(Reihe(0, 600, abspannAb: null), Regeln), Is.Null);
|
|
}
|
|
|
|
/// <summary>Eine Schwarzblende mitten im Film zählt nicht.</summary>
|
|
[Test]
|
|
public void Eine_Schwarzblende_in_der_Mitte_zaehlt_nicht()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: null);
|
|
for (var i = 20; i < 30; i++) proben[i] = proben[i] with { Helligkeit = 10 };
|
|
|
|
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null,
|
|
"sie hält nicht bis zum Ende durch");
|
|
}
|
|
|
|
/// <summary>Eine Szene nach dem Abspann nimmt den Fund weg.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Lieber keine Marke als eine, die über die Szene hinwegspringt.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Eine_Szene_nach_dem_Abspann_nimmt_den_Fund_weg()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: 420);
|
|
for (var i = proben.Count - 5; i < proben.Count; i++)
|
|
proben[i] = proben[i] with { Helligkeit = 120 };
|
|
|
|
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null);
|
|
}
|
|
|
|
/// <summary>Eine helle Schrifttafel mittendrin bricht die Strecke nicht.</summary>
|
|
[Test]
|
|
public void Eine_kurze_helle_Tafel_bricht_nichts()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: 420);
|
|
// Zwei Proben, also zwanzig Sekunden, sind hell.
|
|
proben[50] = proben[50] with { Helligkeit = 140 };
|
|
proben[51] = proben[51] with { Helligkeit = 140 };
|
|
|
|
Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(420));
|
|
}
|
|
|
|
/// <summary>Zwanzig Sekunden Dunkelheit sind kein Abspann.</summary>
|
|
[Test]
|
|
public void Zu_kurz_zaehlt_nicht()
|
|
{
|
|
Assert.That(Abspannkante.Suchen(Reihe(0, 600, abspannAb: 580), Regeln), Is.Null);
|
|
}
|
|
|
|
/// <summary>Ein halber Film im Dunkeln ist ein Fehlfund.</summary>
|
|
[Test]
|
|
public void Zu_lang_zaehlt_auch_nicht()
|
|
{
|
|
Assert.That(Abspannkante.Suchen(Reihe(0, 3600, abspannAb: 600), Regeln), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void Die_letzte_helle_Probe_begrenzt_die_Suche()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: 420);
|
|
|
|
Assert.That(Abspannkante.HellDavor(proben, 420, Regeln), Is.EqualTo(410));
|
|
}
|
|
|
|
// ---------- Nachmessen ----------
|
|
|
|
/// <summary>Die binäre Suche findet die Kante auf vier Sekunden genau.</summary>
|
|
[Test]
|
|
public async Task Die_Kante_wird_eingegrenzt()
|
|
{
|
|
const double echteKante = 417.3;
|
|
var sonden = 0;
|
|
|
|
var kante = await Abspannkante.VerfeinernAsync(410, 420, Regeln, stelle =>
|
|
{
|
|
sonden++;
|
|
return Task.FromResult<double?>(stelle >= echteKante ? 18 : 125);
|
|
});
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(kante, Is.EqualTo(echteKante).Within(4));
|
|
Assert.That(kante, Is.GreaterThanOrEqualTo(echteKante),
|
|
"lieber ein paar Sekunden Abspann zu viel als eine Sekunde Handlung");
|
|
Assert.That(sonden, Is.LessThanOrEqualTo(4), "zehn Sekunden auf vier: zwei Halbierungen");
|
|
});
|
|
}
|
|
|
|
/// <summary>Antwortet eine Sonde nicht, gilt der grobe Fund.</summary>
|
|
[Test]
|
|
public async Task Ohne_Antwort_bleibt_es_beim_groben_Fund()
|
|
{
|
|
var kante = await Abspannkante.VerfeinernAsync(
|
|
410, 420, Regeln, _ => Task.FromResult<double?>(null));
|
|
|
|
Assert.That(kante, Is.EqualTo(420));
|
|
}
|
|
|
|
/// <summary>Ein schon enger Bereich braucht keine Sonde.</summary>
|
|
[Test]
|
|
public async Task Ein_enger_Bereich_wird_nicht_angefasst()
|
|
{
|
|
var sonden = 0;
|
|
var kante = await Abspannkante.VerfeinernAsync(418, 420, Regeln, _ =>
|
|
{
|
|
sonden++;
|
|
return Task.FromResult<double?>(18);
|
|
});
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(kante, Is.EqualTo(420));
|
|
Assert.That(sonden, Is.Zero);
|
|
});
|
|
}
|
|
}
|