From ec5a283e9ed160c587f452c742314f1f4f3c5645 Mon Sep 17 00:00:00 2001 From: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:20:34 +0100 Subject: [PATCH] Handle autoplay failures by showing video controls (#29412) GitOrigin-RevId: afe5d70d00bfda97cde7e3479875b9cd7be5c46f --- .../js/features/autoplay-video/index.js | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/services/web/frontend/js/features/autoplay-video/index.js b/services/web/frontend/js/features/autoplay-video/index.js index 0f66f67d65..080f167170 100644 --- a/services/web/frontend/js/features/autoplay-video/index.js +++ b/services/web/frontend/js/features/autoplay-video/index.js @@ -1,5 +1,12 @@ import { debugConsole } from '@/utils/debugging' +let hasHandledAutoplayFailure = false + +function enableControls(videoEl, reason) { + debugConsole.log(`Enabling video controls: ${reason}`) + videoEl.setAttribute('controls', '') +} + function setup(videoEl) { const reducedMotionReduce = window.matchMedia( '(prefers-reduced-motion: reduce)' @@ -10,7 +17,7 @@ function setup(videoEl) { // in console, if user seeks the control seek bar relatively fast // AbortError: The fetching process for the media resource was aborted by the user agent at the user's request. // this is only a problem in firefox (tested in macOS), chrome and safari is fine - videoEl.setAttribute('controls', '') + enableControls(videoEl, 'reduced motion preference') return } @@ -21,11 +28,29 @@ function setup(videoEl) { let videoIsVisible + function handleAutoplayFailure(error) { + // Possible HTMLMediaElement.play() errors + // Only show controls for errors where manual play would work + // NotAllowedError: autoplay blocked by browser/device settings + // AbortError: play attempt interrupted by browser + if (error.name === 'NotAllowedError' || error.name === 'AbortError') { + if (!hasHandledAutoplayFailure) { + hasHandledAutoplayFailure = true + + document.querySelectorAll('[data-ol-autoplay-video]').forEach(video => { + enableControls(video, `autoplay blocked (${error.name})`) + }) + } + } else { + debugConsole.error('Video playback error:', error) + } + } + videoEl.addEventListener('ended', () => { setTimeout(() => { videoEl.currentTime = 0 - if (videoIsVisible) { - videoEl.play() + if (videoIsVisible && !hasHandledAutoplayFailure) { + videoEl.play().catch(handleAutoplayFailure) } }, DELAY_BEFORE_REPLAY) }) @@ -35,23 +60,18 @@ function setup(videoEl) { for (const change of changes) { if (change.isIntersecting) { videoIsVisible = true - if (videoEl.readyState >= videoEl.HAVE_FUTURE_DATA) { - if (!videoEl.ended) { - videoEl - .play() - .catch(error => - debugConsole.error('Video autoplay failed:', error) - ) - } else { - videoEl - .play() - .catch(error => - debugConsole.error('Video autoplay failed:', error) - ) - } + if ( + !hasHandledAutoplayFailure && + videoEl.readyState >= videoEl.HAVE_FUTURE_DATA + ) { + videoEl.play().catch(handleAutoplayFailure) } } else { videoIsVisible = false + // Pause video when it leaves viewport to save resources + if (!videoEl.paused) { + videoEl.pause() + } } } },