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:
co-authored by
Claude Opus 5
parent
34ade18ed6
commit
b88e58d5eb
@@ -143,7 +143,7 @@ public static class StreamEndpoints
|
|||||||
// weiterhin `index.m3u8`.
|
// weiterhin `index.m3u8`.
|
||||||
app.MapGet("/api/v1/stream/{sitzung:guid}/master.m3u8", async (
|
app.MapGet("/api/v1/stream/{sitzung:guid}/master.m3u8", async (
|
||||||
Guid sitzung, HttpResponse antwort, Streamkoordinator koordinator,
|
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
|
// Dieselbe Begründung wie bei der Medienliste darunter: eine
|
||||||
// Liste, die sich ändern kann, darf nicht zwischengespeichert
|
// Liste, die sich ändern kann, darf nicht zwischengespeichert
|
||||||
@@ -157,8 +157,23 @@ public static class StreamEndpoints
|
|||||||
.OrderBy(x => x.Index)
|
.OrderBy(x => x.Index)
|
||||||
.ToListAsync(ct);
|
.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 => x.Codec is null || !Untertitelvorrat.IstBild(x.Codec))
|
||||||
|
.Where(x => vorrat.Liegtvor(datei.Id, datei.ModifiedAt, x.Index))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var liste = new StringBuilder();
|
var liste = new StringBuilder();
|
||||||
|
|||||||
@@ -259,14 +259,34 @@ public sealed class Streamkoordinator(
|
|||||||
/// <summary>Ob die Playlist schon etwas enthält, das sich abspielen lässt.</summary>
|
/// <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;
|
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>
|
/// <summary>Welche Beschleunigung benutzt wird.</summary>
|
||||||
///
|
///
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <b><c>auto</c> prüft, statt zu behaupten.</b> Liegt kein
|
/// <para>
|
||||||
/// <c>/dev/dri/renderD128</c> vor — auf einem Mac, in einem Container ohne
|
/// <b><c>auto</c> fragt ffmpeg, statt die Datei anzusehen.</b> Hier stand
|
||||||
/// durchgereichtes Gerät —, läuft es in Software weiter. Langsamer, aber
|
/// <c>File.Exists("/dev/dri/renderD128")</c>, und das war keine Prüfung,
|
||||||
/// es läuft; ein ffmpeg, das mit „No VA display found" abbricht, wäre für
|
/// sondern eine Vermutung: auf dem NAS liegt das Gerät durchgereicht da,
|
||||||
/// den Zuschauer dasselbe wie ein kaputter Server.
|
/// 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>
|
/// </remarks>
|
||||||
private Beschleunigung Hardware()
|
private Beschleunigung Hardware()
|
||||||
{
|
{
|
||||||
@@ -276,10 +296,62 @@ public sealed class Streamkoordinator(
|
|||||||
{
|
{
|
||||||
"none" or "software" => Beschleunigung.Software,
|
"none" or "software" => Beschleunigung.Software,
|
||||||
"vaapi" => Beschleunigung.Vaapi,
|
"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)
|
private Process Starten(Stromanfrage anfrage, Stromsitzung sitzung)
|
||||||
{
|
{
|
||||||
var argumente = HlsArgumente.Fuer(anfrage);
|
var argumente = HlsArgumente.Fuer(anfrage);
|
||||||
|
|||||||
@@ -137,6 +137,25 @@ public sealed class Untertitelvorrat(
|
|||||||
if (offen > 0) log.LogInformation("Untertitel: {Anzahl} Datei(en) werden im Hintergrund gelesen.", offen);
|
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>
|
/// <summary>Holt eine Spur — und wartet dabei höchstens <paramref name="geduld"/>.</summary>
|
||||||
///
|
///
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
|||||||
@@ -463,6 +463,14 @@ public class StreamTests
|
|||||||
var strom = await Strom(client, datei.Id, Browser());
|
var strom = await Strom(client, datei.Id, Browser());
|
||||||
|
|
||||||
var spur = strom.SubtitleStreams.Single();
|
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!);
|
var liste = await client.GetStringAsync(strom.MasterUrl!);
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user