using System.Diagnostics; using Flaro.Core.Delivery; using Flaro.Core.Library; namespace Flaro.Media.Tests; /// Dieselbe Zeile, diesmal wirklich ausgeführt. /// /// /// /// Weil die reine Prüfung nur die Form zeigt. Sie sagt, dass /// -hls_segment_type fmp4 in der Zeile steht — nicht, dass ffmpeg die /// Kombination annimmt. Genau dort sitzen die teuren Fehler: eine Option, die /// für sich richtig aussieht und in Verbindung mit einer anderen zum Abbruch /// führt. /// /// /// Übersprungen, wo kein ffmpeg liegt. Eine Prüfung, die auf einem /// Rechner ohne Werkzeug rot wird, sagt nichts über den Code. /// /// public class HlsLaufTests { private string _ordner = ""; [SetUp] public void Anlegen() { if (!Testdateien.FfmpegVorhanden) Assert.Ignore("Ohne ffmpeg nicht prüfbar."); _ordner = Directory.CreateDirectory( Path.Combine(Path.GetTempPath(), "flaro-hls-" + Guid.CreateVersion7().ToString("N"))).FullName; } [TearDown] public void Aufraeumen() { try { Directory.Delete(_ordner, recursive: true); } catch (IOException) { } } private static readonly DeviceProfile Browser = new( Containers: new HashSet(StringComparer.OrdinalIgnoreCase) { "mp4" }, VideoCodecs: new HashSet(StringComparer.OrdinalIgnoreCase) { "h264", "avc1" }, AudioCodecs: new HashSet(StringComparer.OrdinalIgnoreCase) { "aac", "mp3" }); /// Führt die gebaute Zeile aus und gibt den Zielordner zurück. private string Laufen(string quelle, int? hoehe = null, int? tonspur = null, double ab = 0) { var spuren = Spuren(quelle); var ziel = Directory.CreateDirectory(Path.Combine(_ordner, "aus" + Guid.NewGuid().ToString("N")[..6])).FullName; var plan = Stromentscheid.Fuer(Path.GetExtension(quelle).TrimStart('.'), spuren, Browser, hoehe); var argumente = HlsArgumente.Fuer(new Stromanfrage(quelle, ziel, spuren, plan, tonspur, ab)); var start = new ProcessStartInfo(Testdateien.Suchen("ffmpeg")!) { RedirectStandardError = true, UseShellExecute = false, }; foreach (var a in argumente) start.ArgumentList.Add(a); using var prozess = Process.Start(start)!; var fehler = prozess.StandardError.ReadToEnd(); prozess.WaitForExit(); Assert.That(prozess.ExitCode, Is.Zero, $"ffmpeg lehnte die gebaute Zeile ab:\n{string.Join(' ', argumente)}\n\n{fehler}"); return ziel; } /// Was wirklich in der Datei steht — nicht, was wir annehmen. private static List Spuren(string datei) { var start = new ProcessStartInfo(Testdateien.Suchen("ffprobe")!) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; foreach (var a in new[] { "-v", "error", "-show_entries", "stream=index,codec_type,codec_name,height", "-of", "csv=p=0", datei, }) { start.ArgumentList.Add(a); } using var prozess = Process.Start(start)!; var ausgabe = prozess.StandardOutput.ReadToEnd(); prozess.WaitForExit(); var spuren = new List(); foreach (var zeile in ausgabe.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { var teile = zeile.Trim().Split(','); if (teile.Length < 3) continue; MediaStreamKind? art = teile[2] switch { "video" => MediaStreamKind.Video, "audio" => MediaStreamKind.Audio, "subtitle" => MediaStreamKind.Subtitle, // Anhänge und Datenströme sind hier nichts, womit sich rechnen // liesse — sie zu einer Art zu erklären wäre eine Erfindung. _ => null, }; if (art is null) continue; int? hoehe = teile.Length > 3 && int.TryParse(teile[3], out var h) ? h : null; spuren.Add(new ProbedStream( int.Parse(teile[0]), art.Value, teile[1], null, null, false, false, null, null, hoehe)); } return spuren; } // ------------------------------------------------------------------ /// Eine MKV wird umgepackt, und es entstehen abspielbare Segmente. [Test] public void Eine_mkv_wird_zu_Playlist_und_Segmenten() { var quelle = Path.Combine(_ordner, "film.mkv"); Testdateien.MitZweiTonspuren(quelle); var ziel = Laufen(quelle); var playlist = Path.Combine(ziel, "index.m3u8"); Assert.Multiple(() => { Assert.That(File.Exists(playlist), "die Liste muss da sein"); Assert.That(File.Exists(Path.Combine(ziel, "init.mp4")), "fMP4 braucht ein Anfangsstück, sonst kann kein Spieler einsteigen"); Assert.That(Directory.GetFiles(ziel, "*.m4s"), Is.Not.Empty); }); var text = File.ReadAllText(playlist); Assert.Multiple(() => { Assert.That(text, Does.StartWith("#EXTM3U")); Assert.That(text, Does.Contain("#EXT-X-MAP:URI=\"init.mp4\"")); }); } /// Was herauskommt, ist H.264 und AAC — und nichts anderes. /// /// /// Das ist der eigentliche Punkt. Die Quelle hat eine Tonspur, die /// kein Browser kennt; wäre sie kopiert worden, bliebe der Film stumm — /// und genau so war es vorher. /// [Test] public void Aus_untauglichem_Ton_wird_AAC() { var quelle = Path.Combine(_ordner, "dts.mkv"); Testdateien.MitUntauglichemTon(quelle); var ziel = Laufen(quelle, tonspur: 2); // die DTS-Spur ausdrücklich wählen var codecs = Spuren(Path.Combine(ziel, "index.m3u8")).Select(s => s.Codec).ToList(); Assert.Multiple(() => { Assert.That(codecs, Does.Contain("h264")); Assert.That(codecs, Does.Contain("aac")); Assert.That(codecs, Does.Not.Contain("dts")); }); } /// Herunterskalieren ändert die Bildhöhe wirklich. [Test] public void Herunterskalieren_aendert_die_Hoehe() { var quelle = Path.Combine(_ordner, "gross.mkv"); Gross(quelle); var ziel = Laufen(quelle, hoehe: 120); var bild = Spuren(Path.Combine(ziel, "index.m3u8")) .First(s => s.Kind == MediaStreamKind.Video); Assert.That(bild.Height, Is.EqualTo(120)); } /// Ein Sprung mitten hinein liefert ebenfalls Segmente. [Test] public void Ein_Sprungpunkt_liefert_den_Rest_des_Films() { var quelle = Path.Combine(_ordner, "lang.mkv"); Lang(quelle); var ziel = Laufen(quelle, ab: 4); Assert.That(Directory.GetFiles(ziel, "*.m4s"), Is.Not.Empty); } private static void Gross(string ziel) => Werkzeug( [ "-y", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=duration=1:size=640x480:rate=10", "-f", "lavfi", "-i", "sine=frequency=440:duration=1", "-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac", ziel, ]); private static void Lang(string ziel) => Werkzeug( [ "-y", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=duration=8:size=160x120:rate=10", "-f", "lavfi", "-i", "sine=frequency=440:duration=8", "-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac", ziel, ]); private static void Werkzeug(string[] argumente) { var start = new ProcessStartInfo(Testdateien.Suchen("ffmpeg")!) { RedirectStandardError = true, UseShellExecute = false, }; foreach (var a in argumente) start.ArgumentList.Add(a); using var prozess = Process.Start(start)!; var fehler = prozess.StandardError.ReadToEnd(); prozess.WaitForExit(); if (prozess.ExitCode != 0) throw new InvalidOperationException(fehler); } }