Zwei Dinge in derselben Ecke, und ein Modus, den niemand bestellt hat

Der Gesehen-Haken und die drei Punkte sassen beide oben rechts — auf dem
Plakat sah das aus wie ein Fehler. Jetzt hat jede Ecke eine Aufgabe: oben
links die Auswahl, oben rechts der Stand, unten links Abspielen, unten rechts
das Menü.

Der Knopf „Auswählen" ist weg. Gemessen, warum er stören musste: der längere
Text „Auswahl beenden" brach die Filterzeile um und schob das Raster um
zwanzig Bildpunkte nach unten. Und grundsätzlicher ist ein Modus ein Zustand,
den man kennen muss, bevor man etwas tun kann. Jetzt trägt jede Kachel oben
links ein Kästchen, das beim Überfahren erscheint — wie die drei Punkte auch.
Der erste Haken fängt die Auswahl an, der letzte beendet sie.

Der Spieler lässt sich verkleinern. Er rutscht in eine Leiste am unteren Rand
und läuft weiter, während man die Mediathek aufräumt; ein Klick darauf holt
ihn zurück. Dasselbe Videoelement, nur anders eingerahmt — ein zweiter
Spieler unten hiesse, das Video auszuhängen, und dabei hört es auf zu
spielen.

Das Zahnrad mit acht Strahlen sah aus wie eine Sonne. Jetzt: Wellenform für
den Ton, CC-Kasten für die Untertitel, Schieberegler für die Qualität. Alle
drei Menüs stehen immer da — vorher verschwanden sie bei nur einer Tonspur
oder ohne Untertitel, und wer sie suchte, fand nichts und wusste nicht warum.

Dabei gefunden und behoben: die Wiedergabe blieb nach zwanzig Sekunden
stehen, obwohl längst der ganze Rest auf der Platte lag. Results.File setzte
Last-Modified, der Browser legte die Playlist mit zwei Einträgen ab, und
hls.js bekam bei jedem Nachfragen dieselben zwei zurück. Eine wachsende Liste
ist das Gegenteil von zwischenspeicherbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-05 20:31:12 +02:00
co-authored by Claude Opus 5
parent eda9e67802
commit 7df9fa6c01
7 changed files with 471 additions and 120 deletions
@@ -74,8 +74,17 @@ public static class StreamEndpoints
// **Die Playlist.** Sie wächst, während ffmpeg schreibt.
app.MapGet("/api/v1/stream/{sitzung:guid}/index.m3u8", async (
Guid sitzung, Streamkoordinator koordinator, CancellationToken ct) =>
Guid sitzung, HttpResponse antwort, Streamkoordinator koordinator, CancellationToken ct) =>
{
// **Diese Datei darf nie zwischengespeichert werden**, und das ist
// kein Vorsichtsmass, sondern aus einem Fehler gelernt: der Film
// blieb nach zwanzig Sekunden stehen, obwohl auf der Platte längst
// der ganze Rest lag. `Results.File` setzt `Last-Modified`, der
// Browser legte die Liste mit zwei Einträgen ab, und hls.js bekam
// bei jedem Nachfragen dieselben zwei zurück. Eine wachsende Liste
// ist das Gegenteil von zwischenspeicherbar.
antwort.Headers.CacheControl = "no-store, no-cache, must-revalidate";
antwort.Headers.Pragma = "no-cache";
if (koordinator.Sitzung(sitzung) is not { } laufend) return Results.NotFound();
koordinator.Lebenszeichen(sitzung);
@@ -104,9 +113,10 @@ public static class StreamEndpoints
statusCode: StatusCodes.Status500InternalServerError);
}
if (Streamkoordinator.PlaylistBereit(jetzt.Ordner))
return Results.File(Path.Combine(jetzt.Ordner, HlsArgumente.Playlist),
"application/vnd.apple.mpegurl");
// **Als Inhalt, nicht als Datei.** `Results.File` hängt
// `Last-Modified` an und lädt damit zum Zwischenspeichern ein.
if (Streamkoordinator.Playlist(jetzt.Ordner) is { } liste)
return Results.Text(liste, "application/vnd.apple.mpegurl", Encoding.UTF8);
// **Hört ffmpeg auf, ohne ein Segment geschrieben zu haben**,
// kommt auch keines mehr. Weiterzuwarten hiesse, eine halbe
@@ -129,7 +139,7 @@ public static class StreamEndpoints
// **Die Segmente.** Jedes Abholen ist zugleich das Lebenszeichen.
app.MapGet("/api/v1/stream/{sitzung:guid}/{datei}", (
Guid sitzung, string datei, Streamkoordinator koordinator) =>
Guid sitzung, string datei, HttpResponse antwort, Streamkoordinator koordinator) =>
{
if (koordinator.Sitzung(sitzung) is not { } laufend) return Results.NotFound();
@@ -148,9 +158,14 @@ public static class StreamEndpoints
koordinator.Lebenszeichen(sitzung);
var pfad = Path.Combine(laufend.Ordner, datei);
return File.Exists(pfad)
? Results.File(pfad, "video/mp4", enableRangeProcessing: true)
: Results.NotFound();
if (!File.Exists(pfad)) return Results.NotFound();
// **Segmente ändern sich nie.** Anders als die Liste: einmal
// geschrieben, bleibt eines, wie es ist — es darf also beliebig
// lange liegenbleiben. Das spart beim Zurückspringen den ganzen
// Weg über die Leitung.
antwort.Headers.CacheControl = "private, max-age=86400, immutable";
return Results.File(pfad, "video/mp4", enableRangeProcessing: true);
}).RequireAuthorization();
// **Schliessen heisst aufräumen.** Ohne das liefe ffmpeg weiter, bis