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
@@ -216,4 +216,84 @@ public class MarkenTests
|
||||
"die Folgenzahl löst den nächsten Lauf aus");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ein Film trägt seine Abspannmarke genauso.</summary>
|
||||
[Test]
|
||||
public async Task Ein_Film_bekommt_seine_Marke()
|
||||
{
|
||||
Datei("Matrix (1999)/Matrix (1999).mkv");
|
||||
|
||||
using var server = new FlaroServer();
|
||||
var client = await Eingelesen(server, "Movies");
|
||||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||||
var film = filme!.Items[0].Id;
|
||||
|
||||
await server.InDerDatenbank(async db =>
|
||||
{
|
||||
db.Marken.Add(new Marke
|
||||
{
|
||||
MovieId = film, Art = Markenart.Abspann,
|
||||
VonSekunden = 6900, BisSekunden = 7200, ErkanntAm = DateTimeOffset.UtcNow,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var einzeln = await client.GetFromJsonAsync<MovieResponse>($"/api/v1/movies/{film}");
|
||||
var liste = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(einzeln!.Markers!.Single().Kind, Is.EqualTo("credits"));
|
||||
Assert.That(einzeln.Markers!.Single().StartSeconds, Is.EqualTo(6900));
|
||||
Assert.That(liste!.Items[0].Markers, Has.Count.EqualTo(1), "auch in der Liste");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Eine falsche Marke lässt sich wieder wegnehmen.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Ohne diesen Weg wäre die Erkennung eine Blackbox, die manchmal recht
|
||||
/// hat: ein Fehlgriff bliebe stehen, und niemand könnte etwas dagegen
|
||||
/// tun ausser die Datenbank anzufassen.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public async Task Eine_falsche_Filmmarke_laesst_sich_verwerfen()
|
||||
{
|
||||
Datei("Matrix (1999)/Matrix (1999).mkv");
|
||||
|
||||
using var server = new FlaroServer();
|
||||
var client = await Eingelesen(server, "Movies");
|
||||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||||
var film = filme!.Items[0].Id;
|
||||
|
||||
await server.InDerDatenbank(async db =>
|
||||
{
|
||||
db.Marken.Add(new Marke
|
||||
{
|
||||
MovieId = film, Art = Markenart.Abspann,
|
||||
VonSekunden = 60, BisSekunden = 7200, ErkanntAm = DateTimeOffset.UtcNow,
|
||||
});
|
||||
db.Markensuchen.Add(new Markensuche
|
||||
{
|
||||
MovieId = film, Art = Markenart.Abspann, Folgen = 1,
|
||||
GelaufenAm = DateTimeOffset.UtcNow,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var weg = await client.DeleteAsync($"/api/v1/movies/{film}/marken");
|
||||
var danach = await client.GetFromJsonAsync<MovieResponse>($"/api/v1/movies/{film}");
|
||||
|
||||
var vermerke = 0;
|
||||
await server.InDerDatenbank(async db =>
|
||||
vermerke = await db.Markensuchen.CountAsync(x => x.MovieId == film));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
|
||||
Assert.That(danach!.Markers, Is.Empty);
|
||||
Assert.That(vermerke, Is.EqualTo(1),
|
||||
"der Vermerk bleibt, sonst legte der nächste Scan dieselbe Marke wieder an");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Flaro.Media;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Flaro.Media.Tests;
|
||||
|
||||
/// <summary>Sagt die Schlüsselbildsuche dasselbe wie ffmpeg?</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Der Fehler, für den diese Prüfung steht.</b> Die Suche las mit
|
||||
/// <c>-read_intervals</c> bis eine halbe Sekunde hinter den Wunsch — und
|
||||
/// ffprobe hörte damit regelmässig eine ganze Bildgruppe zu früh auf. Der
|
||||
/// Server meldete dem Spieler dann einen Anfang, der vier Sekunden vor dem
|
||||
/// lag, an dem der Strom wirklich begann; die Untertitel kamen vier
|
||||
/// Sekunden zu spät.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Gegen ffmpeg geprüft, nicht gegen eine Erwartung.</b> Was zählt, ist
|
||||
/// nicht, ob die Zahl „schön" ist, sondern ob sie mit der Stelle
|
||||
/// übereinstimmt, an der der Strom tatsächlich anfängt. Die holt derselbe
|
||||
/// Aufruf wie im Betrieb, nur mit <c>-copyts</c>, damit die Zeit absolut
|
||||
/// bleibt.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class SchluesselbilderTests
|
||||
{
|
||||
private string _ordner = "";
|
||||
private string _datei = "";
|
||||
|
||||
/// <summary>Vier Sekunden — so kodiert, was von Streamingdiensten kommt.</summary>
|
||||
private const double Bildgruppe = 4;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void Erzeugen()
|
||||
{
|
||||
_ordner = Directory.CreateDirectory(Path.Combine(
|
||||
Path.GetTempPath(), "flaro-keyframe-" + Guid.CreateVersion7().ToString("N"))).FullName;
|
||||
_datei = Path.Combine(_ordner, "quelle.mp4");
|
||||
|
||||
var gelungen = Starten("ffmpeg",
|
||||
"-hide_banner", "-loglevel", "error", "-y",
|
||||
"-f", "lavfi", "-i", "testsrc2=size=320x180:rate=25:duration=120",
|
||||
"-c:v", "libx264", "-preset", "ultrafast",
|
||||
"-g", "100", "-pix_fmt", "yuv420p", _datei);
|
||||
|
||||
if (!gelungen || !File.Exists(_datei))
|
||||
Assert.Ignore("Ohne ffmpeg lässt sich das nicht prüfen.");
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public void Aufraeumen()
|
||||
{
|
||||
try { Directory.Delete(_ordner, recursive: true); } catch (IOException) { }
|
||||
}
|
||||
|
||||
/// <summary>Die Stelle, an der ffmpeg beim Kopieren wirklich anfängt.</summary>
|
||||
private double? EchterAnfang(double ab)
|
||||
{
|
||||
var ziel = Path.Combine(_ordner, "schnitt.mp4");
|
||||
var gelungen = Starten("ffmpeg",
|
||||
"-hide_banner", "-loglevel", "error", "-y",
|
||||
"-noaccurate_seek", "-ss", ab.ToString("0.###", CultureInfo.InvariantCulture),
|
||||
"-i", _datei, "-map", "0:0", "-c", "copy", "-copyts", "-f", "mp4", ziel);
|
||||
if (!gelungen) return null;
|
||||
|
||||
var ausgabe = Lesen("ffprobe",
|
||||
"-v", "error", "-select_streams", "v:0",
|
||||
"-show_entries", "stream=start_time", "-of", "csv=p=0", ziel);
|
||||
|
||||
return double.TryParse(ausgabe?.Trim(), NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture, out var wert) ? wert : null;
|
||||
}
|
||||
|
||||
/// <summary>Jeder dieser Werte lag vorher um eine ganze Bildgruppe daneben.</summary>
|
||||
[TestCase(50.0)]
|
||||
[TestCase(50.5)]
|
||||
[TestCase(52.0)]
|
||||
[TestCase(52.1)]
|
||||
[TestCase(56.0)]
|
||||
[TestCase(73.3)]
|
||||
[TestCase(100.2)]
|
||||
public async Task Die_Suche_trifft_den_Anfang_des_Stroms(double wunsch)
|
||||
{
|
||||
var suche = new Schluesselbilder(
|
||||
FfmpegOptions.FromEnvironment(), NullLogger<Schluesselbilder>.Instance);
|
||||
|
||||
var gemeldet = await suche.DavorAsync(_datei, wunsch);
|
||||
var echt = EchterAnfang(wunsch);
|
||||
|
||||
if (echt is null) Assert.Ignore("ffmpeg lieferte keinen Anfang.");
|
||||
var stelle = echt!.Value;
|
||||
|
||||
Assert.That(gemeldet, Is.EqualTo(stelle).Within(0.05),
|
||||
$"gemeldet {gemeldet}, tatsächlich {stelle} — die Differenz landet "
|
||||
+ "eins zu eins im Versatz der Untertitel");
|
||||
}
|
||||
|
||||
/// <summary>Und der Fund ist nie später als der Wunsch.</summary>
|
||||
[TestCase(50.5)]
|
||||
[TestCase(73.3)]
|
||||
public async Task Nie_hinter_dem_Wunsch(double wunsch)
|
||||
{
|
||||
var suche = new Schluesselbilder(
|
||||
FfmpegOptions.FromEnvironment(), NullLogger<Schluesselbilder>.Instance);
|
||||
|
||||
Assert.That(await suche.DavorAsync(_datei, wunsch), Is.LessThanOrEqualTo(wunsch));
|
||||
}
|
||||
|
||||
// ---------- Werkzeug ----------
|
||||
|
||||
private static bool Starten(string programm, params string[] argumente)
|
||||
{
|
||||
var start = new ProcessStartInfo(programm)
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
foreach (var a in argumente) start.ArgumentList.Add(a);
|
||||
|
||||
try
|
||||
{
|
||||
using var prozess = Process.Start(start);
|
||||
if (prozess is null) return false;
|
||||
prozess.WaitForExit(120_000);
|
||||
return prozess.ExitCode == 0;
|
||||
}
|
||||
catch (Exception) { return false; }
|
||||
}
|
||||
|
||||
private static string? Lesen(string programm, params string[] argumente)
|
||||
{
|
||||
var start = new ProcessStartInfo(programm)
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
foreach (var a in argumente) start.ArgumentList.Add(a);
|
||||
|
||||
try
|
||||
{
|
||||
using var prozess = Process.Start(start);
|
||||
if (prozess is null) return null;
|
||||
var text = prozess.StandardOutput.ReadToEnd();
|
||||
prozess.WaitForExit(60_000);
|
||||
return prozess.ExitCode == 0 ? text : null;
|
||||
}
|
||||
catch (Exception) { return null; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user