Die Untertitel holen, bevor jemand sie einschaltet

Zwei Minuten „wird geholt", und noch immer kein Text. Die Ablage von vorhin
war richtig und reichte nicht: beim ersten Mal musste immer noch jemand
warten, und zwar der, der gerade einen Film anschaut.

Das Lesen selbst lässt sich nicht beschleunigen. Untertitelpakete liegen
über die ganze Datei verteilt; sie zu finden heisst, alles zu lesen. Bei
vierzig Gigabyte auf einem NAS sind das Minuten, und daran ändert kein
Schalter etwas. Also nicht schneller lesen, sondern früher.

Der Vorrat arbeitet die Mediathek jetzt von sich aus durch: beim Start des
Servers und nach jedem Scan, die zuletzt hinzugekommenen zuerst. Eine Datei
nach der anderen, mit zwei Sekunden Pause dazwischen — das Lesen ist von
der Platte begrenzt, und eine laufende Wiedergabe liest von derselben
Platte und ist wichtiger. Wer einen Film startet, drängelt sich vor: seine
Datei ist als Nächstes dran statt als Dreissigste.

Einmal je Datei, nie wieder: der Name der Ablage trägt den
Änderungszeitpunkt. Eine ersetzte Datei bekommt dadurch von selbst einen
neuen Namen, eine unveränderte wird nie zweimal gelesen.

Gemessen am Prüfserver mit leerer Ablage:

    Start, ohne jeden Abruf   -> „1 Datei(en) werden im Hintergrund
                                  gelesen", danach „2 von 2 Spur(en)"
    Klick auf „Deutsch"       -> 153 ms bis zum Text, 10 Einblendungen,
                                  die erste bei 0 bis 3 s

Landet jemand trotzdem im Wartefall, gibt der Spieler nicht mehr nach zwei
Minuten auf, sondern fragt zehn Minuten lang nach — und sagt dazu, dass es
einmalig ist.

