Skip to content

Commit 7e14771

Browse files
authored
Merge pull request #15671 from guardian/doml/sh-default
Support default video style
2 parents c2a48df + da9d6c4 commit 7e14771

6 files changed

Lines changed: 170 additions & 215 deletions

File tree

dotcom-rendering/fixtures/manual/trails.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,6 @@ export const selfHostedLoopVideo53Card = {
747747
height: 720,
748748
},
749749
],
750-
751750
aspectRatio: 5 / 3,
752751
},
753752
} satisfies DCRFrontCard;

dotcom-rendering/src/components/FeatureCard.stories.tsx

Lines changed: 9 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { css } from '@emotion/react';
22
import { from } from '@guardian/source/foundations';
33
import type { Meta, StoryObj } from '@storybook/react-webpack5';
4+
import {
5+
selfHostedLoopVideo45Card,
6+
selfHostedLoopVideo53Card,
7+
} from '../../fixtures/manual/trails';
48
import {
59
ArticleDesign,
610
ArticleDisplay,
@@ -422,21 +426,7 @@ export const WithSelfHostedLoopVideo = {
422426
args: {
423427
...cardProps,
424428
showVideo: true,
425-
mainMedia: {
426-
type: 'SelfHostedVideo',
427-
videoStyle: 'Loop',
428-
atomId: 'atom-id-123',
429-
sources: [
430-
{
431-
src: 'https://uploads.guim.co.uk/2026/01/09/Front_loop__Iran_TiF_Latest--64220ebf-d63d-48dd-9317-16b3b150a4ac-1.1.m3u8',
432-
mimeType: 'application/vnd.apple.mpegurl',
433-
width: 576,
434-
height: 720,
435-
},
436-
],
437-
aspectRatio: 4 / 5,
438-
duration: 18,
439-
},
429+
mainMedia: selfHostedLoopVideo45Card.mainMedia,
440430
},
441431
} satisfies Story;
442432

@@ -469,18 +459,7 @@ export const WithSelfHostedImmersiveLoopVideo = {
469459
args: {
470460
...WithSelfHostedLoopVideo.args,
471461
...Immersive.args,
472-
mainMedia: {
473-
...WithSelfHostedLoopVideo.args.mainMedia,
474-
sources: [
475-
{
476-
src: 'https://uploads.guim.co.uk/2025/11/27/5_3_Test--26763e61-c16b-4c10-8c16-3f11882da154-1.0.mp4',
477-
mimeType: 'video/mp4',
478-
width: 1200,
479-
height: 720,
480-
},
481-
],
482-
aspectRatio: 5 / 3,
483-
},
462+
mainMedia: selfHostedLoopVideo53Card.mainMedia,
484463
},
485464
} satisfies Story;
486465

@@ -512,19 +491,7 @@ export const WithReplacementMediaOnGalleryCard = {
512491
...Gallery.args,
513492
showVideo: true,
514493
mainMedia: {
515-
type: 'SelfHostedVideo',
516-
videoStyle: 'Loop',
517-
atomId: 'atom-id-123',
518-
sources: [
519-
{
520-
src: 'https://uploads.guim.co.uk/2026/01/09/Front_loop__Iran_TiF_Latest--64220ebf-d63d-48dd-9317-16b3b150a4ac-1.1.m3u8',
521-
mimeType: 'application/vnd.apple.mpegurl',
522-
width: 576,
523-
height: 720,
524-
},
525-
],
526-
aspectRatio: 4 / 5,
527-
duration: 18,
494+
...WithSelfHostedLoopVideo.args.mainMedia,
528495
},
529496
},
530497
} satisfies Story;
@@ -534,19 +501,7 @@ export const WithReplacementMediaOnVideoCard = {
534501
...YoutubeVideo.args,
535502
showVideo: true,
536503
mainMedia: {
537-
type: 'SelfHostedVideo',
538-
videoStyle: 'Loop',
539-
atomId: 'atom-id-123',
540-
sources: [
541-
{
542-
src: 'https://uploads.guim.co.uk/2026/01/09/Front_loop__Iran_TiF_Latest--64220ebf-d63d-48dd-9317-16b3b150a4ac-1.1.m3u8',
543-
mimeType: 'application/vnd.apple.mpegurl',
544-
width: 576,
545-
height: 720,
546-
},
547-
],
548-
aspectRatio: 4 / 5,
549-
duration: 18,
504+
...WithSelfHostedLoopVideo.args.mainMedia,
550505
},
551506
},
552507
} satisfies Story;
@@ -556,19 +511,7 @@ export const WithReplacementMediaOnPodcastCard = {
556511
...Podcast.args,
557512
showVideo: true,
558513
mainMedia: {
559-
type: 'SelfHostedVideo',
560-
videoStyle: 'Loop',
561-
atomId: 'atom-id-123',
562-
sources: [
563-
{
564-
src: 'https://uploads.guim.co.uk/2026/01/09/Front_loop__Iran_TiF_Latest--64220ebf-d63d-48dd-9317-16b3b150a4ac-1.1.m3u8',
565-
mimeType: 'application/vnd.apple.mpegurl',
566-
width: 576,
567-
height: 720,
568-
},
569-
],
570-
aspectRatio: 4 / 5,
571-
duration: 18,
514+
...WithSelfHostedLoopVideo.args.mainMedia,
572515
},
573516
},
574517
} satisfies Story;

