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_Schluesselbilder_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));
});
}
/// 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"));
});
}
}