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(() =>