* 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
243 lines
6.3 KiB
TypeScript
243 lines
6.3 KiB
TypeScript
'use client'
|
|
|
|
import { cx, VariantProps } from 'class-variance-authority'
|
|
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'
|
|
import { useIntl } from 'react-intl'
|
|
import { VideoPlayerButton } from './Button'
|
|
import { variants } from './variants'
|
|
import styles from './videoPlayer.module.css'
|
|
|
|
interface Caption {
|
|
src: string
|
|
srcLang: Lang
|
|
isDefault: boolean
|
|
}
|
|
|
|
export interface VideoPlayerProps extends VariantProps<typeof variants> {
|
|
sources: {
|
|
src: string
|
|
type: string
|
|
}[]
|
|
className?: string
|
|
captions?: Caption[]
|
|
focalPoint?: FocalPoint
|
|
autoPlay?: boolean
|
|
hasOverlay?: boolean
|
|
}
|
|
|
|
export function VideoPlayer({
|
|
sources,
|
|
captions,
|
|
focalPoint = { x: 50, y: 50 },
|
|
className,
|
|
variant = 'inline',
|
|
autoPlay,
|
|
hasOverlay,
|
|
}: VideoPlayerProps) {
|
|
const intl = useIntl()
|
|
const videoRef = useRef<HTMLVideoElement>(null)
|
|
const [isInteractedWith, setIsInteractedWith] = useState(false)
|
|
const [isPlaying, setIsPlaying] = useState(
|
|
(variant === 'hero' && (autoPlay ?? true)) || !!autoPlay
|
|
)
|
|
const [isMuted, setIsMuted] = useState(true)
|
|
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 {
|
|
setIsInteractedWith(true)
|
|
videoElement.play()
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleMuteToggle() {
|
|
const videoElement = videoRef.current
|
|
if (videoElement) {
|
|
const currentlyMuted = videoElement.muted
|
|
videoElement.muted = !currentlyMuted
|
|
setIsMuted(!currentlyMuted)
|
|
}
|
|
}
|
|
|
|
function handleVolumeChangeEvent(event: React.UIEvent<HTMLVideoElement>) {
|
|
if (event.currentTarget.muted && !isMuted) {
|
|
setIsMuted(true)
|
|
} else if (!event.currentTarget.muted && isMuted) {
|
|
setIsMuted(false)
|
|
}
|
|
}
|
|
|
|
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.hasOverlay]: hasOverlay })}>
|
|
<video
|
|
ref={videoRef}
|
|
className={styles.video}
|
|
style={
|
|
focalPoint
|
|
? { objectPosition: `${focalPoint.x}% ${focalPoint.y}%` }
|
|
: undefined
|
|
}
|
|
onPlay={() => setIsPlaying(true)}
|
|
onPause={() => setIsPlaying(false)}
|
|
onVolumeChange={handleVolumeChangeEvent}
|
|
{...defaultProps}
|
|
>
|
|
{sortedSources.map(({ src, type }) => (
|
|
<source key={src} src={src} type={type} />
|
|
))}
|
|
{captions?.length
|
|
? captions.map(({ src, srcLang, isDefault }) => (
|
|
<track
|
|
key={src}
|
|
src={src}
|
|
kind="captions"
|
|
srcLang={srcLang}
|
|
label={languages[srcLang] || srcLang}
|
|
default={isDefault}
|
|
/>
|
|
))
|
|
: null}
|
|
</video>
|
|
{showPlayButton ? (
|
|
<VideoPlayerButton
|
|
className={styles.playButton}
|
|
onPress={togglePlay}
|
|
iconName={isPlaying ? 'pause' : 'play_arrow'}
|
|
ariaLabel={
|
|
isPlaying
|
|
? intl.formatMessage({
|
|
id: 'videoPlayer.pause',
|
|
defaultMessage: 'Pause video',
|
|
})
|
|
: intl.formatMessage({
|
|
id: 'videoPlayer.play',
|
|
defaultMessage: 'Play video',
|
|
})
|
|
}
|
|
/>
|
|
) : null}
|
|
{showMuteButton ? (
|
|
<VideoPlayerButton
|
|
className={styles.muteButton}
|
|
onPress={handleMuteToggle}
|
|
iconName={isMuted ? 'volume_off' : 'volume_up'}
|
|
ariaLabel={
|
|
isMuted
|
|
? intl.formatMessage({
|
|
id: 'videoPlayer.mute',
|
|
defaultMessage: 'Mute video',
|
|
})
|
|
: intl.formatMessage({
|
|
id: 'videoPlayer.unmute',
|
|
defaultMessage: 'Unmute video',
|
|
})
|
|
}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function getVideoPropsByVariant(
|
|
variant: VideoPlayerProps['variant'],
|
|
isInteractedWith: boolean,
|
|
autoPlay?: boolean
|
|
): VideoHTMLAttributes<HTMLVideoElement> {
|
|
switch (variant) {
|
|
case 'hero':
|
|
return {
|
|
controls: false,
|
|
controlsList: 'nodownload nofullscreen noremoteplayback',
|
|
autoPlay: autoPlay ?? true,
|
|
muted: true,
|
|
loop: true,
|
|
playsInline: true,
|
|
}
|
|
case 'inline':
|
|
default:
|
|
return {
|
|
controls: isInteractedWith,
|
|
controlsList: 'nodownload noremoteplayback',
|
|
autoPlay: autoPlay ?? isInteractedWith,
|
|
muted: true,
|
|
loop: false,
|
|
playsInline: true,
|
|
}
|
|
}
|
|
}
|