Das Gerät war da, die VA-Anzeige dahinter nicht

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-10 19:59:49 +02:00
co-authored by Claude Opus 5
parent 34ade18ed6
commit b88e58d5eb
4 changed files with 122 additions and 8 deletions
@@ -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();
+78 -6
View File
@@ -259,14 +259,34 @@ public sealed class Streamkoordinator(
/// <summary>Ob die Playlist schon etwas enthält, das sich abspielen lässt.</summary>
public static bool PlaylistBereit(string ordner) => Playlist(ordner) is not null;
/// <summary>Das Gerät, über das VAAPI läuft.</summary>
private const string Vaapigeraet = "/dev/dri/renderD128";
/// <summary>Das Ergebnis der einmaligen Probe — <c>null</c>, solange keine lief.</summary>
private bool? _vaapiLaeuft;
/// <summary>Welche Beschleunigung benutzt wird.</summary>
///
/// <remarks>
/// <b><c>auto</c> prüft, statt zu behaupten.</b> Liegt kein
/// <c>/dev/dri/renderD128</c> 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.
/// <para>
/// <b><c>auto</c> fragt ffmpeg, statt die Datei anzusehen.</b> Hier stand
/// <c>File.Exists("/dev/dri/renderD128")</c>, 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 — <c>No VA display found for device</c>, Rückgabe
/// 234 —, und für den Zuschauer sieht das aus wie ein kaputter Server.
/// </para>
/// <para>
/// <b>Einmal je Lauf.</b> 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.
/// </para>
/// <para>
/// <b><c>FLARO_HWACCEL=vaapi</c> überspringt sie</b> — wer es
/// ausdrücklich verlangt, hat einen Grund, und ein Fehlschlag steht dann
/// im Protokoll.
/// </para>
/// </remarks>
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,
};
}
/// <summary>Lässt ffmpeg das Gerät einmal wirklich öffnen.</summary>
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);
+19
View File
@@ -137,6 +137,25 @@ public sealed class Untertitelvorrat(
if (offen > 0) log.LogInformation("Untertitel: {Anzahl} Datei(en) werden im Hintergrund gelesen.", offen);
}
/// <summary>Ob diese Spur jetzt sofort ausgeliefert werden kann.</summary>
///
/// <remarks>
/// <b>Ohne zu warten und ohne anzustossen.</b> 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.
///
/// <b>Eine leere Datei zählt nicht.</b> Sie ist das Ergebnis „diese Spur
/// hat keinen Text"; sie anzubieten wäre ein Menüeintrag, der zu nichts
/// führt.
/// </remarks>
public bool Liegtvor(Guid dateiId, DateTimeOffset geaendert, int spur)
{
var ziel = Pfad(dateiId, geaendert, spur);
return File.Exists(ziel) && new FileInfo(ziel).Length > 0;
}
/// <summary>Holt eine Spur — und wartet dabei höchstens <paramref name="geduld"/>.</summary>
///
/// <remarks>