„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:
co-authored by
Claude Opus 5
parent
e30158b84c
commit
9c2e2b5f47
@@ -610,7 +610,18 @@ public sealed record StreamRequest(
|
|||||||
IReadOnlyList<string>? AudioCodecs,
|
IReadOnlyList<string>? AudioCodecs,
|
||||||
int? Height = null,
|
int? Height = null,
|
||||||
int? AudioStream = 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>
|
/// <summary>Eine wählbare Spur, so wie sie im Menü steht.</summary>
|
||||||
public sealed record SpurAuskunft(int Index, string Label, string? Language, bool IsDefault);
|
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
|
// **Nur ohne Wünsche.** Wer eine Tonspur oder eine Stufe
|
||||||
// wählt, bekommt eine Sitzung: beides lässt sich an einer
|
// wählt, bekommt eine Sitzung: beides lässt sich an einer
|
||||||
// durchgereichten Datei nicht einstellen.
|
// 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)
|
&& await Durchreichbar(db, id, profil) is { } datei)
|
||||||
{
|
{
|
||||||
return Results.Ok(await Direktauskunft(db, datei, anfrage.StartSeconds ?? 0));
|
return Results.Ok(await Direktauskunft(db, datei, anfrage.StartSeconds ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
var sitzung = await koordinator.StartenAsync(
|
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));
|
return Results.Ok(await Auskunft(db, sitzung));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,9 +93,14 @@ public sealed class Streamkoordinator(
|
|||||||
public Stromsitzung? Sitzung(Guid kennung) => _sitzungen.GetValueOrDefault(kennung)?.Sitzung;
|
public Stromsitzung? Sitzung(Guid kennung) => _sitzungen.GetValueOrDefault(kennung)?.Sitzung;
|
||||||
|
|
||||||
/// <summary>Startet einen Strom und gibt die Sitzung zurück.</summary>
|
/// <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(
|
public async Task<Stromsitzung> StartenAsync(
|
||||||
Guid dateiId, Guid benutzer, DeviceProfile profil,
|
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();
|
AbgelaufeneWegraeumen();
|
||||||
|
|
||||||
@@ -123,7 +128,7 @@ public sealed class Streamkoordinator(
|
|||||||
s.Index, s.Kind, s.Codec, s.Language, s.Title,
|
s.Index, s.Kind, s.Codec, s.Language, s.Title,
|
||||||
s.IsDefault, s.IsForced, s.Channels, s.Width, s.Height)).ToList();
|
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 kennung = Guid.CreateVersion7();
|
||||||
var ordner = Path.Combine(pfade.Transcode, "stroeme", kennung.ToString("N"));
|
var ordner = Path.Combine(pfade.Transcode, "stroeme", kennung.ToString("N"));
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ public enum Stromgrund
|
|||||||
BildcodecUnbekannt,
|
BildcodecUnbekannt,
|
||||||
TonUnbekannt,
|
TonUnbekannt,
|
||||||
KleinereAufloesungGewuenscht,
|
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>
|
/// <summary>Was für diesen Strom zu tun ist.</summary>
|
||||||
@@ -87,11 +97,16 @@ public static class Stromentscheid
|
|||||||
/// <param name="hoeheGewuenscht">
|
/// <param name="hoeheGewuenscht">
|
||||||
/// Die vom Spieler gewählte Stufe, oder <c>null</c> für das Original.
|
/// Die vom Spieler gewählte Stufe, oder <c>null</c> für das Original.
|
||||||
/// </param>
|
/// </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(
|
public static Streamplan Fuer(
|
||||||
string? schachtel,
|
string? schachtel,
|
||||||
IReadOnlyList<ProbedStream> spuren,
|
IReadOnlyList<ProbedStream> spuren,
|
||||||
DeviceProfile profil,
|
DeviceProfile profil,
|
||||||
int? hoeheGewuenscht = null)
|
int? hoeheGewuenscht = null,
|
||||||
|
bool erzwingen = false)
|
||||||
{
|
{
|
||||||
var gruende = new List<Stromgrund>();
|
var gruende = new List<Stromgrund>();
|
||||||
|
|
||||||
@@ -108,7 +123,9 @@ public static class Stromentscheid
|
|||||||
: (int?)null;
|
: (int?)null;
|
||||||
if (hoehe is not null) gruende.Add(Stromgrund.KleinereAufloesungGewuenscht);
|
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);
|
return new Streamplan(Stromart.Umkodieren, gruende, hoehe);
|
||||||
|
|
||||||
var schachtelPasst = Passt(schachtel, profil.Containers);
|
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>
|
/// <summary>Beim Umpacken ist die Stufe gegenstandslos.</summary>
|
||||||
///
|
///
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
|||||||
@@ -627,6 +627,8 @@ export type Stromwunsch = {
|
|||||||
height?: number | null;
|
height?: number | null;
|
||||||
audioStream?: number | null;
|
audioStream?: number | null;
|
||||||
startSeconds?: number;
|
startSeconds?: number;
|
||||||
|
/** Umkodieren, egal was das Profil sagt — für den zweiten Versuch. */
|
||||||
|
transcode?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Legt einen Strom an, der sofort anfängt.
|
/** Legt einen Strom an, der sofort anfängt.
|
||||||
|
|||||||
+92
-18
@@ -18,34 +18,57 @@ import { icons, icon } from './icons';
|
|||||||
*
|
*
|
||||||
* `canPlayType` antwortet mit `"probably"`, `"maybe"` oder `""`. Alles ausser
|
* `canPlayType` antwortet mit `"probably"`, `"maybe"` oder `""`. Alles ausser
|
||||||
* der leeren Zeichenkette zählt: `"maybe"` heisst „der Codec ist bekannt, über
|
* 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
|
* das Profil sage ich nichts".
|
||||||
* besser als einer, der sicher umgerechnet wird.
|
*
|
||||||
|
* **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(): {
|
export function geraeteprofil(): {
|
||||||
containers: string[]; videoCodecs: string[]; audioCodecs: string[];
|
containers: string[]; videoCodecs: string[]; audioCodecs: string[];
|
||||||
} {
|
} {
|
||||||
const v = document.createElement('video');
|
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 `<video>`-Element oder als Strom
|
||||||
|
* durch den MSE-Puffer —, also muss sie auf beiden Wegen gehen.
|
||||||
|
*
|
||||||
|
* Ohne Puffer (Safari auf dem iPhone) gibt es nur den einen Weg, und dort
|
||||||
|
* stimmt `canPlayType` auch. */
|
||||||
|
const kann = (...typen: string[]): boolean => typen.some(t =>
|
||||||
|
v.canPlayType(t) !== '' && (!hatPuffer || MediaSource.isTypeSupported(t)));
|
||||||
|
|
||||||
const schachteln = ['mp4'];
|
const schachteln = ['mp4'];
|
||||||
if (kann('video/webm')) schachteln.push('webm');
|
if (kann('video/webm; codecs="vp8"', 'video/webm; codecs="vp9"')) schachteln.push('webm');
|
||||||
|
|
||||||
|
// **Jeder Typ in der Form, in der er wirklich ankommt.** Ein Codec steckt
|
||||||
|
// hier in einem Film, nicht in einer nackten Tondatei: also `audio/mp4` und
|
||||||
|
// nicht `audio/flac`. Genau dieser Unterschied entschied bei FLAC zwischen
|
||||||
|
// „geht" und „geht nicht".
|
||||||
const bild: string[] = [];
|
const bild: string[] = [];
|
||||||
// H.264 kann jeder Browser, der Video kann — die Prüfung steht trotzdem da,
|
|
||||||
// weil „jeder" eine Behauptung ist und `canPlayType` eine Auskunft.
|
|
||||||
if (kann('video/mp4; codecs="avc1.640029"')) bild.push('h264', 'avc1');
|
if (kann('video/mp4; codecs="avc1.640029"')) bild.push('h264', 'avc1');
|
||||||
if (kann('video/mp4; codecs="hvc1.1.6.L93.B0"')
|
if (kann('video/mp4; codecs="hvc1.1.6.L93.B0"', 'video/mp4; codecs="hev1.1.6.L93.B0"')) {
|
||||||
|| kann('video/mp4; codecs="hev1.1.6.L93.B0"')) bild.push('hevc', 'h265', 'hvc1');
|
bild.push('hevc', 'h265', 'hvc1');
|
||||||
|
}
|
||||||
if (kann('video/webm; codecs="vp9"')) bild.push('vp9');
|
if (kann('video/webm; codecs="vp9"')) bild.push('vp9');
|
||||||
if (kann('video/mp4; codecs="av01.0.05M.08"')) bild.push('av1');
|
if (kann('video/mp4; codecs="av01.0.05M.08"')) bild.push('av1');
|
||||||
|
|
||||||
const ton: string[] = [];
|
const ton: string[] = [];
|
||||||
if (kann('audio/mp4; codecs="mp4a.40.2"')) ton.push('aac');
|
if (kann('audio/mp4; codecs="mp4a.40.2"')) ton.push('aac');
|
||||||
if (kann('audio/mpeg')) ton.push('mp3');
|
if (kann('audio/mp4; codecs="mp3"')) ton.push('mp3');
|
||||||
if (kann('audio/mp4; codecs="ac-3"')) ton.push('ac3');
|
if (kann('audio/mp4; codecs="ac-3"')) ton.push('ac3');
|
||||||
if (kann('audio/mp4; codecs="ec-3"')) ton.push('eac3');
|
if (kann('audio/mp4; codecs="ec-3"')) ton.push('eac3');
|
||||||
if (kann('audio/ogg; codecs="opus"') || kann('audio/webm; codecs="opus"')) ton.push('opus');
|
if (kann('audio/mp4; codecs="opus"', 'audio/webm; codecs="opus"')) ton.push('opus');
|
||||||
if (kann('audio/flac')) ton.push('flac');
|
if (kann('audio/mp4; codecs="flac"')) ton.push('flac');
|
||||||
|
|
||||||
// **Eine leere Liste wäre schlimmer als eine ungenaue.** Der Server hielte
|
// **Eine leere Liste wäre schlimmer als eine ungenaue.** Der Server hielte
|
||||||
// sie für „kein Profil mitgeschickt" und fiele auf Apples zurück — also
|
// sie für „kein Profil mitgeschickt" und fiele auf Apples zurück — also
|
||||||
@@ -127,6 +150,15 @@ export function spieler(anfrage: Spieleranfrage): void {
|
|||||||
let tonspur: number | null = null;
|
let tonspur: number | null = null;
|
||||||
let untertitel: number | null = null;
|
let untertitel: number | null = null;
|
||||||
|
|
||||||
|
/** Ob der Server umkodieren soll, egal was das Profil sagt.
|
||||||
|
*
|
||||||
|
* **Das Urteil des Browsers schlägt seine eigene Auskunft.** `canPlayType`
|
||||||
|
* ist eine Behauptung — „kenne ich" heisst nicht „kann ich diese Datei".
|
||||||
|
* Scheitert die Wiedergabe trotzdem, wird einmal umgerechnet statt den
|
||||||
|
* Benutzer ans Zahnrad zu schicken. Wer eine Fassung von Hand wählen soll,
|
||||||
|
* weiss selten, welche. */
|
||||||
|
let umkodierenErzwingen = false;
|
||||||
|
|
||||||
const filmzeit = (): number => vorlauf + (Number.isFinite(video.currentTime) ? video.currentTime : 0);
|
const filmzeit = (): number => vorlauf + (Number.isFinite(video.currentTime) ? video.currentTime : 0);
|
||||||
|
|
||||||
/* ---------- Aufbau ---------- */
|
/* ---------- Aufbau ---------- */
|
||||||
@@ -331,7 +363,10 @@ export function spieler(anfrage: Spieleranfrage): void {
|
|||||||
video.onplaying = () => { dreher.classList.add('hidden'); verbergenPlanen(); };
|
video.onplaying = () => { dreher.classList.add('hidden'); verbergenPlanen(); };
|
||||||
video.ontimeupdate = () => { zeitAuffrischen(); melden(); };
|
video.ontimeupdate = () => { zeitAuffrischen(); melden(); };
|
||||||
video.onended = () => { melden(true, true); };
|
video.onended = () => { melden(true, true); };
|
||||||
video.onerror = () => zeigeFehler(videofehler());
|
video.onerror = () => {
|
||||||
|
if (nochmalMitUmkodieren(video.error?.code)) return;
|
||||||
|
zeigeFehler(videofehler());
|
||||||
|
};
|
||||||
|
|
||||||
zeitleiste.oninput = () => { stand.textContent = dauer(Number(zeitleiste.value)); };
|
zeitleiste.oninput = () => { stand.textContent = dauer(Number(zeitleiste.value)); };
|
||||||
zeitleiste.onchange = () => void springen(Number(zeitleiste.value));
|
zeitleiste.onchange = () => void springen(Number(zeitleiste.value));
|
||||||
@@ -422,6 +457,7 @@ export function spieler(anfrage: Spieleranfrage): void {
|
|||||||
height: stufe,
|
height: stufe,
|
||||||
audioStream: tonspur,
|
audioStream: tonspur,
|
||||||
startSeconds: ab,
|
startSeconds: ab,
|
||||||
|
transcode: umkodierenErzwingen,
|
||||||
});
|
});
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
zeigeFehler(meldung(fehler));
|
zeigeFehler(meldung(fehler));
|
||||||
@@ -472,6 +508,10 @@ export function spieler(anfrage: Spieleranfrage): void {
|
|||||||
});
|
});
|
||||||
hls.on(Hls.Events.ERROR, (_: unknown, daten: { fatal: boolean; type: string }) => {
|
hls.on(Hls.Events.ERROR, (_: unknown, daten: { fatal: boolean; type: string }) => {
|
||||||
if (!daten.fatal) return;
|
if (!daten.fatal) return;
|
||||||
|
// Dasselbe wie beim `<video>`: ein Medienfehler heisst, dass der
|
||||||
|
// Browser mit dem Bild nicht zurechtkam. Dann wird gerechnet, nicht
|
||||||
|
// gemeldet.
|
||||||
|
if (daten.type === Hls.ErrorTypes.MEDIA_ERROR && nochmalMitUmkodieren()) return;
|
||||||
zeigeFehler(hlsfehler(daten.type, Hls.ErrorTypes));
|
zeigeFehler(hlsfehler(daten.type, Hls.ErrorTypes));
|
||||||
});
|
});
|
||||||
hls.loadSource(auskunft.url);
|
hls.loadSource(auskunft.url);
|
||||||
@@ -589,6 +629,38 @@ export function spieler(anfrage: Spieleranfrage): void {
|
|||||||
|
|
||||||
/* ---------- Fehler ---------- */
|
/* ---------- Fehler ---------- */
|
||||||
|
|
||||||
|
/** Zweiter Versuch, diesmal gerechnet.
|
||||||
|
*
|
||||||
|
* **Der Benutzer soll nicht raten müssen.** Hier stand „über das Zahnrad
|
||||||
|
* lässt sich eine Fassung wählen, die er versteht" — und wer das liest,
|
||||||
|
* weiss weder, welche das wäre, noch warum der Server sie ihm nicht gleich
|
||||||
|
* gegeben hat. Er hat recht: das ist die Aufgabe des Servers.
|
||||||
|
*
|
||||||
|
* **Genau einmal.** Scheitert auch der gerechnete Strom, liegt es nicht am
|
||||||
|
* Codec, und ein dritter Anlauf wäre eine Schleife statt einer Lösung.
|
||||||
|
*
|
||||||
|
* Gibt `true` zurück, wenn der Versuch läuft — dann bleibt die Meldung aus.
|
||||||
|
*/
|
||||||
|
function nochmalMitUmkodieren(code?: number): boolean {
|
||||||
|
const kannAmCodecLiegen = code === undefined
|
||||||
|
|| code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||||
|
|| code === MediaError.MEDIA_ERR_DECODE;
|
||||||
|
|
||||||
|
if (!kannAmCodecLiegen || umkodierenErzwingen || geschlossen) return false;
|
||||||
|
|
||||||
|
// **Lief schon ein gerechneter Strom, hilft ein zweiter nicht.** Dann
|
||||||
|
// liegt es nicht am Codec, und dreissig Sekunden zu warten, um dasselbe
|
||||||
|
// noch einmal zu bekommen, ist keine Hilfe, sondern eine Verzögerung.
|
||||||
|
if (auskunft?.method === 'transcode') return false;
|
||||||
|
|
||||||
|
umkodierenErzwingen = true;
|
||||||
|
meldungsfeld.classList.add('hidden');
|
||||||
|
dreher.classList.remove('hidden');
|
||||||
|
// Die Stelle, an der es hing — nicht von vorn.
|
||||||
|
void stromAufbauen(filmzeit());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function zeigeFehler(text: string): void {
|
function zeigeFehler(text: string): void {
|
||||||
dreher.classList.add('hidden');
|
dreher.classList.add('hidden');
|
||||||
meldungsfeld.textContent = text;
|
meldungsfeld.textContent = text;
|
||||||
@@ -606,11 +678,12 @@ export function spieler(anfrage: Spieleranfrage): void {
|
|||||||
case MediaError.MEDIA_ERR_NETWORK:
|
case MediaError.MEDIA_ERR_NETWORK:
|
||||||
return 'Die Verbindung zum Server ist abgerissen. Läuft er noch?';
|
return 'Die Verbindung zum Server ist abgerissen. Läuft er noch?';
|
||||||
case MediaError.MEDIA_ERR_DECODE:
|
case MediaError.MEDIA_ERR_DECODE:
|
||||||
return 'Der Browser konnte das Bild nicht dekodieren. '
|
|
||||||
+ 'Versuche eine kleinere Auflösung über das Zahnrad.';
|
|
||||||
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
||||||
return 'Dieser Browser kann die Spuren dieser Datei nicht öffnen. '
|
// Hierher kommt nur, wer den gerechneten Strom auch nicht öffnen
|
||||||
+ 'Über das Zahnrad lässt sich eine Fassung wählen, die er versteht.';
|
// konnte — der erste Versuch wird ohne Rückfrage wiederholt.
|
||||||
|
return 'Dieser Browser kommt mit dem Bild nicht zurecht, auch '
|
||||||
|
+ 'umgerechnet nicht. Eine kleinere Auflösung über das Zahnrad hilft '
|
||||||
|
+ 'vielleicht; sonst lade den Film herunter.';
|
||||||
default:
|
default:
|
||||||
return 'Die Wiedergabe ist gescheitert. Die Einzelheiten stehen im Protokoll von heute.';
|
return 'Die Wiedergabe ist gescheitert. Die Einzelheiten stehen im Protokoll von heute.';
|
||||||
}
|
}
|
||||||
@@ -622,8 +695,9 @@ export function spieler(anfrage: Spieleranfrage): void {
|
|||||||
+ 'oder der Server hat die Sitzung aufgeräumt — fang die Wiedergabe neu an.';
|
+ 'oder der Server hat die Sitzung aufgeräumt — fang die Wiedergabe neu an.';
|
||||||
}
|
}
|
||||||
if (art === arten.MEDIA_ERROR) {
|
if (art === arten.MEDIA_ERROR) {
|
||||||
return 'Der Browser kam mit dem Bild nicht zurecht. '
|
return 'Der Browser kommt mit dem Bild nicht zurecht, auch umgerechnet '
|
||||||
+ 'Versuche eine kleinere Auflösung über das Zahnrad.';
|
+ 'nicht. Eine kleinere Auflösung über das Zahnrad hilft vielleicht; '
|
||||||
|
+ 'sonst lade den Film herunter.';
|
||||||
}
|
}
|
||||||
return 'Die Wiedergabe ist gescheitert. Die Einzelheiten stehen im Protokoll von heute.';
|
return 'Die Wiedergabe ist gescheitert. Die Einzelheiten stehen im Protokoll von heute.';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user