iPhone: step probes + full hls.js error stream to server

The play-probe now lands (iPhone: iOS 18.7 Safari, hls.js path, MSE ok),
but the /stream XHR never reaches the server on the failing attempts —
impossible under the known code path. Instrument each step
(new-hls-ok / loadsource-called / attach-ok) and log EVERY hls.js error
(fatal+non-fatal, with retry counter) so the client timeline is visible
server-side. Also fixed JSV constant drift (was stuck at 10).
events endpoint: accept hls-err, forward step/fatal/details/buffer/retry/http.
This commit is contained in:
jozamudi committed 2026-08-31 20:48:02 +00:00
1 parent 3df074a5fe
commit aba266ee4b
3 files changed
+14 -5

No files matched your search

+3 -2
View File
@@ -285,12 +285,13 @@ def api_events(evt: dict = Body(...), request: Request = None):
# client-reported events; the diagnostics the browser can uniquely see # client-reported events; the diagnostics the browser can uniquely see
# (playback failures, capability probes) happen client-side, not in the # (playback failures, capability probes) happen client-side, not in the
# request log — so the phone reports them here # request log — so the phone reports them here
if evt.get("type") not in ("error", "client-fail", "play-probe"): if evt.get("type") not in ("error", "client-fail", "play-probe", "hls-err"):
raise HTTPException(422, "unknown event type") raise HTTPException(422, "unknown event type")
c = db.connect() c = db.connect()
db.event(c, request.state.user, evt["type"], db.event(c, request.state.user, evt["type"],
{k: evt[k] for k in ("ep_id", "ep", "lang", "msg", "detail", {k: evt[k] for k in ("ep_id", "ep", "lang", "msg", "detail",
"jsv", "branch", "hls_lib", "mse", "native_hls", "ua") if k in evt}) "jsv", "branch", "hls_lib", "mse", "native_hls", "ua",
"step", "fatal", "details", "buffer", "retry", "http") if k in evt})
c.commit() c.commit()
c.close() c.close()
return {"ok": True} return {"ok": True}
+9 -1
View File
@@ -1,5 +1,5 @@
// app.js — ani-web frontend: search -> detail -> play, vanilla JS + hls.js // app.js — ani-web frontend: search -> detail -> play, vanilla JS + hls.js
const JSV = 10; // bump with every app.js change; play-probe reports it so a const JSV = 12; // bump with every app.js change; play-probe reports it so a
// stale-cache phone is detectable server-side // stale-cache phone is detectable server-side
// BASE = this page's directory, so the app works at / or behind a // BASE = this page's directory, so the app works at / or behind a
// prefixing reverse proxy (e.g. https://host/ani/) // prefixing reverse proxy (e.g. https://host/ani/)
@@ -915,14 +915,22 @@ async function play(ep_id, ep_no) {
timeoutRetry: { maxNumRetry: 6, retryDelayMs: 1000, maxRetryDelayMs: 20000, backoff: "linear" }, timeoutRetry: { maxNumRetry: 6, retryDelayMs: 1000, maxRetryDelayMs: 20000, backoff: "linear" },
errorRetry: { maxNumRetry: 8, 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
hls.on(Hls.Events.ERROR, (_, data) => { hls.on(Hls.Events.ERROR, (_, data) => {
try { postEvent({type: "hls-err", jsv: JSV, 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) { if (data.fatal) {
stopHls(); stopHls();
playerFail(data.details + (data.response ? ` (HTTP ${data.response.code})` : "")); playerFail(data.details + (data.response ? ` (HTTP ${data.response.code})` : ""));
} }
}); });
try { postEvent({type: "play-probe", jsv: JSV, step: "new-hls-ok"}); } catch {}
hls.loadSource("/stream/" + s.token); hls.loadSource("/stream/" + s.token);
try { postEvent({type: "play-probe", jsv: JSV, step: "loadsource-called"}); } catch {}
hls.attachMedia(v); hls.attachMedia(v);
try { postEvent({type: "play-probe", jsv: JSV, step: "attach-ok"}); } catch {}
} }
currentEp = {ep_id, ep_no, v}; currentEp = {ep_id, ep_no, v};
lastSavedPos = 0; lastSavedPos = 0;
+2 -2
View File
@@ -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=11"> <link rel="stylesheet" href="style.css?v=12">
</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=11"></script> <script src="app.js?v=12"></script>
</body> </body>
</html> </html>