Zwölf Sekunden für eine Hülle um nichts

Die Messung sagt es deutlich:

    20:15:53  Umpacken    — Schlüsselbildsuche 2348 ms, Vorbereitung 2394 ms.
    20:16:05  Umkodieren  — Schlüsselbildsuche 0 ms, Vorbereitung 0 ms.
    20:16:08  erstes Stück nach 2750 ms.

Fünfzehn Sekunden, und zwölf davon liegen zwischen zwei Bestellungen für
denselben Film. Der erste Strom wurde angelegt und dann nie abgeholt —
keine Medienliste, kein Stück, kein ffmpeg-Fehler. Danach gab die App auf
und bestellte neu, diesmal mit Umkodieren, und das lief nach knapp drei
Sekunden.

Dazwischen lag die Hauptliste. Es gibt sie allein wegen der
Untertitelspuren, und bei diesem Film waren die noch gar nicht aus der
Datei gezogen — sie war also leer bis auf einen Verweis auf die
Medienliste. Eine Hülle um nichts, und keine umsonst: ohne `CODECS` muss
ein Spieler sie erst aufmachen, um zu sehen, was darin liegt.

Jetzt gibt es sie nur, wenn wenigstens eine Spur abholbar ist. Sonst
bekommt die App die Medienliste unmittelbar — denselben Weg wie der
Browser, der nie gestolpert ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-10 20:20:16 +02:00
co-authored by Claude Opus 5
parent a6b3217d6e
commit 19ec17da19
2 changed files with 37 additions and 14 deletions
@@ -118,7 +118,7 @@ public static class StreamEndpoints
erzwingen: anfrage.Transcode,
tonversatz: Math.Clamp(anfrage.AudioOffsetMs, -5000, 5000) / 1000.0);
return Results.Ok(await Auskunft(db, sitzung));
return Results.Ok(await Auskunft(db, vorrat, sitzung));
}
catch (InvalidOperationException fehler)
{
@@ -511,17 +511,35 @@ public static class StreamEndpoints
new HashSet<string>(anfrage.AudioCodecs, StringComparer.OrdinalIgnoreCase));
}
private static async Task<StreamResponse> Auskunft(FlaroContext db, Stromsitzung sitzung)
private static async Task<StreamResponse> Auskunft(
FlaroContext db, Untertitelvorrat vorrat, Stromsitzung sitzung)
{
var spuren = await db.MediaStreams.AsNoTracking()
.Where(s => s.MediaFileId == sitzung.DateiId)
.OrderBy(s => s.Index)
.ToListAsync();
// **Die Hauptliste nur, wenn etwas drinsteht.**
//
// Sie gibt es allein wegen der Untertitelspuren; ohne sie ist sie
// eine Hülle um die Medienliste. Und eine Hülle ist nicht umsonst:
// eine Hauptliste ohne `CODECS` muss der Spieler erst aufmachen, um
// zu sehen, was darin liegt. Gemessen an einem Film, dessen Spuren
// noch nicht herausgezogen waren: zwölf Sekunden vergingen, ohne dass
// die Medienliste auch nur abgeholt wurde, dann gab die App auf und
// bestellte neu.
var datei = await db.MediaFiles.AsNoTracking()
.FirstOrDefaultAsync(f => f.Id == sitzung.DateiId);
var hatText = datei is not null && spuren
.Where(x => x.Kind == MediaStreamKind.Subtitle)
.Where(x => x.Codec is null || !Untertitelvorrat.IstBild(x.Codec))
.Any(x => vorrat.Liegtvor(datei.Id, datei.ModifiedAt, x.Index));
return new StreamResponse(
SessionId: sitzung.Kennung,
Url: $"/api/v1/stream/{sitzung.Kennung}/index.m3u8",
MasterUrl: $"/api/v1/stream/{sitzung.Kennung}/master.m3u8",
MasterUrl: hatText ? $"/api/v1/stream/{sitzung.Kennung}/master.m3u8" : null,
Method: sitzung.Art switch
{
Stromart.Direkt => "direct",
+16 -11
View File
@@ -432,11 +432,18 @@ public class StreamTests
[Test]
public async Task Die_Hauptliste_nennt_die_Bildspur()
{
Film("Hauptliste (2020)");
FilmMitUntertitel("Hauptliste (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
Assert.That(strom.MasterUrl, Is.Not.Null, "Eine Sitzung hat eine Hauptliste.");
(await client.GetAsync(
$"/api/v1/files/{datei.Id}/subtitles/{strom.SubtitleStreams.Single().Index}.vtt"))
.EnsureSuccessStatusCode();
// Noch einmal bestellen: die Hauptliste gibt es erst, wenn eine Spur
// abholbar ist — vorher wäre sie eine Hülle um die Medienliste.
strom = await Strom(client, datei.Id, Browser());
Assert.That(strom.MasterUrl, Is.Not.Null);
var liste = await client.GetStringAsync(strom.MasterUrl!);
@@ -465,13 +472,14 @@ public class StreamTests
var spur = strom.SubtitleStreams.Single();
// **Erst holen, dann fragen.** In der Hauptliste steht nur, was
// sofort abholbar ist; der Abruf der Spur wartet, bis sie aus der
// Datei gezogen ist. Ohne diesen Schritt hinge die Prüfung daran,
// wie schnell der Rechner gerade ist.
// sofort abholbar ist, und es gibt sie überhaupt nur dann; der Abruf
// der Spur wartet, bis sie aus der Datei gezogen ist. Ohne diesen
// Schritt hinge die Prüfung daran, wie schnell der Rechner ist.
(await client.GetAsync(
$"/api/v1/files/{datei.Id}/subtitles/{spur.Index}.vtt")).EnsureSuccessStatusCode();
var liste = await client.GetStringAsync(strom.MasterUrl!);
var zweiter = await Strom(client, datei.Id, Browser());
var liste = await client.GetStringAsync(zweiter.MasterUrl!);
Assert.Multiple(() =>
{
@@ -585,11 +593,8 @@ public class StreamTests
var strom = await Strom(client, datei.Id, new StreamRequest(
Containers: null, VideoCodecs: null, AudioCodecs: null, StartSeconds: 0));
Assert.That(strom.MasterUrl, Is.Not.Null, "Eine MKV ist für Apple nichts zum Durchreichen.");
var haupt = await client.GetAsync(strom.MasterUrl!);
Assert.That(haupt.StatusCode, Is.EqualTo(HttpStatusCode.OK),
await haupt.Content.ReadAsStringAsync());
Assert.That(strom.Method, Is.Not.EqualTo("direct"),
"Eine MKV ist für Apple nichts zum Durchreichen.");
var medien = await client.GetAsync(strom.Url);
Assert.That(medien.StatusCode, Is.EqualTo(HttpStatusCode.OK),