Player: defer play() to canplay; friendly tap-to-play fallback

Root cause of the spurious 'Browser blocked playback' on initial play:
play() was called right after hls.js attachMedia, before any data was
buffered. Chrome rejects such early play() calls with NotAllowedError —
indistinguishable from a real autoplay block — so the scary
'allow autoplay in site settings' overlay appeared on every start.

- wait for first data (canplay; hls.js's recommended pattern), then play
- a NotAllowedError at that point is a genuine policy block (common on
  phones): show a simple 'Tap to play' + Play button
- escalate to the 'allow autoplay' settings hint only if the tap is
  itself rejected
- dead-stream case is unaffected: hls.js fatal error still shows its
  own 'Playback failed + Retry' overlay
- also aligns with test_autoplay_blocked's expected 'Tap to play' text
This commit is contained in:
jozamudi committed 2026-08-30 21:20:39 +00:00
1 parent 0613cf7e27
commit eb5082bd3b
1 file changed
+16 -16
+16 -16
View File
@@ -822,23 +822,23 @@ async function play(ep_id, ep_no) {
if (currentEp && !playerBroken && !v.ended) playerOverlay("Buffering\u2026");
});
v.addEventListener("ended", onEnded);
// NotAllowedError = the browser blocked autoplay: 'playing' will never
// fire, so the Loading… overlay (which only clears on 'playing') would
// sit forever over the unreachable native controls. Other rejection
// reasons (AbortError when the stream has no data yet, …) belong to the
// hls.js fatal-error path — leave its overlay alone.
v.play().catch(err => {
if (!err || err.name !== "NotAllowedError") return;
// Not fatal: a tap (a real gesture) usually starts playback. If the
// browser rejects even that (autoplay disallowed for the site, …),
// stay on the tappable overlay and say what to change — retrying
// play() would just hit the same wall
const tap = () => v.play().catch(() => blocked());
const blocked = () => playerOverlay(
"Browser blocked playback. Tap Play, or allow autoplay for this " +
"site in the browser's site settings", tap, "Play");
blocked();
// ponytail: play() before first data can reject with NotAllowedError in
// Chrome — the same error a real autoplay block produces — so it used to
// show a scary "browser blocked" overlay on every initial play. Wait for
// first data (canplay; hls.js's recommended pattern), THEN play(). A
// NotAllowedError at that point is a genuine policy block (common on
// phones) — offer a simple tap-to-play; escalate to the settings hint
// only if the tap itself is rejected. 'playing' still clears the
// Loading… overlay either way.
const tap = () => v.play().catch(() => blocked(true));
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);
});
if (v.readyState >= 2) doPlay();
else v.addEventListener("canplay", doPlay, {once: true});
watchPos(v);
setupIntro(v, ep_no);
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,