-
Notifications
You must be signed in to change notification settings - Fork 30
Add looping videos to articles #14843
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
abd631d
2940598
54b2612
5e3caba
b88200a
701f590
672c50b
aad6e2d
760c0aa
3d2050a
3e683c9
3af9a68
503658c
aa2c34c
41b8cff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import type { ArticleFormat } from '../lib/articleFormat'; | ||
| import type { Source } from '../lib/video'; | ||
| import type { VideoAssets } from '../types/content'; | ||
| import { Caption } from './Caption'; | ||
| import type { Props as CardPictureProps } from './CardPicture'; | ||
| import { LoopVideo } from './LoopVideo.importable'; | ||
| import type { SubtitleSize } from './LoopVideoPlayer'; | ||
|
|
||
| type LoopVideoInArticleProps = { | ||
| assets: VideoAssets[]; | ||
| atomId: string; | ||
| caption?: string; | ||
| fallbackImage: CardPictureProps['mainImage']; | ||
| fallbackImageAlt: CardPictureProps['alt']; | ||
| fallbackImageAspectRatio: CardPictureProps['aspectRatio']; | ||
| fallbackImageLoading: CardPictureProps['loading']; | ||
| fallbackImageSize: CardPictureProps['imageSize']; | ||
| format: ArticleFormat; | ||
| height: number; | ||
| isMainMedia: boolean; | ||
| linkTo: string; | ||
| posterImage: string; | ||
| uniqueId: string; | ||
| width: number; | ||
| }; | ||
|
|
||
| // The looping video player types its `sources` attribute as `Sources` | ||
| // However, looping videos in articles are delivered as media atoms, which type their `assets` as `VideoAssets` | ||
| // Which means that we need to alter the shape of the incoming `assets` to match the requirements of the outgoing `sources` | ||
| const convertAssetsToSources = (assets: VideoAssets[]): Source[] => { | ||
| return assets.map((asset) => { | ||
| return { | ||
| src: asset.url ?? '', | ||
| mimeType: 'video/mp4', | ||
| }; | ||
| }); | ||
| }; | ||
|
|
||
| const getSubtitleAsset = (assets: VideoAssets[]) => { | ||
| // Get the first subtitle asset from assets with a mimetype of 'text/vtt' | ||
|
|
||
| const candidate = assets.find((asset) => asset.mimeType === 'text/vtt'); | ||
| return candidate?.url; | ||
| }; | ||
|
|
||
| export const LoopVideoInArticle = ({ | ||
| assets, | ||
| atomId, | ||
| caption, | ||
| fallbackImage, | ||
| fallbackImageAlt, | ||
| fallbackImageAspectRatio, | ||
| fallbackImageLoading, | ||
| fallbackImageSize, | ||
| format, | ||
| height = 400, | ||
| isMainMedia, | ||
| linkTo, | ||
| posterImage, | ||
| uniqueId, | ||
| width = 500, | ||
| }: LoopVideoInArticleProps) => { | ||
| return ( | ||
| <> | ||
| <LoopVideo | ||
| atomId={atomId} | ||
| fallbackImage={fallbackImage} | ||
| fallbackImageAlt={fallbackImageAlt} | ||
| fallbackImageAspectRatio={fallbackImageAspectRatio} | ||
| fallbackImageLoading={fallbackImageLoading} | ||
| fallbackImageSize={fallbackImageSize} | ||
| height={height} | ||
| linkTo={linkTo} | ||
| posterImage={posterImage} | ||
| sources={convertAssetsToSources(assets)} | ||
| subtitleSize={'small' as SubtitleSize} | ||
| subtitleSource={getSubtitleAsset(assets)} | ||
| uniqueId={uniqueId} | ||
| width={width} | ||
| /> | ||
| {!!caption && ( | ||
| <Caption | ||
| captionText={caption} | ||
| format={format} | ||
| isMainMedia={isMainMedia} | ||
| mediaType="SelfHostedVideo" | ||
| /> | ||
| )} | ||
| </> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2698,6 +2698,9 @@ | |
| }, | ||
| "duration": { | ||
| "type": "number" | ||
| }, | ||
| "videoPlayerFormat": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "required": [ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ import { Island } from '../components/Island'; | |
| import { ItemLinkBlockElement } from '../components/ItemLinkBlockElement'; | ||
| import { KeyTakeaways } from '../components/KeyTakeaways'; | ||
| import { KnowledgeQuizAtom } from '../components/KnowledgeQuizAtom.importable'; | ||
| import { LoopVideoInArticle } from '../components/LoopVideoInArticle.importable'; | ||
| import { MainMediaEmbedBlockComponent } from '../components/MainMediaEmbedBlockComponent'; | ||
| import { MapEmbedBlockComponent } from '../components/MapEmbedBlockComponent.importable'; | ||
| import { MiniProfiles } from '../components/MiniProfiles'; | ||
|
|
@@ -490,15 +491,67 @@ export const renderElement = ({ | |
| </Island> | ||
| ); | ||
| case 'model.dotcomrendering.pageElements.MediaAtomBlockElement': | ||
| return ( | ||
| <VideoAtom | ||
| format={format} | ||
| assets={element.assets} | ||
| poster={element.posterImage?.[0]?.url} | ||
| caption={element.title} | ||
| isMainMedia={isMainMedia} | ||
| /> | ||
| ); | ||
| /* | ||
| - MediaAtomBlockElement is used for self-hosted videos | ||
| - Historically, these videos have been self-hosted for legal or sensitive reasons | ||
| - These videos play in the `VideoAtom` component | ||
| - Looping videos, introduced in July 2025, are also self-hosted | ||
| - Thus they are delivered as a MediaAtomBlockElement | ||
| - However they need to display in a different video player | ||
| - This is handled in the new `LoopVideoInArticle` component | ||
| - We need to differentiate between the two forms of video | ||
| - We can do this by interrogating the atom's metadata, which includes the new attribute `videoPlayerFormat` | ||
|
|
||
| - Note: we'll probably extend this functionality to handle new 'Cinemagraph' videos | ||
| - These may use the looping video, or yet another new, video player | ||
| - But they will still be Media Atoms | ||
| */ | ||
|
|
||
| if (element?.videoPlayerFormat === 'Loop') { | ||
| const posterImageExists = !!element.posterImage?.[0]?.url; | ||
| return ( | ||
| <> | ||
| {posterImageExists && ( | ||
| <Island | ||
| priority="critical" | ||
| defer={{ until: 'visible' }} | ||
| > | ||
| <LoopVideoInArticle | ||
| assets={element.assets} | ||
| atomId={element.id} | ||
| uniqueId={element.id} | ||
| height={400} | ||
| width={500} | ||
|
Comment on lines
+523
to
+524
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do these numbers come from somewhere?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Magic numbers: currently represents the 5:4 aspect ratio used by loop videos. Could double to 1000/800 maybe for people using very wide displays? I think CSS overrides this in any case - need to check. |
||
| posterImage={ | ||
| element.posterImage?.[0]?.url ?? '' | ||
| } | ||
| fallbackImage={ | ||
| element.posterImage?.[0]?.url ?? '' | ||
| } | ||
| fallbackImageSize="small" | ||
| fallbackImageLoading="lazy" | ||
| fallbackImageAlt={element.title} | ||
| fallbackImageAspectRatio="5:4" | ||
| linkTo="Article-embed-MediaAtomBlockElement" | ||
| format={format} | ||
| caption={element.title} | ||
| isMainMedia={isMainMedia} | ||
| /> | ||
| </Island> | ||
| )} | ||
| </> | ||
| ); | ||
| } else { | ||
| return ( | ||
| <VideoAtom | ||
| format={format} | ||
| assets={element.assets} | ||
| poster={element.posterImage?.[0]?.url} | ||
| caption={element.title} | ||
| isMainMedia={isMainMedia} | ||
| /> | ||
| ); | ||
| } | ||
| case 'model.dotcomrendering.pageElements.MiniProfilesBlockElement': | ||
| return ( | ||
| <MiniProfiles | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2186,6 +2186,9 @@ | |
| }, | ||
| "duration": { | ||
| "type": "number" | ||
| }, | ||
| "videoPlayerFormat": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "required": [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Introducing state feels a bit unnecessary here, what was your thinking?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bug in the new LoopVideo player. The bug tries to access the browser
windowenvironment for the DPR value before the element has been mounted. So I decided to move the value to state and do thewindowcheck in auseUpdatehook. Thus once we get the correct DPR the component will rectify itself.