iPhone: full client timeline + self-diagnosing watchdog
Every step now beacons with millisecond offsets:
branch-decision -> new-hls-ok -> loadsource-called -> attach-ok ->
manifest-loaded -> level-loaded -> frag-loaded(1..2) -> buffer-appended
-> do-play -> play-resolved/play-rejected(name) + EVERY hls.js error
(fatal and silent retries). Plus a watchdog: if not playing within 15s,
it snapshots {readyState, networkState, paused, buffered, hls.state,
overlay text} every 15s (x6) — so a silent death anywhere produces a
state dump instead of nothing.
Verified on desktop: healthy timeline lands end-to-end, playback OK,
18/18 tests. assets v=13.
This commit is contained in:
1 parent
aba266ee4b
commit
aaaf8e9308
3 files changed
+42
-17
No files matched your search
+3
-1
@@ -291,7 +291,9 @@ def api_events(evt: dict = Body(...), request: Request = None):
|
||||
db.event(c, request.state.user, evt["type"],
|
||||
{k: evt[k] for k in ("ep_id", "ep", "lang", "msg", "detail",
|
||||
"jsv", "branch", "hls_lib", "mse", "native_hls", "ua",
|
||||
"step", "fatal", "details", "buffer", "retry", "http") if k in evt})
|
||||
"step", "fatal", "details", "buffer", "retry", "http",
|
||||
"dt", "n", "rs", "ns", "paused", "bufEnd", "ct",
|
||||
"hls_state", "ov", "name") if k in evt})
|
||||
c.commit()
|
||||
c.close()
|
||||
return {"ok": True}
|
||||
|
||||
+37
-14
@@ -769,6 +769,7 @@ function playerFail(msg) {
|
||||
|
||||
// ---------- buffering visuals: buffer-ahead gauge + CDN chip ----------
|
||||
let bufTimer = null;
|
||||
let wdTimer = null; // play() watchdog (stuck-snapshot interval)
|
||||
function bufferGauge() {
|
||||
// thin bar under the video: how much is buffered AHEAD of playhead
|
||||
// (0-30s scale). Fills while loading, drains as it plays — you can
|
||||
@@ -864,6 +865,9 @@ async function play(ep_id, ep_no) {
|
||||
playerBroken = false;
|
||||
setNowEp(ep_id, ep_no);
|
||||
localStorage.setItem("aniweb_lang_" + detail.slug_id, lang);
|
||||
const t0 = performance.now();
|
||||
const dbg = (step, extra) => { try { postEvent({type: "play-probe", jsv: JSV, dt: Math.round(performance.now() - t0), step, ...(extra || {})}); } catch {} };
|
||||
if (wdTimer) { clearInterval(wdTimer); wdTimer = null; }
|
||||
try {
|
||||
const s = await api(`/api/episode/${ep_id}/stream?lang=${lang}`);
|
||||
stopHls();
|
||||
@@ -874,15 +878,13 @@ async function play(ep_id, ep_no) {
|
||||
bufferGauge();
|
||||
cdnChip();
|
||||
// DECISIVE PROBE (iPhone debugging): report exactly which branch we're
|
||||
// about to take + the browser's real capabilities. One retry on the
|
||||
// phone tells us where play() dies without any guessing.
|
||||
// about to take + the browser's real capabilities.
|
||||
try {
|
||||
const nativeOK = (() => { try { return document.createElement("video").canPlayType("application/vnd.apple.mpegurl") || "none"; } catch { return "err"; } })();
|
||||
const mse = (() => { try { return (window.Hls && Hls.isSupported) ? Hls.isSupported() : null; } catch { return "err"; } })();
|
||||
postEvent({type: "play-probe", jsv: JSV,
|
||||
dbg("branch-decision", {
|
||||
branch: (!window.Hls || !Hls.isSupported()) ? (nativeOK !== "none" && nativeOK !== "" ? "native" : "none-throw") : "hlsjs",
|
||||
hls_lib: typeof window.Hls,
|
||||
mse, native_hls: nativeOK, ua: navigator.userAgent});
|
||||
hls_lib: typeof window.Hls, mse, native_hls: nativeOK, ua: navigator.userAgent});
|
||||
} catch {}
|
||||
if (!window.Hls || !Hls.isSupported()) {
|
||||
// No MSE (e.g. iOS < 17): fall back to the browser's NATIVE HLS player.
|
||||
@@ -915,10 +917,11 @@ async function play(ep_id, ep_no) {
|
||||
timeoutRetry: { maxNumRetry: 6, retryDelayMs: 1000, maxRetryDelayMs: 20000, backoff: "linear" },
|
||||
errorRetry: { maxNumRetry: 8, retryDelayMs: 1000, maxRetryDelayMs: 20000, backoff: "linear" },
|
||||
} } });
|
||||
// iPhone debugging: log EVERY hls.js error (fatal and not) so the
|
||||
// client-side timeline is visible server-side
|
||||
// iPhone debugging: log EVERY hls.js error (fatal and not) + the
|
||||
// success milestones, so the client-side timeline is visible server-side
|
||||
hls.on(Hls.Events.ERROR, (_, data) => {
|
||||
try { postEvent({type: "hls-err", jsv: JSV, fatal: data.fatal, details: data.details,
|
||||
try { postEvent({type: "hls-err", jsv: JSV, dt: Math.round(performance.now() - t0),
|
||||
fatal: data.fatal, details: data.details,
|
||||
buffer: data.buffer || null, retry: data.retry ?? null,
|
||||
http: data.response ? data.response.code : null}); } catch {}
|
||||
if (data.fatal) {
|
||||
@@ -926,11 +929,28 @@ async function play(ep_id, ep_no) {
|
||||
playerFail(data.details + (data.response ? ` (HTTP ${data.response.code})` : ""));
|
||||
}
|
||||
});
|
||||
try { postEvent({type: "play-probe", jsv: JSV, step: "new-hls-ok"}); } catch {}
|
||||
hls.on(Hls.Events.MANIFEST_LOADED, () => dbg("manifest-loaded"));
|
||||
hls.on(Hls.Events.LEVEL_LOADED, () => dbg("level-loaded"));
|
||||
let _fn = 0; hls.on(Hls.Events.FRAG_LOADED, () => { if (++_fn <= 2) dbg("frag-loaded", {n: _fn}); });
|
||||
let _ba = false; hls.on(Hls.Events.BUFFER_APPENDED, () => { if (!_ba) { _ba = true; dbg("buffer-appended"); } });
|
||||
dbg("new-hls-ok");
|
||||
hls.loadSource("/stream/" + s.token);
|
||||
try { postEvent({type: "play-probe", jsv: JSV, step: "loadsource-called"}); } catch {}
|
||||
dbg("loadsource-called");
|
||||
hls.attachMedia(v);
|
||||
try { postEvent({type: "play-probe", jsv: JSV, step: "attach-ok"}); } catch {}
|
||||
dbg("attach-ok");
|
||||
// self-diagnosing watchdog: if not playing within 15s, snapshot the full
|
||||
// client state every 15s (max 6) — catches any silent death, anywhere
|
||||
let _wd = 0;
|
||||
wdTimer = setInterval(() => {
|
||||
const m = document.getElementById("player")?.querySelector("video");
|
||||
if (!m || m.paused === false) { clearInterval(wdTimer); wdTimer = null; return; }
|
||||
if (++_wd >= 6) { clearInterval(wdTimer); wdTimer = null; return; }
|
||||
dbg("stuck-snapshot", {
|
||||
rs: m.readyState, ns: m.networkState, paused: m.paused,
|
||||
bufEnd: m.buffered.length ? m.buffered.end(m.buffered.length - 1) : 0,
|
||||
ct: m.currentTime, hls_state: hls ? hls.state : null,
|
||||
ov: (document.querySelector(".pl-overlay")?.textContent || "").slice(0, 60)});
|
||||
}, 15000);
|
||||
}
|
||||
currentEp = {ep_id, ep_no, v};
|
||||
lastSavedPos = 0;
|
||||
@@ -962,9 +982,12 @@ async function play(ep_id, ep_no) {
|
||||
const blocked = (again) => playerOverlay(
|
||||
again ? "Playback still blocked — allow autoplay for this site in " +
|
||||
"the browser's site settings" : "Tap to play", tap, "\u25b6 Play");
|
||||
const doPlay = () => v.play().catch(err => {
|
||||
if (err && err.name === "NotAllowedError") blocked(false);
|
||||
});
|
||||
const doPlay = () => {
|
||||
dbg("do-play");
|
||||
v.play().then(() => dbg("play-resolved"))
|
||||
.catch(err => { dbg("play-rejected", {name: err && err.name});
|
||||
if (err && err.name === "NotAllowedError") blocked(false); });
|
||||
};
|
||||
if (v.readyState >= 2) doPlay();
|
||||
else v.addEventListener("canplay", doPlay, {once: true});
|
||||
watchPos(v);
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
<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="stylesheet" href="style.css?v=12">
|
||||
<link rel="stylesheet" href="style.css?v=13">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@@ -30,6 +30,6 @@
|
||||
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 -->
|
||||
<script src="hls.min.js"></script>
|
||||
<script src="app.js?v=12"></script>
|
||||
<script src="app.js?v=13"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user