'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 { 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(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) { 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 (
{showPlayButton ? ( ) : null} {showMuteButton ? ( ) : null}
) } function getVideoPropsByVariant( variant: VideoPlayerProps['variant'], isInteractedWith: boolean, autoPlay?: boolean ): VideoHTMLAttributes { 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, } } }