Player: native-HLS fallback + visible stream errors
Root cause of 'eternal spinner': a browser without working hls.js (no MSE, or hls.min.js blocked) threw 'HLS not supported' which the old showStreamError hid UNDER the spinner overlay, and there was no fallback — so the player sat on Loading… forever with no /stream request ever leaving the browser (matched the prod log exactly). - no-hls.js + canPlayType(mpegurl) -> v.src = proxied m3u8, native <video> plays it (iOS < 17 path). Verified: readyState 4, playing, no hls.js at all. - no-hls.js + no native HLS -> VISIBLE 'Stream error: ...' overlay with Retry (was: hidden under the spinner) - every play() catch routes through the visible overlay now Desktop hls.js regression: still plays. assets v=6.
This commit is contained in:
1 parent
733fa673c2
commit
9b90be3a10
2 files changed
+16
-8
No files matched your search
+14
-6
@@ -67,10 +67,10 @@ function toast(msg, action) {
|
|||||||
setTimeout(() => t.remove(), action ? 6000 : 2600);
|
setTimeout(() => t.remove(), action ? 6000 : 2600);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showStreamError(msg) {
|
function showStreamError(msg, retry) {
|
||||||
const p = el("p", "error", "Stream error: " + msg);
|
// visible overlay + Retry — never leave a silent spinner (this is what
|
||||||
const player = document.getElementById("player");
|
// an unplayable browser used to hit: 'HLS not supported' under the overlay)
|
||||||
if (player) player.append(p); else fail(new Error(msg));
|
playerOverlay("Stream error: " + msg, retry || (() => { if (currentEp) play(currentEp.ep_id, currentEp.ep_no); }), "Retry");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- history (server-persisted, incl. watch position) ----------
|
// ---------- history (server-persisted, incl. watch position) ----------
|
||||||
@@ -862,7 +862,14 @@ async function play(ep_id, ep_no) {
|
|||||||
playerOverlay("Loading\u2026", null, null, true);
|
playerOverlay("Loading\u2026", null, null, true);
|
||||||
bufferGauge();
|
bufferGauge();
|
||||||
cdnChip();
|
cdnChip();
|
||||||
if (!window.Hls || !Hls.isSupported()) throw new Error("HLS not supported in this browser");
|
if (!window.Hls || !Hls.isSupported()) {
|
||||||
|
// No MSE (e.g. iOS < 17): fall back to the browser's NATIVE HLS player.
|
||||||
|
// iOS Safari plays standard m3u8 in <video> directly — same proxied URL,
|
||||||
|
// same-origin, so no CORS needed.
|
||||||
|
if (!v.canPlayType("application/vnd.apple.mpegurl"))
|
||||||
|
throw new Error("this browser can't play HLS (no MSE support) — use a recent Chrome, Safari or Firefox");
|
||||||
|
v.src = "/stream/" + s.token;
|
||||||
|
} else {
|
||||||
// the upstream CDN occasionally stalls on segment bytes (0 bytes for
|
// the upstream CDN occasionally stalls on segment bytes (0 bytes for
|
||||||
// 10s+). hls.js's default 5s time-to-first-byte abort would drop the XHR
|
// 10s+). hls.js's default 5s time-to-first-byte abort would drop the XHR
|
||||||
// before our proxy's 12s upstream attempt (+300ms retry) can answer with
|
// before our proxy's 12s upstream attempt (+300ms retry) can answer with
|
||||||
@@ -893,6 +900,7 @@ async function play(ep_id, ep_no) {
|
|||||||
});
|
});
|
||||||
hls.loadSource("/stream/" + s.token);
|
hls.loadSource("/stream/" + s.token);
|
||||||
hls.attachMedia(v);
|
hls.attachMedia(v);
|
||||||
|
}
|
||||||
currentEp = {ep_id, ep_no, v};
|
currentEp = {ep_id, ep_no, v};
|
||||||
lastSavedPos = 0;
|
lastSavedPos = 0;
|
||||||
if (nextCta) { nextCta.remove(); nextCta = null; }
|
if (nextCta) { nextCta.remove(); nextCta = null; }
|
||||||
@@ -933,7 +941,7 @@ async function play(ep_id, ep_no) {
|
|||||||
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
||||||
ep_id, lang, ep: ep_no, pos: saved?.pos || 0});
|
ep_id, lang, ep: ep_no, pos: saved?.pos || 0});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showStreamError(e.message);
|
showStreamError(e.message, () => play(ep_id, ep_no));
|
||||||
}
|
}
|
||||||
document.title = `${detail.name} E${ep_no} — ani-web`;
|
document.title = `${detail.name} E${ep_no} — ani-web`;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@
|
|||||||
<title>ani-web</title>
|
<title>ani-web</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%23f97316'/%3E%3Ctext x='32' y='45' font-family='sans-serif' font-size='42' font-weight='700' text-anchor='middle' fill='%230e0f13'%3Ea%3C/text%3E%3C/svg%3E">
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%23f97316'/%3E%3Ctext x='32' y='45' font-family='sans-serif' font-size='42' font-weight='700' text-anchor='middle' fill='%230e0f13'%3Ea%3C/text%3E%3C/svg%3E">
|
||||||
|
|
||||||
<link rel="stylesheet" href="style.css?v=5">
|
<link rel="stylesheet" href="style.css?v=6">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
@@ -30,6 +30,6 @@
|
|||||||
max-age=300 in browser AND possibly in your HTTPS proxy, and a stale
|
max-age=300 in browser AND possibly in your HTTPS proxy, and a stale
|
||||||
copy is exactly how users keep seeing an old UI after a deploy -->
|
copy is exactly how users keep seeing an old UI after a deploy -->
|
||||||
<script src="hls.min.js"></script>
|
<script src="hls.min.js"></script>
|
||||||
<script src="app.js?v=5"></script>
|
<script src="app.js?v=6"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user