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:
Scarriffle
2026-10-07 22:00:06 +02:00
co-authored by Claude Opus 5
parent 339a6c07d0
commit c2fd2a86fd
13 changed files with 514 additions and 54 deletions
@@ -0,0 +1,100 @@
using Microsoft.Extensions.Logging.Abstractions;
namespace Flaro.Media.Tests;
/// <summary>Untertitel aus einer Datei holen und ablegen.</summary>
///
/// <remarks>
/// <b>Der Anlass.</b> Im Spieler stand „Diese Untertitelspur liess sich nicht
/// laden", und auf der zweiten Spur kam gar nichts. Nicht das Format war
/// schuld, sondern die Zeit: jeder Abruf startete ffmpeg, ffmpeg las die
/// ganze Datei, und nach dreissig Sekunden wurde abgebrochen. An einem
/// Prüffilm von fünfzehn Megabyte fällt das nie auf — deshalb prüft hier
/// nicht die Dauer, sondern das Verfahren: ein Durchlauf für alle Spuren,
/// und was herauskommt, bleibt liegen.
/// </remarks>
[TestFixture]
public class UntertitelLeserTests
{
private string _ordner = "";
[SetUp]
public void Anlegen() => _ordner = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-ut-" + Guid.CreateVersion7().ToString("N"))).FullName;
[TearDown]
public void Aufraeumen()
{
try { Directory.Delete(_ordner, recursive: true); } catch (IOException) { /* Kür */ }
}
private UntertitelLeser Leser() => new(
new FfmpegOptions(Testdateien.Suchen("ffmpeg")!, Testdateien.Suchen("ffprobe")!),
NullLogger<UntertitelLeser>.Instance);
/// <summary>Die Spur landet als WebVTT auf der Platte.</summary>
[Test]
public async Task Eine_Textspur_wird_abgelegt()
{
if (!Testdateien.FfmpegVorhanden) Assert.Ignore("Ohne ffmpeg nicht prüfbar.");
var film = Path.Combine(_ordner, "film.mkv");
Testdateien.MitZweiTonspuren(film);
var ziel = Path.Combine(_ordner, "ablage", "spur3.vtt");
// Bild 0, Ton 1 und 2, Untertitel 3 — so legt der Erzeuger sie an.
var fertig = await Leser().ZiehenAsync(film, [new UntertitelLeser.Auftrag(3, ziel)]);
Assert.Multiple(() =>
{
Assert.That(fertig, Is.EquivalentTo(new[] { 3 }));
Assert.That(File.Exists(ziel), Is.True, "die Spur muss liegen bleiben");
Assert.That(File.ReadAllText(ziel), Does.Contain("-->"));
Assert.That(File.ReadAllText(ziel), Does.Contain("Ein Untertitel"));
});
}
/// <summary>Nichts Halbes bleibt liegen.</summary>
///
/// <remarks>
/// Geschrieben wird unter <c>.teil</c> und erst danach umbenannt. Bliebe
/// eine abgebrochene Datei am richtigen Namen liegen, gälte sie für immer
/// als fertig — und niemand käme je wieder an die vollständige Spur.
/// </remarks>
[Test]
public async Task Keine_Teildatei_bleibt_zurueck()
{
if (!Testdateien.FfmpegVorhanden) Assert.Ignore("Ohne ffmpeg nicht prüfbar.");
var film = Path.Combine(_ordner, "film.mkv");
Testdateien.MitZweiTonspuren(film);
var ziel = Path.Combine(_ordner, "ablage", "spur3.vtt");
await Leser().ZiehenAsync(film, [new UntertitelLeser.Auftrag(3, ziel)]);
Assert.That(Directory.GetFiles(Path.GetDirectoryName(ziel)!, "*.teil"), Is.Empty);
}
/// <summary>Eine Spur ohne Text hinterlässt eine leere Datei, keine fehlende.</summary>
///
/// <remarks>
/// <b>Die leere Datei ist die Antwort „hier ist nichts".</b> Ohne sie
/// würde bei jedem Abruf aufs Neue die ganze Datei gelesen, um wieder
/// nichts zu finden — und der Spieler bekäme nie eine Auskunft, sondern
/// immer nur eine Wartezeit.
/// </remarks>
[Test]
public async Task Eine_Tonspur_ergibt_eine_leere_Ablage()
{
if (!Testdateien.FfmpegVorhanden) Assert.Ignore("Ohne ffmpeg nicht prüfbar.");
var film = Path.Combine(_ordner, "film.mkv");
Testdateien.MitZweiTonspuren(film);
var ziel = Path.Combine(_ordner, "ablage", "spur1.vtt");
// Spur 1 ist Ton. ffmpeg bricht ab, und das Ergebnis ist kein Text.
var fertig = await Leser().ZiehenAsync(film, [new UntertitelLeser.Auftrag(1, ziel)]);
Assert.That(fertig, Is.Empty);
}
}
@@ -78,6 +78,28 @@ public class UntertitelversatzTests
});
}
/// <summary>Weggefallene Sätze hinterlassen keine Leerzeilen.</summary>
///
/// <remarks>
/// <b>Gemessen am echten Film.</b> Bei einem Einstieg nach achteinhalb
/// Minuten standen vor dem ersten Satz 52 leere Zeilen — eine je
/// weggefallenem Block. Lesbar war das noch, sauber nicht: die Trennzeile
/// gehört dem Block und fällt mit ihm weg.
/// </remarks>
[Test]
public void Weggefallene_Saetze_hinterlassen_keine_Leerzeilen()
{
var heraus = Untertitelversatz.Verschieben(Probe, 10);
Assert.Multiple(() =>
{
Assert.That(heraus, Does.StartWith("WEBVTT\n\n2\n00:00:00.000"),
"nach dem Kopf kommt sofort der erste Satz, der noch gilt");
Assert.That(heraus, Does.Not.Contain("\n\n\n"),
"zwei Leerzeilen hintereinander heisst, dass ein Block seine Trennzeile zurückliess");
});
}
[Test]
public void Der_Kopf_bleibt_stehen() =>
Assert.That(Untertitelversatz.Verschieben(Probe, 10), Does.StartWith("WEBVTT"));