From aaaf8e9308f22dc0ea5721a0482e87fee1f07868 Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Mon, 31 Aug 2026 20:53:18 +0000 Subject: [PATCH] iPhone: full client timeline + self-diagnosing watchdog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- server/app.py | 4 +++- static/app.js | 51 ++++++++++++++++++++++++++++++++++------------- static/index.html | 4 ++-- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/server/app.py b/server/app.py index 5171364..f88b317 100644 --- a/server/app.py +++ b/server/app.py @@ -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} diff --git a/static/app.js b/static/app.js index 08a5dfd..1625ab5 100644 --- a/static/app.js +++ b/static/app.js @@ -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); diff --git a/static/index.html b/static/index.html index b808329..9ec3071 100644 --- a/static/index.html +++ b/static/index.html @@ -6,7 +6,7 @@ ani-web - +
@@ -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 --> - +