From a6c7ea569528d00c00f20ac34496763c24e2033f Mon Sep 17 00:00:00 2001 From: Dominik Lander Date: Thu, 23 Apr 2026 11:49:01 +0100 Subject: [PATCH] Refactor aspect ratio on MainMedia to contain string representation for image --- dotcom-rendering/fixtures/manual/trails.ts | 25 ++++++++++--- .../src/components/Card/Card.stories.tsx | 2 +- .../src/components/SelfHostedVideo.island.tsx | 25 +++++++------ .../src/frontend/schemas/feArticle.json | 14 +++++++- .../src/frontend/schemas/feFront.json | 14 +++++++- .../src/frontend/schemas/feTagPage.json | 14 +++++++- dotcom-rendering/src/lib/video.test.ts | 20 ++++++++--- dotcom-rendering/src/lib/video.ts | 22 +++++++++--- .../src/model/enhanceCards.test.ts | 35 +++++++++++++++---- dotcom-rendering/src/types/mainMedia.ts | 7 +++- 10 files changed, 142 insertions(+), 36 deletions(-) diff --git a/dotcom-rendering/fixtures/manual/trails.ts b/dotcom-rendering/fixtures/manual/trails.ts index a453a31ff3e..d777c1e2330 100644 --- a/dotcom-rendering/fixtures/manual/trails.ts +++ b/dotcom-rendering/fixtures/manual/trails.ts @@ -706,7 +706,10 @@ export const selfHostedLoopVideo54Card = { height: 400, }, ], - aspectRatio: 5 / 4, + aspectRatio: { + numberRepresentation: 5 / 4, + stringRepresentation: '5:4', + }, duration: 30, image: 'https://media.guim.co.uk/6537e163c9164d25ec6102641f6a04fa5ba76560/0_210_5472_3283/master/5472.jpg', }, @@ -730,7 +733,10 @@ export const selfHostedLoopVideo45Card = { height: 720, }, ], - aspectRatio: 4 / 5, + aspectRatio: { + numberRepresentation: 4 / 5, + stringRepresentation: '4:5', + }, }, } satisfies DCRFrontCard; @@ -747,7 +753,10 @@ export const selfHostedLoopVideo53Card = { height: 720, }, ], - aspectRatio: 5 / 3, + aspectRatio: { + numberRepresentation: 5 / 3, + stringRepresentation: '5:3', + }, }, } satisfies DCRFrontCard; @@ -764,7 +773,10 @@ export const selfHostedLoopVideo916Card = { height: 720, }, ], - aspectRatio: 9 / 16, + aspectRatio: { + numberRepresentation: 9 / 16, + stringRepresentation: '9:16', + }, }, } satisfies DCRFrontCard; @@ -781,7 +793,10 @@ export const selfHostedLoopVideo169Card = { height: 720, }, ], - aspectRatio: 16 / 9, + aspectRatio: { + numberRepresentation: 16 / 9, + stringRepresentation: '16:9', + }, }, } satisfies DCRFrontCard; diff --git a/dotcom-rendering/src/components/Card/Card.stories.tsx b/dotcom-rendering/src/components/Card/Card.stories.tsx index b6658c85f2c..00cab2ed51e 100644 --- a/dotcom-rendering/src/components/Card/Card.stories.tsx +++ b/dotcom-rendering/src/components/Card/Card.stories.tsx @@ -84,7 +84,7 @@ const mainSelfHostedVideo: MainMedia = { height: 1080, }, ], - aspectRatio: 16 / 9, + aspectRatio: { numberRepresentation: 16 / 9, stringRepresentation: '16:9' }, image: `https://i.guim.co.uk/img/media/2eb01d138eb8fba6e59ce7589a60e3ff984f6a7a/0_0_1920_1080/1920.jpg?width=1200&quality=45&dpr=2&s=none`, duration: 100, }; diff --git a/dotcom-rendering/src/components/SelfHostedVideo.island.tsx b/dotcom-rendering/src/components/SelfHostedVideo.island.tsx index c89b626dc25..31296acc974 100644 --- a/dotcom-rendering/src/components/SelfHostedVideo.island.tsx +++ b/dotcom-rendering/src/components/SelfHostedVideo.island.tsx @@ -25,7 +25,7 @@ import { } from '../lib/video'; import { palette } from '../palette'; import type { RoleType } from '../types/content'; -import type { VideoPlayerFormat } from '../types/mainMedia'; +import type { AspectRatio, VideoPlayerFormat } from '../types/mainMedia'; import type { RenderingTarget } from '../types/renderingTarget'; import { Caption } from './Caption'; import { CardPicture, type Props as CardPictureProps } from './CardPicture'; @@ -185,7 +185,10 @@ const dispatchOphanAttentionEvent = ( document.dispatchEvent(event); }; -const getOptimisedPosterImage = (mainImage: string): string => { +const getOptimisedPosterImage = ( + mainImage: string, + aspectRatio: string, +): string => { // This only runs on the client const resolution = window.devicePixelRatio >= 2 ? 'high' : 'low'; @@ -193,7 +196,7 @@ const getOptimisedPosterImage = (mainImage: string): string => { mainImage, imageWidth: 940, // The widest a video can be: flexible special container, giga-boosted slot resolution, - aspectRatio: '5:4', + aspectRatio, }); }; @@ -284,7 +287,7 @@ type Props = { atomId: string; uniqueId: string; videoStyle: VideoPlayerFormat; - aspectRatio: number; + aspectRatio: AspectRatio; posterImage: string; fallbackImage: CardPictureProps['mainImage']; fallbackImageSize: CardPictureProps['imageSize']; @@ -880,13 +883,15 @@ export const SelfHostedVideo = ({ /** The aspect ratio of the video will be clamped within the specified range */ const aspectRatioOfVisibleVideo = getAspectRatioOfVisibleVideo( - aspectRatio, + aspectRatio.numberRepresentation, minAspectRatio, maxAspectRatio, ); - const isVideoCroppedAtTopBottom = aspectRatio < aspectRatioOfVisibleVideo; - const isVideoCroppedAtLeftRight = aspectRatio > aspectRatioOfVisibleVideo; + const isVideoCroppedAtTopBottom = + aspectRatio.numberRepresentation < aspectRatioOfVisibleVideo; + const isVideoCroppedAtLeftRight = + aspectRatio.numberRepresentation > aspectRatioOfVisibleVideo; const isGreyBarsAtSidesOnDesktop = containerAspectRatioDesktop !== undefined && @@ -899,7 +904,7 @@ export const SelfHostedVideo = ({ const AudioIcon = isMuted ? SvgAudioMute : SvgAudio; const optimisedPosterImage = showPosterImage - ? getOptimisedPosterImage(posterImage) + ? getOptimisedPosterImage(posterImage, aspectRatio.stringRepresentation) : undefined; return ( @@ -922,7 +927,7 @@ export const SelfHostedVideo = ({ >
{ width: 480, aspectRatio: '5:3', }; - expect(getAspectRatioFromSources([testSource])).toEqual(5 / 3); + expect(getAspectRatioFromSources([testSource])).toEqual({ + numberRepresentation: 5 / 3, + stringRepresentation: '5:3', + }); }); it('should calculate the aspect ratio from the width and height if aspect ratio is missing', () => { @@ -179,7 +182,10 @@ describe('video', () => { width: 480, aspectRatio: undefined, }; - expect(getAspectRatioFromSources([testSource])).toEqual(2 / 3); + expect(getAspectRatioFromSources([testSource])).toEqual({ + numberRepresentation: 2 / 3, + stringRepresentation: '480:720', + }); }); it('should return the default aspect ratio if the aspect ratio is undefined and width is 0', () => { @@ -189,7 +195,10 @@ describe('video', () => { width: 0, aspectRatio: undefined, }; - expect(getAspectRatioFromSources([testSource])).toEqual(5 / 4); + expect(getAspectRatioFromSources([testSource])).toEqual({ + numberRepresentation: 5 / 4, + stringRepresentation: '5:4', + }); }); it('should return the default aspect ratio if the aspect ratio is undefined and height is 0', () => { @@ -199,7 +208,10 @@ describe('video', () => { width: 480, aspectRatio: undefined, }; - expect(getAspectRatioFromSources([testSource])).toEqual(5 / 4); + expect(getAspectRatioFromSources([testSource])).toEqual({ + numberRepresentation: 5 / 4, + stringRepresentation: '5:4', + }); }); }); diff --git a/dotcom-rendering/src/lib/video.ts b/dotcom-rendering/src/lib/video.ts index 7db70f47599..407b74c10c7 100644 --- a/dotcom-rendering/src/lib/video.ts +++ b/dotcom-rendering/src/lib/video.ts @@ -4,7 +4,8 @@ import type { VideoAssets } from '../types/content'; export type CustomPlayEventDetail = { uniqueId: string }; /** We expect all videos to include dimensions since the field was added to FEMediaAsset */ -export const DEFAULT_ASPECT_RATIO = 5 / 4; +const DEFAULT_ASPECT_RATIO_NUMBER = 5 / 4; +const DEFAULT_ASPECT_RATIO_STRING = '5:4'; export const customSelfHostedVideoPlayAudioEventName = 'self-hosted-video:play-with-audio'; @@ -77,7 +78,9 @@ export const convertFEMediaAssetsToVideoAssets = ( * We use the first source to calculate aspect ratio, but we could use any of the sources. * We make an assumption that all sources will have the same aspect ratio. */ -export const getAspectRatioFromSources = (sources: Source[]): number => { +export const getAspectRatioFromSources = ( + sources: Source[], +): { numberRepresentation: number; stringRepresentation: string } => { const firstSource = sources[0]; if (firstSource?.aspectRatio !== undefined) { @@ -88,15 +91,24 @@ export const getAspectRatioFromSources = (sources: Source[]): number => { width > 0 && height > 0 ) { - return width / height; + return { + numberRepresentation: width / height, + stringRepresentation: `${width}:${height}`, + }; } } if (!firstSource || firstSource.width === 0 || firstSource.height === 0) { - return DEFAULT_ASPECT_RATIO; + return { + numberRepresentation: DEFAULT_ASPECT_RATIO_NUMBER, + stringRepresentation: DEFAULT_ASPECT_RATIO_STRING, + }; } - return firstSource.width / firstSource.height; + return { + numberRepresentation: firstSource.width / firstSource.height, + stringRepresentation: `${firstSource.width}:${firstSource.height}`, + }; }; export const getSubtitleAsset = (assets: VideoAssets[]): string | undefined => diff --git a/dotcom-rendering/src/model/enhanceCards.test.ts b/dotcom-rendering/src/model/enhanceCards.test.ts index a0ecf278b89..8f75fb99b4d 100644 --- a/dotcom-rendering/src/model/enhanceCards.test.ts +++ b/dotcom-rendering/src/model/enhanceCards.test.ts @@ -87,7 +87,10 @@ describe('Enhance Cards', () => { ).toEqual({ atomId: 'atomID', duration: 15, - aspectRatio: 5 / 4, + aspectRatio: { + numberRepresentation: 5 / 4, + stringRepresentation: '500:400', + }, image: '', type: 'SelfHostedVideo', videoStyle: 'Loop', @@ -178,7 +181,10 @@ describe('Enhance Cards', () => { ).toEqual({ atomId: 'atomID', duration: 15, - aspectRatio: 5 / 4, + aspectRatio: { + numberRepresentation: 5 / 4, + stringRepresentation: '500:400', + }, image: '', type: 'SelfHostedVideo', videoStyle: 'Loop', @@ -225,7 +231,10 @@ describe('Enhance Cards', () => { ).toEqual({ atomId: 'atomID', duration: 15, - aspectRatio: 5 / 4, + aspectRatio: { + numberRepresentation: 5 / 4, + stringRepresentation: '500:400', + }, image: '', type: 'SelfHostedVideo', videoStyle: 'Loop', @@ -276,7 +285,10 @@ describe('Enhance Cards', () => { videoStyle: 'Loop', atomId: 'atomID', sources: [], - aspectRatio: 5 / 4, + aspectRatio: { + numberRepresentation: 5 / 4, + stringRepresentation: '500:400', + }, duration: 151, }; @@ -425,7 +437,10 @@ describe('Enhance Cards', () => { type: 'SelfHostedVideo', atomId: 'atomID', duration: 15, - aspectRatio: 5 / 4, + aspectRatio: { + numberRepresentation: 5 / 4, + stringRepresentation: '500:400', + }, image: 'https://guim-example.co.uk/video-image', sources: [ { @@ -465,7 +480,10 @@ describe('Enhance Cards', () => { ).toEqual({ atomId: 'atomID', duration: 15, - aspectRatio: 5 / 4, + aspectRatio: { + numberRepresentation: 5 / 4, + stringRepresentation: '500:400', + }, sources: [ { mimeType: 'video/mp4', @@ -490,7 +508,10 @@ describe('Enhance Cards', () => { type: 'SelfHostedVideo', atomId: 'atomID', duration: 15, - aspectRatio: 5 / 4, + aspectRatio: { + numberRepresentation: 5 / 4, + stringRepresentation: '500:400', + }, image: undefined, sources: [ { diff --git a/dotcom-rendering/src/types/mainMedia.ts b/dotcom-rendering/src/types/mainMedia.ts index e586200c7fb..77e14d5b74d 100644 --- a/dotcom-rendering/src/types/mainMedia.ts +++ b/dotcom-rendering/src/types/mainMedia.ts @@ -7,6 +7,11 @@ type Media = { type: 'YoutubeVideo' | 'SelfHostedVideo' | 'Audio' | 'Gallery'; }; +export type AspectRatio = { + numberRepresentation: number; + stringRepresentation: string; +}; + /** For displaying embedded, playable videos directly in cards */ export type YoutubeVideo = Media & { type: 'YoutubeVideo'; @@ -28,7 +33,7 @@ type SelfHostedVideo = Media & { videoStyle: VideoPlayerFormat; atomId: string; sources: Source[]; - aspectRatio: number; + aspectRatio: AspectRatio; duration: number; subtitleSource?: string; image?: string;