Fix/BOOK-240 video fixes

* fix(BOOK-240): Added support for multiple sources and fixed issue with play/pause on mobile

* fix(BOOK-240): Pausing hero video when scrolling out of view


Approved-by: Christel Westerberg
This commit is contained in:
Erik Tiekstra
2025-12-16 09:09:17 +00:00
parent 713ca6562e
commit bf7a2ac2fe
10 changed files with 121 additions and 118 deletions

View File

@@ -1,7 +1,13 @@
'use client'
import { cx, VariantProps } from 'class-variance-authority'
import { useRef, useState, VideoHTMLAttributes } from 'react'
import {
useCallback,
useEffect,
useRef,
useState,
VideoHTMLAttributes,
} from 'react'
import { Lang, languages } from '@scandic-hotels/common/constants/language'
import { FocalPoint } from '@scandic-hotels/common/utils/imageVault'
@@ -17,7 +23,10 @@ interface Caption {
}
export interface VideoPlayerProps extends VariantProps<typeof variants> {
src: string
sources: {
src: string
type: string
}[]
className?: string
captions?: Caption[]
focalPoint?: FocalPoint
@@ -26,7 +35,7 @@ export interface VideoPlayerProps extends VariantProps<typeof variants> {
}
export function VideoPlayer({
src,
sources,
captions,
focalPoint = { x: 50, y: 50 },
className,
@@ -36,29 +45,51 @@ export function VideoPlayer({
}: VideoPlayerProps) {
const intl = useIntl()
const videoRef = useRef<HTMLVideoElement>(null)
const [isActivated, setIsActivated] = useState(
const [isInteractedWith, setIsInteractedWith] = useState(false)
const [isPlaying, setIsPlaying] = useState(
(variant === 'hero' && (autoPlay ?? true)) || !!autoPlay
)
const [isPlaying, setIsPlaying] = useState(autoPlay ?? false)
const [isMuted, setIsMuted] = useState(true)
const defaultProps = getVideoPropsByVariant(variant, isActivated, autoPlay)
const [userPaused, setUserPaused] = useState(false)
const defaultProps = getVideoPropsByVariant(
variant,
isInteractedWith,
autoPlay
)
const classNames = variants({
className,
variant,
})
const showPlayButton =
variant === 'hero' || (variant === 'inline' && !isInteractedWith)
const showMuteButton = variant === 'inline' && isInteractedWith
const handleIntersection = useCallback(
(entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.intersectionRatio >= 0.1 && !userPaused) {
videoRef.current?.play()
} else if (entry.intersectionRatio < 0.1) {
videoRef.current?.pause()
}
})
},
[userPaused]
)
function togglePlay() {
const videoElement = videoRef.current
if (videoElement) {
if (variant === 'hero') {
if (videoElement.paused) {
setUserPaused(false)
videoElement.play()
} else {
setUserPaused(true)
videoElement.pause()
}
} else {
setIsActivated(true)
setIsInteractedWith(true)
videoElement.play()
}
}
@@ -81,22 +112,41 @@ export function VideoPlayer({
}
}
const showPlayButton =
variant === 'hero' || (variant === 'inline' && !isActivated)
const showMuteButton = variant === 'inline' && isActivated
useEffect(() => {
const videoElement = videoRef.current
if (!videoElement || variant !== 'hero') {
return
}
const observer = new IntersectionObserver(handleIntersection, {
// Play video when at least 10% of it is visible
threshold: [0, 0.1, 1],
})
observer.observe(videoElement)
return () => {
observer.disconnect()
}
}, [variant, handleIntersection])
if (!sources.length) {
return null
}
// Sort sources to prioritize WebM format for better compression
const sortedSources = [...sources].sort((a, b) => {
const aIsWebM = a.type.includes('webm')
const bIsWebM = b.type.includes('webm')
return aIsWebM === bIsWebM ? 0 : aIsWebM ? -1 : 1
})
return (
<div
className={cx(
classNames,
{ [styles.isActivated]: isActivated },
{ [styles.hasOverlay]: hasOverlay }
)}
>
<div className={cx(classNames, { [styles.hasOverlay]: hasOverlay })}>
<video
ref={videoRef}
className={styles.video}
src={src}
style={
focalPoint
? { objectPosition: `${focalPoint.x}% ${focalPoint.y}%` }
@@ -107,6 +157,9 @@ export function VideoPlayer({
onVolumeChange={handleVolumeChangeEvent}
{...defaultProps}
>
{sortedSources.map(({ src, type }) => (
<source key={src} src={src} type={type} />
))}
{captions?.length
? captions.map(({ src, srcLang, isDefault }) => (
<track
@@ -162,7 +215,7 @@ export function VideoPlayer({
function getVideoPropsByVariant(
variant: VideoPlayerProps['variant'],
isActive: boolean,
isInteractedWith: boolean,
autoPlay?: boolean
): VideoHTMLAttributes<HTMLVideoElement> {
switch (variant) {
@@ -173,15 +226,17 @@ function getVideoPropsByVariant(
autoPlay: autoPlay ?? true,
muted: true,
loop: true,
playsInline: true,
}
case 'inline':
default:
return {
controls: isActive,
controls: isInteractedWith,
controlsList: 'nodownload noremoteplayback',
autoPlay: autoPlay ?? isActive,
autoPlay: autoPlay ?? isInteractedWith,
muted: true,
loop: false,
playsInline: true,
}
}
}