From d96fdeb88003c48898262604de872049bfc17e88 Mon Sep 17 00:00:00 2001 From: Max Duval Date: Wed, 28 Jun 2023 11:31:03 +0100 Subject: [PATCH] feat(Card): Support mainVideo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The settings comes from the facia-tool via Frontend, when “Show Video” is enabled for a trail in a container, it should be displayed as a playable video inline instead or redirecting to the article itself. Currently, only Youtube Atoms are supported. There is currently no guarantee that the first atom will be the expected video, but it happens to be the case the vast majority of the time A minimum Card width of one third or three columns is required for the YouTube atom to be embedded, similar to the existing Frontend logic: https://github.com/guardian/frontend/blob/1c4555a9f/common/app/layout/cards/CardType.scala#L20-L23 --- dotcom-rendering/fixtures/manual/trails.ts | 56 +++--- dotcom-rendering/index.d.ts | 7 +- dotcom-rendering/makefile | 1 + .../src/components/Card/Card.stories.tsx | 56 +++--- dotcom-rendering/src/components/Card/Card.tsx | 62 ++++++- .../Card/components/ImageWrapper.tsx | 2 +- .../src/components/Carousel.importable.tsx | 14 +- .../src/components/DynamicPackage.stories.tsx | 2 +- dotcom-rendering/src/components/FrontCard.tsx | 4 +- .../components/SupportingContent.stories.tsx | 1 + dotcom-rendering/src/lib/cardWrappers.tsx | 8 + .../src/model/article-schema.json | 60 +++++- dotcom-rendering/src/model/enhanceCards.ts | 45 ++++- dotcom-rendering/src/model/front-schema.json | 172 +++++++++++++++++- .../src/model/tag-front-schema.json | 112 +++++++++++- dotcom-rendering/src/types/front.ts | 25 ++- dotcom-rendering/src/types/trails.ts | 3 +- dotcom-rendering/src/types/video.ts | 12 ++ 18 files changed, 566 insertions(+), 76 deletions(-) create mode 100644 dotcom-rendering/src/types/video.ts diff --git a/dotcom-rendering/fixtures/manual/trails.ts b/dotcom-rendering/fixtures/manual/trails.ts index a6df1cfe974..1768ef10f65 100644 --- a/dotcom-rendering/fixtures/manual/trails.ts +++ b/dotcom-rendering/fixtures/manual/trails.ts @@ -71,7 +71,7 @@ export const trails: [ kickerText: 'Kicker', }, ], - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -88,10 +88,24 @@ export const trails: [ display: ArticleDisplay.Standard, }, mediaType: 'Video', - mediaDuration: 378, dataLinkName: 'news | group-0 | card-@2', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: { + elementId: 'abcdef', + videoId: 'abcd', + title: 'some title', + duration: 378, + width: 480, + height: 288, + origin: 'The Guardian', + expired: false, + images: [ + { + url: 'https://i.guim.co.uk/img/media/e060e9b7c92433b3dfeccc98b9206778cda8b8e8/0_180_6680_4009/master/6680.jpg?width=600&quality=45&dpr=2&s=none', + width: 600, + }, + ], + }, isExternalLink: false, showLivePlayable: false, }, @@ -110,7 +124,7 @@ export const trails: [ kickerText: 'Live', dataLinkName: 'news | group-0 | card-@3', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -128,7 +142,7 @@ export const trails: [ }, dataLinkName: 'news | group-0 | card-@4', showQuotedHeadline: true, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -146,7 +160,7 @@ export const trails: [ }, dataLinkName: 'news | group-0 | card-@5', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -165,7 +179,7 @@ export const trails: [ }, dataLinkName: 'news | group-0 | card-@6', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -185,7 +199,7 @@ export const trails: [ }, dataLinkName: 'news | group-0 | card-@7', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -204,7 +218,7 @@ export const trails: [ }, dataLinkName: 'news | group-0 | card-@8', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -225,7 +239,7 @@ export const trails: [ 'UK Covid live: England lockdown to be eased in stages, says PM, amid reports of nationwide mass testing', dataLinkName: 'news | group-0 | card-@9', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -245,7 +259,7 @@ export const trails: [ 'UK to infect up to 90 healthy volunteers with Covid in world first trial', dataLinkName: 'news | group-0 | card-@10', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -265,7 +279,7 @@ export const trails: [ 'Scottish government inadequately prepared for Covid, says watchdog', dataLinkName: 'news | group-0 | card-@11', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -285,7 +299,7 @@ export const trails: [ '‘Encouraging’ signs for Covid vaccine as over-80s deaths fall in England', dataLinkName: 'news | group-0 | card-@12', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -305,7 +319,7 @@ export const trails: [ 'Contact tracing alone has little impact on curbing Covid spread, report finds', dataLinkName: 'news | group-0 | card-@1', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -325,7 +339,7 @@ export const trails: [ 'Ethnicity and poverty are Covid risk factors, new Oxford modelling tool shows', dataLinkName: 'news | group-0 | card-@13', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -345,7 +359,7 @@ export const trails: [ 'UK Covid: 799 more deaths and 10,625 new cases reported; Scottish schools in phased return from Monday – as it happened', dataLinkName: 'news | group-0 | card-@14', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -365,7 +379,7 @@ export const trails: [ 'QCovid: how improved algorithm can identify more higher-risk adults', dataLinkName: 'news | group-0 | card-@1', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -383,7 +397,7 @@ export const trails: [ }, dataLinkName: 'news | group-0 | card-@15', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -402,7 +416,7 @@ export const trails: [ }, dataLinkName: 'news | group-0 | card-@16', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -421,7 +435,7 @@ export const trails: [ }, dataLinkName: 'news | group-0 | card-@17', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, @@ -440,7 +454,7 @@ export const trails: [ }, dataLinkName: 'news | group-0 | card-@18', showQuotedHeadline: false, - showMainVideo: false, + mainVideo: undefined, isExternalLink: false, showLivePlayable: false, }, diff --git a/dotcom-rendering/index.d.ts b/dotcom-rendering/index.d.ts index b5d5b838932..b2cc9b66268 100644 --- a/dotcom-rendering/index.d.ts +++ b/dotcom-rendering/index.d.ts @@ -286,7 +286,12 @@ interface FEKeyEventsRequest { filterKeyEvents: boolean; } -type CardImageType = 'mainMedia' | 'avatar' | 'crossword' | 'slideshow'; +type CardImageType = + | 'mainMedia' + | 'avatar' + | 'crossword' + | 'slideshow' + | 'video'; type SmallHeadlineSize = | 'tiny' diff --git a/dotcom-rendering/makefile b/dotcom-rendering/makefile index a2c8c01ee64..1350d204b83 100644 --- a/dotcom-rendering/makefile +++ b/dotcom-rendering/makefile @@ -200,6 +200,7 @@ gen-schema: @git add src/model/article-schema.json @git add src/model/front-schema.json @git add src/model/block-schema.json + @git add src/model/tag-front-schema.json check-stories: $(call log, "Checking Storybook stories") diff --git a/dotcom-rendering/src/components/Card/Card.stories.tsx b/dotcom-rendering/src/components/Card/Card.stories.tsx index eabc96330ae..4a891006deb 100644 --- a/dotcom-rendering/src/components/Card/Card.stories.tsx +++ b/dotcom-rendering/src/components/Card/Card.stories.tsx @@ -7,6 +7,7 @@ import { } from '@guardian/libs'; import { from } from '@guardian/source-foundations'; import React from 'react'; +import type { Video } from '../../types/video'; import { Section } from '../Section'; import type { Props as CardProps } from './Card'; import { Card } from './Card'; @@ -31,6 +32,7 @@ const basicCardProps: CardProps = { imagePosition: 'top', showAge: true, isExternalLink: false, + videoSize: 'large enough to play: at least 480px', }; const aBasicLink = { @@ -43,6 +45,21 @@ const aBasicLink = { }, }; +const mainVideo: Video = { + elementId: '1234-abcdef-09876-xyz', + videoId: '8M_yH-e9cq8', + title: '’I care, but I don’t care’: Life after the Queen’s death | Anywhere but Westminster', + expired: false, + duration: 200, + images: [480, 640, 960, 1024, 1200].map((width) => ({ + url: `https://i.guim.co.uk/img/media/2eb01d138eb8fba6e59ce7589a60e3ff984f6a7a/0_0_1920_1080/1920.jpg?width=${width}&quality=45&dpr=2&s=none`, + width, + })), + width: 480, + height: 288, + origin: 'The Guardian', +}; + const CardWrapper = ({ children }: { children: React.ReactNode }) => { return (
{ theme: ArticlePillar.Sport, }} mediaType="Video" - mediaDuration={30} + mainVideo={{ ...mainVideo, duration: 30 }} headlineText="Video" /> @@ -259,7 +276,7 @@ export const WithMediaType = () => { theme: ArticlePillar.Sport, }} mediaType="Audio" - mediaDuration={90} + mainVideo={{ ...mainVideo, duration: 90 }} headlineText="Audio" /> @@ -291,7 +308,7 @@ export const WithMediaTypeSpecialReportAlt = () => { theme: ArticleSpecial.SpecialReportAlt, }} mediaType="Video" - mediaDuration={30} + mainVideo={{ ...mainVideo, duration: 30 }} headlineText="Video" /> @@ -304,7 +321,7 @@ export const WithMediaTypeSpecialReportAlt = () => { theme: ArticleSpecial.SpecialReportAlt, }} mediaType="Audio" - mediaDuration={90} + mainVideo={{ ...mainVideo, duration: 90 }} headlineText="Audio" /> @@ -918,9 +935,8 @@ export const WhenVideoWithPlayButton = () => { imagePosition="top" imageSize="jumbo" imagePositionOnMobile="top" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} /> @@ -936,9 +952,8 @@ export const WhenVideoWithPlayButton = () => { imagePosition="right" imageSize="large" imagePositionOnMobile="top" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} />
  • @@ -950,9 +965,9 @@ export const WhenVideoWithPlayButton = () => { theme: ArticlePillar.News, }} imagePosition="top" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} + videoSize="too small to play: 479px or less" />
  • @@ -968,9 +983,8 @@ export const WhenVideoWithPlayButton = () => { imagePosition="top" imageSize="medium" imagePositionOnMobile="bottom" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} />
  • @@ -984,9 +998,9 @@ export const WhenVideoWithPlayButton = () => { theme: ArticlePillar.News, }} imagePosition="left" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} + videoSize="too small to play: 479px or less" />
  • @@ -998,9 +1012,9 @@ export const WhenVideoWithPlayButton = () => { theme: ArticlePillar.News, }} imagePosition="right" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} + videoSize="too small to play: 479px or less" />
  • @@ -1013,9 +1027,9 @@ export const WhenVideoWithPlayButton = () => { theme: ArticlePillar.News, }} imagePosition="right" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} + videoSize="too small to play: 479px or less" /> @@ -1034,9 +1048,8 @@ export const WhenVideoWithPlayButton = () => { imagePosition="right" imageSize="large" imagePositionOnMobile="top" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} />
  • @@ -1050,9 +1063,8 @@ export const WhenVideoWithPlayButton = () => { imagePosition="top" imagePositionOnMobile="left" imageSize="medium" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} />
  • diff --git a/dotcom-rendering/src/components/Card/Card.tsx b/dotcom-rendering/src/components/Card/Card.tsx index d68774d1527..22e1bd0a183 100644 --- a/dotcom-rendering/src/components/Card/Card.tsx +++ b/dotcom-rendering/src/components/Card/Card.tsx @@ -14,6 +14,7 @@ import type { DCRSupportingContent, } from '../../types/front'; import type { Palette } from '../../types/palette'; +import type { Video } from '../../types/video'; import { Avatar } from '../Avatar'; import { CardHeadline } from '../CardHeadline'; import { CardPicture } from '../CardPicture'; @@ -27,6 +28,7 @@ import { SnapCssSandbox } from '../SnapCssSandbox'; import { StarRating } from '../StarRating/StarRating'; import type { Alignment } from '../SupportingContent'; import { SupportingContent } from '../SupportingContent'; +import { YoutubeBlockComponent } from '../YoutubeBlockComponent.importable'; import { AvatarContainer } from './components/AvatarContainer'; import { CardAge } from './components/CardAge'; import { CardBranding } from './components/CardBranding'; @@ -43,6 +45,11 @@ import type { import { ImageWrapper } from './components/ImageWrapper'; import { TrailTextWrapper } from './components/TrailTextWrapper'; +/** Note YouTube recommends a minimum width of 480px @see https://developers.google.com/youtube/terms/required-minimum-functionality#embedded-youtube-player-size */ +export type VideoSize = + | 'large enough to play: at least 480px' + | 'too small to play: 479px or less'; + export type Props = { linkTo: string; format: ArticleFormat; @@ -63,8 +70,8 @@ export type Props = { avatarUrl?: string; showClock?: boolean; mediaType?: MediaType; - mediaDuration?: number; - showMainVideo?: boolean; + mainVideo?: Video; + videoSize: VideoSize; kickerText?: string; showPulsingDot?: boolean; starRating?: number; @@ -210,12 +217,19 @@ const getImage = ({ avatarUrl, isCrossword, slideshowImages, + mainVideo, + videoSize, }: { imageUrl?: string; avatarUrl?: string; isCrossword?: boolean; slideshowImages?: DCRSlideshowImage[]; + mainVideo?: Video; + videoSize: VideoSize; }) => { + if (mainVideo && videoSize === 'large enough to play: at least 480px') { + return { type: 'video', mainVideo } as const; + } if (slideshowImages) return { type: 'slideshow', slideshowImages } as const; if (avatarUrl) return { type: 'avatar', avatarUrl } as const; if (imageUrl) { @@ -257,8 +271,8 @@ export const Card = ({ avatarUrl, showClock, mediaType, - mediaDuration, - showMainVideo, + mainVideo, + videoSize, kickerText, showPulsingDot, starRating, @@ -375,6 +389,8 @@ export const Card = ({ avatarUrl, isCrossword, slideshowImages, + mainVideo, + videoSize, }); return ( @@ -402,7 +418,10 @@ export const Card = ({ imageType={image.type} imagePosition={imagePosition} imagePositionOnMobile={imagePositionOnMobile} - showPlayIcon={showMainVideo ?? false} + showPlayIcon={ + !!mainVideo && + videoSize === 'too small to play: 479px or less' + } > {image.type === 'slideshow' && ( )} + {image.type === 'video' && ( +
    + + + +
    + )} {image.type === 'mainMedia' && ( )} diff --git a/dotcom-rendering/src/components/Card/components/ImageWrapper.tsx b/dotcom-rendering/src/components/Card/components/ImageWrapper.tsx index 9dd3ed5e986..3bb030810cb 100644 --- a/dotcom-rendering/src/components/Card/components/ImageWrapper.tsx +++ b/dotcom-rendering/src/components/Card/components/ImageWrapper.tsx @@ -72,7 +72,7 @@ export const ImageWrapper = ({ flexBasisStyles({ imageSize, }), - imageType === 'mainMedia' && + (imageType === 'mainMedia' || imageType === 'video') && isHorizontal && flexBasisStyles({ imageSize, diff --git a/dotcom-rendering/src/components/Carousel.importable.tsx b/dotcom-rendering/src/components/Carousel.importable.tsx index b7b2407f566..07943617938 100644 --- a/dotcom-rendering/src/components/Carousel.importable.tsx +++ b/dotcom-rendering/src/components/Carousel.importable.tsx @@ -20,6 +20,7 @@ import type { Branding } from '../types/branding'; import type { DCRContainerPalette } from '../types/front'; import type { OnwardsSource } from '../types/onwards'; import type { TrailType } from '../types/trails'; +import type { Video } from '../types/video'; import { Card } from './Card/Card'; import { LI } from './Card/components/LI'; import { FetchCommentCounts } from './FetchCommentCounts.importable'; @@ -431,8 +432,7 @@ type CarouselCardProps = { discussionId?: string; /** Only used on Labs cards */ branding?: Branding; - showMainVideo?: boolean; - mediaDuration?: number; + mainVideo?: Video; verticalDividerColour?: string; }; @@ -447,8 +447,7 @@ const CarouselCard = ({ dataLinkName, discussionId, branding, - showMainVideo, - mediaDuration, + mainVideo, verticalDividerColour, }: CarouselCardProps) => (
  • ); @@ -901,8 +900,7 @@ export const Carousel = ({ : undefined } branding={branding} - showMainVideo={trail.showMainVideo} - mediaDuration={trail.mediaDuration} + mainVideo={trail.mainVideo} verticalDividerColour={ carouselColours.borderColour } diff --git a/dotcom-rendering/src/components/DynamicPackage.stories.tsx b/dotcom-rendering/src/components/DynamicPackage.stories.tsx index 72a1f02869d..05755d612be 100644 --- a/dotcom-rendering/src/components/DynamicPackage.stories.tsx +++ b/dotcom-rendering/src/components/DynamicPackage.stories.tsx @@ -354,7 +354,7 @@ export const SpecialReportWithoutPalette = () => ( 'inside the firm that helps the super-rich hide their money', showQuotedHeadline: false, dataLinkName: 'news | group-0 | card-@1', - showMainVideo: false, + mainVideo: undefined, showLivePlayable: false, isExternalLink: false, webPublicationDate: '2016-04-08T12:15:09.000Z', diff --git a/dotcom-rendering/src/components/FrontCard.tsx b/dotcom-rendering/src/components/FrontCard.tsx index 772fc9c5009..6d39019711a 100644 --- a/dotcom-rendering/src/components/FrontCard.tsx +++ b/dotcom-rendering/src/components/FrontCard.tsx @@ -39,13 +39,13 @@ export const FrontCard = (props: Props) => { imageUrl: trail.image, isCrossword: trail.isCrossword, mediaType: trail.mediaType, - mediaDuration: trail.mediaDuration, + videoSize: 'large enough to play: at least 480px', starRating: trail.starRating, dataLinkName: trail.dataLinkName, snapData: trail.snapData, discussionId: trail.discussionId, avatarUrl: trail.avatarUrl, - showMainVideo: trail.showMainVideo, + mainVideo: trail.mainVideo, isExternalLink: trail.isExternalLink, branding: trail.branding, slideshowImages: trail.slideshowImages, diff --git a/dotcom-rendering/src/components/SupportingContent.stories.tsx b/dotcom-rendering/src/components/SupportingContent.stories.tsx index a61a300bb97..4aaf8bdcbd3 100644 --- a/dotcom-rendering/src/components/SupportingContent.stories.tsx +++ b/dotcom-rendering/src/components/SupportingContent.stories.tsx @@ -32,6 +32,7 @@ const basicCardProps: CardProps = { imagePosition: 'top', isExternalLink: false, showLivePlayable: false, + videoSize: 'large enough to play: at least 480px', }; const aBasicLink = { diff --git a/dotcom-rendering/src/lib/cardWrappers.tsx b/dotcom-rendering/src/lib/cardWrappers.tsx index a30364c588d..c043d06bdb7 100644 --- a/dotcom-rendering/src/lib/cardWrappers.tsx +++ b/dotcom-rendering/src/lib/cardWrappers.tsx @@ -266,6 +266,7 @@ export const Card25Media25 = ({ imageSize="small" headlineSize="medium" headlineSizeOnMobile="medium" + videoSize="too small to play: 479px or less" /> ); }; @@ -301,6 +302,7 @@ export const Card25Media25SmallHeadline = ({ imageSize="small" headlineSize="small" headlineSizeOnMobile="medium" + videoSize="too small to play: 479px or less" /> ); }; @@ -343,6 +345,7 @@ export const Card25Media25Tall = ({ : undefined } supportingContent={trail.supportingContent?.slice(0, 2)} + videoSize="too small to play: 479px or less" /> ); }; @@ -377,6 +380,7 @@ export const Card25Media25TallNoTrail = ({ headlineSize="medium" headlineSizeOnMobile="medium" supportingContent={trail.supportingContent?.slice(0, 2)} + videoSize="too small to play: 479px or less" /> ); }; @@ -411,6 +415,7 @@ export const Card25Media25TallSmallHeadline = ({ headlineSize="small" headlineSizeOnMobile="medium" supportingContent={trail.supportingContent?.slice(0, 2)} + videoSize="too small to play: 479px or less" /> ); }; @@ -651,6 +656,7 @@ export const CardDefault = ({ avatarUrl={undefined} headlineSize="small" headlineSizeOnMobile="small" + videoSize="too small to play: 479px or less" /> ); }; @@ -682,6 +688,7 @@ export const CardDefaultMedia = ({ imagePositionOnMobile="none" headlineSize="small" headlineSizeOnMobile="small" + videoSize="too small to play: 479px or less" /> ); }; @@ -713,6 +720,7 @@ export const CardDefaultMediaMobile = ({ imagePositionOnMobile="left" headlineSize="small" headlineSizeOnMobile="small" + videoSize="too small to play: 479px or less" /> ); }; diff --git a/dotcom-rendering/src/model/article-schema.json b/dotcom-rendering/src/model/article-schema.json index 4f0575a5b1d..2f686813f8a 100644 --- a/dotcom-rendering/src/model/article-schema.json +++ b/dotcom-rendering/src/model/article-schema.json @@ -4391,8 +4391,64 @@ "isCommentable" ] }, - "showMainVideo": { - "type": "boolean" + "mainVideo": { + "description": "For displaying embedded, playable videos directly in cards", + "type": "object", + "properties": { + "elementId": { + "type": "string" + }, + "videoId": { + "type": "string" + }, + "height": { + "type": "number" + }, + "width": { + "type": "number" + }, + "origin": { + "type": "string" + }, + "title": { + "type": "string" + }, + "duration": { + "type": "number" + }, + "expired": { + "type": "boolean" + }, + "images": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "width": { + "type": "number" + } + }, + "required": [ + "url", + "width" + ] + } + } + }, + "required": [ + "duration", + "elementId", + "expired", + "height", + "images", + "origin", + "title", + "videoId", + "width" + ] } }, "required": [ diff --git a/dotcom-rendering/src/model/enhanceCards.ts b/dotcom-rendering/src/model/enhanceCards.ts index 24734190a85..52737ad2d27 100644 --- a/dotcom-rendering/src/model/enhanceCards.ts +++ b/dotcom-rendering/src/model/enhanceCards.ts @@ -10,9 +10,11 @@ import type { DCRSlideshowImage, DCRSupportingContent, FEFrontCard, + FEMediaAtoms, FESupportingContent, } from '../types/front'; import type { FETagType, TagType } from '../types/tag'; +import type { Video } from '../types/video'; import { enhanceSnaps } from './enhanceSnaps'; /** @@ -189,6 +191,39 @@ const enhanceTags = (tags: FETagType[]): TagType[] => { }); }; +/** + * While the first Media Atom is *not* guaranteed to be the main video, + * it *happens to be* correct in the majority of cases. + * @see https://github.com/guardian/frontend/pull/26247 for inspiration + */ +const decideVideo = (mediaAtom?: FEMediaAtoms): Video | undefined => { + if (!mediaAtom) return undefined; + const asset = mediaAtom.assets.find( + ({ version }) => version === mediaAtom.activeVersion, + ); + if (asset?.platform === 'Youtube') { + return { + elementId: mediaAtom.id, + videoId: asset.id, + duration: mediaAtom.duration ?? 0, + title: mediaAtom.title, + // Size fixed to a 5:3 ratio + width: 500, + height: 300, + origin: mediaAtom.source ?? 'Unknown origin', + expired: !!mediaAtom.expired, + images: + mediaAtom.posterImage?.allImages.map( + ({ url, fields: { width } }) => ({ + url, + width: Number(width), + }), + ) ?? [], + }; + } + return undefined; +}; + export const enhanceCards = ( collections: FEFrontCard[], { @@ -285,10 +320,12 @@ export const enhanceCards = ( ) : undefined, mediaType: decideMediaType(format), - mediaDuration: - faciaCard.properties.maybeContent?.elements.mediaAtoms[0] - ?.duration, - showMainVideo: faciaCard.properties.showMainVideo, + mainVideo: faciaCard.properties.showMainVideo + ? decideVideo( + faciaCard.properties.maybeContent?.elements + .mediaAtoms[0], + ) + : undefined, isExternalLink: faciaCard.card.cardStyle.type === 'ExternalLink', embedUri: faciaCard.properties.embedUri ?? undefined, branding, diff --git a/dotcom-rendering/src/model/front-schema.json b/dotcom-rendering/src/model/front-schema.json index dcb7fa9d020..4bf8d4aff5d 100644 --- a/dotcom-rendering/src/model/front-schema.json +++ b/dotcom-rendering/src/model/front-schema.json @@ -2839,10 +2839,120 @@ "FEMediaAtoms": { "type": "object", "properties": { + "id": { + "type": "string" + }, + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/MediaAsset" + } + }, + "title": { + "type": "string" + }, "duration": { "type": "number" + }, + "source": { + "type": "string" + }, + "posterImage": { + "type": "object", + "properties": { + "allImages": { + "type": "array", + "items": { + "$ref": "#/definitions/Image" + } + } + }, + "required": [ + "allImages" + ] + }, + "expired": { + "type": "boolean" + }, + "activeVersion": { + "type": "number" } - } + }, + "required": [ + "assets", + "id", + "title" + ] + }, + "MediaAsset": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "number" + }, + "platform": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "id", + "platform", + "version" + ] + }, + "Image": { + "type": "object", + "properties": { + "index": { + "type": "number" + }, + "fields": { + "type": "object", + "properties": { + "height": { + "type": "string" + }, + "width": { + "type": "string" + }, + "isMaster": { + "type": "string" + }, + "source": { + "type": "string" + }, + "caption": { + "type": "string" + } + }, + "required": [ + "height", + "width" + ] + }, + "mediaType": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "fields", + "index", + "mediaType", + "mimeType", + "url" + ] }, "EditionId": { "enum": [ @@ -3382,8 +3492,64 @@ "isCommentable" ] }, - "showMainVideo": { - "type": "boolean" + "mainVideo": { + "description": "For displaying embedded, playable videos directly in cards", + "type": "object", + "properties": { + "elementId": { + "type": "string" + }, + "videoId": { + "type": "string" + }, + "height": { + "type": "number" + }, + "width": { + "type": "number" + }, + "origin": { + "type": "string" + }, + "title": { + "type": "string" + }, + "duration": { + "type": "number" + }, + "expired": { + "type": "boolean" + }, + "images": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "width": { + "type": "number" + } + }, + "required": [ + "url", + "width" + ] + } + } + }, + "required": [ + "duration", + "elementId", + "expired", + "height", + "images", + "origin", + "title", + "videoId", + "width" + ] } }, "required": [ diff --git a/dotcom-rendering/src/model/tag-front-schema.json b/dotcom-rendering/src/model/tag-front-schema.json index b3732d5bf20..8a9802313e3 100644 --- a/dotcom-rendering/src/model/tag-front-schema.json +++ b/dotcom-rendering/src/model/tag-front-schema.json @@ -1248,10 +1248,120 @@ "FEMediaAtoms": { "type": "object", "properties": { + "id": { + "type": "string" + }, + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/MediaAsset" + } + }, + "title": { + "type": "string" + }, "duration": { "type": "number" + }, + "source": { + "type": "string" + }, + "posterImage": { + "type": "object", + "properties": { + "allImages": { + "type": "array", + "items": { + "$ref": "#/definitions/Image" + } + } + }, + "required": [ + "allImages" + ] + }, + "expired": { + "type": "boolean" + }, + "activeVersion": { + "type": "number" } - } + }, + "required": [ + "assets", + "id", + "title" + ] + }, + "MediaAsset": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "number" + }, + "platform": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "id", + "platform", + "version" + ] + }, + "Image": { + "type": "object", + "properties": { + "index": { + "type": "number" + }, + "fields": { + "type": "object", + "properties": { + "height": { + "type": "string" + }, + "width": { + "type": "string" + }, + "isMaster": { + "type": "string" + }, + "source": { + "type": "string" + }, + "caption": { + "type": "string" + } + }, + "required": [ + "height", + "width" + ] + }, + "mediaType": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "fields", + "index", + "mediaType", + "mimeType", + "url" + ] }, "EditionId": { "enum": [ diff --git a/dotcom-rendering/src/types/front.ts b/dotcom-rendering/src/types/front.ts index 2cc8c45c966..e66779e25c4 100644 --- a/dotcom-rendering/src/types/front.ts +++ b/dotcom-rendering/src/types/front.ts @@ -4,10 +4,12 @@ import type { EditionId } from '../lib/edition'; import type { DCRBadgeType } from './badge'; import type { Branding } from './branding'; import type { ServerSideTests, Switches } from './config'; +import type { Image } from './content'; import type { FooterType } from './footer'; import type { FETagType } from './tag'; import type { Territory } from './territory'; import type { FETrailType, TrailType } from './trails'; +import type { Video } from './video'; export interface FEFrontType { pressedPage: FEPressedPageType; @@ -134,8 +136,26 @@ export type DCRContainerPalette = // TODO: These may need to be declared differently than the front types in the future export type DCRContainerType = FEContainerType; -interface FEMediaAtoms { +/** @see https://github.com/guardian/frontend/blob/0bf69f55a/common/app/model/content/Atom.scala#L191-L196 */ +interface MediaAsset { + id: string; + version: number; + platform: string; + mimeType?: string; +} + +/** @see https://github.com/guardian/frontend/blob/0bf69f55a/common/app/model/content/Atom.scala#L158-L169 */ +export interface FEMediaAtoms { + id: string; + // defaultHtml: string; // currently unused + assets: MediaAsset[]; + title: string; duration?: number; + source?: string; + posterImage?: { allImages: Image[] }; + expired?: boolean; + activeVersion?: number; + // channelId?: string; // currently unused } export type FEFrontCard = { @@ -290,8 +310,7 @@ export type DCRFrontCard = { showByline?: boolean; avatarUrl?: string; mediaType?: MediaType; - mediaDuration?: number; - showMainVideo: boolean; + mainVideo?: Video; isExternalLink: boolean; embedUri?: string; branding?: Branding; diff --git a/dotcom-rendering/src/types/trails.ts b/dotcom-rendering/src/types/trails.ts index 2331e325e11..bc7fbf70966 100644 --- a/dotcom-rendering/src/types/trails.ts +++ b/dotcom-rendering/src/types/trails.ts @@ -1,5 +1,6 @@ import type { Branding } from './branding'; import type { DCRSnapType, DCRSupportingContent } from './front'; +import type { Video } from './video'; type MediaType = 'Video' | 'Audio' | 'Gallery'; @@ -29,7 +30,7 @@ interface BaseTrailType { isClosedForComments: boolean; discussionId?: string; }; - showMainVideo?: boolean; + mainVideo?: Video; } export interface TrailType extends BaseTrailType { diff --git a/dotcom-rendering/src/types/video.ts b/dotcom-rendering/src/types/video.ts new file mode 100644 index 00000000000..de6e8b76d4e --- /dev/null +++ b/dotcom-rendering/src/types/video.ts @@ -0,0 +1,12 @@ +/** For displaying embedded, playable videos directly in cards */ +export type Video = { + elementId: string; + videoId: string; + height: number; + width: number; + origin: string; + title: string; + duration: number; + expired: boolean; + images: Array<{ url: string; width: number }>; +};