Files
Flaro/server/tests/Flaro.Media.Tests/BildabtasterTests.cs
T
ScarriffleandClaude Opus 5 70ab4cc421 Mein Prüfbild war dunkler als jeder echte Abspann
Die Abspannsuche lief gegen eine feste Schwelle von 35. Die stammte aus
einem erzeugten Prüfbild mit 0,8 Prozent weisser Fläche — Helligkeit 18,
weit unter 35, alles schien zu stimmen. Ein echter Abspann hat mehr
Schrift.

Nachgemessen an 1080p mit zwei Spalten Namen, durch dieselbe ffmpeg-Kette
wie im Betrieb:

    Handlung                              126
    Abspann, realistische Schriftdichte     32
    Abspann, dicht gesetzt                  47

Die Fünfunddreissig lag mitten zwischen den beiden Abspannfällen und
schnitt sie auseinander. Dasselbe noch einmal an der erzeugten Prüfdatei,
nachdem ihr Abspann realistische Dichte bekam: 40,2 — und die Prüfung,
die vorher grün war, wurde rot. Das ist der Fehler, lokal reproduziert.

**Dunkel ist keine absolute Grösse.** Entscheidend ist nicht, wie dunkel
der Abspann ist, sondern dass er viel dunkler ist als das, was davor
lief. Gemessen wird jetzt gegen den Grundton des Films — das obere
Viertel der Proben, mal 0,55. Die alte Schwelle bleibt als Boden, damit
ein durchweg dämmriger Film nicht in Fehlalarme rutscht.

**Und die Logotafel.** Die Suche verlangte, dass die allerletzte Probe
dunkel ist. Hinter dem Abspann steht bei vielen Produktionen noch eine
Tafel — Sender, Studio, Förderer —, oft hell. Ein einziges weisses
Schlussbild machte damit den ganzen Abspann unsichtbar. Zwanzig Sekunden
sind jetzt erlaubt, in Sekunden gezählt und nicht in Proben: wie viele
Messpunkte auf eine Tafel fallen, hängt allein an der Schlüsselbilddichte
der Datei.

**Ein Lauf ohne Fund sagt jetzt, warum.** Anzahl Proben, Schwelle,
letzte Probe — ohne diese drei Zahlen ist „kein Abspann erkannt" keine
Auskunft, und man rät wieder.

Sieben neue Prüfungen mit den gemessenen Werten. Die beiden
entscheidenden sind mit der alten Schwelle rot; gegengeprüft, nicht
behauptet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 21:57:39 +02:00

187 lines
6.9 KiB
C#

