Vier Sekunden, die ffprobe zu früh aufhörte zu lesen
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8112c15d4d
commit
7673c73757
@@ -0,0 +1,159 @@
|
||||
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",
|
||||
"-f", "lavfi", "-i", "color=c=white:s=180x14: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, 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.</summary>
|
||||
[Test]
|
||||
public async Task Eine_Sekunde_im_Abspann_ist_dunkel()
|
||||
{
|
||||
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(Abspannregeln.Vorgabe.Dunkelschwelle));
|
||||
Assert.That(hell, Is.Not.Null.And.GreaterThan(Abspannregeln.Vorgabe.Dunkelschwelle));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user