From 9bb6f54ce1f9bfcd97cd10abb8403a02ddf3a83d Mon Sep 17 00:00:00 2001 From: Dominik Lander Date: Tue, 21 Apr 2026 13:22:30 +0100 Subject: [PATCH] minor self-hosted video refactor --- .../src/components/SelfHostedVideo.island.tsx | 57 ++++++++++++------- .../components/SelfHostedVideo.stories.tsx | 1 - .../src/components/SelfHostedVideoPlayer.tsx | 43 +++++++------- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/dotcom-rendering/src/components/SelfHostedVideo.island.tsx b/dotcom-rendering/src/components/SelfHostedVideo.island.tsx index bc614be35d8..29ad22d545d 100644 --- a/dotcom-rendering/src/components/SelfHostedVideo.island.tsx +++ b/dotcom-rendering/src/components/SelfHostedVideo.island.tsx @@ -377,7 +377,10 @@ export const SelfHostedVideo = ({ const shouldLoop = isLoop || isCinemagraph; - const showProgressBar = !hideProgressBar && !isCinemagraph; + const showProgressBar = + !hideProgressBar && !isCinemagraph && playerState !== 'NOT_STARTED'; + + const showIcons = !isCinemagraph && playerState !== 'NOT_STARTED'; const iconSize = isDefault ? 'large' : 'small'; @@ -454,6 +457,13 @@ export const SelfHostedVideo = ({ } }; + const updateCurrentTime = (newTime: number) => { + if (vidRef.current) { + vidRef.current.currentTime = newTime; + setCurrentTime(newTime); + } + }; + const FallbackImageComponent = ( { void submitClickComponentEvent(event.currentTarget, renderingTarget); event.stopPropagation(); // Don't pause the video - const video = vidRef.current; + const video = vidRef.current; if (!video) return; if (shouldUseWebkitFullscreen(video)) { @@ -772,27 +783,31 @@ export const SelfHostedVideo = ({ }; const seekForward = () => { - if (vidRef.current) { - const newTime = Math.min( - vidRef.current.currentTime + 1, - vidRef.current.duration, - ); + const video = vidRef.current; + if (!video) return; - vidRef.current.currentTime = newTime; - setCurrentTime(newTime); - } + const increment = 1; + const newTime = Math.min(video.currentTime + increment, video.duration); + + updateCurrentTime(newTime); }; const seekBackward = () => { - if (vidRef.current) { - // Allow the user to cycle to the end of the video using the arrow keys - const newTime = - (((vidRef.current.currentTime - 1) % vidRef.current.duration) + - vidRef.current.duration) % - vidRef.current.duration; + const video = vidRef.current; + if (!video) return; - vidRef.current.currentTime = newTime; - setCurrentTime(newTime); + const increment = 1; + const newTime = Math.max(video.currentTime - increment, 0); + + updateCurrentTime(newTime); + }; + + const handleTimeUpdate = () => { + const video = vidRef.current; + if (!video) return; + + if (playerState === 'PLAYING') { + setCurrentTime(video.currentTime); } }; @@ -920,10 +935,7 @@ export const SelfHostedVideo = ({ posterImage={optimisedPosterImage} FallbackImageComponent={FallbackImageComponent} currentTime={currentTime} - setCurrentTime={setCurrentTime} ref={vidRef} - isPlayable={isPlayable} - playerState={playerState} isMuted={isMuted} handleLoadedMetadata={handleLoadedMetadata} handleLoadedData={handleLoadedData} @@ -931,6 +943,7 @@ export const SelfHostedVideo = ({ handlePlaying={handlePlaying} handlePlayPauseClick={handlePlayPauseClick} handleAudioClick={handleAudioClick} + handleTimeUpdate={handleTimeUpdate} handleKeyDown={handleKeyDown} handlePause={handlePause} handleFullscreenClick={handleFullscreenClick} @@ -943,7 +956,7 @@ export const SelfHostedVideo = ({ showSubtitles={!isCinemagraph} subtitleSource={subtitleSource} subtitleSize={subtitleSize} - showIcons={!isCinemagraph} + showIcons={showIcons} controlsPosition={controlsPosition} activeCue={activeCue} shouldLoop={shouldLoop} diff --git a/dotcom-rendering/src/components/SelfHostedVideo.stories.tsx b/dotcom-rendering/src/components/SelfHostedVideo.stories.tsx index 7e342e51ed2..45deca27b9d 100644 --- a/dotcom-rendering/src/components/SelfHostedVideo.stories.tsx +++ b/dotcom-rendering/src/components/SelfHostedVideo.stories.tsx @@ -48,7 +48,6 @@ export const Loop: Story = { sources: loop54Card.mainMedia.sources, aspectRatio: loop54Card.mainMedia.aspectRatio, uniqueId: 'test-video-1', - atomId: 'test-atom-1', videoStyle: 'Loop', posterImage: 'https://media.guim.co.uk/9bdb802e6da5d3fd249b5060f367b3a817965f0c/0_0_1800_1080/master/1800.jpg', diff --git a/dotcom-rendering/src/components/SelfHostedVideoPlayer.tsx b/dotcom-rendering/src/components/SelfHostedVideoPlayer.tsx index 9fa9bd27db7..6518036938a 100644 --- a/dotcom-rendering/src/components/SelfHostedVideoPlayer.tsx +++ b/dotcom-rendering/src/components/SelfHostedVideoPlayer.tsx @@ -6,12 +6,7 @@ import { textSans20, } from '@guardian/source/foundations'; import type { IconProps } from '@guardian/source/react-components'; -import type { - Dispatch, - ReactElement, - SetStateAction, - SyntheticEvent, -} from 'react'; +import type { ReactElement, SyntheticEvent } from 'react'; import { forwardRef } from 'react'; import type { ActiveCue } from '../lib/useSubtitles'; import type { Source } from '../lib/video'; @@ -66,12 +61,15 @@ const playIconStyles = css` padding: 0; `; -const iconsContainerStyles = (position: ControlsPosition) => css` +const iconsContainerStyles = css` position: absolute; display: flex; flex-direction: column; gap: ${space[2]}px; right: ${space[2]}px; +`; + +const iconsPositionStyles = (position: ControlsPosition) => css` /* Take into account the progress bar height */ ${position === 'bottom' && `bottom: ${space[3]}px;`} ${position === 'top' && `top: ${space[2]}px;`} @@ -106,10 +104,7 @@ export type Props = { aspectRatio: number; videoStyle: VideoPlayerFormat; FallbackImageComponent: ReactElement; - isPlayable: boolean; - playerState: PlayerStates; currentTime: number; - setCurrentTime: Dispatch>; isMuted: boolean; handleLoadedMetadata: (event: SyntheticEvent) => void; handleLoadedData: (event: SyntheticEvent) => void; @@ -118,6 +113,7 @@ export type Props = { handlePlayPauseClick: (event: SyntheticEvent) => void; handleAudioClick: (event: SyntheticEvent) => void; handleKeyDown: (event: React.KeyboardEvent) => void; + handleTimeUpdate: (event: SyntheticEvent) => void; handlePause: (event: SyntheticEvent) => void; handleFullscreenClick?: (event: SyntheticEvent) => void; onError: (event: SyntheticEvent) => void; @@ -160,10 +156,7 @@ export const SelfHostedVideoPlayer = forwardRef( videoStyle, FallbackImageComponent, posterImage, - isPlayable, - playerState, currentTime, - setCurrentTime, isMuted, handleLoadedMetadata, handleLoadedData, @@ -172,6 +165,7 @@ export const SelfHostedVideoPlayer = forwardRef( handlePlayPauseClick, handleAudioClick, handleKeyDown, + handleTimeUpdate, handlePause, handleFullscreenClick, onError, @@ -198,7 +192,7 @@ export const SelfHostedVideoPlayer = forwardRef( const showSubtitles = canShowSubtitles && !!subtitleSource; const showProgressBar = canShowProgressBar && currentRefExists; - const showIcons = canShowIcons && currentRefExists && isPlayable; + const showIcons = canShowIcons && currentRefExists; const dataLinkName = `gu-video-${videoStyle}-${ showPlayIcon ? 'play' : 'pause' @@ -232,22 +226,18 @@ export const SelfHostedVideoPlayer = forwardRef( onCanPlay={handleCanPlay} onCanPlayThrough={handleCanPlay} onPlaying={handlePlaying} - onTimeUpdate={() => { - if (currentRefExists && playerState === 'PLAYING') { - setCurrentTime(ref.current!.currentTime); - } - }} + onTimeUpdate={handleTimeUpdate} onPause={handlePause} onClick={handlePlayPauseClick} onKeyDown={handleKeyDown} onError={onError} > - {sources.map((source) => ( + {sources.map(({ src, mimeType }) => ( ))} {showSubtitles && ( @@ -287,7 +277,12 @@ export const SelfHostedVideoPlayer = forwardRef( /> )} {showIcons && ( -
+
{showFullscreenIcon && (