diff --git a/server/src/Flaro.Api/Endpoints/Contracts.cs b/server/src/Flaro.Api/Endpoints/Contracts.cs index 4812ab7..9487870 100644 --- a/server/src/Flaro.Api/Endpoints/Contracts.cs +++ b/server/src/Flaro.Api/Endpoints/Contracts.cs @@ -610,7 +610,18 @@ public sealed record StreamRequest( IReadOnlyList? AudioCodecs, int? Height = null, int? AudioStream = null, - double? StartSeconds = null); + double? StartSeconds = null, + + /// Umkodieren, egal was das Profil sagt. + /// + /// + /// Für den zweiten Versuch. Der Spieler schickt sein Profil aus + /// canPlayType — und das ist eine Behauptung: „kenne ich" heisst + /// nicht „kann ich diese Datei". Scheitert die Wiedergabe trotzdem, weiss + /// der Browser mehr als jede Abfrage vorher, und sein Urteil schlägt + /// seine eigene Auskunft. + /// + bool Transcode = false); /// Eine wählbare Spur, so wie sie im Menü steht. public sealed record SpurAuskunft(int Index, string Label, string? Language, bool IsDefault); diff --git a/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs b/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs index 0c7b4e7..cbfb0a8 100644 --- a/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs @@ -61,14 +61,20 @@ public static class StreamEndpoints // **Nur ohne Wünsche.** Wer eine Tonspur oder eine Stufe // wählt, bekommt eine Sitzung: beides lässt sich an einer // durchgereichten Datei nicht einstellen. - if (hoehe is null && anfrage.AudioStream is null + // + // **Und nicht, wenn der Spieler schon gescheitert ist.** Dann + // hat er die Datei gesehen und nicht geöffnet; sie ihm ein + // zweites Mal unverändert hinzulegen wäre derselbe Fehler mit + // mehr Geduld. + if (hoehe is null && anfrage.AudioStream is null && !anfrage.Transcode && await Durchreichbar(db, id, profil) is { } datei) { return Results.Ok(await Direktauskunft(db, datei, anfrage.StartSeconds ?? 0)); } var sitzung = await koordinator.StartenAsync( - id, benutzer, profil, hoehe, anfrage.AudioStream, anfrage.StartSeconds ?? 0); + id, benutzer, profil, hoehe, anfrage.AudioStream, anfrage.StartSeconds ?? 0, + erzwingen: anfrage.Transcode); return Results.Ok(await Auskunft(db, sitzung)); } diff --git a/server/src/Flaro.Api/Streamkoordinator.cs b/server/src/Flaro.Api/Streamkoordinator.cs index 83a5877..42a3496 100644 --- a/server/src/Flaro.Api/Streamkoordinator.cs +++ b/server/src/Flaro.Api/Streamkoordinator.cs @@ -93,9 +93,14 @@ public sealed class Streamkoordinator( public Stromsitzung? Sitzung(Guid kennung) => _sitzungen.GetValueOrDefault(kennung)?.Sitzung; /// Startet einen Strom und gibt die Sitzung zurück. + /// + /// Umkodieren, auch wenn das Profil etwas anderes sagt — der Spieler hat + /// es mit dem ersten Plan probiert und ist gescheitert. + /// public async Task StartenAsync( Guid dateiId, Guid benutzer, DeviceProfile profil, - int? hoehe, int? tonspur, double ab, CancellationToken ct = default) + int? hoehe, int? tonspur, double ab, CancellationToken ct = default, + bool erzwingen = false) { AbgelaufeneWegraeumen(); @@ -123,7 +128,7 @@ public sealed class Streamkoordinator( s.Index, s.Kind, s.Codec, s.Language, s.Title, s.IsDefault, s.IsForced, s.Channels, s.Width, s.Height)).ToList(); - var plan = Stromentscheid.Fuer(datei.Container, gelesen, profil, hoehe); + var plan = Stromentscheid.Fuer(datei.Container, gelesen, profil, hoehe, erzwingen); var kennung = Guid.CreateVersion7(); var ordner = Path.Combine(pfade.Transcode, "stroeme", kennung.ToString("N")); diff --git a/server/src/Flaro.Core/Delivery/Streamplan.cs b/server/src/Flaro.Core/Delivery/Streamplan.cs index 7077f14..431b23d 100644 --- a/server/src/Flaro.Core/Delivery/Streamplan.cs +++ b/server/src/Flaro.Core/Delivery/Streamplan.cs @@ -38,6 +38,16 @@ public enum Stromgrund BildcodecUnbekannt, TonUnbekannt, KleinereAufloesungGewuenscht, + + /// Der Spieler hat es versucht und kam nicht zurecht. + /// + /// + /// Das Urteil des Browsers schlägt seine eigene Auskunft. + /// canPlayType ist eine Behauptung — „kenne ich" heisst nicht + /// „kann ich diese Datei". Wer es ausprobiert hat und gescheitert ist, + /// weiss mehr als jede Abfrage vorher. + /// + SpielerKamNichtZurecht, } /// Was für diesen Strom zu tun ist. @@ -87,11 +97,16 @@ public static class Stromentscheid /// /// Die vom Spieler gewählte Stufe, oder null für das Original. /// + /// + /// Umkodieren, egal was das Profil sagt. Für den zweiten Versuch: + /// der Spieler hat es mit dem ersten Plan probiert und ist gescheitert. + /// public static Streamplan Fuer( string? schachtel, IReadOnlyList spuren, DeviceProfile profil, - int? hoeheGewuenscht = null) + int? hoeheGewuenscht = null, + bool erzwingen = false) { var gruende = new List(); @@ -108,7 +123,9 @@ public static class Stromentscheid : (int?)null; if (hoehe is not null) gruende.Add(Stromgrund.KleinereAufloesungGewuenscht); - if (!bildPasst || hoehe is not null) + if (erzwingen) gruende.Add(Stromgrund.SpielerKamNichtZurecht); + + if (erzwingen || !bildPasst || hoehe is not null) return new Streamplan(Stromart.Umkodieren, gruende, hoehe); var schachtelPasst = Passt(schachtel, profil.Containers); diff --git a/server/tests/Flaro.Media.Tests/HlsArgumenteTests.cs b/server/tests/Flaro.Media.Tests/HlsArgumenteTests.cs index 90c417e..f98f3c9 100644 --- a/server/tests/Flaro.Media.Tests/HlsArgumenteTests.cs +++ b/server/tests/Flaro.Media.Tests/HlsArgumenteTests.cs @@ -308,6 +308,50 @@ public class HlsArgumenteTests }); } + // ------------------------------------------------------------------ + // Der zweite Versuch + // ------------------------------------------------------------------ + + /// Wer es probiert hat und scheiterte, bekommt gerechnet. + /// + /// + /// Das Urteil des Browsers schlägt seine eigene Auskunft. + /// canPlayType ist eine Behauptung: „kenne ich" heisst nicht „kann + /// ich diese Datei". Gemessen in Chromium — canPlayType('audio/flac') + /// sagt „probably", MediaSource.isTypeSupported derselben + /// Zeichenkette sagt false. Wer es ausprobiert hat, weiss mehr. + /// + [Test] + public void Erzwungen_wird_gerechnet_auch_wenn_alles_passt() + { + IReadOnlyList spuren = [Bild("h264"), Ton(1, "aac")]; + + var ohne = Stromentscheid.Fuer("mp4", spuren, Browser); + var mit = Stromentscheid.Fuer("mp4", spuren, Browser, erzwingen: true); + + Assert.Multiple(() => + { + Assert.That(ohne.Art, Is.EqualTo(Stromart.Direkt), "ohne Zwang geht die Datei durch"); + Assert.That(mit.Art, Is.EqualTo(Stromart.Umkodieren)); + Assert.That(mit.Gruende, Does.Contain(Stromgrund.SpielerKamNichtZurecht)); + Assert.That(mit.BildKopieren, Is.False, "umkodieren heisst: das Bild wird angefasst"); + }); + } + + /// Und die gewählte Stufe bleibt dabei erhalten. + [Test] + public void Erzwungen_bleibt_die_gewaehlte_Stufe() + { + var plan = Stromentscheid.Fuer( + "mp4", [Bild("h264", 2160), Ton(1, "aac")], Browser, 720, erzwingen: true); + + Assert.Multiple(() => + { + Assert.That(plan.Art, Is.EqualTo(Stromart.Umkodieren)); + Assert.That(plan.Hoehe, Is.EqualTo(720)); + }); + } + /// Beim Umpacken ist die Stufe gegenstandslos. /// /// diff --git a/web/app/src/api.ts b/web/app/src/api.ts index 5ed375a..70f4470 100644 --- a/web/app/src/api.ts +++ b/web/app/src/api.ts @@ -627,6 +627,8 @@ export type Stromwunsch = { height?: number | null; audioStream?: number | null; startSeconds?: number; + /** Umkodieren, egal was das Profil sagt — für den zweiten Versuch. */ + transcode?: boolean; }; /** Legt einen Strom an, der sofort anfängt. diff --git a/web/app/src/spieler.ts b/web/app/src/spieler.ts index 999ee21..1f62752 100644 --- a/web/app/src/spieler.ts +++ b/web/app/src/spieler.ts @@ -18,34 +18,57 @@ import { icons, icon } from './icons'; * * `canPlayType` antwortet mit `"probably"`, `"maybe"` oder `""`. Alles ausser * der leeren Zeichenkette zählt: `"maybe"` heisst „der Codec ist bekannt, über - * das Profil sage ich nichts" — und ein Film, der vielleicht läuft, ist - * besser als einer, der sicher umgerechnet wird. + * das Profil sage ich nichts". + * + * **Und es zählt nur, wenn der MSE-Puffer mitgeht.** Gemessen in Chromium: + * `canPlayType('audio/flac')` sagt „probably", `MediaSource.isTypeSupported` + * derselben Zeichenkette sagt `false`. Ein Strom läuft über den Puffer, eine + * durchgereichte Datei über das Element — und welcher der beiden Wege es + * wird, entscheidet der Server erst danach. Also muss beides gehen. */ export function geraeteprofil(): { containers: string[]; videoCodecs: string[]; audioCodecs: string[]; } { const v = document.createElement('video'); - const kann = (typ: string): boolean => v.canPlayType(typ) !== ''; + const hatPuffer = typeof MediaSource !== 'undefined' + && typeof MediaSource.isTypeSupported === 'function'; + + /** Ein Typ zählt nur, wenn **beide** Auskünfte ihn bestätigen. + * + * **Gemessen, und es ist der Grund für „kann die Spuren nicht öffnen".** + * In Chromium sagt `canPlayType('audio/flac')` „probably", und + * `MediaSource.isTypeSupported` derselben Zeichenkette sagt `false`. Für + * MP3 in einer MP4 ebenso. Der Spieler weiss vorher nicht, auf welchem Weg + * die Datei kommt — als fertige Datei im `