From 675751115b3d0b4308cd11a91a684c45d8ec0319 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 | 57 +++++- .../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 | 175 +++++++++++++++++- .../src/model/tag-front-schema.json | 115 +++++++++++- dotcom-rendering/src/types/front.ts | 27 ++- dotcom-rendering/src/types/trails.ts | 3 +- dotcom-rendering/src/types/video.ts | 12 ++ 18 files changed, 569 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..1054b0dcce5 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: 'playable', }; 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="unplayable" />
  • @@ -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="unplayable" />
  • @@ -998,9 +1012,9 @@ export const WhenVideoWithPlayButton = () => { theme: ArticlePillar.News, }} imagePosition="right" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} + videoSize="unplayable" />
  • @@ -1013,9 +1027,9 @@ export const WhenVideoWithPlayButton = () => { theme: ArticlePillar.News, }} imagePosition="right" - mediaDuration={200} mediaType="Video" - showMainVideo={true} + mainVideo={mainVideo} + videoSize="unplayable" /> @@ -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..b452c014aed 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,8 @@ import type { import { ImageWrapper } from './components/ImageWrapper'; import { TrailTextWrapper } from './components/TrailTextWrapper'; +export type VideoSize = 'playable' | 'unplayable'; + export type Props = { linkTo: string; format: ArticleFormat; @@ -63,8 +67,9 @@ export type Props = { avatarUrl?: string; showClock?: boolean; mediaType?: MediaType; - mediaDuration?: number; - showMainVideo?: boolean; + mainVideo?: Video; + /** Note YouTube recommends a minimum width of 480px @see https://developers.google.com/youtube/terms/required-minimum-functionality#embedded-youtube-player-size */ + videoSize: VideoSize; kickerText?: string; showPulsingDot?: boolean; starRating?: number; @@ -210,12 +215,19 @@ const getImage = ({ avatarUrl, isCrossword, slideshowImages, + mainVideo, + videoSize, }: { imageUrl?: string; avatarUrl?: string; isCrossword?: boolean; slideshowImages?: DCRSlideshowImage[]; + mainVideo?: Video; + videoSize: VideoSize; }) => { + if (mainVideo && videoSize === 'playable') { + 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 +269,8 @@ export const Card = ({ avatarUrl, showClock, mediaType, - mediaDuration, - showMainVideo, + mainVideo, + videoSize, kickerText, showPulsingDot, starRating, @@ -375,6 +387,8 @@ export const Card = ({ avatarUrl, isCrossword, slideshowImages, + mainVideo, + videoSize, }); return ( @@ -402,7 +416,7 @@ export const Card = ({ imageType={image.type} imagePosition={imagePosition} imagePositionOnMobile={imagePositionOnMobile} - showPlayIcon={showMainVideo ?? false} + showPlayIcon={!!mainVideo && videoSize === 'unplayable'} > {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..8ffd54aac5d 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..1184690afd9 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: 'playable', 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..5344312fe26 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: 'playable', }; const aBasicLink = { diff --git a/dotcom-rendering/src/lib/cardWrappers.tsx b/dotcom-rendering/src/lib/cardWrappers.tsx index a30364c588d..59538b55fde 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={'unplayable'} /> ); }; @@ -301,6 +302,7 @@ export const Card25Media25SmallHeadline = ({ imageSize="small" headlineSize="small" headlineSizeOnMobile="medium" + videoSize={'unplayable'} /> ); }; @@ -343,6 +345,7 @@ export const Card25Media25Tall = ({ : undefined } supportingContent={trail.supportingContent?.slice(0, 2)} + videoSize={'unplayable'} /> ); }; @@ -377,6 +380,7 @@ export const Card25Media25TallNoTrail = ({ headlineSize="medium" headlineSizeOnMobile="medium" supportingContent={trail.supportingContent?.slice(0, 2)} + videoSize={'unplayable'} /> ); }; @@ -411,6 +415,7 @@ export const Card25Media25TallSmallHeadline = ({ headlineSize="small" headlineSizeOnMobile="medium" supportingContent={trail.supportingContent?.slice(0, 2)} + videoSize={'unplayable'} /> ); }; @@ -651,6 +656,7 @@ export const CardDefault = ({ avatarUrl={undefined} headlineSize="small" headlineSizeOnMobile="small" + videoSize={'unplayable'} /> ); }; @@ -682,6 +688,7 @@ export const CardDefaultMedia = ({ imagePositionOnMobile="none" headlineSize="small" headlineSizeOnMobile="small" + videoSize={'unplayable'} /> ); }; @@ -713,6 +720,7 @@ export const CardDefaultMediaMobile = ({ imagePositionOnMobile="left" headlineSize="small" headlineSizeOnMobile="small" + videoSize={'unplayable'} /> ); }; 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..62269c3a037 100644 --- a/dotcom-rendering/src/model/front-schema.json +++ b/dotcom-rendering/src/model/front-schema.json @@ -2839,10 +2839,123 @@ "FEMediaAtoms": { "type": "object", "properties": { + "id": { + "type": "string" + }, + "defaultHtml": {}, + "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" + }, + "channelId": {} + }, + "required": [ + "assets", + "defaultHtml", + "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 +3495,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..42062b7fcee 100644 --- a/dotcom-rendering/src/model/tag-front-schema.json +++ b/dotcom-rendering/src/model/tag-front-schema.json @@ -1248,10 +1248,123 @@ "FEMediaAtoms": { "type": "object", "properties": { + "id": { + "type": "string" + }, + "defaultHtml": {}, + "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" + }, + "channelId": {} + }, + "required": [ + "assets", + "defaultHtml", + "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..1c279e3e483 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,28 @@ 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; + /** @deprecated */ + defaultHtml: unknown; + assets: MediaAsset[]; + title: string; duration?: number; + source?: string; + posterImage?: { allImages: Image[] }; + expired?: boolean; + activeVersion?: number; + /** @deprecated */ + channelId?: unknown; } export type FEFrontCard = { @@ -290,8 +312,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 }>; +};