Einmal aus der Datei holen, nicht bei jedem Abruf
„Diese Untertitelspur liess sich nicht laden", und auf der zweiten Spur kam gar nichts. Nicht das Format war schuld und auch nicht das Verschieben, sondern die Zeit: jeder Abruf startete ffmpeg, ffmpeg musste die ganze Datei durchlesen, und nach dreissig Sekunden wurde abgebrochen. An einem Prüffilm von fünfzehn Megabyte fällt das nie auf; bei einem Dreistünder auf einem NAS reicht es nie. Schlimmer wurde es durch meine eigene Korrektur von gestern: seit die Adresse den Einstiegspunkt mitführt (`?ab=`), ändert sie sich bei jedem Sprung — der Browser konnte nichts mehr aus seinem Zwischenspeicher nehmen, und jeder Sprung stiess einen neuen Durchlauf an. Jetzt: - Ein ffmpeg-Durchlauf holt alle Textspuren auf einmal. Das Lesen der Datei ist der teure Teil und wird nicht mehr je Spur bezahlt. - Das Ergebnis bleibt liegen, ungeschoben; das Verschieben ist eine Zeilenersetzung und kostet nichts. Gemessen: erster Abruf 0,42 s, jeder weitere 2 bis 3 ms — und der zweite Sprung in derselben Datei trifft dieselbe Ablage statt eines neuen Durchlaufs. - Angestossen wird beim Start des Stroms, nicht erst beim Einschalten. Wer nach einer halben Stunde Untertitel will, wartet dann gar nicht. - Geschrieben wird unter `.teil` und erst danach umbenannt. Eine abgebrochene Datei am richtigen Namen gälte sonst für immer als fertig. - Die Geduld steigt von 30 Sekunden auf 30 Minuten. Sie ist jetzt eine Obergrenze gegen einen hängenden Prozess, keine Wartezeit mehr. Und der Spieler kann endlich unterscheiden, was los ist. Ein `<track>` meldet jeden Misserfolg gleich — „es gibt sie nicht", „sie enthält keinen Text" und „der Server holt sie gerade" sahen identisch aus. Geholt wird deshalb selbst, mit Token statt Keks (gemessen: ohne Keks kam jeder Abruf mit 401 zurück, ohne dass irgendwo etwas stand), und bei 503 fragt der Spieler nach, statt aufzugeben. Der Text geht als Blob an den `<track>` — dieselbe Anzeige, ein ehrlicher Weg dahin. Dazu eine Unsauberkeit im Verschieben: die Trennzeile eines weggefallenen Blocks blieb stehen. Gemessen am echten Film 52 Leerzeilen vor dem ersten Satz. Geprüft im Spieler, kalter Vorrat, Einstieg bei Sekunde 500: Blob am `<track>`, zehn Einblendungen, die erste bei 0 bis 3 s mit „Deutsch bei 500 s". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
339a6c07d0
commit
c2fd2a86fd
@@ -672,6 +672,48 @@ export const untertitelAdresse = (datei: string, spur: number, vorlauf = 0) =>
|
||||
`/api/v1/files/${datei}/subtitles/${spur}.vtt`
|
||||
+ (vorlauf > 0 ? `?ab=${vorlauf.toFixed(3)}` : '');
|
||||
|
||||
/** Was beim Holen einer Untertitelspur herauskam. */
|
||||
export type Untertitelstand =
|
||||
| { art: 'da'; text: string }
|
||||
| { art: 'laeuft' }
|
||||
| { art: 'fehler'; satz: string };
|
||||
|
||||
/** Holt eine Untertitelspur — und unterscheidet „geht nicht" von „dauert noch".
|
||||
*
|
||||
* **Warum nicht einfach `track.src`.** Dann lädt der Browser selbst, und im
|
||||
* Fehlerfall sagt er nur „error" — ohne Unterschied zwischen „diese Spur gibt
|
||||
* es nicht", „sie enthält keinen Text" und „der Server holt sie gerade erst
|
||||
* aus der Datei". Der letzte Fall ist keine Seltenheit: einen Untertitel aus
|
||||
* einem Dreistünder zu ziehen heisst, die ganze Datei zu lesen, und das
|
||||
* dauert beim ersten Mal Minuten.
|
||||
*
|
||||
* **Und mit Token, nicht mit Keks.** Ein `<track>` wies sich über das Cookie
|
||||
* aus. Das geht, solange es eines gibt — gemessen fiel es auf, als keines da
|
||||
* war und jeder Abruf mit 401 zurückkam, ohne dass irgendwo etwas stand. */
|
||||
export async function untertitelHolen(
|
||||
datei: string, spur: number, vorlauf = 0,
|
||||
): Promise<Untertitelstand> {
|
||||
const kopf = new Headers();
|
||||
if (sitzung.token) kopf.set('Authorization', `Bearer ${sitzung.token}`);
|
||||
|
||||
let antwort: Response;
|
||||
try {
|
||||
antwort = await fetch(untertitelAdresse(datei, spur, vorlauf), { headers: kopf });
|
||||
} catch {
|
||||
return { art: 'fehler', satz: 'Der Server war nicht erreichbar.' };
|
||||
}
|
||||
|
||||
if (antwort.ok) return { art: 'da', text: await antwort.text() };
|
||||
if (antwort.status === 503) return { art: 'laeuft' };
|
||||
|
||||
// Der Server schreibt seinen Satz nach `detail`; steht dort nichts, bleibt
|
||||
// ein eigener — ein Fehlercode ist keine Auskunft.
|
||||
const satz = await antwort.json()
|
||||
.then((r: { detail?: string }) => r.detail)
|
||||
.catch(() => undefined);
|
||||
return { art: 'fehler', satz: satz || 'Diese Untertitelspur liess sich nicht laden.' };
|
||||
}
|
||||
|
||||
/* ---------- Verwaltung ---------- */
|
||||
|
||||
export const bibliotheken = () => anfrage<Bibliothek[]>('/libraries');
|
||||
|
||||
+55
-12
@@ -1,6 +1,6 @@
|
||||
import type HlsTyp from 'hls.js';
|
||||
import {
|
||||
stromStarten, stromBeenden, untertitelAdresse, fortschritt,
|
||||
stromStarten, stromBeenden, untertitelHolen, fortschritt,
|
||||
type Stromauskunft, type Spurauskunft,
|
||||
} from './api';
|
||||
import { dauer, knopf, meldung, hinweisZeigen } from './util';
|
||||
@@ -751,10 +751,21 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
? 'showing' : 'disabled';
|
||||
});
|
||||
menuesAuffrischen();
|
||||
|
||||
// Geholt wird erst beim Einschalten: einen Untertitel aus einer grossen
|
||||
// Datei zu ziehen kostet den Server das Lesen der ganzen Datei, und die
|
||||
// meisten Spuren schaltet nie jemand ein.
|
||||
if (spur !== null) void spurAnhaengen(spur);
|
||||
}
|
||||
|
||||
/** Die Adressen der schon geholten Spuren — damit nichts zweimal läuft. */
|
||||
const geholt = new Map<number, string>();
|
||||
|
||||
function untertitelAnhaengen(): void {
|
||||
video.querySelectorAll('track').forEach(t => t.remove());
|
||||
geholt.forEach(adresse => URL.revokeObjectURL(adresse));
|
||||
geholt.clear();
|
||||
|
||||
(auskunft?.subtitleStreams ?? []).forEach(s => {
|
||||
const t = document.createElement('track');
|
||||
t.kind = 'subtitles';
|
||||
@@ -764,23 +775,55 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
// wäre „de". Lieber keine Angabe als eine falsche: die Beschriftung
|
||||
// steht ohnehin in `label`.
|
||||
if (s.language?.length === 2) t.srclang = s.language;
|
||||
|
||||
// Mit dem Vorlauf: der Strom beginnt bei null, die Datei nicht.
|
||||
t.src = untertitelAdresse(anfrage.dateiId, s.index, vorlauf);
|
||||
t.dataset.spur = String(s.index);
|
||||
|
||||
// **Ein gescheiterter Abruf war bisher vollkommen stumm.** Der Haken
|
||||
// stand im Menü, als liefe es — und niemand konnte unterscheiden, ob
|
||||
// die Spur leer ist oder der Abruf schiefging.
|
||||
t.onerror = () => {
|
||||
if (untertitel === s.index) hinweisZeigen('Diese Untertitelspur liess sich nicht laden.', 'fehler');
|
||||
};
|
||||
|
||||
video.appendChild(t);
|
||||
});
|
||||
if (untertitel !== null) untertitelWaehlen(untertitel);
|
||||
}
|
||||
|
||||
/** Holt eine Spur und hängt sie an — mit Geduld, wenn der Server noch zieht.
|
||||
*
|
||||
* Der fertige Text geht als Blob an den `<track>`: dieselbe Anzeige wie
|
||||
* vorher, nur mit einem Weg dahin, der sagen kann, was los ist. Warum
|
||||
* nicht `track.src`, steht bei [untertitelHolen] in `api.ts`. */
|
||||
async function spurAnhaengen(spur: number): Promise<void> {
|
||||
if (geholt.has(spur)) return;
|
||||
|
||||
const element = [...video.querySelectorAll('track')]
|
||||
.find(t => t.dataset.spur === String(spur));
|
||||
if (!element) return;
|
||||
|
||||
// Rund zwei Minuten lang nachfragen. Wer länger braucht, hat ein
|
||||
// Problem, das kein Warten löst — und bekommt es dann auch gesagt.
|
||||
for (let versuch = 0; versuch < 40; versuch++) {
|
||||
if (geschlossen || untertitel !== spur) return;
|
||||
|
||||
const stand = await untertitelHolen(anfrage.dateiId, spur, vorlauf);
|
||||
|
||||
if (stand.art === 'da') {
|
||||
const blob = URL.createObjectURL(new Blob([stand.text], { type: 'text/vtt' }));
|
||||
geholt.set(spur, blob);
|
||||
element.src = blob;
|
||||
// Das Anhängen setzt den Modus zurück; nach dem Laden noch einmal
|
||||
// setzen, sonst bliebe die Spur stumm im Hintergrund.
|
||||
if (untertitel === spur) untertitelWaehlen(spur);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stand.art === 'fehler') {
|
||||
hinweisZeigen(stand.satz, 'fehler');
|
||||
return;
|
||||
}
|
||||
|
||||
if (versuch === 0) {
|
||||
hinweisZeigen('Die Untertitel werden aus der Datei geholt — das dauert beim ersten Mal.');
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
|
||||
hinweisZeigen('Die Untertitel brauchen zu lange. Versuche es später noch einmal.', 'fehler');
|
||||
}
|
||||
|
||||
/* ---------- Fehler ---------- */
|
||||
|
||||
/** Zweiter Versuch, diesmal gerechnet.
|
||||
|
||||
Reference in New Issue
Block a user