using System.Diagnostics;
using System.Globalization;
using Flaro.Media;
using Microsoft.Extensions.Logging.Abstractions;
namespace Flaro.Media.Tests;
/// Sagt die Keyframesuche dasselbe wie ffmpeg?
///
///
///
/// Der Fehler, für den diese Prüfung steht. Die Suche las mit
/// -read_intervals 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.
///
///
/// Gegen ffmpeg geprüft, nicht gegen eine Erwartung. 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 -copyts, damit die Zeit absolut
/// bleibt.
///
///
public class KeyframesTests
{
private string _ordner = "";
private string _datei = "";
/// Vier Sekunden — so kodiert, was von Streamingdiensten kommt.
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) { }
}
/// Die Stelle, an der ffmpeg beim Kopieren wirklich anfängt.
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;
}
/// Jeder dieser Werte lag vorher um eine ganze Bildgruppe daneben.
[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 Keyframes(
FfmpegOptions.FromEnvironment(), NullLogger.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");
}
/// Und der Fund ist nie später als der Wunsch.
[TestCase(50.5)]
[TestCase(73.3)]
public async Task Nie_hinter_dem_Wunsch(double wunsch)
{
var suche = new Keyframes(
FfmpegOptions.FromEnvironment(), NullLogger.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; }
}
}