Die App schickt keine Codec-Listen; dann nimmt der Server Apples Profil. Das führt zu einer anderen Entscheidung als beim Browser — dort wird umgerechnet, hier umgepackt — und damit auf einen Weg, den keine Prüfung abging. Jetzt schon: bestellen, Hauptliste, Medienliste, erstes Stück. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
598 lines
24 KiB
C#
598 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()
|
|
{
|
|
Film("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.");
|
|
|
|
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();
|
|
var liste = await client.GetStringAsync(strom.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.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());
|
|
|
|
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));
|
|
}
|
|
}
|