From c2fd2a86fda14bc6eb5460498920afca1f3d90ba Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Wed, 7 Oct 2026 22:00:06 +0200 Subject: [PATCH] Einmal aus der Datei holen, nicht bei jedem Abruf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit „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 `` 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 `` — 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 ``, zehn Einblendungen, die erste bei 0 bis 3 s mit „Deutsch bei 500 s". Co-Authored-By: Claude Opus 5 --- scratchpad/neu/data/flaro.db | Bin 4096 -> 389120 bytes scratchpad/neu/data/flaro.db-shm | Bin 32768 -> 0 bytes scratchpad/neu/data/flaro.db-wal | Bin 968232 -> 0 bytes scratchpad/neu/data/logs/flaro-2026-10-07.log | 1 + .../Flaro.Api/Endpoints/StreamEndpoints.cs | 65 +++++++- server/src/Flaro.Api/Program.cs | 1 + server/src/Flaro.Api/Untertitelvorrat.cs | 113 ++++++++++++++ server/src/Flaro.Media/UntertitelLeser.cs | 143 ++++++++++++++---- server/src/Flaro.Media/Untertitelversatz.cs | 14 +- .../Flaro.Media.Tests/UntertitelLeserTests.cs | 100 ++++++++++++ .../UntertitelversatzTests.cs | 22 +++ web/app/src/api.ts | 42 +++++ web/app/src/spieler.ts | 67 ++++++-- 13 files changed, 514 insertions(+), 54 deletions(-) delete mode 100644 scratchpad/neu/data/flaro.db-shm delete mode 100644 scratchpad/neu/data/flaro.db-wal create mode 100644 server/src/Flaro.Api/Untertitelvorrat.cs create mode 100644 server/tests/Flaro.Media.Tests/UntertitelLeserTests.cs diff --git a/scratchpad/neu/data/flaro.db b/scratchpad/neu/data/flaro.db index c9cf4b4e788deb2a37a4a5eef5db1c380729a4e1..719a7a9665be0a0c3d1a11e340402a255ad48f96 100644 GIT binary patch literal 389120 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 z z5*n1#bdcl)2Cq7|Q*M&n!4H4TYZWwYUZOYYekBsU9Krh zO2r2l%O?NP$noV%iIMp`Ny&P(mx=LVQ#ZA`x@)%W&k>k*-!Bs5zya!}&qo8&JyW zo|E`Rx+ktVM}M#^dMIMdMGvqiGB~yO#>F?>o(30^0mTNLi(a*v?DxKrlJ1BOoFC8u zTfevc%g<}yEj6flFFD&iQcOt&(fp+W&HJZNbbzlvuLFL$L0t$kWZ%A*lvcBYmVdP2 zdM^5T<#Tx{C1taTA3Gr`ni~C7ay#vYsc7Q<166Oic&J*?jcwg9H0yfSbJDv3CK$aN z6oVkMs6J`$c0>pmSU7aS8B~8sHO#f9`m`tV1g&^0AlAH9s?gqT^$A#R-`CoPZe0Qf z_OBwY5UtHna?or=RIG1@hZdyy=R~Ye!8LWzc~cfTN&oVw&j+Sq+V zI8(d|r-$J@*0lO-P3*3{#)55!pBmYC%;m*RkLbI9(-W@LJDd7;eQ{r1cTYUOhWoHu%n00IagfB*srAb{vv|n@q1PL zUJ<`0R#|0009ILKmY**5I_I{1P-YH-~S)diKE*HAbU|9I>hB4hZcduAggEx(%R7W7s_ z`?&a^uIR0%+SqMvYOQ*${a9-)>f(K8r=z#ba9W|FM*} zua@OZsFsX;In&!nCj0nXDQW&fqMP>mw4ycL9#yrDt{KIKf8?RtkDvp7Qr*k!H0vL? z8ugR1->Igg+_^+I;Wbv(woG+XtFN@5=vuMieq4*S)059>#64&=;g-*2Kd7XnInlz2 z-WHS{-Do$0HdY6=;pdFC5~O55y}y={&Yeqq{?t@Anwr}c_vIUI-L?GPk|K-o+k+JS z9FxUQrr5~HHRVA~F0YE;^-@VLtd^^_O1@aG$(glfVd)lFs}xuAm51`O^6-jNcHPiA zvGQ)3hu~H&Ds~fse#eq(>SjhPsG72*RQ&$9#j4ulU9nJG8Cfj9Qcr5Q%Zq-O>uytq z_r~h$;%Y@HE|slOh^Y!XlNI$YDix(%P^x03Ium4A(wF*loYY<3;Ajxzo0nX z8qlykc*9#ry^VS_(I%ZTU$8-ICXBi_AJH+oN29IT1qL@Zmc4pEDdo>4M(m|)eAv`Y zt*-8xZTqvgDC#+Pbu4$^UQJ1>*~GJx-deYtHMQ-kshXPCH~-9xroQE_(O680W^)JV zSK^!#(LC0e>?4iL6>GISQA5n&d-ufbhK|U^s=tc5$JLZn6u~g(^*$7;>V=LbhS0eC z)TuX=^zGPr8%pdg=kzv|!Rl&H-I?r@@1&&Lq9;>(^dujS$M5f_4|#L1)A9qBi_5lV=C}Q%Z^jm>pA;|UELA^4 z^o$NF&t$iLMOg-nUd~{rk3KPRys|s zp>K-e)`QOZ)x5Vy!+lHk8GVq?ifKV3nQZG}O1dSwb-GWtygi2Bm3Q~*ikG`z^L|?P zN2&Kx(uE6&Uw_|@5`NYGo0{8(yW8-O?>|0-io~OX8`=Aw?%rg%iT-&OTm)h%!R0@e zRMk|Y-74>HZ)lyqXMesU&ilyD$=j)zTC=J3EwkL--qu>CDr)RkGBmS6ym7Z>Zsnn+ z5M&94>LtJ178@m}2|uwX-uTYm?&z1dO$wTJHa*_i`U~7B1x-3zg3ul!8dXC(hVX{a zIc&v*18fI_My+-0?SXnn>8|MmhVR^iq@-LJ6uyI&bN<`)?i=r>q$P1b@&hkgxVtwm z1dIK*Hx+J5)jgeK8?(VBodDvdM`O3OzZU__J5l{TnRg(vC7W3n_owGV8_GUS`sc5k zRyja-{j$&RaFEsSa&Rcz>1IvbzY5?N1_Qf2y&sFk@!>tEV_Clc4?oc0Jpu?IfB*sr zAbdf5{r_;zE2BaH0R#|0009ILKmY**5I`U-!2N$%gZBs^fB*srAbvY>!Hkfk^E^d^Tn`~Y6G2JQd= diff --git a/scratchpad/neu/data/flaro.db-shm b/scratchpad/neu/data/flaro.db-shm deleted file mode 100644 index 956044943b39cd049a8e610cced0f28e3b3f782c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI5`IpUg7{}ifibAU~$P7s$S*D0EEn_#9q*7_2MWuyAQKG#jw9vk4v!s31q9{uX zN-HX~S^fd%bWXjWj@$XV=jwj0<~z^#G0$_(>)iXf^L#$f`}@8#^W*!Ix8X=g6_6@G z-7S1Qo2(k%Z0?vT)64RUwvGR@bm+VVWn0&e8?>-~UXJ}d>YtKx|BBT1kN_he?wMiXPm+VLOCkK$|IR|om5ILCCBYC7g$tMj+ zL(+&GLK>3-Qb?MRrX+fn^#9j7b>^Y$Yeo*s(w}}_YJYR?JDjv2N064J6_IbRHHm6S z?UCN2NE_0Yv?J|F5jmP1LpqR-e3-PyB8d*s>D*RoHZRc7=gy@>QgKkv0~I$q)+yoLCz#+ zkzqvs4@A!wjq*3@-LJ{)=ikVL^5F>E~ zuEHeTh}%$x8F&nH@CM$&`}hbe@FRX#P+f|r@uI@q+(&+w`<8OuA&DI}>T+e7Rha~_ zMOj1G`2KhXFNL)#cYUuszd(6@`1otXTVv_0#tfl+=I69!7ETvt*_1F-%nj88_n&OvUSX3-4kf7Go9G;b&}$MMw5SCB4W-tx=ZQglS$q znEZ4p#$X(7!JU|f53yNURi^P1GksWrpVh3uS^sa8nkzD9K*;3UNRZ?`_n>$};6H3>zi44tRw) zAa_vMs7I7#$}KQ9>Kq#~6QHtX0yKn4coehoDwbh2)`x8<=PtSb$VQ!OLuLY0%6&~X zYPqsZx$VnFoo7R40#tTNfQB#$pN6%?)+P7c*{I<*WF|mm=LBd7lkizsTWno&!=sHF zfl;^y_uv71g0E~yR#Mpuk7KUV?A*qFo?Cmlch@WT6XQDY{J0L@?au9uCos>%Q+=VV zGEaGa`1lLLTPt~IAKqtsH}1!i_!wU%+KaE1)iqR}A3pxV@YYJ+*%wdY1rr{8qpY%# O^8E1e7lpUR()l-6aJ3Hr diff --git a/scratchpad/neu/data/flaro.db-wal b/scratchpad/neu/data/flaro.db-wal deleted file mode 100644 index 1536221aa0d297b090345cbf73e9e2ce48c1a152..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 968232 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 z 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 w5kLeG0Yu + Streamkoordinator koordinator, FlaroContext db, TimeProvider uhr, + Untertitelvorrat vorrat) => { if (PlaybackEndpoints.Benutzer(wer) is not { } benutzer) return Results.Unauthorized(); @@ -44,6 +45,17 @@ public static class StreamEndpoints if (await DeliveryEndpoints.FreiAb(db, gewuenschte, uhr.GetUtcNow()) is { } nochNicht) return DeliveryEndpoints.NochNicht(nochNicht); + // **Die Untertitel jetzt holen, nicht erst beim Einschalten.** + // + // Sie aus der Datei zu ziehen heisst, die Datei zu lesen — bei + // einem Dreistünder auf einem NAS dauert das Minuten. Wer sie + // nach einer halben Stunde Film einschaltet, soll dann nicht + // warten; der Vorrat ist bis dahin längst gefüllt. Wer sie sofort + // einschaltet, wartet so lange wie vorher, bekommt aber eine + // Antwort statt eines Fehlers. + vorrat.Anstossen(gewuenschte.Id, gewuenschte.Path, gewuenschte.ModifiedAt, + await Textspuren(db, id, CancellationToken.None)); + var profil = Profil(anfrage); var hoehe = anfrage.Height is { } gewuenscht && Stromentscheid.Stufen.Contains(gewuenscht) ? gewuenscht : (int?)null; @@ -219,17 +231,38 @@ public static class StreamEndpoints // im Spieler: bei „Fortsetzen ab 8:37" stand der Satz von Sekunde 0 // auf dem Bild. app.MapGet("/api/v1/files/{id:guid}/subtitles/{spur:int}.vtt", async ( - Guid id, int spur, FlaroContext db, UntertitelLeser leser, + Guid id, int spur, FlaroContext db, Untertitelvorrat vorrat, CancellationToken ct, double ab = 0) => { var datei = await db.MediaFiles.AsNoTracking().FirstOrDefaultAsync(f => f.Id == id, ct); - if (datei is null || !File.Exists(datei.Path)) return Results.NotFound(); + if (datei is null || !File.Exists(datei.Path)) + return Einwand.Fehlt("Diese Datei liegt nicht mehr da."); - var vtt = await leser.AlsWebVttAsync(datei.Path, spur, ct); - if (vtt is null) return Results.NotFound(); + var spuren = await Textspuren(db, id, ct); + if (!spuren.Contains(spur)) + return Einwand.Fehlt("Diese Spur enthält keinen Text, sondern Bilder."); - return Results.Text( - Untertitelversatz.Verschieben(vtt, Math.Max(0, ab)), "text/vtt", Encoding.UTF8); + var (stand, text) = await vorrat.HolenAsync( + datei.Id, datei.Path, datei.ModifiedAt, spuren, spur, + TimeSpan.FromSeconds(20), ct); + + return stand switch + { + Untertitelvorrat.Stand.Fertig => Results.Text( + Untertitelversatz.Verschieben(text, Math.Max(0, ab)), "text/vtt", Encoding.UTF8), + + Untertitelvorrat.Stand.Leer => + Einwand.Fehlt("In dieser Spur steht kein Text."), + + // **Kein Fehler, sondern ein Zwischenstand.** Der Spieler + // fragt gleich noch einmal; ihm hier „nicht gefunden" zu + // sagen hiesse, ihn auf etwas warten zu lassen, das er für + // aussichtslos hält. + _ => Results.Problem( + "Die Untertitel werden gerade aus der Datei geholt. " + + "Bei einem grossen Film dauert das einen Moment.", + statusCode: StatusCodes.Status503ServiceUnavailable), + }; }).RequireAuthorization(); } @@ -342,6 +375,24 @@ public static class StreamEndpoints private static bool IstBild(string codec) => codec is "hdmv_pgs_subtitle" or "pgssub" or "dvd_subtitle" or "dvdsub" or "xsub"; + /// Die Spurnummern, aus denen sich Text holen lässt. + /// + /// + /// Dieselbe Auswahl, die auch im Menü steht — sonst böte der Spieler eine + /// Spur an, die der Vorrat gar nicht erst zieht. + /// + private static async Task> 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(); + } + /// Wie eine Spur im Menü heisst. private static string Benennung(MediaStream spur) { diff --git a/server/src/Flaro.Api/Program.cs b/server/src/Flaro.Api/Program.cs index 8b7baba..0989bba 100644 --- a/server/src/Flaro.Api/Program.cs +++ b/server/src/Flaro.Api/Program.cs @@ -111,6 +111,7 @@ builder.Services.AddSingleton(); // zum Mitnehmen; dieser lässt sie laufen, während sie entsteht. builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); // Im Arbeitsspeicher und nur dort: wer gerade was schaut, ist eine Frage // ueber den Augenblick. Nach einem Neustart lautet die Antwort: niemand. builder.Services.AddSingleton(); diff --git a/server/src/Flaro.Api/Untertitelvorrat.cs b/server/src/Flaro.Api/Untertitelvorrat.cs new file mode 100644 index 0000000..9fe517d --- /dev/null +++ b/server/src/Flaro.Api/Untertitelvorrat.cs @@ -0,0 +1,113 @@ +using System.Collections.Concurrent; +using Flaro.Media; + +namespace Flaro.Api; + +/// Herausgezogene Untertitel, abgelegt für den zweiten Abruf. +/// +/// +/// +/// Der Anlass. Im Spieler stand „Diese Untertitelspur liess sich 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 +/// 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. +/// +/// +/// Und es wurde schlimmer, bevor es besser wurde. Seit die Adresse den +/// Einstiegspunkt mitführt (?ab=), ändert sie sich bei jedem Sprung — +/// der Browser konnte nichts mehr aus seinem eigenen Zwischenspeicher +/// nehmen, und jeder Sprung stiess einen neuen Durchlauf an. Deshalb liegt +/// hier die ungeschobene Spur; das Verschieben ist eine Zeilenersetzung und +/// kostet nichts. +/// +/// +/// Warm, bevor jemand fragt. Angestossen wird beim Start des Stroms, +/// nicht erst beim Öffnen des Menüs. Wer nach fünf Minuten Untertitel +/// einschaltet, wartet dann gar nicht mehr. +/// +/// +public sealed class Untertitelvorrat(FlaroPaths pfade, UntertitelLeser leser) +{ + /// Was ein Abruf vorfindet. + public enum Stand + { + /// Liegt da, mit Text. + Fertig, + + /// Liegt da und ist leer — diese Spur hat keinen Text. + Leer, + + /// Wird gerade herausgezogen. + Laeuft, + } + + /// Ein Lauf je Datei, nicht je Spur — er holt ohnehin alle. + private readonly ConcurrentDictionary _laeuft = new(); + + public string Pfad(Guid dateiId, DateTimeOffset geaendert, int spur) => + Path.Combine(pfade.Transcode, "untertitel", + $"{dateiId:N}-{geaendert.ToUnixTimeMilliseconds()}-{spur}.vtt"); + + /// Stösst das Herausziehen an, wenn etwas fehlt — und kehrt sofort zurück. + public void Anstossen(Guid dateiId, string quelle, DateTimeOffset geaendert, IReadOnlyList spuren) + { + if (spuren.Count == 0) return; + + 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); } + finally { _laeuft.TryRemove(dateiId, out Task? _); } + })); + } + + /// Holt eine Spur — und wartet dabei höchstens . + /// + /// + /// Warum überhaupt gewartet wird. Ein Abruf, der sofort „noch + /// nicht" sagt, zwingt den Spieler in eine Schleife, auch wenn die Datei + /// in zwei Sekunden fertig gewesen wäre. Ein Abruf, der unbegrenzt + /// wartet, hängt hinter jedem Reverse Proxy ins Zeitlimit. Zwanzig + /// Sekunden decken den gewöhnlichen Fall ab; der seltene bekommt eine + /// Antwort, mit der sich etwas anfangen lässt. + /// + public async Task<(Stand Stand, string Text)> HolenAsync( + Guid dateiId, string quelle, DateTimeOffset geaendert, + IReadOnlyList spuren, int spur, TimeSpan geduld, CancellationToken ct) + { + var ziel = Pfad(dateiId, geaendert, spur); + + Anstossen(dateiId, quelle, geaendert, spuren); + + var bis = DateTimeOffset.UtcNow + geduld; + while (true) + { + if (File.Exists(ziel)) + { + var text = await File.ReadAllTextAsync(ziel, ct); + return (text.Length == 0 ? Stand.Leer : Stand.Fertig, text); + } + + // Der Lauf ist durch und die Datei fehlt trotzdem: diese Spur + // liess sich nicht herausziehen. Weiterzuwarten hiesse, auf + // etwas zu warten, das niemand mehr tut. + if (!_laeuft.ContainsKey(dateiId)) return (Stand.Leer, ""); + + if (DateTimeOffset.UtcNow >= bis) return (Stand.Laeuft, ""); + + await Task.Delay(TimeSpan.FromMilliseconds(400), ct); + } + } +} diff --git a/server/src/Flaro.Media/UntertitelLeser.cs b/server/src/Flaro.Media/UntertitelLeser.cs index e94df2a..59c11aa 100644 --- a/server/src/Flaro.Media/UntertitelLeser.cs +++ b/server/src/Flaro.Media/UntertitelLeser.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging; namespace Flaro.Media; -/// Holt eine Untertitelspur als WebVTT aus einer Datei. +/// Holt Untertitelspuren als WebVTT aus einer Datei. /// /// /// @@ -18,24 +18,55 @@ namespace Flaro.Media; /// zu machen hiesse, Schrift zu erkennen. Sie werden gar nicht erst /// angeboten — siehe StreamEndpoints. /// +/// +/// Alle Spuren in einem Durchlauf, und warum das der ganze Unterschied +/// ist. Untertitelpakete liegen über die ganze Datei verteilt; ffmpeg +/// muss sie bis zum Ende lesen, auch wenn am Ende nur vierzig Kilobyte +/// herauskommen. Bei einem Dreistünder auf einem NAS sind das Minuten — je +/// Spur. Vorher lief dieser Durchlauf bei jedem Abruf neu und brach nach +/// dreissig Sekunden ab; gemeldet wurde das als „Diese Untertitelspur liess +/// sich nicht laden". Jetzt holt ein Durchlauf alle Spuren auf einmal, und +/// der Untertitelvorrat sorgt dafür, dass es bei diesem einen bleibt. +/// /// public sealed class UntertitelLeser(FfmpegOptions optionen, ILogger log) { - /// Wie lange das Herausziehen höchstens dauern darf. + /// Wie lange ein Durchlauf höchstens dauern darf. /// /// - /// Untertitel sind ein paar Dutzend Kilobyte; braucht es dafür länger als - /// eine halbe Minute, stimmt etwas mit der Datei nicht — und ein Spieler, - /// der ewig auf einen Text wartet, ist schlimmer als einer ohne. + /// Grosszügig, weil es einmal geschieht. Vorher standen hier + /// dreissig Sekunden, mit der Begründung, Untertitel seien ein paar + /// Dutzend Kilobyte. Das stimmt für das Ergebnis und nicht für den Weg + /// dahin: gelesen wird die ganze Datei. Eine halbe Stunde ist keine + /// Geduld, sondern eine Obergrenze gegen einen hängenden Prozess — der + /// Vorrat fragt nicht so lange, er legt nur ab, was zurückkommt. /// - private static readonly TimeSpan Geduld = TimeSpan.FromSeconds(30); + private static readonly TimeSpan Geduld = TimeSpan.FromMinutes(30); - /// null, wenn es die Spur nicht gibt oder sie kein Text ist. - public async Task AlsWebVttAsync(string datei, int spur, CancellationToken ct = default) + /// Eine Spur und die Datei, in die sie soll. + public readonly record struct Auftrag(int Spur, string Ziel); + + /// Zieht die genannten Spuren heraus und legt sie ab. + /// + /// + /// Die Spuren, für die wirklich Text herauskam. Eine Spur, die leer + /// bleibt, fehlt hier — ffmpeg gibt auch für eine Bildspur eine gültige, + /// aber inhaltslose WebVTT-Datei aus, und die auszuliefern hiesse, leere + /// Untertitel anzubieten. + /// + public async Task> ZiehenAsync( + string datei, IReadOnlyList auftraege, CancellationToken ct = default) { + if (auftraege.Count == 0) return []; + using var zeitlimit = CancellationTokenSource.CreateLinkedTokenSource(ct); zeitlimit.CancelAfter(Geduld); + foreach (var auftrag in auftraege) + { + Directory.CreateDirectory(Path.GetDirectoryName(auftrag.Ziel)!); + } + var start = new ProcessStartInfo(optionen.Ffmpeg) { RedirectStandardOutput = true, @@ -43,19 +74,27 @@ public sealed class UntertitelLeser(FfmpegOptions optionen, ILogger", StringComparison.Ordinal) ? text : null; + return Abnehmen(auftraege); } catch (OperationCanceledException) when (!ct.IsCancellationRequested) { - log.LogWarning("Untertitelspur {Spur} aus {Datei} brauchte länger als {Geduld}.", - spur, Path.GetFileName(datei), Geduld); - return null; + log.LogWarning("Untertitel aus {Datei} brauchten länger als {Geduld}.", + Path.GetFileName(datei), Geduld); + Wegraeumen(auftraege); + return []; + } + } + + /// Nimmt ab, was brauchbar ist, und rückt es an seinen Platz. + /// + /// + /// Erst unter .teil, dann umbenennen. Bricht der Lauf + /// mittendrin ab, liegt sonst eine halbe Spur am richtigen Namen und + /// gilt für immer als fertig. Umbenennen ist auf demselben Dateisystem + /// ein Zug und kann nicht halb geschehen. + /// + private static IReadOnlyList Abnehmen(IReadOnlyList auftraege) + { + var fertig = new List(); + + foreach (var auftrag in auftraege) + { + var teil = auftrag.Ziel + ".teil"; + if (!File.Exists(teil)) continue; + + var text = File.ReadAllText(teil); + File.Delete(teil); + + // Eine Spur ohne eine einzige Einblendung ist keine Spur. Die + // leere Datei bleibt trotzdem liegen: sie ist die Antwort + // „hier ist nichts", und ohne sie würde bei jedem Abruf neu + // die ganze Datei gelesen. + if (!text.Contains("-->", StringComparison.Ordinal)) + { + File.WriteAllText(auftrag.Ziel, ""); + continue; + } + + File.WriteAllText(auftrag.Ziel, text); + fertig.Add(auftrag.Spur); + } + + return fertig; + } + + private static void Wegraeumen(IReadOnlyList auftraege) + { + foreach (var auftrag in auftraege) + { + try { File.Delete(auftrag.Ziel + ".teil"); } catch (IOException) { /* Kür */ } } } } diff --git a/server/src/Flaro.Media/Untertitelversatz.cs b/server/src/Flaro.Media/Untertitelversatz.cs index 919aa60..2f1de0e 100644 --- a/server/src/Flaro.Media/Untertitelversatz.cs +++ b/server/src/Flaro.Media/Untertitelversatz.cs @@ -59,8 +59,16 @@ public static partial class Untertitelversatz if (zeile.Trim().Length == 0) { - if (behalten) foreach (var b in block) heraus.Append(b).Append('\n'); - heraus.Append('\n'); + // **Die Trennzeile gehört dem Block, nicht der Datei.** Fällt + // der Block weg, fällt sie mit — sonst stünden am Anfang + // eines mittendrin begonnenen Films so viele Leerzeilen, wie + // Sätze davor lagen. Gemessen: 52 Stück vor dem ersten Satz. + if (behalten && block.Count > 0) + { + foreach (var b in block) heraus.Append(b).Append('\n'); + heraus.Append('\n'); + } + block.Clear(); behalten = true; continue; @@ -82,7 +90,7 @@ public static partial class Untertitelversatz block.Add(zeile); } - if (behalten) foreach (var b in block) heraus.Append(b).Append('\n'); + if (behalten && block.Count > 0) foreach (var b in block) heraus.Append(b).Append('\n'); return heraus.ToString(); } diff --git a/server/tests/Flaro.Media.Tests/UntertitelLeserTests.cs b/server/tests/Flaro.Media.Tests/UntertitelLeserTests.cs new file mode 100644 index 0000000..7765f1c --- /dev/null +++ b/server/tests/Flaro.Media.Tests/UntertitelLeserTests.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace Flaro.Media.Tests; + +/// Untertitel aus einer Datei holen und ablegen. +/// +/// +/// Der Anlass. 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. +/// +[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.Instance); + + /// Die Spur landet als WebVTT auf der Platte. + [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")); + }); + } + + /// Nichts Halbes bleibt liegen. + /// + /// + /// Geschrieben wird unter .teil 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. + /// + [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); + } + + /// Eine Spur ohne Text hinterlässt eine leere Datei, keine fehlende. + /// + /// + /// Die leere Datei ist die Antwort „hier ist nichts". 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. + /// + [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); + } +} diff --git a/server/tests/Flaro.Media.Tests/UntertitelversatzTests.cs b/server/tests/Flaro.Media.Tests/UntertitelversatzTests.cs index cdd16e9..97aa7d7 100644 --- a/server/tests/Flaro.Media.Tests/UntertitelversatzTests.cs +++ b/server/tests/Flaro.Media.Tests/UntertitelversatzTests.cs @@ -78,6 +78,28 @@ public class UntertitelversatzTests }); } + /// Weggefallene Sätze hinterlassen keine Leerzeilen. + /// + /// + /// Gemessen am echten Film. 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. + /// + [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")); diff --git a/web/app/src/api.ts b/web/app/src/api.ts index e74a912..5609cc5 100644 --- a/web/app/src/api.ts +++ b/web/app/src/api.ts @@ -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 `` 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 { + 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('/libraries'); diff --git a/web/app/src/spieler.ts b/web/app/src/spieler.ts index 1193842..07511e7 100644 --- a/web/app/src/spieler.ts +++ b/web/app/src/spieler.ts @@ -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(); + 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 ``: 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 { + 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.