diff --git a/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs b/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs
index 7773a9e..32db75d 100644
--- a/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs
+++ b/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs
@@ -143,7 +143,7 @@ public static class StreamEndpoints
// weiterhin `index.m3u8`.
app.MapGet("/api/v1/stream/{sitzung:guid}/master.m3u8", async (
Guid sitzung, HttpResponse antwort, Streamkoordinator koordinator,
- FlaroContext db, CancellationToken ct) =>
+ FlaroContext db, Untertitelvorrat vorrat, CancellationToken ct) =>
{
// Dieselbe Begründung wie bei der Medienliste darunter: eine
// Liste, die sich ändern kann, darf nicht zwischengespeichert
@@ -157,8 +157,23 @@ public static class StreamEndpoints
.OrderBy(x => x.Index)
.ToListAsync(ct);
- var text = spuren
+ var datei = await db.MediaFiles.AsNoTracking()
+ .FirstOrDefaultAsync(f => f.Id == laufend.DateiId, ct);
+
+ // **Nur, was jetzt sofort abholbar ist.**
+ //
+ // Untertitel werden beim ersten Bedarf aus der Datei gezogen; bei
+ // einem Dreistünder auf einem NAS dauert das Minuten. Der Browser
+ // verträgt das, er fragt die Spur einzeln an und wartet. AVPlayer
+ // nicht: eine Spur, die in der Hauptliste steht und beim Abruf
+ // noch nicht da ist, bricht ihm die **ganze** Wiedergabe ab, nicht
+ // bloss den Untertitel.
+ //
+ // Das Füllen ist beim Anlegen der Sitzung schon angestossen — beim
+ // nächsten Abspielen stehen sie drin.
+ var text = datei is null ? [] : spuren
.Where(x => x.Codec is null || !Untertitelvorrat.IstBild(x.Codec))
+ .Where(x => vorrat.Liegtvor(datei.Id, datei.ModifiedAt, x.Index))
.ToList();
var liste = new StringBuilder();
diff --git a/server/src/Flaro.Api/Streamkoordinator.cs b/server/src/Flaro.Api/Streamkoordinator.cs
index 7deacbc..9488704 100644
--- a/server/src/Flaro.Api/Streamkoordinator.cs
+++ b/server/src/Flaro.Api/Streamkoordinator.cs
@@ -259,14 +259,34 @@ public sealed class Streamkoordinator(
/// Ob die Playlist schon etwas enthält, das sich abspielen lässt.
public static bool PlaylistBereit(string ordner) => Playlist(ordner) is not null;
+ /// Das Gerät, über das VAAPI läuft.
+ private const string Vaapigeraet = "/dev/dri/renderD128";
+
+ /// Das Ergebnis der einmaligen Probe — null, solange keine lief.
+ private bool? _vaapiLaeuft;
+
/// Welche Beschleunigung benutzt wird.
///
///
- /// auto prüft, statt zu behaupten. Liegt kein
- /// /dev/dri/renderD128 vor — auf einem Mac, in einem Container ohne
- /// durchgereichtes Gerät —, läuft es in Software weiter. Langsamer, aber
- /// es läuft; ein ffmpeg, das mit „No VA display found" abbricht, wäre für
- /// den Zuschauer dasselbe wie ein kaputter Server.
+ ///
+ /// auto fragt ffmpeg, statt die Datei anzusehen. Hier stand
+ /// File.Exists("/dev/dri/renderD128"), und das war keine Prüfung,
+ /// sondern eine Vermutung: auf dem NAS liegt das Gerät durchgereicht da,
+ /// aber dahinter steht keine VA-Anzeige. ffmpeg bricht dann schon an der
+ /// globalen Option ab — No VA display found for device, Rückgabe
+ /// 234 —, und für den Zuschauer sieht das aus wie ein kaputter Server.
+ ///
+ ///
+ /// Einmal je Lauf. Die Probe kostet einen ffmpeg-Start; sie vor
+ /// jeder Wiedergabe zu wiederholen wäre Aufwand für eine Antwort, die
+ /// sich nicht ändert. Wechselt die Hardware, wechselt auch der
+ /// Container.
+ ///
+ ///
+ /// FLARO_HWACCEL=vaapi überspringt sie — wer es
+ /// ausdrücklich verlangt, hat einen Grund, und ein Fehlschlag steht dann
+ /// im Protokoll.
+ ///
///
private Beschleunigung Hardware()
{
@@ -276,10 +296,62 @@ public sealed class Streamkoordinator(
{
"none" or "software" => Beschleunigung.Software,
"vaapi" => Beschleunigung.Vaapi,
- _ => File.Exists("/dev/dri/renderD128") ? Beschleunigung.Vaapi : Beschleunigung.Software,
+ _ => (_vaapiLaeuft ??= VaapiLaeuft()) ? Beschleunigung.Vaapi : Beschleunigung.Software,
};
}
+ /// Lässt ffmpeg das Gerät einmal wirklich öffnen.
+ private bool VaapiLaeuft()
+ {
+ if (!File.Exists(Vaapigeraet)) return false;
+
+ try
+ {
+ var start = new ProcessStartInfo(ffmpeg.Ffmpeg)
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ };
+ // Ein Bild aus dem Nichts, nirgendwohin. Es geht allein darum, ob
+ // die Zeile vor dem ersten Bild durchgeht.
+ foreach (var a in new[]
+ {
+ "-hide_banner", "-loglevel", "error",
+ "-vaapi_device", Vaapigeraet,
+ "-f", "lavfi", "-i", "nullsrc=s=64x64:d=0.1",
+ "-frames:v", "1", "-f", "null", "-",
+ })
+ {
+ start.ArgumentList.Add(a);
+ }
+
+ using var prozess = Process.Start(start);
+ if (prozess is null) return false;
+
+ // **Mit Frist.** Ein hängender Prozess hielte sonst die erste
+ // Wiedergabe nach dem Start auf.
+ if (!prozess.WaitForExit(5000))
+ {
+ try { prozess.Kill(entireProcessTree: true); } catch (InvalidOperationException) { }
+ log.LogWarning("Die VAAPI-Probe antwortete nicht; es läuft in Software.");
+ return false;
+ }
+
+ if (prozess.ExitCode == 0) return true;
+
+ log.LogInformation(
+ "VAAPI ist nicht benutzbar ({Code}); es läuft in Software. {Grund}",
+ prozess.ExitCode, prozess.StandardError.ReadToEnd().Trim());
+ return false;
+ }
+ catch (Exception fehler) when (fehler is System.ComponentModel.Win32Exception or IOException)
+ {
+ log.LogWarning(fehler, "Die VAAPI-Probe liess sich nicht starten; es läuft in Software.");
+ return false;
+ }
+ }
+
private Process Starten(Stromanfrage anfrage, Stromsitzung sitzung)
{
var argumente = HlsArgumente.Fuer(anfrage);
diff --git a/server/src/Flaro.Api/Untertitelvorrat.cs b/server/src/Flaro.Api/Untertitelvorrat.cs
index e3f85ce..b404e82 100644
--- a/server/src/Flaro.Api/Untertitelvorrat.cs
+++ b/server/src/Flaro.Api/Untertitelvorrat.cs
@@ -137,6 +137,25 @@ public sealed class Untertitelvorrat(
if (offen > 0) log.LogInformation("Untertitel: {Anzahl} Datei(en) werden im Hintergrund gelesen.", offen);
}
+ /// Ob diese Spur jetzt sofort ausgeliefert werden kann.
+ ///
+ ///
+ /// Ohne zu warten und ohne anzustossen. Für die HLS-Hauptliste:
+ /// was dort steht, muss ein Spieler in dem Augenblick abholen können, in
+ /// dem er es liest. AVPlayer nimmt ein „gleich" nicht an — eine Spur,
+ /// die beim Abruf noch nicht da ist, lässt ihn die ganze Wiedergabe
+ /// abbrechen, nicht bloss den Untertitel.
+ ///
+ /// Eine leere Datei zählt nicht. Sie ist das Ergebnis „diese Spur
+ /// hat keinen Text"; sie anzubieten wäre ein Menüeintrag, der zu nichts
+ /// führt.
+ ///
+ public bool Liegtvor(Guid dateiId, DateTimeOffset geaendert, int spur)
+ {
+ var ziel = Pfad(dateiId, geaendert, spur);
+ return File.Exists(ziel) && new FileInfo(ziel).Length > 0;
+ }
+
/// Holt eine Spur — und wartet dabei höchstens .
///
///
diff --git a/server/tests/Flaro.Api.Tests/StreamTests.cs b/server/tests/Flaro.Api.Tests/StreamTests.cs
index 93986f0..b4e5500 100644
--- a/server/tests/Flaro.Api.Tests/StreamTests.cs
+++ b/server/tests/Flaro.Api.Tests/StreamTests.cs
@@ -463,6 +463,14 @@ public class StreamTests
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; 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.
+ (await client.GetAsync(
+ $"/api/v1/files/{datei.Id}/subtitles/{spur.Index}.vtt")).EnsureSuccessStatusCode();
+
var liste = await client.GetStringAsync(strom.MasterUrl!);
Assert.Multiple(() =>