„Kenne ich" heisst nicht „kann ich diese Datei"

Der Spieler schickte sein Profil aus `canPlayType` — und das ist eine
Auskunft des `<video>`-Elements. Ein Strom läuft aber nicht durch das
Element, sondern durch den MSE-Puffer, und die beiden sind sich nicht
einig.

Gemessen in Chromium:

| | canPlayType | MediaSource.isTypeSupported |
|---|---|---|
| `audio/flac` | probably | **false** |
| `audio/mp4; codecs="mp3"` | probably | **false** |

Der Server glaubte dem Profil, packte die Datei pflichtschuldig um und
liess die Spur unberührt — und der Browser stand vor etwas, das er nach
eigener Auskunft können müsste. Heraus kam „Dieser Browser kann die
Spuren dieser Datei nicht öffnen".

Jetzt zählt ein Codec nur, wenn **beide** Auskünfte ihn bestätigen; welcher
der beiden Wege es wird, entscheidet der Server erst danach. Und geprüft
wird jeder Typ in der Form, in der er wirklich ankommt — `audio/mp4` und
nicht `audio/flac`: genau dieser Unterschied entschied bei FLAC zwischen
„geht" und „geht nicht". Gemessen: MP3 fällt dadurch aus dem Profil, der
Server rechnet es um, statt es durchzureichen.

**Und wenn doch etwas durchrutscht, wird gerechnet statt gemeldet.**
Scheitert die Wiedergabe mit einem Codec- oder Dekodierfehler, baut der
Spieler den Strom genau einmal neu auf — mit `transcode: true`, an
derselben Stelle. Der Satz „über das Zahnrad lässt sich eine Fassung
wählen, die er versteht" ist weg: wer das liest, weiss weder welche, noch
warum der Server sie nicht gleich gegeben hat.

Lief schon ein gerechneter Strom, unterbleibt der zweite Versuch — dann
liegt es nicht am Codec, und dreissig Sekunden Warten auf dasselbe sind
keine Hilfe.

**Nicht geprüft:** dass der Rückfall im Betrieb auslöst. Jede Datei im
Prüfbestand spielt; ein echtes Scheitern liess sich hier nicht
herbeiführen. Gemessen ist der Serverteil (`remux` wird zu `transcode`)
und das neue Profil.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-06 17:44:11 +02:00
co-authored by Claude Opus 5
parent e30158b84c
commit 9c2e2b5f47
7 changed files with 184 additions and 25 deletions
+12 -1
View File
@@ -610,7 +610,18 @@ public sealed record StreamRequest(
IReadOnlyList<string>? AudioCodecs,
int? Height = null,
int? AudioStream = null,
double? StartSeconds = null);
double? StartSeconds = null,
/// <summary>Umkodieren, egal was das Profil sagt.</summary>
///
/// <remarks>
/// <b>Für den zweiten Versuch.</b> Der Spieler schickt sein Profil aus
/// <c>canPlayType</c> — 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.
/// </remarks>
bool Transcode = false);
/// <summary>Eine wählbare Spur, so wie sie im Menü steht.</summary>
public sealed record SpurAuskunft(int Index, string Label, string? Language, bool IsDefault);
@@ -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));
}
+7 -2
View File
@@ -93,9 +93,14 @@ public sealed class Streamkoordinator(
public Stromsitzung? Sitzung(Guid kennung) => _sitzungen.GetValueOrDefault(kennung)?.Sitzung;
/// <summary>Startet einen Strom und gibt die Sitzung zurück.</summary>
/// <param name="erzwingen">
/// Umkodieren, auch wenn das Profil etwas anderes sagt — der Spieler hat
/// es mit dem ersten Plan probiert und ist gescheitert.
/// </param>
public async Task<Stromsitzung> 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"));
+19 -2
View File
@@ -38,6 +38,16 @@ public enum Stromgrund
BildcodecUnbekannt,
TonUnbekannt,
KleinereAufloesungGewuenscht,
/// <summary>Der Spieler hat es versucht und kam nicht zurecht.</summary>
///
/// <remarks>
/// <b>Das Urteil des Browsers schlägt seine eigene Auskunft.</b>
/// <c>canPlayType</c> ist eine Behauptung — „kenne ich" heisst nicht
/// „kann ich diese Datei". Wer es ausprobiert hat und gescheitert ist,
/// weiss mehr als jede Abfrage vorher.
/// </remarks>
SpielerKamNichtZurecht,
}
/// <summary>Was für diesen Strom zu tun ist.</summary>
@@ -87,11 +97,16 @@ public static class Stromentscheid
/// <param name="hoeheGewuenscht">
/// Die vom Spieler gewählte Stufe, oder <c>null</c> für das Original.
/// </param>
/// <param name="erzwingen">
/// Umkodieren, egal was das Profil sagt. <b>Für den zweiten Versuch:</b>
/// der Spieler hat es mit dem ersten Plan probiert und ist gescheitert.
/// </param>
public static Streamplan Fuer(
string? schachtel,
IReadOnlyList<ProbedStream> spuren,
DeviceProfile profil,
int? hoeheGewuenscht = null)
int? hoeheGewuenscht = null,
bool erzwingen = false)
{
var gruende = new List<Stromgrund>();
@@ -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);
@@ -308,6 +308,50 @@ public class HlsArgumenteTests
});
}
// ------------------------------------------------------------------
// Der zweite Versuch
// ------------------------------------------------------------------
/// <summary>Wer es probiert hat und scheiterte, bekommt gerechnet.</summary>
///
/// <remarks>
/// <b>Das Urteil des Browsers schlägt seine eigene Auskunft.</b>
/// <c>canPlayType</c> ist eine Behauptung: „kenne ich" heisst nicht „kann
/// ich diese Datei". Gemessen in Chromium — <c>canPlayType('audio/flac')</c>
/// sagt „probably", <c>MediaSource.isTypeSupported</c> derselben
/// Zeichenkette sagt <c>false</c>. Wer es ausprobiert hat, weiss mehr.
/// </remarks>
[Test]
public void Erzwungen_wird_gerechnet_auch_wenn_alles_passt()
{
IReadOnlyList<ProbedStream> 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");
});
}
/// <summary>Und die gewählte Stufe bleibt dabei erhalten.</summary>
[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));
});
}
/// <summary>Beim Umpacken ist die Stufe gegenstandslos.</summary>
///
/// <remarks>