dotcom-rendering/src/components/FeatureCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ export const FeatureCard = ({
589589
aspectRatio
590590
}
591591
linkTo={linkTo}
592-
showProgressBar={false}
592+
hideProgressBar={true}
593593
subtitleSource={
594594
media.mainMedia.subtitleSource
595595
}

dotcom-rendering/src/components/SelfHostedVideo.island.tsx

Lines changed: 71 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ const dispatchOphanAttentionEvent = (
184184
};
185185

186186
const getOptimisedPosterImage = (mainImage: string): string => {
187+
// This only runs on the client
187188
const resolution = window.devicePixelRatio >= 2 ? 'high' : 'low';
188189

189190
return generateImageURL({
@@ -218,8 +219,9 @@ const shouldUseWebkitFullscreen = (video: HTMLVideoElement): boolean => {
218219
};
219220

220221
/**
221-
* Ensure the aspect ratio of the video is within the boundary, if specified.
222-
* For example, we may not want to render a square video inside a 4:5 feature card.
222+
* Ensures the aspect ratio falls between the minimum and maximum allowed aspect ratios, if specified.
223+
* For example, we may not want to render a square video inside a 4:5 feature card. In this case, the
224+
* minimum & maximum aspect ratio would be 4:5, so that the video fits the fixed-aspect ratio feature card
223225
*/
224226
const getAspectRatioOfVisibleVideo = (
225227
aspectRatio: number,
@@ -236,42 +238,6 @@ const getAspectRatioOfVisibleVideo = (
236238
return aspectRatio;
237239
};
238240

239-
type Props = {
240-
sources: Source[];
241-
atomId: string;
242-
uniqueId: string;
243-
videoStyle: VideoPlayerFormat;
244-
aspectRatio: number;
245-
posterImage: string;
246-
fallbackImage: CardPictureProps['mainImage'];
247-
fallbackImageSize: CardPictureProps['imageSize'];
248-
fallbackImageLoading: CardPictureProps['loading'];
249-
fallbackImageAlt: CardPictureProps['alt'];
250-
fallbackImageAspectRatio: CardPictureProps['aspectRatio'];
251-
linkTo: string;
252-
showProgressBar?: boolean;
253-
subtitleSource?: string;
254-
subtitleSize: SubtitleSize;
255-
/** The position of subtitles and the audio icon. Usually at the bottom, with the exception of Feature Cards. */
256-
controlsPosition?: 'top' | 'bottom';
257-
/**
258-
* The minimum/maximum aspect ratio the video will have. The video will be cropped if this
259-
* value is defined and the video aspect ratio is less/greater than this value.
260-
*/
261-
minAspectRatio?: number;
262-
maxAspectRatio?: number;
263-
/**
264-
* Specify this value to enforce the size of the video container on mobile/desktop.
265-
* Grey bars will appear if this value is defined and differs from the video aspect ratio.
266-
*/
267-
containerAspectRatioMobile?: number;
268-
containerAspectRatioDesktop?: number;
269-
caption?: string;
270-
format?: ArticleFormat;
271-
isMainMedia?: boolean;
272-
role?: RoleType;
273-
};
274-
275241
const doesUserPermitAutoplayOnWeb = (): boolean => {
276242
/**
277243
* The user indicates a preference for reduced motion: https://web.dev/articles/prefers-reduced-motion
@@ -294,7 +260,6 @@ const doesUserPermitAutoplayOnWeb = (): boolean => {
294260
const doesUserPermitAutoplayOnApps = async (): Promise<boolean> => {
295261
/* isAutoplayEnabled is available on the video client from 8.8.0 onwards */
296262
const isBridgetCompatible = await hasMinimumBridgetVersion('8.8.0');
297-
298263
if (!isBridgetCompatible) return true;
299264

300265
try {
@@ -312,6 +277,44 @@ const doesUserPermitAutoplayOnApps = async (): Promise<boolean> => {
312277
}
313278
};
314279

280+
type Props = {
281+
sources: Source[];
282+
atomId: string;
283+
uniqueId: string;
284+
videoStyle: VideoPlayerFormat;
285+
aspectRatio: number;
286+
posterImage: string;
287+
fallbackImage: CardPictureProps['mainImage'];
288+
fallbackImageSize: CardPictureProps['imageSize'];
289+
fallbackImageLoading: CardPictureProps['loading'];
290+
fallbackImageAlt: CardPictureProps['alt'];
291+
fallbackImageAspectRatio: CardPictureProps['aspectRatio'];
292+
linkTo: string;
293+
hideProgressBar?: boolean;
294+
subtitleSource?: string;
295+
subtitleSize: SubtitleSize;
296+
/**
297+
* The position of subtitles and the audio icon.
298+
*/
299+
controlsPosition?: 'top' | 'bottom';
300+
/**
301+
* The minimum/maximum aspect ratio the video will have. The video will be cropped if this
302+
* value is defined and the video aspect ratio is less/greater than this value.
303+
*/
304+
minAspectRatio?: number;
305+
maxAspectRatio?: number;
306+
/**
307+
* Specify this value to enforce the size of the video container on mobile/desktop.
308+
* Grey bars will appear if this value is defined and differs from the video aspect ratio.
309+
*/
310+
containerAspectRatioMobile?: number;
311+
containerAspectRatioDesktop?: number;
312+
caption?: string;
313+
format?: ArticleFormat;
314+
isMainMedia?: boolean;
315+
role?: RoleType;
316+
};
317+
315318
export const SelfHostedVideo = ({
316319
sources,
317320
atomId,
@@ -325,7 +328,7 @@ export const SelfHostedVideo = ({
325328
fallbackImageAlt,
326329
fallbackImageAspectRatio,
327330
linkTo,
328-
showProgressBar = true,
331+
hideProgressBar = false,
329332
subtitleSource,
330333
subtitleSize,
331334
controlsPosition = 'bottom',
@@ -361,10 +364,20 @@ export const SelfHostedVideo = ({
361364
const isApps = renderingTarget === 'Apps';
362365

363366
/**
364-
* All controls on the video are hidden: the video looks like a GIF.
367+
* In a cinemagraph, all controls are hidden: the video looks like a GIF.
365368
* This includes but may not be limited to: audio icon, play/pause icon, subtitles, progress bar.
366369
*/
367370
const isCinemagraph = videoStyle === 'Cinemagraph';
371+
const isLoop = videoStyle === 'Loop';
372+
const isDefault = videoStyle === 'Default';
373+
374+
const canAutoplay = isLoop || isCinemagraph;
375+
376+
const shouldAutoplay = canAutoplay && isAutoplayAllowed;
377+
378+
const shouldLoop = isLoop || isCinemagraph;
379+
380+
const showProgressBar = !hideProgressBar && !isCinemagraph;
368381

369382
const ophanVideoStyle = videoStyle.toLowerCase() as OphanVideoStyle;
370383

@@ -603,12 +616,12 @@ export const SelfHostedVideo = ({
603616
*/
604617
useEffect(() => {
605618
if (
606-
isAutoplayAllowed === false ||
619+
shouldAutoplay === false ||
607620
(isInView === false && playerState === 'NOT_STARTED')
608621
) {
609622
setShowPosterImage(true);
610623
}
611-
}, [isAutoplayAllowed, isInView, playerState]);
624+
}, [shouldAutoplay, isInView, playerState]);
612625

613626
if (adapted) {
614627
return FallbackImageComponent;
@@ -813,9 +826,9 @@ export const SelfHostedVideo = ({
813826
*
814827
* Stops playback when the video is scrolled out of view.
815828
*/
816-
if (vidRef.current && isPlayable) {
829+
if (isPlayable) {
817830
if (
818-
isAutoplayAllowed &&
831+
shouldAutoplay &&
819832
isInView &&
820833
(playerState === 'NOT_STARTED' ||
821834
playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' ||
@@ -831,14 +844,15 @@ export const SelfHostedVideo = ({
831844
}
832845

833846
/**
834-
* Show the play icon when the video is not playing, except for when it is scrolled
835-
* out of view. In this case, the intersection observer will resume playback and
836-
* having a play icon would falsely indicate a user action is required to resume playback.
847+
* Show the play icon when the video is not playing, except for when it is scrolled out of view,
848+
* i.e. paused by intersection observer. In this case, the intersection observer will resume playback
849+
* and having a play icon would falsely indicate a user action is required to resume playback.
837850
*/
838851
const showPlayIcon =
839-
playerState === 'PAUSED_BY_USER' ||
840-
playerState === 'PAUSED_BY_BROWSER' ||
841-
(playerState === 'NOT_STARTED' && !isAutoplayAllowed);
852+
!isCinemagraph &&
853+
(playerState === 'PAUSED_BY_USER' ||
854+
playerState === 'PAUSED_BY_BROWSER' ||
855+
(playerState === 'NOT_STARTED' && shouldAutoplay === false));
842856

843857
/** The aspect ratio of the video will be clamped within the specified range */
844858
const aspectRatioOfVisibleVideo = getAspectRatioOfVisibleVideo(
@@ -864,12 +878,6 @@ export const SelfHostedVideo = ({
864878
? getOptimisedPosterImage(posterImage)
865879
: undefined;
866880

867-
/**
868-
* We almost always want to preload some of the video data. The exception
869-
* is when autoplay is off and the video is only partially in view.
870-
*/
871-
const preloadPartialData = !!isAutoplayAllowed || !!isInView;
872-
873881
return (
874882
<figure
875883
className={`video-container ${videoStyle.toLocaleLowerCase()} ${
@@ -926,13 +934,18 @@ export const SelfHostedVideo = ({
926934
handleFullscreenClick={handleFullscreenClick}
927935
onError={onError}
928936
AudioIcon={hasAudio ? AudioIcon : null}
929-
preloadPartialData={preloadPartialData}
937+
preloadPartialData={!!shouldAutoplay}
930938
showPlayIcon={showPlayIcon}
931939
showProgressBar={showProgressBar}
940+
showSubtitles={!isCinemagraph}
932941
subtitleSource={subtitleSource}
933942
subtitleSize={subtitleSize}
943+
showIcons={!isCinemagraph}
934944
controlsPosition={controlsPosition}
935945
activeCue={activeCue}
946+
shouldLoop={shouldLoop}
947+
showFullscreenIcon={isDefault}
948+
isInteractive={!isCinemagraph}
936949
/>
937950
</div>
938951
</div>

dotcom-rendering/src/components/SelfHostedVideo.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const Default: Story = {
7474
export const WithoutProgressBar: Story = {
7575
args: {
7676
...Loop.args,
77-
showProgressBar: false,
77+
hideProgressBar: false,
7878
},
7979
} satisfies Story;
8080

0 commit comments

Comments
 (0)