From bf7a2ac2fe1dc19cc66dc8038ba7fdeb42545de3 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 16 Dec 2025 09:09:17 +0000 Subject: [PATCH] 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 --- .../CollectionPage/collectionPage.module.css | 5 + .../ContentType/CollectionPage/index.tsx | 2 +- .../ContentType/ContentPage/index.tsx | 2 +- .../VideoPlayer/VideoPlayer.stories.tsx | 20 +++- .../VideoPlayer/VideoWithCard/VideoCard.tsx | 82 --------------- .../VideoWithCard/VideoWithCard.stories.tsx | 15 ++- .../VideoPlayer/VideoWithCard/index.tsx | 2 +- .../lib/components/VideoPlayer/index.tsx | 99 ++++++++++++++----- .../lib/graphql/Fragments/Video.graphql.ts | 1 + .../lib/routers/contentstack/schemas/video.ts | 11 ++- 10 files changed, 121 insertions(+), 118 deletions(-) delete mode 100644 packages/design-system/lib/components/VideoPlayer/VideoWithCard/VideoCard.tsx diff --git a/apps/scandic-web/components/ContentType/CollectionPage/collectionPage.module.css b/apps/scandic-web/components/ContentType/CollectionPage/collectionPage.module.css index 2a9ae8c87..67fa8e683 100644 --- a/apps/scandic-web/components/ContentType/CollectionPage/collectionPage.module.css +++ b/apps/scandic-web/components/ContentType/CollectionPage/collectionPage.module.css @@ -22,6 +22,11 @@ height: 100%; align-content: end; padding-bottom: var(--Space-x15); + pointer-events: none; + + & > * { + pointer-events: auto; + } } } diff --git a/apps/scandic-web/components/ContentType/CollectionPage/index.tsx b/apps/scandic-web/components/ContentType/CollectionPage/index.tsx index c57e62352..7dd29a723 100644 --- a/apps/scandic-web/components/ContentType/CollectionPage/index.tsx +++ b/apps/scandic-web/components/ContentType/CollectionPage/index.tsx @@ -50,7 +50,7 @@ export async function CollectionPage() {
diff --git a/packages/design-system/lib/components/VideoPlayer/VideoPlayer.stories.tsx b/packages/design-system/lib/components/VideoPlayer/VideoPlayer.stories.tsx index 5a42773e8..34c39247e 100644 --- a/packages/design-system/lib/components/VideoPlayer/VideoPlayer.stories.tsx +++ b/packages/design-system/lib/components/VideoPlayer/VideoPlayer.stories.tsx @@ -5,7 +5,7 @@ import { VideoPlayer } from '.' import { config as videoPlayerConfig } from './variants' const meta: Meta = { - title: 'Core Components/🚧 Video 🚧/VideoPlayer', + title: 'Core Components/Video/VideoPlayer', component: VideoPlayer, parameters: { @@ -23,11 +23,12 @@ const meta: Meta = { disable: true, }, }, - src: { + sources: { table: { - type: { summary: 'string' }, + type: { summary: '{src: string; type: string}[]' }, }, - description: 'The source URL of the video.', + description: + 'The different sources of the video, including their formats.', }, captions: { table: { @@ -77,7 +78,16 @@ export default meta type Story = StoryObj const defaultArgs = { - src: 'https://eu-assets.contentstack.com/v3/assets/bltfd73aa2de3a5c4e3/bltad0fe3c2ce340947/68eced6c14e5a8150ebba18c/Scandic_EB_Master.mp4', + sources: [ + { + src: 'https://eu-assets.contentstack.com/v3/assets/bltfd73aa2de3a5c4e3/bltc3aa53ac9bf6798c/693ad4b65b0889d6348893f3/Test_video.mp4', + type: 'video/mp4', + }, + { + src: 'https://eu-assets.contentstack.com/v3/assets/bltfd73aa2de3a5c4e3/blt029be07ddd444eea/693c251c09e17b33c93c1dd6/hero-banner-1920-vp9.webm', + type: 'video/webm', + }, + ], captions: [ { src: './video/captions_en.vtt', diff --git a/packages/design-system/lib/components/VideoPlayer/VideoWithCard/VideoCard.tsx b/packages/design-system/lib/components/VideoPlayer/VideoWithCard/VideoCard.tsx deleted file mode 100644 index 64a1d86c6..000000000 --- a/packages/design-system/lib/components/VideoPlayer/VideoWithCard/VideoCard.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { VariantProps } from 'class-variance-authority' -import { Typography } from '../../Typography' -import { variants } from './variants' - -import { VideoPlayer, VideoPlayerProps } from '..' -import styles from './videoWithCard.module.css' - -interface TextCardProps { - variant: 'text' - heading: string - text?: string -} -interface QuoteCardProps { - variant: 'quote' - quote: string - author: string - authorDescription?: string -} - -type VideoWithCardProps = VariantProps & - (TextCardProps | QuoteCardProps) & { - video: Pick - } - -export function VideoWithCard(props: VideoWithCardProps) { - const { variant, style, video } = props - const classNames = variants({ - variant, - style, - }) - - return ( -
-
- -
- -
-
-
- ) -} - -function CardContent(props: VideoWithCardProps) { - if (props.variant === 'quote') { - const { quote, author, authorDescription } = props - - return ( - <> - -
{quote}
-
- - - - {author} - - {authorDescription ? ( - - {authorDescription} - - ) : null} - - - ) - } - - const { heading, text } = props - - return ( - <> - -

{heading}

-
- {text ? ( - -

{text}

-
- ) : null} - - ) -} diff --git a/packages/design-system/lib/components/VideoPlayer/VideoWithCard/VideoWithCard.stories.tsx b/packages/design-system/lib/components/VideoPlayer/VideoWithCard/VideoWithCard.stories.tsx index 4fd9fbf16..370362315 100644 --- a/packages/design-system/lib/components/VideoPlayer/VideoWithCard/VideoWithCard.stories.tsx +++ b/packages/design-system/lib/components/VideoPlayer/VideoWithCard/VideoWithCard.stories.tsx @@ -4,7 +4,7 @@ import { VideoWithCard } from '.' import { config } from './variants' const meta: Meta = { - title: 'Core Components/🚧 Video 🚧/VideoWithCard', + title: 'Core Components/Video/VideoWithCard', component: VideoWithCard, parameters: { docs: { @@ -80,7 +80,7 @@ const meta: Meta = { table: { type: { summary: - '{ src: string; captions?: Caption[]; focalPoint?: FocalPoint}', + '{ sources: { src: string; type: string }[]; captions?: Caption[]; focalPoint?: FocalPoint}', }, }, description: @@ -94,7 +94,16 @@ export default meta type Story = StoryObj const videoProps = { - src: 'https://eu-assets.contentstack.com/v3/assets/bltfd73aa2de3a5c4e3/bltad0fe3c2ce340947/68eced6c14e5a8150ebba18c/Scandic_EB_Master.mp4', + sources: [ + { + src: 'https://eu-assets.contentstack.com/v3/assets/bltfd73aa2de3a5c4e3/bltc3aa53ac9bf6798c/693ad4b65b0889d6348893f3/Test_video.mp4', + type: 'video/mp4', + }, + { + src: 'https://eu-assets.contentstack.com/v3/assets/bltfd73aa2de3a5c4e3/blt029be07ddd444eea/693c251c09e17b33c93c1dd6/hero-banner-1920-vp9.webm', + type: 'video/webm', + }, + ], } const quoteCardProps = { diff --git a/packages/design-system/lib/components/VideoPlayer/VideoWithCard/index.tsx b/packages/design-system/lib/components/VideoPlayer/VideoWithCard/index.tsx index c2a9d58a2..fd20261a2 100644 --- a/packages/design-system/lib/components/VideoPlayer/VideoWithCard/index.tsx +++ b/packages/design-system/lib/components/VideoPlayer/VideoWithCard/index.tsx @@ -18,7 +18,7 @@ interface QuoteCardProps { type VideoWithCardProps = VariantProps & (TextCardProps | QuoteCardProps) & { - video: Pick + video: Pick } export function VideoWithCard(props: VideoWithCardProps) { diff --git a/packages/design-system/lib/components/VideoPlayer/index.tsx b/packages/design-system/lib/components/VideoPlayer/index.tsx index ee321472f..40d251950 100644 --- a/packages/design-system/lib/components/VideoPlayer/index.tsx +++ b/packages/design-system/lib/components/VideoPlayer/index.tsx @@ -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 { - src: string + sources: { + src: string + type: string + }[] className?: string captions?: Caption[] focalPoint?: FocalPoint @@ -26,7 +35,7 @@ export interface VideoPlayerProps extends VariantProps { } 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(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 ( -
+