Files
Flaro/server/tests/Flaro.Media.Tests/BildabtasterTests.cs
T
ScarriffleandClaude Opus 5 f2752e6c05 Ein doppelter Platzhalter riss jede Serie mit
`log.LogInformation("… {Folgen} … {Folgen}.", a, b, c, d, e)` — sechs
Platzhalter, fünf Argumente. Vorlagen sind positionsbezogen: auch ein
wiederholter Name verbraucht ein weiteres Argument. Der Logger warf eine
Ausnahme, die riss `StaffelMeldenAsync` mit, und die wiederum den ganzen
Serienlauf. Gefunden war dann alles umsonst — „A Knight of the Seven
Kingdoms" und „Alice in Borderland" haben ihre Marken deshalb verloren,
nachdem sie längst berechnet waren.

**CA2017 hatte das gemeldet.** Als Warnung, unter hundert anderen Zeilen,
und ich habe beim Bauen nach „warning CS" gegriffen. Eine neue
`Directory.Build.props` macht genau diese eine Regel zum Fehler — nicht
alle Warnungen, das wäre eine andere Entscheidung. Gegengeprüft: mit der
kaputten Zeile bricht der Bau jetzt ab.

**Und der zweite Fund, aus „Alice im Wunderland".** Die Zeile meldete
„Schwelle 247,6", und das kann bei 0–255 nicht entstehen: die Schwelle
ist Grundton mal 0,55, das wären 450 gewesen. `signalstats` rechnet in
der Skala des Bildformats — bei acht Bit 0–255, bei zehn Bit 0–1023.
Gemessen am selben erzeugten Bild: YAVG 127 gegen YAVG 500. Alle
Schwellen hier sind an acht Bit gemessen; bei einer 10-Bit-Datei bedeutete
dieselbe Zahl ein Viertel.

`format=gray` vor `signalstats` bringt jede Datei auf dieselbe Skala. Es
rechnet dabei auch den Fernsehbereich 16–235 auf 0–255 um — linear, und
die dunklen Werte trifft das am stärksten: der gemessene Abspann liegt
jetzt bei 28 statt bei 40. Der Boden wandert entsprechend von 35 auf 22.

Dazu: die Schlusszeile je Serie trennt Vorspann und Abspann, statt eine
Zahl aus beidem zu machen — „14 Marken" beantwortet nicht, ob die Serie
ihre Vorspänne bekommen hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 14:06:16 +02:00

266 lines
9.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
});
}
}
/// <summary>Dieselbe Helligkeit, gleich welche Bittiefe die Datei hat.</summary>
///
/// <remarks>
/// <para>
/// <b>Der Fehler, für den diese Prüfung steht.</b> <c>signalstats</c>
/// rechnet in der Skala des Bildformats: bei acht Bit 0–255, bei zehn Bit
/// 0–1023. Alle Schwellen der Abspannsuche sind an acht Bit gemessen —
/// bei einer 10-Bit-Datei bedeutete dieselbe Zahl ein Viertel.
/// </para>
/// <para>
/// Aufgefallen ist es an einer Protokollzeile: „Schwelle 247,6". Die kann
/// bei 0–255 nicht entstehen, denn sie ist Grundton mal 0,55 — das wären
/// 450 gewesen.
/// </para>
/// </remarks>
public class BildabtasterBittiefeTests
{
private string _ordner = "";
[OneTimeSetUp]
public void Erzeugen() =>
_ordner = Directory.CreateDirectory(Path.Combine(
Path.GetTempPath(), "flaro-bittiefe-" + Guid.CreateVersion7().ToString("N"))).FullName;
[OneTimeTearDown]
public void Aufraeumen()
{
try { Directory.Delete(_ordner, recursive: true); } catch (IOException) { }
}
private string Erzeugen(string pixelformat)
{
var datei = Path.Combine(_ordner, $"{pixelformat}.mp4");
var start = new ProcessStartInfo("ffmpeg")
{
ArgumentList =
{
"-hide_banner", "-loglevel", "error", "-y",
"-f", "lavfi", "-i", "testsrc2=size=320x180:rate=24:duration=4",
"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", pixelformat, 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 kann {pixelformat} hier nicht erzeugen.");
}
catch (Exception)
{
Assert.Ignore("Ohne ffmpeg lässt sich das nicht prüfen.");
}
return datei;
}
[Test]
public async Task Acht_und_zehn_Bit_ergeben_dieselbe_Zahl()
{
var abtaster = new Bildabtaster(
FfmpegOptions.FromEnvironment(), NullLogger<Bildabtaster>.Instance);
var acht = await abtaster.HelligkeitAsync(Erzeugen("yuv420p"), bei: 2);
var zehn = await abtaster.HelligkeitAsync(Erzeugen("yuv420p10le"), bei: 2);
Assert.Multiple(() =>
{
Assert.That(acht, Is.Not.Null);
Assert.That(zehn, Is.Not.Null.And.EqualTo(acht!.Value).Within(3),
"ohne `format=gray` stünde hier das Vierfache");
Assert.That(zehn, Is.LessThan(256), "die Skala muss 0–255 sein");
});
}
}