From b88e58d5eb33d0b8a2cda00f4f1afd565b196ffe Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 10 Oct 2026 19:59:49 +0200 Subject: [PATCH] =?UTF-8?q?Das=20Ger=C3=A4t=20war=20da,=20die=20VA-Anzeige?= =?UTF-8?q?=20dahinter=20nicht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ffmpeg endete mit 234. No VA display found for device /dev/dri/renderD128. Device creation failed: -22. 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 — eine VA-Anzeige steht nicht dahinter. ffmpeg bricht dann schon an der globalen Option ab, vor dem ersten Bild, und für den Zuschauer sieht das aus wie ein kaputter Server: HTTP 500 auf die Abspielliste, „Die Wiedergabe wurde unterbrochen". Jetzt wird ffmpeg einmal je Lauf wirklich gefragt: ein Bild aus dem Nichts, nirgendwohin, mit Frist. Geht die Zeile vor dem ersten Bild nicht durch, läuft es in Software weiter — langsamer, aber es läuft. FLARO_HWACCEL=vaapi überspringt die Probe für den, der es ausdrücklich verlangt. Dazu, aus derselben Gegend: in der Hauptliste stehen nur Untertitelspuren, die jetzt sofort abholbar sind. Sie werden beim ersten Bedarf aus der Datei gezogen, und bei einem Dreistünder dauert das Minuten. Der Browser verträgt das — er fragt die Spur einzeln an und wartet. AVPlayer bricht daran die ganze Wiedergabe ab, nicht bloss den Untertitel. Das Füllen läuft beim Anlegen der Sitzung schon an; beim nächsten Abspielen stehen sie drin. Co-Authored-By: Claude Opus 5 --- .../Flaro.Api/Endpoints/StreamEndpoints.cs | 19 ++++- server/src/Flaro.Api/Streamkoordinator.cs | 84 +++++++++++++++++-- server/src/Flaro.Api/Untertitelvorrat.cs | 19 +++++ server/tests/Flaro.Api.Tests/StreamTests.cs | 8 ++ 4 files changed, 122 insertions(+), 8 deletions(-) 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(() =>