Files
Flaro/server/tests/Flaro.Media.Tests/HlsArgumenteTests.cs
T
ScarriffleandClaude Opus 5 ed13d5fa43 Kratzplatz, Markenerkennung, Tonabdruck — erfundene Wörter für Fachbegriffe
„Scratch disk" wörtlich zu übersetzen ist derselbe Fehler wie „Wachhund"
für Watchdog und „Keks" für Cookie. Und „Marke" ist kein Fachbegriff für
das, was gemeint ist: Jellyfin nennt diese Abschnitte Media Segments, und
das ist auch das Wort, in dem sich darüber reden lässt.

    Kratzplatz*       → Transcode*          (was es ist, steht in FLARO_TRANSCODE)
    Marke, Marken     → Segment, Segmente
    Markenerkennung   → Segmenterkennung
    Markenart/-suche  → Segmentart/-suche
    Tonabdruck        → Fingerprint         (Chromaprint nennt es so)
    Tonabgleich       → Fingerprintabgleich
    Schlüsselbild     → Keyframe
    Bildabtaster      → Frameabtaster

Gewöhnliches Deutsch bleibt gewöhnliches Deutsch: Wartung, Freigabe,
Meldung, Abspann, Vorspann. Das war nie das Problem.

**Was sich nicht ändert, und zwar mit Absicht.** Die drei
Einstellungsschlüssel `wartung.kratzplatz.paket.*` bleiben, wie sie sind —
sie stehen in der Datenbank, und ein neuer Name hiesse, dass die Werte
des Nutzers stillschweigend auf die Vorgabe zurückfallen. Dasselbe für
die Endpunktpfade `/marken`: sie sind nicht sichtbar, und ein Umbenennen
wäre Diff ohne Gewinn.

**Die Migration ist von Hand geschrieben.** `dotnet ef` hat ein Gerüst
erzeugt, das beide Tabellen löscht und neu anlegt — es sieht eine
verschwundene und eine neue Tabelle, nicht eine umbenannte. Damit wäre
jedes erkannte Segment und jeder Vermerk „hier wurde schon gesucht" weg
gewesen. `RenameTable` und `RenameIndex` tun dasselbe, ohne eine Zeile
anzufassen; nachgemessen an einer echten SQLite-Datei: Zeilen vorher 1
und 1, nachher 1 und 1, Inhalt unverändert.

Nachgezogen: in `bilder.ts` und `main.ts` heisst „marke" ein Häkchen und
das Logo — die blieben unberührt. Und „Segment" ist sächlich, was eine
Blindersetzung nicht weiss: „eine Segment" stand danach an vierzehn
Stellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-16 16:26:57 +02:00

374 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Flaro.Core.Delivery;
using Flaro.Core.Library;
namespace Flaro.Media.Tests;
/// <summary>Die ffmpeg-Zeile für einen laufenden Strom, ohne sie auszuführen.</summary>
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);
/// <summary>Was ein gewöhnlicher Browser kann.</summary>
private static readonly DeviceProfile Browser = new(
Containers: new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "mp4", "webm" },
VideoCodecs: new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "h264", "avc1", "vp9" },
AudioCodecs: new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "aac", "mp3", "opus" });
private static string Zeile(
IReadOnlyList<ProbedStream> 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
// ------------------------------------------------------------------
/// <summary>Eine MKV mit H.264 wird umgepackt, nicht neu gerechnet.</summary>
///
/// <remarks>
/// <b>Der häufigste Fall und der billigste.</b> Der Browser kennt die
/// Schachtel nicht, das Bild darin sehr wohl.
/// </remarks>
[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"));
});
}
/// <summary>Der Ton wird immer nach AAC gerechnet — das war der Fehler.</summary>
///
/// <remarks>
/// <b>Hier lag der Grund, aus dem gar nichts lief.</b> 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.
/// </remarks>
[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
// ------------------------------------------------------------------
/// <summary>HEVC kann kein Browser — da hilft Umpacken nichts.</summary>
[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"));
});
}
/// <summary>Ein 4K-Film soll sich auch in 480p ansehen lassen.</summary>
[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"));
});
}
/// <summary>Hochrechnen ist keine Stufe.</summary>
///
/// <remarks>
/// Ein 720p-Film in „1080p" kostet mehr Last und mehr Daten für kein
/// besseres Bild. Die Stufe wird dann stillschweigend übergangen.
/// </remarks>
[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"));
});
}
/// <summary>Eine Spur, die es nicht gibt, macht den Film nicht stumm.</summary>
[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"));
}
/// <summary>Der Sprungpunkt steht vor der Eingabe.</summary>
///
/// <remarks>
/// <b>Hinter <c>-i</c> wäre es ein genauer, aber langsamer Sprung:</b>
/// ffmpeg läse die Datei bis dorthin durch. Bei Minute 90 eines Films
/// dauert das länger als das Ansehen.
/// </remarks>
[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"));
}
/// <summary>Die Beschleunigung gehört vor die Eingabe, sonst wirkt sie nicht.</summary>
[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")));
});
}
/// <summary>Untertitel bleiben draussen — sie kommen als eigene Datei.</summary>
[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"));
});
}
/// <summary>Beim Rechnen wird jedes Segment gleich lang.</summary>
[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"));
});
}
/// <summary>Eine Datei ohne Ton bleibt eine Datei ohne Ton.</summary>
[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
// ------------------------------------------------------------------
/// <summary>Jede Stufe rechnet anders — und „ausgewogen" wie bisher.</summary>
///
/// <remarks>
/// <b>Preset und CRF gehören zusammen.</b> 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.
/// </remarks>
[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));
});
}
/// <summary>Die Grafikeinheit kennt kein Preset, nur den Quantisierer.</summary>
[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
// ------------------------------------------------------------------
/// <summary>Wer es probiert hat und scheiterte, bekommt gerechnet.</summary>
///
/// <remarks>
/// <b>Das Urteil des Browsers schlägt seine eigene Auskunft.</b>
/// <c>canPlayType</c> ist eine Behauptung: „kenne ich" heisst nicht „kann
/// ich diese Datei". Gemessen in Chromium — <c>canPlayType('audio/flac')</c>
/// sagt „probably", <c>MediaSource.isTypeSupported</c> derselben
/// Zeichenkette sagt <c>false</c>. Wer es ausprobiert hat, weiss mehr.
/// </remarks>
[Test]
public void Erzwungen_wird_gerechnet_auch_wenn_alles_passt()
{
IReadOnlyList<ProbedStream> 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");
});
}
/// <summary>Und die gewählte Stufe bleibt dabei erhalten.</summary>
[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));
});
}
/// <summary>Beim Umpacken ist die Stufe gegenstandslos.</summary>
///
/// <remarks>
/// Wird das Bild kopiert, gibt es nichts zu kodieren — dann darf auch kein
/// Preset in der Zeile stehen, sonst lehnt ffmpeg sie ab.
/// </remarks>
[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"));
});
}
}