index.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { debugConsole } from '@/utils/debugging'
  2. let hasHandledAutoplayFailure = false
  3. function enableControls(videoEl, reason) {
  4. debugConsole.log(`Enabling video controls: ${reason}`)
  5. videoEl.setAttribute('controls', '')
  6. }
  7. function isIOS() {
  8. if (typeof navigator === 'undefined') {
  9. return false
  10. }
  11. // Detects iPhone/iPod and iPad in mobile mode
  12. // Note: Modern iPad in desktop mode reports as Mac in UA and is intentionally not detected
  13. // to allow autoplay if the browser permits it
  14. return /iPad|iPhone|iPod/.test(navigator.userAgent)
  15. }
  16. function setup(videoEl) {
  17. // iOS has strict autoplay policies - always show controls first
  18. if (isIOS()) {
  19. enableControls(videoEl, 'iOS devices')
  20. return
  21. }
  22. const reducedMotionReduce = window.matchMedia(
  23. '(prefers-reduced-motion: reduce)'
  24. )
  25. if (reducedMotionReduce.matches) {
  26. // TODO: on firefox, if user enters this mode, video can throw error
  27. // in console, if user seeks the control seek bar relatively fast
  28. // AbortError: The fetching process for the media resource was aborted by the user agent at the user's request.
  29. // this is only a problem in firefox (tested in macOS), chrome and safari is fine
  30. enableControls(videoEl, 'reduced motion preference')
  31. return
  32. }
  33. const DELAY_BEFORE_REPLAY = 15 * 1000
  34. // 0.7 will enable the autoplay on the desktop main homepage video for all users
  35. const INTERSECTION_THRESHOLD = 0.7
  36. let videoIsVisible
  37. function handleAutoplayFailure(error) {
  38. // Possible HTMLMediaElement.play() errors
  39. // Only show controls for errors where manual play would work
  40. // NotAllowedError: autoplay blocked by browser/device settings
  41. // AbortError: play attempt interrupted by browser
  42. if (error.name === 'NotAllowedError' || error.name === 'AbortError') {
  43. if (!hasHandledAutoplayFailure) {
  44. hasHandledAutoplayFailure = true
  45. document.querySelectorAll('[data-ol-autoplay-video]').forEach(video => {
  46. enableControls(video, `autoplay blocked (${error.name})`)
  47. })
  48. }
  49. } else {
  50. debugConsole.error('Video playback error:', error)
  51. }
  52. }
  53. function attemptPlay() {
  54. if (!videoIsVisible || hasHandledAutoplayFailure) {
  55. return
  56. }
  57. if (videoEl.readyState >= videoEl.HAVE_FUTURE_DATA) {
  58. videoEl.play().catch(handleAutoplayFailure)
  59. }
  60. }
  61. // Retry play when video has enough data
  62. videoEl.addEventListener('canplay', attemptPlay, { once: true })
  63. videoEl.addEventListener('loadeddata', attemptPlay, { once: true })
  64. videoEl.addEventListener('ended', () => {
  65. setTimeout(() => {
  66. videoEl.currentTime = 0
  67. attemptPlay()
  68. }, DELAY_BEFORE_REPLAY)
  69. })
  70. const observer = new IntersectionObserver(
  71. function onIntersecting(changes) {
  72. for (const change of changes) {
  73. if (change.isIntersecting) {
  74. videoIsVisible = true
  75. attemptPlay()
  76. } else {
  77. videoIsVisible = false
  78. // Pause video when it leaves viewport to save resources
  79. if (!videoEl.paused) {
  80. videoEl.pause()
  81. }
  82. }
  83. }
  84. },
  85. {
  86. threshold: INTERSECTION_THRESHOLD,
  87. }
  88. )
  89. observer.observe(videoEl)
  90. }
  91. document.querySelectorAll('[data-ol-autoplay-video]').forEach(setup)