Files
Flaro/server/tests/Flaro.Api.Tests/StreamTests.cs
T
ScarriffleandClaude Opus 5 19ec17da19 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>
2026-10-10 20:20:16 +02:00

611 lines
24 KiB
C#

using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Net.Http.Json;
using Flaro.Api.Endpoints;
namespace Flaro.Api.Tests;
/// <summary>Wiedergabe, die sofort anfängt — von der Anfrage bis zum Segment.</summary>
///
/// <remarks>
/// <b>Mit echtem ffmpeg, wie bei den Lieferungsprüfungen.</b> Ob eine Playlist
/// entsteht und ob darin Segmente stehen, lässt sich nicht behaupten; es muss
/// jemand nachsehen.
/// </remarks>
public class StreamTests
{
private string _bestand = "";
private FlaroServer? _server;
private static string? Werkzeug(string name) =>
(Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator)
.Select(o => o.Length == 0 ? null : Path.Combine(o, name))
.FirstOrDefault(p => p is not null && File.Exists(p));
[SetUp]
public void Anlegen()
{
if (Werkzeug("ffmpeg") is null) Assert.Ignore("ffmpeg ist auf diesem Rechner nicht installiert.");
_bestand = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-strom-" + Guid.CreateVersion7().ToString("N"))).FullName;
}
[TearDown]
public void Aufraeumen()
{
_server?.Dispose();
_server = null;
try { if (_bestand.Length > 0) Directory.Delete(_bestand, recursive: true); } catch (IOException) { }
}
/// <summary>Was ein gewöhnlicher Browser kann — kein HEVC, kein EAC3.</summary>
private static StreamRequest Browser(int? hoehe = null, int? ton = null, double? ab = null) => new(
Containers: ["mp4"],
VideoCodecs: ["h264", "avc1", "vp9"],
AudioCodecs: ["aac", "mp3", "opus"],
Height: hoehe, AudioStream: ton, StartSeconds: ab);
/// <summary>Eine MKV mit zwei Tonspuren, die zweite in einem Codec für Apple.</summary>
private string Film(string name, int hoehe = 240, int sekunden = 4)
{
var ziel = Path.Combine(_bestand, $"{name}.mkv");
var start = new ProcessStartInfo(Werkzeug("ffmpeg")!)
{
RedirectStandardError = true,
UseShellExecute = false,
};
foreach (var a in new[]
{
"-y", "-loglevel", "error",
"-f", "lavfi", "-i", $"testsrc=duration={sekunden}:size={hoehe * 4 / 3}x{hoehe}:rate=10",
"-f", "lavfi", "-i", $"sine=frequency=440:duration={sekunden}",
"-f", "lavfi", "-i", $"sine=frequency=880:duration={sekunden}",
"-map", "0:v", "-map", "1:a", "-map", "2:a",
"-metadata:s:a:0", "language=deu", "-metadata:s:a:0", "title=Deutsch",
"-metadata:s:a:1", "language=eng", "-metadata:s:a:1", "title=English",
"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p",
"-c:a:0", "aac", "-c:a:1", "eac3",
ziel,
})
{
start.ArgumentList.Add(a);
}
using var prozess = Process.Start(start)!;
var fehler = prozess.StandardError.ReadToEnd();
prozess.WaitForExit();
if (prozess.ExitCode != 0) throw new InvalidOperationException(fehler);
return ziel;
}
/// <summary>Derselbe Film, dazu eine Textspur auf Deutsch.</summary>
///
/// <remarks>
/// Für die Hauptliste braucht es etwas, das darin stehen kann. Eine
/// Bildspur (PGS) wäre dafür untauglich und soll ausdrücklich fehlen —
/// die prüft <see cref="Eine_Bildspur_steht_nicht_in_der_Hauptliste"/>
/// nicht, weil ffmpeg sie nicht nebenbei erzeugt.
/// </remarks>
private string FilmMitUntertitel(string name, int sekunden = 4)
{
var srt = Path.Combine(_bestand, "text.srt");
File.WriteAllText(srt,
"1\n00:00:00,500 --> 00:00:02,000\nEin Satz.\n\n"
+ "2\n00:00:02,500 --> 00:00:03,500\nNoch einer.\n");
var ziel = Path.Combine(_bestand, $"{name}.mkv");
var start = new ProcessStartInfo(Werkzeug("ffmpeg")!)
{
RedirectStandardError = true,
UseShellExecute = false,
};
foreach (var a in new[]
{
"-y", "-loglevel", "error",
"-f", "lavfi", "-i", $"testsrc=duration={sekunden}:size=320x240:rate=10",
"-f", "lavfi", "-i", $"sine=frequency=440:duration={sekunden}",
"-i", srt,
"-map", "0:v", "-map", "1:a", "-map", "2:s",
"-metadata:s:s:0", "language=deu", "-metadata:s:s:0", "title=Deutsch",
"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p",
"-c:a", "aac", "-c:s", "srt",
ziel,
})
{
start.ArgumentList.Add(a);
}
using var prozess = Process.Start(start)!;
var fehler = prozess.StandardError.ReadToEnd();
prozess.WaitForExit();
if (prozess.ExitCode != 0) throw new InvalidOperationException(fehler);
File.Delete(srt);
return ziel;
}
private async Task<(HttpClient Client, MediaFileResponse Datei)> Eingelesen()
{
var server = _server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var client = server.AlsAngemeldeter(admin.Token);
var angelegt = await client.PostAsJsonAsync("/api/v1/libraries",
new CreateLibraryRequest("Prüfbestand", "Movies", _bestand));
angelegt.EnsureSuccessStatusCode();
var bibliothek = (await angelegt.Content.ReadFromJsonAsync<LibraryResponse>())!;
(await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode();
for (var i = 0; i < 200; i++)
{
var stand = await client.GetFromJsonAsync<ScanStatus>($"/api/v1/libraries/{bibliothek.Id}/scan");
if (stand!.State == "finished") break;
await Task.Delay(50);
}
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
Assert.That(filme!.Items, Is.Not.Empty, "Der Scan hat nichts gefunden");
return (client, filme.Items[0].Files.Single());
}
private static async Task<StreamResponse> Strom(
HttpClient client, Guid datei, StreamRequest anfrage)
{
var antwort = await client.PostAsJsonAsync($"/api/v1/files/{datei}/stream", anfrage);
antwort.EnsureSuccessStatusCode();
return (await antwort.Content.ReadFromJsonAsync<StreamResponse>())!;
}
// ------------------------------------------------------------------
/// <summary>Eine MKV wird für den Browser umgepackt, nicht neu gerechnet.</summary>
[Test]
public async Task Eine_mkv_wird_fuer_den_Browser_umgepackt()
{
Film("Umpacken (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
Assert.Multiple(() =>
{
Assert.That(strom.Method, Is.EqualTo("remux"),
"H.264 kennt jeder Browser — nur die Schachtel nicht");
Assert.That(strom.Height, Is.Null, "ohne Wunsch bleibt die Auflösung, wie sie ist");
Assert.That(strom.AudioStreams, Has.Count.EqualTo(2));
});
}
/// <summary>Die Playlist kommt, sobald das erste Segment da ist.</summary>
///
/// <remarks>
/// <b>Und nicht als 204.</b> Erst stand hier eine leere Antwort, solange
/// noch nichts fertig war — hls.js hielt das für einen Fehler und gab auf.
/// Gewartet wird jetzt serverseitig.
/// </remarks>
[Test]
public async Task Die_Playlist_enthaelt_Segmente()
{
Film("Playlist (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
var antwort = await client.GetAsync(strom.Url);
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var text = await antwort.Content.ReadAsStringAsync();
Assert.Multiple(() =>
{
Assert.That(text, Does.StartWith("#EXTM3U"));
Assert.That(text, Does.Contain(".m4s"), "ohne Segment ist die Liste wertlos");
Assert.That(text, Does.Contain("#EXT-X-MAP:URI=\"init.mp4\""));
});
// Das Anfangsstück und das erste Segment müssen auch abholbar sein.
var anfang = await client.GetAsync($"/api/v1/stream/{strom.SessionId}/init.mp4");
var erstes = await client.GetAsync($"/api/v1/stream/{strom.SessionId}/seg00000.m4s");
Assert.Multiple(() =>
{
Assert.That(anfang.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(erstes.StatusCode, Is.EqualTo(HttpStatusCode.OK));
});
}
/// <summary>Die Liste darf nicht zwischengespeichert werden, die Segmente schon.</summary>
///
/// <remarks>
/// <b>Aus einem Fehler gelernt.</b> Die Wiedergabe blieb nach zwanzig
/// Sekunden stehen, obwohl auf der Platte längst der ganze Rest lag:
/// <c>Results.File</c> setzte <c>Last-Modified</c>, 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 — ein fertiges Segment dagegen ändert sich nie.
/// </remarks>
[Test]
public async Task Die_Playlist_wird_nicht_zwischengespeichert()
{
Film("Zwischenspeicher (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
var liste = await client.GetAsync(strom.Url);
var segment = await client.GetAsync($"/api/v1/stream/{strom.SessionId}/seg00000.m4s");
Assert.Multiple(() =>
{
Assert.That(liste.Headers.CacheControl?.NoStore, Is.True,
"eine wachsende Liste darf nirgends liegenbleiben");
Assert.That(liste.Content.Headers.LastModified, Is.Null,
"sonst fragt der Browser bedingt nach und bekommt 304 auf eine veraltete Liste");
Assert.That(segment.Headers.CacheControl?.MaxAge, Is.Not.Null,
"ein fertiges Segment darf liegenbleiben — das spart den Weg beim Zurückspringen");
});
}
/// <summary>Ein unbekannter Bildcodec führt zum Rechnen, nicht zum Umpacken.</summary>
///
/// <remarks>
/// <b>Das war der Fehler, an dem jede Wiedergabe scheiterte.</b> Der
/// Lieferweg für Downloads antwortete auch hier mit „umpacken" — und
/// Umpacken fasst das Bild nicht an. Der Browser bekam eine tadellose MP4
/// mit einem Codec, den er nicht kennt.
/// </remarks>
[Test]
public async Task Ein_unbekannter_Bildcodec_wird_gerechnet()
{
Film("Rechnen (2020)");
var (client, datei) = await Eingelesen();
// Ein Gerät, das H.264 **nicht** kann — der kürzeste Weg zu „muss gerechnet werden".
var strom = await Strom(client, datei.Id, new StreamRequest(
Containers: ["mp4"], VideoCodecs: ["vp9"], AudioCodecs: ["aac"]));
Assert.That(strom.Method, Is.EqualTo("transcode"));
}
/// <summary>Eine kleinere Auflösung wird wirklich kleiner.</summary>
[Test]
public async Task Eine_kleinere_Stufe_wird_herunterskaliert()
{
Film("Gross (2020)", hoehe: 720);
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser(hoehe: 480));
Assert.Multiple(() =>
{
Assert.That(strom.Method, Is.EqualTo("transcode"));
Assert.That(strom.Height, Is.EqualTo(480));
});
}
/// <summary>Hochrechnen ist keine Stufe.</summary>
[Test]
public async Task Eine_Stufe_ueber_der_Quelle_bleibt_beim_Umpacken()
{
Film("Klein (2020)", hoehe: 240);
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser(hoehe: 1080));
Assert.Multiple(() =>
{
Assert.That(strom.Height, Is.Null);
Assert.That(strom.Method, Is.EqualTo("remux"),
"240p auf 1080p zu rechnen kostet Last für kein besseres Bild");
});
}
/// <summary>Der Spieler erfährt, welche Tonspur wirklich läuft.</summary>
///
/// <remarks>
/// <b>Nicht die gewünschte.</b> Vorher meldete der Server zurück, was
/// gefragt worden war — meist <c>null</c> —, und im Menü stand bei keiner
/// Spur ein Haken, obwohl eine davon lief.
/// </remarks>
[Test]
public async Task Der_Strom_nennt_die_benutzte_Tonspur()
{
Film("Tonwahl (2020)");
var (client, datei) = await Eingelesen();
var ohneWunsch = await Strom(client, datei.Id, Browser());
var mitWunsch = await Strom(client, datei.Id, Browser(ton: 2));
var unsinn = await Strom(client, datei.Id, Browser(ton: 99));
Assert.Multiple(() =>
{
Assert.That(ohneWunsch.AudioStream, Is.EqualTo(1), "ohne Wunsch die erste");
Assert.That(mitWunsch.AudioStream, Is.EqualTo(2));
Assert.That(unsinn.AudioStream, Is.EqualTo(1),
"eine Spur, die es nicht gibt, macht den Film nicht stumm");
});
}
/// <summary>Der Einstiegspunkt kommt zurück — und zwar der echte.</summary>
///
/// <remarks>
/// <para>
/// <b>Sonst ginge jeder gemeldete Stand um den Vorlauf daneben.</b> Der
/// Strom fängt bei null an, der Film bei Minute 30.
/// </para>
/// <para>
/// <b>Und „echt" heisst: beim Schlüsselbild.</b> Dieser Test stand vorher
/// auf „genau zwei Sekunden" und hielt damit einen Fehler fest. Ein
/// kopierter Bildstrom lässt sich nicht mitten in einer Bildgruppe
/// aufschneiden: der Strom fängt beim Schlüsselbild davor an, und wer
/// trotzdem „zwei" zurückmeldet, verschiebt die Uhr des Spielers um den
/// Unterschied. Diese Testdatei hat genau ein Schlüsselbild, bei null —
/// also ist null die richtige Antwort.
/// </para>
/// </remarks>
[Test]
public async Task Der_Einstiegspunkt_steht_in_der_Antwort()
{
Film("Sprung (2020)", sekunden: 4);
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser(ab: 2));
Assert.That(strom.StartSeconds, Is.LessThanOrEqualTo(2),
"Weiter als gewünscht darf der Strom nie anfangen — das überspränge Film.");
Assert.That(strom.StartSeconds, Is.EqualTo(0),
"Diese Datei hat nur ein Schlüsselbild, bei null.");
}
/// <summary>Schliessen räumt auf — sonst füllt sich der Kratzplatz.</summary>
[Test]
public async Task Schliessen_raeumt_die_Sitzung_weg()
{
Film("Aufraeumen (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
(await client.GetAsync(strom.Url)).EnsureSuccessStatusCode();
var weg = await client.DeleteAsync($"/api/v1/stream/{strom.SessionId}");
var danach = await client.GetAsync(strom.Url);
Assert.Multiple(() =>
{
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
Assert.That(danach.StatusCode, Is.EqualTo(HttpStatusCode.NotFound),
"eine beendete Sitzung gibt es nicht mehr");
});
}
/// <summary>Ein Pfad aus der Anfrage darf nie ans Dateisystem durchgereicht werden.</summary>
[Test]
public async Task Aus_der_Sitzung_kommt_man_nicht_heraus()
{
Film("Sicher (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
var hinaus = await client.GetAsync($"/api/v1/stream/{strom.SessionId}/..%2F..%2Fflaro.db");
var fremd = await client.GetAsync($"/api/v1/stream/{strom.SessionId}/index.m3u8x");
Assert.Multiple(() =>
{
Assert.That(hinaus.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
Assert.That(fremd.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
});
}
/// <summary>Ohne Anmeldung kommt niemand an einen Strom.</summary>
[Test]
public async Task Ohne_Anmeldung_gibt_es_keinen_Strom()
{
Film("Zutritt (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
var ohne = _server!.OhneGedaechtnis();
var antwort = await ohne.GetAsync(strom.Url);
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
/// <summary>Eine Sitzung, die es nicht gibt, ist eine 404 und kein Absturz.</summary>
[Test]
public async Task Eine_unbekannte_Sitzung_ist_nicht_gefunden()
{
Film("Unbekannt (2020)");
var (client, _) = await Eingelesen();
var antwort = await client.GetAsync($"/api/v1/stream/{Guid.CreateVersion7()}/index.m3u8");
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
// --- Die Hauptliste, für Abspieler ohne eigenen Weg zu Untertiteln ---
/// <summary>Die Hauptliste nennt die Bildspur und eine Bandbreite.</summary>
///
/// <remarks>
/// <b>BANDWIDTH ist Pflicht.</b> Eine Hauptliste ohne diese Angabe weist
/// AVPlayer ab — und zwar wortlos, mit einem schwarzen Bild.
/// </remarks>
[Test]
public async Task Die_Hauptliste_nennt_die_Bildspur()
{
FilmMitUntertitel("Hauptliste (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
(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!);
Assert.Multiple(() =>
{
Assert.That(liste, Does.StartWith("#EXTM3U"));
Assert.That(liste, Does.Contain("#EXT-X-STREAM-INF:BANDWIDTH="));
Assert.That(liste, Does.Contain("index.m3u8"));
});
}
/// <summary>Jede Textspur steht als eigene Spur in der Hauptliste.</summary>
///
/// <remarks>
/// <b>Das ist der ganze Grund für diese Liste.</b> AVPlayer kennt keinen
/// Weg, einer laufenden Wiedergabe eine Textspur von aussen beizulegen;
/// was er findet, findet er hier oder gar nicht.
/// </remarks>
[Test]
public async Task Die_Hauptliste_nennt_jede_Textspur()
{
FilmMitUntertitel("Mit Text (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
var spur = strom.SubtitleStreams.Single();
// **Erst holen, dann fragen.** In der Hauptliste steht nur, was
// 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 zweiter = await Strom(client, datei.Id, Browser());
var liste = await client.GetStringAsync(zweiter.MasterUrl!);
Assert.Multiple(() =>
{
Assert.That(liste, Does.Contain("#EXT-X-MEDIA:TYPE=SUBTITLES"));
Assert.That(liste, Does.Contain($"URI=\"untertitel/{spur.Index}.m3u8\""));
Assert.That(liste, Does.Contain("SUBTITLES=\"untertitel\""));
Assert.That(liste, Does.Contain("DEFAULT=NO"),
"Was gezeigt wird, entscheidet der Spieler — nicht die Liste.");
});
}
/// <summary>Die Spurliste zeigt auf die Spur, mit dem Vorlauf der Sitzung.</summary>
///
/// <remarks>
/// <para>
/// <b>Ohne <c>ab</c> läge jeder Satz in der Zukunft.</b> Die Zeitstempel
/// in der Datei zählen ab Dateianfang, der Strom ab null.
/// </para>
/// <para>
/// <b>Verglichen wird mit dem gemeldeten Vorlauf, nicht mit dem
/// gewünschten</b> — und das ist keine Bequemlichkeit. Beim Kopieren
/// beginnt ffmpeg am Schlüsselbild vor dem Wunsch; in einem Prüfclip von
/// vier Sekunden ist das die Null. Der Untertitel muss zu dem passen, was
/// wirklich läuft.
/// </para>
/// </remarks>
[Test]
public async Task Die_Spurliste_traegt_den_Vorlauf_der_Sitzung()
{
FilmMitUntertitel("Mit Vorlauf (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser(ab: 2));
var spur = strom.SubtitleStreams.Single();
var liste = await client.GetStringAsync(
$"/api/v1/stream/{strom.SessionId}/untertitel/{spur.Index}.m3u8");
var vorlauf = strom.StartSeconds.ToString("0.###", CultureInfo.InvariantCulture);
Assert.Multiple(() =>
{
Assert.That(liste, Does.StartWith("#EXTM3U"));
Assert.That(liste, Does.Contain("#EXT-X-ENDLIST"),
"Eine Textspur ist vollständig, sobald es sie gibt — sie wächst nicht.");
Assert.That(liste, Does.Contain(
$"/api/v1/files/{datei.Id}/subtitles/{spur.Index}.vtt?ab={vorlauf}"));
});
}
/// <summary>Eine Spur, die es nicht gibt, gibt es auch hier nicht.</summary>
[Test]
public async Task Eine_unbekannte_Spur_hat_keine_Liste()
{
FilmMitUntertitel("Ohne diese Spur (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, Browser());
var antwort = await client.GetAsync($"/api/v1/stream/{strom.SessionId}/untertitel/99.m3u8");
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
/// <summary>Ohne Sitzung keine Liste — auch nicht die Hauptliste.</summary>
[Test]
public async Task Ohne_Sitzung_gibt_es_keine_Hauptliste()
{
Film("Keine Sitzung (2020)");
var (client, _) = await Eingelesen();
var antwort = await client.GetAsync($"/api/v1/stream/{Guid.NewGuid()}/master.m3u8");
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
/// <summary>Eine Direktwiedergabe braucht keine Hauptliste.</summary>
///
/// <remarks>
/// Die Datei bringt ihre Spuren selbst mit; eine Liste darum herum wäre
/// eine Hülle um nichts.
/// </remarks>
[Test]
public async Task Eine_Direktwiedergabe_hat_keine_Hauptliste()
{
Film("Direkt (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, new StreamRequest(
Containers: ["matroska", "mkv"],
VideoCodecs: ["h264"],
AudioCodecs: ["aac", "eac3"]));
Assume.That(strom.Method, Is.EqualTo("direct"));
Assert.That(strom.MasterUrl, Is.Null);
}
/// <summary>Was die App bestellt, lässt sich auch abholen.</summary>
///
/// <remarks>
/// <b>Die App schickt keine Codec-Listen</b> — dann nimmt der Server
/// Apples Profil, und das ist hier richtig: dieses Gerät ist Apple. Der
/// Weg danach ist aber ein anderer als der des Browsers, und genau dort
/// kam eine 500 heraus. Diese Prüfung geht ihn ganz ab: bestellen,
/// Hauptliste holen, Medienliste holen, erstes Stück holen.
/// </remarks>
[Test]
public async Task Der_Weg_der_App_fuehrt_bis_zum_ersten_Stueck()
{
FilmMitUntertitel("Wie die App (2020)");
var (client, datei) = await Eingelesen();
var strom = await Strom(client, datei.Id, new StreamRequest(
Containers: null, VideoCodecs: null, AudioCodecs: null, StartSeconds: 0));
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),
await medien.Content.ReadAsStringAsync());
var liste = await medien.Content.ReadAsStringAsync();
var stueck = liste.Split('\n').FirstOrDefault(z => z.Length > 0 && !z.StartsWith('#'));
Assert.That(stueck, Is.Not.Null, liste);
var teil = await client.GetAsync($"/api/v1/stream/{strom.SessionId}/{stueck}");
Assert.That(teil.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
}