using System.Diagnostics; using System.Globalization; using Flaro.Media; using Microsoft.Extensions.Logging.Abstractions; namespace Flaro.Media.Tests; /// Sagt die Schlüsselbildsuche 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 SchluesselbilderTests { 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 Schluesselbilder( 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 Schluesselbilder( 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; } } }