fix: blocked autoplay left 'Loading…' parked over unreachable controls
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.
This commit is contained in:
1 parent
aae5857dba
commit
db715be54b
2 files changed
+57
-5
No files matched your search
+10
-5
@@ -369,14 +369,14 @@ function stopHls() { if (hls) { hls.destroy(); hls = null; } }
|
|||||||
|
|
||||||
let currentEp = null, lastSavedPos = 0, playerBroken = false;
|
let currentEp = null, lastSavedPos = 0, playerBroken = false;
|
||||||
|
|
||||||
function playerOverlay(text, onRetry) {
|
function playerOverlay(text, onAction, actionLabel = "Retry") {
|
||||||
const p = document.getElementById("player");
|
const p = document.getElementById("player");
|
||||||
let o = p.querySelector(".pl-overlay");
|
let o = p.querySelector(".pl-overlay");
|
||||||
if (!o) { o = el("div", "pl-overlay"); p.append(o); }
|
if (!o) { o = el("div", "pl-overlay"); p.append(o); }
|
||||||
o.replaceChildren(el("span", null, text));
|
o.replaceChildren(el("span", null, text));
|
||||||
if (onRetry) {
|
if (onAction) {
|
||||||
const b = el("button", "btn", "Retry");
|
const b = el("button", "btn", actionLabel);
|
||||||
b.onclick = onRetry;
|
b.onclick = onAction;
|
||||||
o.append(b);
|
o.append(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -465,7 +465,12 @@ async function play(ep_id, ep_no) {
|
|||||||
if (currentEp && !playerBroken && !v.ended) playerOverlay("Buffering\u2026");
|
if (currentEp && !playerBroken && !v.ended) playerOverlay("Buffering\u2026");
|
||||||
});
|
});
|
||||||
v.addEventListener("ended", onEnded);
|
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);
|
watchPos(v);
|
||||||
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
||||||
ep_id, lang, ep: ep_no, pos: saved?.pos || 0});
|
ep_id, lang, ep: ep_no, pos: saved?.pos || 0});
|
||||||
|
|||||||
@@ -278,3 +278,50 @@ async def _episode_watch_state():
|
|||||||
await page.wait_for_selector("#eps .ep.nextup", timeout=10000)
|
await page.wait_for_selector("#eps .ep.nextup", timeout=10000)
|
||||||
assert not errors, errors
|
assert not errors, errors
|
||||||
await browser.close()
|
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()
|
||||||
Reference in new issue
Block a user