From db715be54b73ddf17f859368378828baabdef2e6 Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Mon, 24 Aug 2026 02:51:05 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20blocked=20autoplay=20left=20'Loading?= =?UTF-8?q?=E2=80=A6'=20parked=20over=20unreachable=20controls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overlay only clears on 'playing', which never fires when the browser blocks autoplay — and the overlay covers the native controls, so the 'swap to the native play button' assumption from the click-to-play removal never held. A rejected v.play() is the signal: swap the dead Loading… overlay for a tappable one (Play), and a second rejection escalates to the Playback failed overlay. Browser test fakes the policy (reject first gesture-less play()) since headless Chromium allows autoplay. Logs showed the shape of it: six loads of the same ep, 38-40 segments each, all 200, player never started. --- static/app.js | 15 +++++++++----- tests/test_browser.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/static/app.js b/static/app.js index 8262c41..f30b231 100644 --- a/static/app.js +++ b/static/app.js @@ -369,14 +369,14 @@ function stopHls() { if (hls) { hls.destroy(); hls = null; } } let currentEp = null, lastSavedPos = 0, playerBroken = false; -function playerOverlay(text, onRetry) { +function playerOverlay(text, onAction, actionLabel = "Retry") { const p = document.getElementById("player"); let o = p.querySelector(".pl-overlay"); if (!o) { o = el("div", "pl-overlay"); p.append(o); } o.replaceChildren(el("span", null, text)); - if (onRetry) { - const b = el("button", "btn", "Retry"); - b.onclick = onRetry; + if (onAction) { + const b = el("button", "btn", actionLabel); + b.onclick = onAction; o.append(b); } } @@ -465,7 +465,12 @@ async function play(ep_id, ep_no) { if (currentEp && !playerBroken && !v.ended) playerOverlay("Buffering\u2026"); }); v.addEventListener("ended", onEnded); - v.play().catch(() => {}); // autoplay blocked -> user uses the native play button + // The overlay only clears on 'playing', so a blocked autoplay would leave + // "Loading…" parked over the (unreachable) native controls forever — the + // rejected play() is the signal to swap it for a tappable one. + v.play().catch(() => playerOverlay("Tap to play", () => { + v.play().catch(() => playerFail("playback blocked by the browser")); + }, "Play")); watchPos(v); postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId, ep_id, lang, ep: ep_no, pos: saved?.pos || 0}); diff --git a/tests/test_browser.py b/tests/test_browser.py index 890a37e..0879fee 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -278,3 +278,50 @@ async def _episode_watch_state(): await page.wait_for_selector("#eps .ep.nextup", timeout=10000) assert not errors, errors await browser.close() + + + + +def test_autoplay_blocked_shows_tap_to_play(): + asyncio.run(_autoplay_blocked()) + + +async def _autoplay_blocked(): + # Simulates the browser's autoplay policy (headless Chromium here allows + # autoplay, so the policy is faked): the first, gesture-less play() + # rejects; a later user-initiated one goes through. Regression: the + # "Loading…" overlay (which only clears on 'playing') used to sit over + # the unreachable native controls forever in that state. + async with async_playwright() as p: + browser = await p.chromium.launch() + context = await browser.new_context() + await context.add_init_script(""" + const origPlay = HTMLMediaElement.prototype.play; + HTMLMediaElement.prototype.play = function () { + if (window.__firstPlayBlocked) { + window.__firstPlayBlocked = false; + return Promise.reject(new DOMException('blocked', 'NotAllowedError')); + } + return origPlay.call(this); + }; + window.__firstPlayBlocked = true; + """) + page = await context.new_page() + errors = [] + page.on("pageerror", lambda e: errors.append(str(e))) + + await _login(page, "admin", "adminpw12345") + await page.goto(BASE + "/?anime=chainsaw-man-922") + await page.wait_for_selector("#eps .ep", timeout=30000) + # blocked autoplay: the dead "Loading…" must become a tappable overlay + await page.wait_for_function( + "document.querySelector('.pl-overlay') && " + "document.querySelector('.pl-overlay').textContent.includes('Tap to play')", + timeout=15000, polling=POLL) + # tapping (the gesture) starts real playback + await page.click(".pl-overlay .btn", force=True) + await page.wait_for_function( + "document.querySelector('video').currentTime > 0.5", + timeout=120000, polling=250) + assert not errors, errors + await browser.close()