using System.Diagnostics;
using Flaro.Core.Media;
using Flaro.Media;
using Microsoft.Extensions.Logging.Abstractions;
namespace Flaro.Media.Tests;
/// <summary>Die Ausgabe des Abtasters lesen — ohne ffmpeg.</summary>
public class BildabtasterLesenTests
{
[Test]
public void Zeit_und_Wert_werden_zu_Proben()
{
var ausgabe = """
frame:0 pts:30720 pts_time:2.5
lavfi.signalstats.YAVG=125.065
frame:1 pts:61440 pts_time:5
lavfi.signalstats.YAVG=18.4
""";
var proben = Bildabtaster.Lesen(ausgabe, versatz: 60);
Assert.That(proben, Is.EqualTo(new[]
{
new Bildprobe(62.5, 125.065),
new Bildprobe(65, 18.4),
}));
}
/// <summary>Ohne Zeitstempel kein Messpunkt.</summary>
///
/// <remarks>
/// Eine Wertzeile ohne vorangehende Zeitzeile gehört zu keinem
/// Zeitpunkt. Sie trotzdem zu nehmen hiesse, sie der vorigen Zeit
/// zuzuschlagen — und damit eine Messung zu erfinden.
/// </remarks>
[Test]
public void Ein_Wert_ohne_Zeit_faellt_heraus()
{
Assert.That(Bildabtaster.Lesen("lavfi.signalstats.YAVG=18.4\n", 0), Is.Empty);
}
[Test]
public void Leere_Ausgabe_ergibt_nichts()
{
Assert.That(Bildabtaster.Lesen("", 0), Is.Empty);
}
}
/// <summary>Den ganzen Weg an einer erzeugten Datei.</summary>
///
/// <remarks>
/// <para>
/// <b>Die einzige Prüfung hier, die ffmpeg braucht</b> — und sie ist es
/// wert: zwischen „der Filtergraph ist richtig geschrieben" und „die
/// Zahlen kommen an der richtigen Stelle heraus" liegt alles, was man
/// sonst erst am Bestand merkt.
/// </para>
/// <para>
/// Die Datei entsteht im Test: 90 Sekunden Bild, danach 60 Sekunden weisse
/// Schrift auf schwarzem Grund. Fehlt ffmpeg, wird übersprungen statt
/// rotgemeldet — ein fehlendes Werkzeug ist kein Fehler im Quelltext.
/// </para>
/// </remarks>
public class BildabtasterLaufTests
{
private string _ordner = "";
private string _datei = "";
[OneTimeSetUp]
public void Erzeugen()
{
_ordner = Directory.CreateDirectory(Path.Combine(
Path.GetTempPath(), "flaro-abspann-" + Guid.CreateVersion7().ToString("N"))).FullName;
_datei = Path.Combine(_ordner, "pruef.mp4");
var start = new ProcessStartInfo("ffmpeg")
{
ArgumentList =
{
"-hide_banner", "-loglevel", "error", "-y",
"-f", "lavfi", "-i", "testsrc2=size=640x360:rate=24:duration=90",
"-f", "lavfi", "-i", "color=c=black:s=640x360:r=24:d=60",
// **So viel weisse Fläche hat ein echter Abspann.**
// Nachgemessen an 1080p mit zwei Spalten Namen: rund ein
// Zehntel der Fläche, und damit YAVG um 45 statt um 18.
// Mit dem schmalen Balken von vorher war dieses Prüfbild
// dunkler als jeder wirkliche Abspann — und genau deshalb
// blieb die zu enge Schwelle so lange unbemerkt.
"-f", "lavfi", "-i", "color=c=white:s=400x76:r=24:d=60",
"-filter_complex",
"[1][2]overlay=x=230:y='H-mod(t*45,H+h)'[cr];[0][cr]concat=n=2:v=1:a=0[v]",
"-map", "[v]", "-c:v", "libx264", "-preset", "ultrafast",
"-g", "48", "-pix_fmt", "yuv420p", _datei,
},
RedirectStandardError = true,
UseShellExecute = false,
};
try
{
using var prozess = Process.Start(start);
prozess!.WaitForExit(60_000);
if (prozess.ExitCode != 0 || !File.Exists(_datei))
Assert.Ignore("ffmpeg konnte die Prüfdatei nicht erzeugen.");
}
catch (Exception)
{
Assert.Ignore("Ohne ffmpeg lässt sich das nicht prüfen.");
}
}
[OneTimeTearDown]
public void Aufraeumen()
{
try { Directory.Delete(_ordner, recursive: true); } catch (IOException) { }
}
private static Bildabtaster Abtaster() =>
new(FfmpegOptions.FromEnvironment(), NullLogger<Bildabtaster>.Instance);
/// <summary>Die Kante liegt da, wo sie erzeugt wurde: bei 90 Sekunden.</summary>
[Test]
public async Task Der_Abspann_wird_gefunden()
{
var proben = await Abtaster().UeberblickAsync(_datei, ab: 30, dauer: 120);
Assert.That(proben, Is.Not.Empty, "der Filtergraph muss Zahlen liefern");
var grob = Abspannkante.Suchen(proben, Abspannregeln.Vorgabe);
Assert.That(grob, Is.Not.Null, "eine dunkle Strecke bis zum Ende muss auffallen");
var hell = Abspannkante.HellDavor(proben, grob!.Value, Abspannregeln.Vorgabe) ?? 0;
var abtaster = Abtaster();
var kante = await Abspannkante.VerfeinernAsync(
hell, grob.Value, Abspannkante.Schwelle(proben, Abspannregeln.Vorgabe),
stelle => abtaster.HelligkeitAsync(_datei, stelle));
Assert.That(kante, Is.EqualTo(90).Within(4),
"erzeugt wurde der Übergang bei genau 90 Sekunden");
}
/// <summary>Im hellen Teil gibt es nichts zu finden.</summary>
[Test]
public async Task Im_hellen_Teil_kein_Fund()
{
var proben = await Abtaster().UeberblickAsync(_datei, ab: 0, dauer: 80);
Assert.That(Abspannkante.Suchen(proben, Abspannregeln.Vorgabe), Is.Null);
}
/// <summary>Eine einzelne Sekunde lässt sich messen — und liegt richtig.</summary>
///
/// <remarks>
/// <para>
/// <b>Hier stand der Fehler, und hier wurde er sichtbar.</b> Diese
/// Prüfung verlangte früher, dass der Abspann unter der festen
/// Schwelle von 35 liegt — mit dem schmalen Balken von damals lag er
/// bei 18, und alles schien zu stimmen. Mit realistischer
/// Schriftdichte misst dieselbe Stelle <b>40</b>. Die feste Schwelle
/// hätte diesen Abspann also für Handlung gehalten, und genau das
/// geschah im Bestand.
/// </para>
/// <para>
/// Gemessen wird deshalb gegen die Schwelle, die der Film selbst
/// vorgibt — und die liegt hier zwischen 40 und der Handlung.
/// </para>
/// </remarks>
[Test]
public async Task Eine_Sekunde_im_Abspann_liegt_unter_der_Schwelle()
{
var proben = await Abtaster().UeberblickAsync(_datei, ab: 30, dauer: 120);
var schwelle = Abspannkante.Schwelle(proben, Abspannregeln.Vorgabe);
var dunkel = await Abtaster().HelligkeitAsync(_datei, bei: 120);
var hell = await Abtaster().HelligkeitAsync(_datei, bei: 40);
Assert.Multiple(() =>
{
Assert.That(dunkel, Is.Not.Null.And.LessThan(schwelle));
Assert.That(hell, Is.Not.Null.And.GreaterThan(schwelle));
Assert.That(dunkel, Is.GreaterThan(Abspannregeln.Vorgabe.Dunkelschwelle),
"dieser Abspann ist heller als der alte feste Wert — darum ging es");
});
}
}