„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>
374 lines
14 KiB
C#
374 lines
14 KiB
C#
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"));
|
||
});
|
||
}
|
||
}
|