Save watch position on fatal player error; make failure test CDN-independent
playerFail() is the single choke point for every fatal error (hls.js fatal, v.onerror, autoplay block) but never saved the position — periodic saves (timeupdate) stop when hls is destroyed, so a fatal error at e.g. 40s lost everything after the last 10s save. Call savePos() there. test_stream_failure_retry redesigned around that: it no longer needs a healthy CDN (which hangs on segment bytes intermittently). It waits only for playlist metadata (duration comes from the VOD playlist, not segment bytes), wipes the show's history, seeks to 6s (below the 10s periodic-save threshold, above the 3s minimum — so only an error-time save could persist it), aborts /stream to force a deterministic fatal, and asserts the position landed. Retry-recovery now asserts a fresh token fetch instead of segment playback.
This commit is contained in:
1 parent
0fd06f036a
commit
71a4055e89
2 files changed
+38
-9
No files matched your search
@@ -635,6 +635,10 @@ function playerHideOverlay() {
|
||||
function playerFail(msg) {
|
||||
if (!currentEp) return;
|
||||
playerBroken = true;
|
||||
// a fatal error stops the periodic saves (timeupdate/pause never come
|
||||
// again); persist the position now or the user loses everything since
|
||||
// the last 10s save when they leave
|
||||
savePos();
|
||||
const { ep_id, ep_no } = currentEp;
|
||||
postEvent({type: "error", detail: msg, ep_id, ep: ep_no, lang});
|
||||
playerOverlay("Playback failed: " + msg, () => play(ep_id, ep_no));
|
||||
|
||||
+34
-9
@@ -157,22 +157,47 @@ async def _stream_failure_retry():
|
||||
async def abort_stream(route):
|
||||
await route.abort()
|
||||
|
||||
# note: **/stream/* (single star) — must NOT match the
|
||||
# /api/episode/{id}/stream token endpoint, only the /stream/{token} proxy
|
||||
await page.route("**/stream/*", abort_stream)
|
||||
await _login(page, "admin", "adminpw12345")
|
||||
await page.fill("#q", "chainsaw")
|
||||
await page.keyboard.press("Enter")
|
||||
await page.wait_for_selector("a.card", timeout=30000)
|
||||
await page.click("a.card", force=True)
|
||||
# detail auto-plays ep1; with /stream aborted hls.js exhausts its
|
||||
# retries and surfaces the fatal error -> failure overlay with Retry
|
||||
await page.wait_for_selector(".pl-overlay button", timeout=90000)
|
||||
await page.unroute("**/stream/*", abort_stream)
|
||||
await page.click(".pl-overlay button", force=True)
|
||||
# detail auto-plays ep1. Wait only for playlist metadata (duration
|
||||
# comes from the VOD playlist, NOT from segment bytes) — this must
|
||||
# work even when the CDN is slow/flaky on segments
|
||||
await page.wait_for_function(
|
||||
"document.querySelector('video').readyState >= 4 && document.querySelector('video').currentTime > 1",
|
||||
"document.querySelector('video')?.duration > 0",
|
||||
timeout=120000, polling=250)
|
||||
# wipe this show's history: the initial play() write (and any entry
|
||||
# left by earlier tests) would otherwise mask whether the error-time
|
||||
# save happened — from here on the only write can be that save
|
||||
slug = await page.evaluate("new URLSearchParams(location.search).get('anime')")
|
||||
h0 = await page.evaluate("fetch('api/history?all=1').then(r => r.json())")
|
||||
for e in (x for x in h0 if x["slug_id"] == slug):
|
||||
await page.evaluate(
|
||||
"([s, ep]) => fetch('api/history?slug_id=' + encodeURIComponent(s) + '&ep_id=' + ep,"
|
||||
" {method:'DELETE', headers:{'X-Aniweb':'1'}})", [e["slug_id"], e["ep_id"]])
|
||||
# seek to 6s: below the 10s periodic-save threshold (and above the
|
||||
# 3s minimum), so NO periodic save can fire — only an error-time
|
||||
# save could persist ~6s. The seek reflects immediately even though
|
||||
# the bytes never arrive (we kill /stream next)
|
||||
await page.evaluate("document.querySelector('video').currentTime = 6")
|
||||
# kill the stream: hls.js's frag loads abort, retries exhaust,
|
||||
# fatal error -> playerFail, which must save the position
|
||||
# (note: **/stream/* single star — must NOT match the
|
||||
# /api/episode/{id}/stream token endpoint, only the /stream/{token} proxy)
|
||||
await page.route("**/stream/*", abort_stream)
|
||||
await page.wait_for_selector(".pl-overlay button", timeout=90000)
|
||||
# pre-fix nothing was written after the DELETE above
|
||||
hist = await page.evaluate("fetch('api/history?all=1').then(r => r.json())")
|
||||
assert any(e["slug_id"] == slug and e["pos"] >= 5 for e in hist), hist
|
||||
# and the player is recoverable: Retry re-invokes play(), which
|
||||
# starts with a fresh token fetch (assert that — not segment bytes,
|
||||
# which a hanging CDN would make environmental; real playback is
|
||||
# covered by the other video tests)
|
||||
await page.unroute("**/stream/*", abort_stream)
|
||||
async with page.expect_request("**/api/episode/*/stream*", timeout=30000):
|
||||
await page.click(".pl-overlay button", force=True)
|
||||
await browser.close()
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user