using Flaro.Core.Delivery; using Flaro.Core.Library; namespace Flaro.Media.Tests; /// Die ffmpeg-Zeile für einen laufenden Strom, ohne sie auszuführen. public class HlsArgumenteTests { private static ProbedStream Bild(string codec = "h264", int hoehe = 1080) => new(0, MediaStreamKind.Video, codec, null, null, true, false, null, hoehe * 16 / 9, hoehe); private static ProbedStream Ton(int i, string codec = "eac3", int kanaele = 6) => new(i, MediaStreamKind.Audio, codec, "deu", null, false, false, kanaele, null, null); private static ProbedStream Text(int i) => new(i, MediaStreamKind.Subtitle, "subrip", "deu", null, false, false, null, null, null); /// Was ein gewöhnlicher Browser kann. private static readonly DeviceProfile Browser = new( Containers: new HashSet(StringComparer.OrdinalIgnoreCase) { "mp4", "webm" }, VideoCodecs: new HashSet(StringComparer.OrdinalIgnoreCase) { "h264", "avc1", "vp9" }, AudioCodecs: new HashSet(StringComparer.OrdinalIgnoreCase) { "aac", "mp3", "opus" }); private static string Zeile( IReadOnlyList spuren, string schachtel = "mkv", int? hoehe = null, int? tonspur = null, double ab = 0, Beschleunigung hardware = Beschleunigung.Software, Kodierstufe stufe = Kodierstufe.Ausgewogen) { var plan = Stromentscheid.Fuer(schachtel, spuren, Browser, hoehe); return string.Join(' ', HlsArgumente.Fuer(new Stromanfrage( "/medien/film.mkv", "/kratz/sitzung", spuren, plan, tonspur, ab, hardware, stufe))); } // ------------------------------------------------------------------ // Umpacken: das Bild bleibt unberührt // ------------------------------------------------------------------ /// Eine MKV mit H.264 wird umgepackt, nicht neu gerechnet. /// /// /// Der häufigste Fall und der billigste. Der Browser kennt die /// Schachtel nicht, das Bild darin sehr wohl. /// [Test] public void Eine_mkv_mit_bekanntem_Bild_wird_nur_umgepackt() { var zeile = Zeile([Bild(), Ton(1, "aac")]); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-c:v copy")); Assert.That(zeile, Does.Not.Contain("libx264")); Assert.That(zeile, Does.Contain("-f hls")); }); } /// Der Ton wird immer nach AAC gerechnet — das war der Fehler. /// /// /// Hier lag der Grund, aus dem gar nichts lief. Beim Umpacken für /// den Download wird der Ton kopiert, weil Apples Profil EAC3 erlaubt. Der /// Browser bekam dieselbe Entscheidung — und mit ihr eine Datei, deren Ton /// er nicht öffnen kann. /// [Test] public void Der_Ton_wird_immer_nach_AAC_gerechnet() { var zeile = Zeile([Bild(), Ton(1, "eac3")]); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-c:a aac")); Assert.That(zeile, Does.Contain("-ac 2"), "5.1 wird auf Stereo gefaltet"); Assert.That(zeile, Does.Not.Contain("-c:a copy")); }); } // ------------------------------------------------------------------ // Umkodieren: unbekanntes Bild oder kleinere Auflösung // ------------------------------------------------------------------ /// HEVC kann kein Browser — da hilft Umpacken nichts. [Test] public void Ein_unbekannter_Bildcodec_wird_neu_gerechnet() { var zeile = Zeile([Bild("hevc"), Ton(1, "aac")]); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-c:v libx264")); Assert.That(zeile, Does.Not.Contain("-c:v copy")); }); } /// Ein 4K-Film soll sich auch in 480p ansehen lassen. [Test] public void Eine_kleinere_Aufloesung_wird_herunterskaliert() { var zeile = Zeile([Bild("h264", 2160), Ton(1, "aac")], hoehe: 480); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-vf scale=-2:480"), "-2 und nicht -1: die Breite muss gerade sein, sonst lehnt libx264 ab"); Assert.That(zeile, Does.Contain("-c:v libx264")); }); } /// Hochrechnen ist keine Stufe. /// /// /// Ein 720p-Film in „1080p" kostet mehr Last und mehr Daten für kein /// besseres Bild. Die Stufe wird dann stillschweigend übergangen. /// [Test] public void Eine_Stufe_ueber_der_Quelle_wird_nicht_hochgerechnet() { var plan = Stromentscheid.Fuer("mp4", [Bild("h264", 720)], Browser, 1080); Assert.Multiple(() => { Assert.That(plan.Hoehe, Is.Null); Assert.That(plan.Art, Is.EqualTo(Stromart.Direkt)); }); } // ------------------------------------------------------------------ // Spurwahl, Springen, Hardware // ------------------------------------------------------------------ [Test] public void Die_gewaehlte_Tonspur_wird_abgebildet() { var zeile = Zeile([Bild(), Ton(1), Ton(2), Ton(3)], tonspur: 3); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-map 0:0")); Assert.That(zeile, Does.Contain("-map 0:3")); Assert.That(zeile, Does.Not.Contain("-map 0:1")); }); } /// Eine Spur, die es nicht gibt, macht den Film nicht stumm. [Test] public void Eine_unbekannte_Tonspur_faellt_auf_die_erste_zurueck() { var zeile = Zeile([Bild(), Ton(1), Ton(2)], tonspur: 99); Assert.That(zeile, Does.Contain("-map 0:1")); } /// Der Sprungpunkt steht vor der Eingabe. /// /// /// Hinter -i wäre es ein genauer, aber langsamer Sprung: /// ffmpeg läse die Datei bis dorthin durch. Bei Minute 90 eines Films /// dauert das länger als das Ansehen. /// [Test] public void Der_Sprungpunkt_steht_vor_der_Eingabe() { var argumente = HlsArgumente.Fuer(new Stromanfrage( "/medien/film.mkv", "/kratz/sitzung", [Bild(), Ton(1)], Stromentscheid.Fuer("mkv", [Bild(), Ton(1)], Browser), Ab: 930.5)); var ss = argumente.IndexOf("-ss"); var i = argumente.IndexOf("-i"); Assert.Multiple(() => { Assert.That(ss, Is.GreaterThanOrEqualTo(0)); Assert.That(ss, Is.LessThan(i)); Assert.That(argumente[ss + 1], Is.EqualTo("930.5"), "mit Punkt, nicht mit Komma — ffmpeg liest keine deutsche Schreibweise"); }); } [Test] public void Ohne_Sprungpunkt_steht_kein_ss_in_der_Zeile() { Assert.That(Zeile([Bild(), Ton(1)]), Does.Not.Contain("-ss")); } /// Die Beschleunigung gehört vor die Eingabe, sonst wirkt sie nicht. [Test] public void Vaapi_steht_vor_der_Eingabe_und_nur_beim_Rechnen() { var mitRechnen = HlsArgumente.Fuer(new Stromanfrage( "/medien/film.mkv", "/kratz/sitzung", [Bild("hevc"), Ton(1)], Stromentscheid.Fuer("mkv", [Bild("hevc"), Ton(1)], Browser), Hardware: Beschleunigung.Vaapi)); Assert.Multiple(() => { Assert.That(mitRechnen.IndexOf("-hwaccel"), Is.LessThan(mitRechnen.IndexOf("-i"))); Assert.That(string.Join(' ', mitRechnen), Does.Contain("h264_vaapi")); // Beim reinen Umpacken wird nichts dekodiert — die Grafikkarte // hätte dabei nichts zu tun. Assert.That(Zeile([Bild(), Ton(1)], hardware: Beschleunigung.Vaapi), Does.Not.Contain("-hwaccel")); }); } // ------------------------------------------------------------------ // Form der Ausgabe // ------------------------------------------------------------------ [Test] public void Die_Segmente_sind_unabhaengig_und_die_Liste_waechst() { var zeile = Zeile([Bild(), Ton(1)]); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-hls_playlist_type event"), "vod behauptete, die Liste sei vollständig — der Spieler brach nach dem ersten Segment ab"); Assert.That(zeile, Does.Contain("-hls_segment_type fmp4")); Assert.That(zeile, Does.Contain("-hls_flags independent_segments")); Assert.That(zeile, Does.EndWith(Path.Combine("/kratz/sitzung", "index.m3u8"))); }); } /// Untertitel bleiben draussen — sie kommen als eigene Datei. [Test] public void Untertitel_wandern_nicht_in_den_Strom() { var zeile = Zeile([Bild(), Ton(1), Text(2)]); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-sn")); Assert.That(zeile, Does.Not.Contain("-map 0:2")); }); } /// Beim Rechnen wird jedes Segment gleich lang. [Test] public void Beim_Rechnen_stehen_die_Keyframes_fest() { var zeile = Zeile([Bild("hevc"), Ton(1)]); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-g 150"), "6 Sekunden × 25 Bilder"); Assert.That(zeile, Does.Contain("-keyint_min 150")); Assert.That(zeile, Does.Contain("-sc_threshold 0")); }); } /// Eine Datei ohne Ton bleibt eine Datei ohne Ton. [Test] public void Ohne_Tonspur_wird_nichts_erfunden() { var zeile = Zeile([Bild()]); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-an")); Assert.That(zeile, Does.Not.Contain("-c:a aac")); }); } // ------------------------------------------------------------------ // Die Stufe: Bild gegen Tempo // ------------------------------------------------------------------ /// Jede Stufe rechnet anders — und „ausgewogen" wie bisher. /// /// /// Preset und CRF gehören zusammen. Ein langsameres Preset holt bei /// demselben CRF Bild heraus; ein schnelleres braucht einen höheren, sonst /// wächst nur die Datei. Deshalb wird hier beides geprüft und nicht eines. /// [TestCase(Kodierstufe.Qualitaet, "-preset medium", "-crf 19")] [TestCase(Kodierstufe.Ausgewogen, "-preset veryfast", "-crf 21")] [TestCase(Kodierstufe.Tempo, "-preset ultrafast", "-crf 23")] public void Die_Stufe_entscheidet_ueber_Preset_und_Crf( Kodierstufe stufe, string preset, string crf) { var zeile = Zeile([Bild("hevc"), Ton(1)], stufe: stufe); Assert.Multiple(() => { Assert.That(zeile, Does.Contain(preset)); Assert.That(zeile, Does.Contain(crf)); }); } /// Die Grafikeinheit kennt kein Preset, nur den Quantisierer. [TestCase(Kodierstufe.Qualitaet, "-qp 21")] [TestCase(Kodierstufe.Ausgewogen, "-qp 23")] [TestCase(Kodierstufe.Tempo, "-qp 26")] public void Die_Stufe_entscheidet_auch_bei_Vaapi(Kodierstufe stufe, string qp) { var zeile = Zeile([Bild("hevc"), Ton(1)], hardware: Beschleunigung.Vaapi, stufe: stufe); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-c:v h264_vaapi")); Assert.That(zeile, Does.Contain(qp)); }); } // ------------------------------------------------------------------ // Der zweite Versuch // ------------------------------------------------------------------ /// Wer es probiert hat und scheiterte, bekommt gerechnet. /// /// /// Das Urteil des Browsers schlägt seine eigene Auskunft. /// canPlayType ist eine Behauptung: „kenne ich" heisst nicht „kann /// ich diese Datei". Gemessen in Chromium — canPlayType('audio/flac') /// sagt „probably", MediaSource.isTypeSupported derselben /// Zeichenkette sagt false. Wer es ausprobiert hat, weiss mehr. /// [Test] public void Erzwungen_wird_gerechnet_auch_wenn_alles_passt() { IReadOnlyList spuren = [Bild("h264"), Ton(1, "aac")]; var ohne = Stromentscheid.Fuer("mp4", spuren, Browser); var mit = Stromentscheid.Fuer("mp4", spuren, Browser, erzwingen: true); Assert.Multiple(() => { Assert.That(ohne.Art, Is.EqualTo(Stromart.Direkt), "ohne Zwang geht die Datei durch"); Assert.That(mit.Art, Is.EqualTo(Stromart.Umkodieren)); Assert.That(mit.Gruende, Does.Contain(Stromgrund.SpielerKamNichtZurecht)); Assert.That(mit.BildKopieren, Is.False, "umkodieren heisst: das Bild wird angefasst"); }); } /// Und die gewählte Stufe bleibt dabei erhalten. [Test] public void Erzwungen_bleibt_die_gewaehlte_Stufe() { var plan = Stromentscheid.Fuer( "mp4", [Bild("h264", 2160), Ton(1, "aac")], Browser, 720, erzwingen: true); Assert.Multiple(() => { Assert.That(plan.Art, Is.EqualTo(Stromart.Umkodieren)); Assert.That(plan.Hoehe, Is.EqualTo(720)); }); } /// Beim Umpacken ist die Stufe gegenstandslos. /// /// /// Wird das Bild kopiert, gibt es nichts zu kodieren — dann darf auch kein /// Preset in der Zeile stehen, sonst lehnt ffmpeg sie ab. /// [Test] public void Wer_nur_umpackt_bekommt_kein_Preset() { var zeile = Zeile([Bild(), Ton(1)], stufe: Kodierstufe.Qualitaet); Assert.Multiple(() => { Assert.That(zeile, Does.Contain("-c:v copy")); Assert.That(zeile, Does.Not.Contain("-preset")); Assert.That(zeile, Does.Not.Contain("-crf")); }); } }