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"); if (currentEp && !playerBroken && !v.ended) playerOverlay("Buffering\u2026");
}); });
v.addEventListener("ended", onEnded); v.addEventListener("ended", onEnded);
// NotAllowedError = the browser blocked autoplay: 'playing' will never // ponytail: play() before first data can reject with NotAllowedError in
// fire, so the Loading… overlay (which only clears on 'playing') would // Chrome — the same error a real autoplay block produces — so it used to
// sit forever over the unreachable native controls. Other rejection // show a scary "browser blocked" overlay on every initial play. Wait for
// reasons (AbortError when the stream has no data yet, …) belong to the // first data (canplay; hls.js's recommended pattern), THEN play(). A
// hls.js fatal-error path — leave its overlay alone. // NotAllowedError at that point is a genuine policy block (common on
v.play().catch(err => { // phones) — offer a simple tap-to-play; escalate to the settings hint
if (!err || err.name !== "NotAllowedError") return; // only if the tap itself is rejected. 'playing' still clears the
// Not fatal: a tap (a real gesture) usually starts playback. If the // Loading… overlay either way.
// browser rejects even that (autoplay disallowed for the site, …), const tap = () => v.play().catch(() => blocked(true));
// stay on the tappable overlay and say what to change — retrying const blocked = (again) => playerOverlay(
// play() would just hit the same wall again ? "Playback still blocked — allow autoplay for this site in " +
const tap = () => v.play().catch(() => blocked()); "the browser's site settings" : "Tap to play", tap, "\u25b6 Play");
const blocked = () => playerOverlay( const doPlay = () => v.play().catch(err => {
"Browser blocked playback. Tap Play, or allow autoplay for this " + if (err && err.name === "NotAllowedError") blocked(false);
"site in the browser's site settings", tap, "Play");
blocked();
}); });
if (v.readyState >= 2) doPlay();
else v.addEventListener("canplay", doPlay, {once: true});
watchPos(v); watchPos(v);
setupIntro(v, ep_no); setupIntro(v, ep_no);
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,