Die Auswahl der Textspuren stand zweimal im Quelltext und steht jetzt
einmal: im Vorrat. Zwei Listen, von denen eine das Menü füllt und die
andere bestimmt, was geholt wird, laufen sonst irgendwann auseinander.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-07 22:14:20 +02:00
co-authored by Claude Opus 5
parent c2fd2a86fd
commit 1531163bc1
5 changed files with 229 additions and 85 deletions
@@ -53,8 +53,7 @@ public static class StreamEndpoints
// warten; der Vorrat ist bis dahin längst gefüllt. Wer sie sofort // warten; der Vorrat ist bis dahin längst gefüllt. Wer sie sofort
// einschaltet, wartet so lange wie vorher, bekommt aber eine // einschaltet, wartet so lange wie vorher, bekommt aber eine
// Antwort statt eines Fehlers. // Antwort statt eines Fehlers.
vorrat.Anstossen(gewuenschte.Id, gewuenschte.Path, gewuenschte.ModifiedAt, vorrat.Anstellen(gewuenschte.Id, vordraengeln: true);
await Textspuren(db, id, CancellationToken.None));
var profil = Profil(anfrage); var profil = Profil(anfrage);
var hoehe = anfrage.Height is { } gewuenscht var hoehe = anfrage.Height is { } gewuenscht
@@ -238,13 +237,12 @@ public static class StreamEndpoints
if (datei is null || !File.Exists(datei.Path)) if (datei is null || !File.Exists(datei.Path))
return Einwand.Fehlt("Diese Datei liegt nicht mehr da."); return Einwand.Fehlt("Diese Datei liegt nicht mehr da.");
var spuren = await Textspuren(db, id, ct); var spuren = await Untertitelvorrat.TextspurenAsync(db, id, ct);
if (!spuren.Contains(spur)) if (!spuren.Contains(spur))
return Einwand.Fehlt("Diese Spur enthält keinen Text, sondern Bilder."); return Einwand.Fehlt("Diese Spur enthält keinen Text, sondern Bilder.");
var (stand, text) = await vorrat.HolenAsync( var (stand, text) = await vorrat.HolenAsync(
datei.Id, datei.Path, datei.ModifiedAt, spuren, spur, datei.Id, datei.ModifiedAt, spur, TimeSpan.FromSeconds(20), ct);
TimeSpan.FromSeconds(20), ct);
return stand switch return stand switch
{ {
@@ -308,7 +306,7 @@ public static class StreamEndpoints
.Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault)) .Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault))
.ToList(), .ToList(),
SubtitleStreams: spuren.Where(s => s.Kind == MediaStreamKind.Subtitle) SubtitleStreams: spuren.Where(s => s.Kind == MediaStreamKind.Subtitle)
.Where(s => s.Codec is null || !IstBild(s.Codec)) .Where(s => s.Codec is null || !Untertitelvorrat.IstBild(s.Codec))
.Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault)) .Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault))
.ToList()); .ToList());
} }
@@ -360,7 +358,7 @@ public static class StreamEndpoints
.Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault)) .Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault))
.ToList(), .ToList(),
SubtitleStreams: spuren.Where(s => s.Kind == MediaStreamKind.Subtitle) SubtitleStreams: spuren.Where(s => s.Kind == MediaStreamKind.Subtitle)
.Where(s => s.Codec is null || !IstBild(s.Codec)) .Where(s => s.Codec is null || !Untertitelvorrat.IstBild(s.Codec))
.Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault)) .Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault))
.ToList()); .ToList());
} }
@@ -372,26 +370,6 @@ public static class StreamEndpoints
/// PGS ist auf jedem Blu-ray-Rip die Regel — ein Eintrag im Menü, der zu /// PGS ist auf jedem Blu-ray-Rip die Regel — ein Eintrag im Menü, der zu
/// nichts führt, ist schlechter als kein Eintrag. /// nichts führt, ist schlechter als kein Eintrag.
/// </remarks> /// </remarks>
private static bool IstBild(string codec) =>
codec is "hdmv_pgs_subtitle" or "pgssub" or "dvd_subtitle" or "dvdsub" or "xsub";
/// <summary>Die Spurnummern, aus denen sich Text holen lässt.</summary>
///
/// <remarks>
/// Dieselbe Auswahl, die auch im Menü steht — sonst böte der Spieler eine
/// Spur an, die der Vorrat gar nicht erst zieht.
/// </remarks>
private static async Task<IReadOnlyList<int>> Textspuren(
FlaroContext db, Guid dateiId, CancellationToken ct)
{
var alle = await db.MediaStreams.AsNoTracking()
.Where(s => s.MediaFileId == dateiId && s.Kind == MediaStreamKind.Subtitle)
.OrderBy(s => s.Index)
.Select(s => new { s.Index, s.Codec })
.ToListAsync(ct);
return alle.Where(s => s.Codec is null || !IstBild(s.Codec)).Select(s => s.Index).ToList();
}
/// <summary>Wie eine Spur im Menü heisst.</summary> /// <summary>Wie eine Spur im Menü heisst.</summary>
private static string Benennung(MediaStream spur) private static string Benennung(MediaStream spur)
+13 -1
View File
@@ -312,7 +312,19 @@ if (Directory.Exists(oberfläche))
var leben = app.Services.GetRequiredService<IHostApplicationLifetime>(); var leben = app.Services.GetRequiredService<IHostApplicationLifetime>();
leben.ApplicationStarted.Register(() => leben.ApplicationStarted.Register(() =>
tagebuch.Schreiben($"Server gestartet — {Buildkennung.Beides}")); {
tagebuch.Schreiben($"Server gestartet — {Buildkennung.Beides}");
// **Die Untertitel holen, bevor jemand sie einschaltet.**
//
// Eine Spur aus einer Datei zu ziehen heisst, die Datei ganz zu
// lesen — bei einem Dreistünder auf einem NAS sind das Minuten.
// Das im Augenblick des Einschaltens zu tun hiess: zwei Minuten
// „wird geholt", und noch immer kein Text. Hier läuft es einmal je
// Datei durch, eine nach der anderen, während niemand wartet.
_ = app.Services.GetRequiredService<Untertitelvorrat>()
.NachtragenAsync(leben.ApplicationStopping);
});
leben.ApplicationStopping.Register(() => leben.ApplicationStopping.Register(() =>
tagebuch.Schreiben("Server fährt herunter.")); tagebuch.Schreiben("Server fährt herunter."));
} }
+8
View File
@@ -152,6 +152,14 @@ public sealed class ScanCoordinator(
zusammengelegt.Filme, zusammengelegt.Serien, libraryId); zusammengelegt.Filme, zusammengelegt.Serien, libraryId);
} }
// **Untertitel gleich mitnehmen.** Sie aus einer Datei zu holen
// heisst, die Datei ganz zu lesen; das im Augenblick des
// Einschaltens zu tun heisst, den Zuschauer Minuten warten zu
// lassen. Der Vorrat arbeitet das im Hintergrund ab, eine Datei
// nach der anderen.
_ = bereich.ServiceProvider.GetRequiredService<Untertitelvorrat>()
.NachtragenAsync(CancellationToken.None);
_zustand[libraryId] = _zustand[libraryId] with _zustand[libraryId] = _zustand[libraryId] with
{ {
Status = "finished", Status = "finished",
+193 -50
View File
@@ -1,9 +1,11 @@
using System.Collections.Concurrent; using Flaro.Core.Library;
using Flaro.Data;
using Flaro.Media; using Flaro.Media;
using Microsoft.EntityFrameworkCore;
namespace Flaro.Api; namespace Flaro.Api;
/// <summary>Herausgezogene Untertitel, abgelegt für den zweiten Abruf.</summary> /// <summary>Herausgezogene Untertitel — vorher geholt, nicht beim Einschalten.</summary>
/// ///
/// <remarks> /// <remarks>
/// <para> /// <para>
@@ -11,24 +13,33 @@ namespace Flaro.Api;
/// laden", und auf der zweiten Spur kam überhaupt nichts. Der Grund lag nicht /// laden", und auf der zweiten Spur kam überhaupt nichts. Der Grund lag nicht
/// im Format, sondern in der Zeit: jeder Abruf startete ffmpeg, ffmpeg musste /// im Format, sondern in der Zeit: jeder Abruf startete ffmpeg, ffmpeg musste
/// die ganze Datei durchlesen, und nach dreissig Sekunden wurde abgebrochen. /// die ganze Datei durchlesen, und nach dreissig Sekunden wurde abgebrochen.
/// Bei einem kleinen Prüffilm fällt das nie auf; bei einem Dreistünder auf
/// einem NAS reicht es nie.
/// </para> /// </para>
/// <para> /// <para>
/// <b>Und es wurde schlimmer, bevor es besser wurde.</b> Seit die Adresse den /// <b>Und das Lesen lässt sich nicht beschleunigen.</b> Untertitelpakete
/// Einstiegspunkt mitführt (<c>?ab=</c>), ändert sie sich bei jedem Sprung — /// liegen über die ganze Datei verteilt; sie zu finden heisst, alles zu
/// der Browser konnte nichts mehr aus seinem eigenen Zwischenspeicher /// lesen. Bei vierzig Gigabyte auf einem NAS sind das Minuten, und daran
/// nehmen, und jeder Sprung stiess einen neuen Durchlauf an. Deshalb liegt /// ändert kein Schalter etwas. Auch mit Ablage blieb deshalb der erste
/// hier die ungeschobene Spur; das Verschieben ist eine Zeilenersetzung und /// Abruf eine Zumutung: zwei Minuten „wird geholt", und noch immer kein
/// kostet nichts. /// Text.
/// </para> /// </para>
/// <para> /// <para>
/// <b>Warm, bevor jemand fragt.</b> Angestossen wird beim Start des Stroms, /// <b>Also vorher.</b> Dieser Vorrat arbeitet die Mediathek von sich aus
/// nicht erst beim Öffnen des Menüs. Wer nach fünf Minuten Untertitel /// durch — beim Start des Servers und nach jedem Scan, eine Datei nach der
/// einschaltet, wartet dann gar nicht mehr. /// anderen, damit die Platte nicht von zehn ffmpeg-Läufen zugleich belegt
/// wird. Wer einen Film startet, drängelt sich vor: sein Film ist dann als
/// Nächstes dran statt als Dreissigster.
/// </para>
/// <para>
/// <b>Einmal je Datei, nie wieder.</b> Der Name der Ablage trägt den
/// Änderungszeitpunkt der Datei. Eine ersetzte Datei bekommt dadurch von
/// selbst einen neuen Namen und wird neu gelesen; eine unveränderte nie.
/// </para> /// </para>
/// </remarks> /// </remarks>
public sealed class Untertitelvorrat(FlaroPaths pfade, UntertitelLeser leser) public sealed class Untertitelvorrat(
FlaroPaths pfade,
UntertitelLeser leser,
IServiceScopeFactory scopes,
ILogger<Untertitelvorrat> log)
{ {
/// <summary>Was ein Abruf vorfindet.</summary> /// <summary>Was ein Abruf vorfindet.</summary>
public enum Stand public enum Stand
@@ -39,38 +50,78 @@ public sealed class Untertitelvorrat(FlaroPaths pfade, UntertitelLeser leser)
/// <summary>Liegt da und ist leer — diese Spur hat keinen Text.</summary> /// <summary>Liegt da und ist leer — diese Spur hat keinen Text.</summary>
Leer, Leer,
/// <summary>Wird gerade herausgezogen.</summary> /// <summary>Wird gerade geholt oder wartet in der Reihe.</summary>
Laeuft, Laeuft,
} }
/// <summary>Ein Lauf je Datei, nicht je Spur — er holt ohnehin alle.</summary> /// <summary>Die Reihe der noch zu lesenden Dateien. Vorn wird genommen.</summary>
private readonly ConcurrentDictionary<Guid, Task> _laeuft = new(); private readonly LinkedList<Guid> _reihe = new();
private readonly Lock _schloss = new();
private bool _arbeitet;
/// <summary>Welche Datei gerade gelesen wird — für das Protokoll.</summary>
public Guid? Laufende { get; private set; }
/// <summary>Wie viele noch warten.</summary>
public int Wartend { get { lock (_schloss) return _reihe.Count; } }
public string Pfad(Guid dateiId, DateTimeOffset geaendert, int spur) => public string Pfad(Guid dateiId, DateTimeOffset geaendert, int spur) =>
Path.Combine(pfade.Transcode, "untertitel", Path.Combine(pfade.Transcode, "untertitel",
$"{dateiId:N}-{geaendert.ToUnixTimeMilliseconds()}-{spur}.vtt"); $"{dateiId:N}-{geaendert.ToUnixTimeMilliseconds()}-{spur}.vtt");
/// <summary>Stösst das Herausziehen an, wenn etwas fehlt — und kehrt sofort zurück.</summary> /// <summary>Stellt eine Datei an — vorn, wenn jemand auf sie wartet.</summary>
public void Anstossen(Guid dateiId, string quelle, DateTimeOffset geaendert, IReadOnlyList<int> spuren) public void Anstellen(Guid dateiId, bool vordraengeln = false)
{ {
if (spuren.Count == 0) return; lock (_schloss)
var offen = spuren
.Where(s => !File.Exists(Pfad(dateiId, geaendert, s)))
.Select(s => new UntertitelLeser.Auftrag(s, Pfad(dateiId, geaendert, s)))
.ToList();
if (offen.Count == 0) return;
// **Ohne das Abbruchzeichen des Abrufers.** Der Lauf gehört nicht dem
// ersten, der gefragt hat: schaltet der sein Fenster zu, während die
// Datei gelesen wird, wäre sonst alles umsonst und der nächste finge
// von vorn an.
_laeuft.GetOrAdd(dateiId, _ => Task.Run(async () =>
{ {
try { await leser.ZiehenAsync(quelle, offen, CancellationToken.None); } if (Laufende == dateiId) return;
finally { _laeuft.TryRemove(dateiId, out Task? _); }
})); var vorhanden = _reihe.Find(dateiId);
if (vorhanden is not null)
{
if (!vordraengeln) return;
_reihe.Remove(vorhanden);
}
if (vordraengeln) _reihe.AddFirst(dateiId);
else _reihe.AddLast(dateiId);
if (_arbeitet) return;
_arbeitet = true;
}
_ = Task.Run(Abarbeiten);
}
/// <summary>Stellt alles an, was noch keine Ablage hat.</summary>
///
/// <remarks>
/// Die zuletzt hinzugekommenen zuerst: was gerade in die Mediathek kam,
/// wird am ehesten als Nächstes geschaut.
/// </remarks>
public async Task NachtragenAsync(CancellationToken ct = default)
{
using var raum = scopes.CreateScope();
var db = raum.ServiceProvider.GetRequiredService<FlaroContext>();
var dateien = await db.MediaFiles.AsNoTracking()
.OrderByDescending(f => f.Id)
.Select(f => new { f.Id, f.ModifiedAt })
.ToListAsync(ct);
var offen = 0;
foreach (var datei in dateien)
{
var spuren = await TextspurenAsync(db, datei.Id, ct);
if (spuren.Count == 0) continue;
if (spuren.All(s => File.Exists(Pfad(datei.Id, datei.ModifiedAt, s)))) continue;
Anstellen(datei.Id);
offen++;
}
if (offen > 0) log.LogInformation("Untertitel: {Anzahl} Datei(en) werden im Hintergrund gelesen.", offen);
} }
/// <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>
@@ -79,35 +130,127 @@ public sealed class Untertitelvorrat(FlaroPaths pfade, UntertitelLeser leser)
/// <b>Warum überhaupt gewartet wird.</b> Ein Abruf, der sofort „noch /// <b>Warum überhaupt gewartet wird.</b> Ein Abruf, der sofort „noch
/// nicht" sagt, zwingt den Spieler in eine Schleife, auch wenn die Datei /// nicht" sagt, zwingt den Spieler in eine Schleife, auch wenn die Datei
/// in zwei Sekunden fertig gewesen wäre. Ein Abruf, der unbegrenzt /// in zwei Sekunden fertig gewesen wäre. Ein Abruf, der unbegrenzt
/// wartet, hängt hinter jedem Reverse Proxy ins Zeitlimit. Zwanzig /// wartet, hängt hinter jedem Reverse Proxy ins Zeitlimit.
/// Sekunden decken den gewöhnlichen Fall ab; der seltene bekommt eine
/// Antwort, mit der sich etwas anfangen lässt.
/// </remarks> /// </remarks>
public async Task<(Stand Stand, string Text)> HolenAsync( public async Task<(Stand Stand, string Text)> HolenAsync(
Guid dateiId, string quelle, DateTimeOffset geaendert, Guid dateiId, DateTimeOffset geaendert, int spur, TimeSpan geduld, CancellationToken ct)
IReadOnlyList<int> spuren, int spur, TimeSpan geduld, CancellationToken ct)
{ {
var ziel = Pfad(dateiId, geaendert, spur); var ziel = Pfad(dateiId, geaendert, spur);
if (File.Exists(ziel)) return await Lesen(ziel, ct);
Anstossen(dateiId, quelle, geaendert, spuren); Anstellen(dateiId, vordraengeln: true);
var bis = DateTimeOffset.UtcNow + geduld; var bis = DateTimeOffset.UtcNow + geduld;
while (true) while (DateTimeOffset.UtcNow < bis)
{ {
if (File.Exists(ziel)) await Task.Delay(TimeSpan.FromMilliseconds(400), ct);
if (File.Exists(ziel)) return await Lesen(ziel, ct);
// Durch und trotzdem nichts da: diese Spur liess sich nicht
// herausziehen. Weiterzuwarten hiesse, auf etwas zu warten, das
// niemand mehr tut.
lock (_schloss)
{
if (Laufende != dateiId && _reihe.Find(dateiId) is null) return (Stand.Leer, "");
}
}
return (Stand.Laeuft, "");
}
private static async Task<(Stand, string)> Lesen(string ziel, CancellationToken ct)
{ {
var text = await File.ReadAllTextAsync(ziel, ct); var text = await File.ReadAllTextAsync(ziel, ct);
return (text.Length == 0 ? Stand.Leer : Stand.Fertig, text); return (text.Length == 0 ? Stand.Leer : Stand.Fertig, text);
} }
// Der Lauf ist durch und die Datei fehlt trotzdem: diese Spur /// <summary>Eine Datei nach der anderen, bis die Reihe leer ist.</summary>
// liess sich nicht herausziehen. Weiterzuwarten hiesse, auf ///
// etwas zu warten, das niemand mehr tut. /// <remarks>
if (!_laeuft.ContainsKey(dateiId)) return (Stand.Leer, ""); /// <b>Eine, nicht zehn.</b> Das Lesen ist von der Platte begrenzt, nicht
/// vom Rechner. Zehn Läufe nebeneinander machen jeden einzelnen
/// langsamer und bringen die laufende Wiedergabe zum Stocken — und die
/// ist wichtiger als der Vorrat.
/// </remarks>
private async Task Abarbeiten()
{
while (true)
{
Guid naechste;
lock (_schloss)
{
if (_reihe.First is null)
{
_arbeitet = false;
Laufende = null;
return;
}
if (DateTimeOffset.UtcNow >= bis) return (Stand.Laeuft, ""); naechste = _reihe.First.Value;
_reihe.RemoveFirst();
Laufende = naechste;
}
await Task.Delay(TimeSpan.FromMilliseconds(400), ct); try { await Ziehen(naechste); }
catch (Exception fehler)
{
log.LogWarning(fehler, "Untertitel der Datei {Kennung} liessen sich nicht holen.", naechste);
}
// **Luft holen zwischen zwei Dateien.** Der Vorrat liest die
// Platte am Stück leer; eine laufende Wiedergabe liest von
// derselben Platte und ist wichtiger. Zwei Sekunden kosten bei
// einem Lauf über Stunden nichts und nehmen den Druck heraus.
await Task.Delay(TimeSpan.FromSeconds(2));
} }
} }
private async Task Ziehen(Guid dateiId)
{
using var raum = scopes.CreateScope();
var db = raum.ServiceProvider.GetRequiredService<FlaroContext>();
var datei = await db.MediaFiles.AsNoTracking().FirstOrDefaultAsync(f => f.Id == dateiId);
if (datei is null || !File.Exists(datei.Path)) return;
var spuren = await TextspurenAsync(db, dateiId, CancellationToken.None);
var offen = spuren
.Where(s => !File.Exists(Pfad(dateiId, datei.ModifiedAt, s)))
.Select(s => new UntertitelLeser.Auftrag(s, Pfad(dateiId, datei.ModifiedAt, s)))
.ToList();
if (offen.Count == 0) return;
var begonnen = DateTimeOffset.UtcNow;
var fertig = await leser.ZiehenAsync(datei.Path, offen, CancellationToken.None);
log.LogInformation(
"Untertitel aus {Datei}: {Fertig} von {Offen} Spur(en) in {Dauer:0.0} s. Noch {Rest} Datei(en).",
Path.GetFileName(datei.Path), fertig.Count, offen.Count,
(DateTimeOffset.UtcNow - begonnen).TotalSeconds, Wartend);
}
/// <summary>Die Spurnummern, aus denen sich Text holen lässt.</summary>
///
/// <remarks>
/// Dieselbe Auswahl, die auch im Menü steht — sonst zöge der Vorrat
/// Spuren, die niemand anbietet, oder liesse welche aus, die jemand
/// einschaltet. Bildspuren wie PGS lassen sich nicht nach WebVTT
/// wandeln; das hiesse, Schrift zu erkennen.
/// </remarks>
public static async Task<IReadOnlyList<int>> TextspurenAsync(
FlaroContext db, Guid dateiId, CancellationToken ct)
{
var alle = await db.MediaStreams.AsNoTracking()
.Where(s => s.MediaFileId == dateiId && s.Kind == MediaStreamKind.Subtitle)
.OrderBy(s => s.Index)
.Select(s => new { s.Index, s.Codec })
.ToListAsync(ct);
return alle.Where(s => s.Codec is null || !IstBild(s.Codec)).Select(s => s.Index).ToList();
}
/// <summary>Bildbasierte Untertitel lassen sich nicht nach WebVTT wandeln.</summary>
public static bool IstBild(string codec) =>
codec is "hdmv_pgs_subtitle" or "pgssub" or "dvd_subtitle" or "dvdsub" or "xsub";
} }
+8 -5
View File
@@ -793,9 +793,12 @@ export function spieler(anfrage: Spieleranfrage): void {
.find(t => t.dataset.spur === String(spur)); .find(t => t.dataset.spur === String(spur));
if (!element) return; if (!element) return;
// Rund zwei Minuten lang nachfragen. Wer länger braucht, hat ein // **Lange nachfragen, und zwar absichtlich.** Der Server holt die Spur
// Problem, das kein Warten löst — und bekommt es dann auch gesagt. // normalerweise schon im Hintergrund, bevor jemand sie einschaltet;
for (let versuch = 0; versuch < 40; versuch++) { // landet man trotzdem hier, läuft gerade das Lesen einer grossen Datei,
// und das dauert Minuten. Aufzugeben hiesse, kurz vor dem Ziel
// abzubrechen — zehn Minuten sind der Preis dafür, dass es dann klappt.
for (let versuch = 0; versuch < 200; versuch++) {
if (geschlossen || untertitel !== spur) return; if (geschlossen || untertitel !== spur) return;
const stand = await untertitelHolen(anfrage.dateiId, spur, vorlauf); const stand = await untertitelHolen(anfrage.dateiId, spur, vorlauf);
@@ -816,12 +819,12 @@ export function spieler(anfrage: Spieleranfrage): void {
} }
if (versuch === 0) { if (versuch === 0) {
hinweisZeigen('Die Untertitel werden aus der Datei geholt — das dauert beim ersten Mal.'); hinweisZeigen('Diese Untertitel werden einmalig aus der Datei geholt. Bei einem grossen Film dauert das ein paar Minuten — danach sind sie sofort da.');
} }
await new Promise(r => setTimeout(r, 3000)); await new Promise(r => setTimeout(r, 3000));
} }
hinweisZeigen('Die Untertitel brauchen zu lange. Versuche es später noch einmal.', 'fehler'); hinweisZeigen('Die Untertitel brauchen ungewöhnlich lange. Schau später noch einmal — im Hintergrund wird weitergelesen.', 'fehler');
} }
/* ---------- Fehler ---------- */ /* ---------- Fehler ---------- */