From abd631da2d65443a1a4340ad7c66ec037d4cefa4 Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Fri, 14 Nov 2025 11:29:31 +0000 Subject: [PATCH 01/13] Looping video in article - initial work underway --- .../src/components/LoopVideo.importable.tsx | 10 ++- .../LoopVideoInArticle.importable.tsx | 69 ++++++++++++++ .../src/components/LoopVideoPlayer.tsx | 6 +- dotcom-rendering/src/components/MainMedia.tsx | 1 + .../src/frontend/schemas/feArticle.json | 54 +++++++++++ dotcom-rendering/src/lib/renderElement.tsx | 90 +++++++++++++++++-- dotcom-rendering/src/model/block-schema.json | 54 +++++++++++ dotcom-rendering/src/types/content.ts | 14 +++ 8 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx diff --git a/dotcom-rendering/src/components/LoopVideo.importable.tsx b/dotcom-rendering/src/components/LoopVideo.importable.tsx index ea0178c94e3..78f3da22803 100644 --- a/dotcom-rendering/src/components/LoopVideo.importable.tsx +++ b/dotcom-rendering/src/components/LoopVideo.importable.tsx @@ -61,8 +61,8 @@ const dispatchOphanAttentionEvent = ( document.dispatchEvent(event); }; -const getOptimisedPosterImage = (mainImage: string): string => { - const resolution = window.devicePixelRatio >= 2 ? 'high' : 'low'; +const getOptimisedPosterImage = (mainImage: string, dpr: number): string => { + const resolution = dpr >= 2 ? 'high' : 'low'; return generateImageURL({ mainImage, @@ -156,6 +156,8 @@ export const LoopVideo = ({ */ const [hasBeenInView, setHasBeenInView] = useState(false); + const [devicePixelRatio, setDevicePixelRatio] = useState(1); + const VISIBILITY_THRESHOLD = 0.5; const [isInView, setNode] = useIsInView({ @@ -318,6 +320,8 @@ export const LoopVideo = ({ } }); + setDevicePixelRatio(window.devicePixelRatio); + return () => { document.removeEventListener( customLoopPlayAudioEventName, @@ -593,7 +597,7 @@ export const LoopVideo = ({ const AudioIcon = isMuted ? SvgAudioMute : SvgAudio; const optimisedPosterImage = showPosterImage - ? getOptimisedPosterImage(posterImage) + ? getOptimisedPosterImage(posterImage, devicePixelRatio) : undefined; return ( diff --git a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx new file mode 100644 index 00000000000..fe0d5de7305 --- /dev/null +++ b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx @@ -0,0 +1,69 @@ +import { LoopVideo } from './LoopVideo.importable'; +import type { ArticleFormat } from '../lib/articleFormat'; +import { Caption } from './Caption'; +import type { Props as CardPictureProps } from './CardPicture'; +import type { Source } from '../lib/video'; + +type LoopVideoInArticleProps = { + 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; + sources: Source[]; + uniqueId: string; + width: number; +}; + +export const LoopVideoInArticle = ({ + atomId, + caption, + fallbackImage, + fallbackImageAlt, + fallbackImageAspectRatio, + fallbackImageLoading, + fallbackImageSize, + format, + height = 400, + isMainMedia, + linkTo, + posterImage, + sources, + uniqueId, + width = 500, +}: LoopVideoInArticleProps) => { + console.log('LVIAimp - atomId', atomId); + return ( + <> + + {!!caption && ( + + )} + + ); +}; diff --git a/dotcom-rendering/src/components/LoopVideoPlayer.tsx b/dotcom-rendering/src/components/LoopVideoPlayer.tsx index da77d99bdee..3f0239d6564 100644 --- a/dotcom-rendering/src/components/LoopVideoPlayer.tsx +++ b/dotcom-rendering/src/components/LoopVideoPlayer.tsx @@ -175,7 +175,11 @@ export const LoopVideoPlayer = forwardRef( ))} diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index e797ea35623..a49333b7489 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -110,6 +110,7 @@ export const MainMedia = ({ contentType, contentLayout, }: Props) => { + console.log('MainMedia component - elements', elements); return (
{elements.map((element, index) => ( diff --git a/dotcom-rendering/src/frontend/schemas/feArticle.json b/dotcom-rendering/src/frontend/schemas/feArticle.json index d9030c9936b..d543b984b05 100644 --- a/dotcom-rendering/src/frontend/schemas/feArticle.json +++ b/dotcom-rendering/src/frontend/schemas/feArticle.json @@ -680,6 +680,9 @@ { "$ref": "#/definitions/ListBlockElement" }, + { + "$ref": "#/definitions/LoopVideoInArticleElement" + }, { "$ref": "#/definitions/MapBlockElement" }, @@ -2601,6 +2604,57 @@ "elements" ] }, + "LoopVideoInArticleElement": { + "type": "object", + "properties": { + "_type": { + "type": "string", + "const": "model.dotcomrendering.pageElements.MediaAtomBlockElement" + }, + "elementId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/VideoAssets" + } + }, + "posterImage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "width": { + "type": "number" + } + }, + "required": [ + "url", + "width" + ] + } + }, + "title": { + "type": "string" + }, + "duration": { + "type": "number" + } + }, + "required": [ + "_type", + "assets", + "elementId", + "id" + ] + }, "MapBlockElement": { "type": "object", "properties": { diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index eaf02865a40..4519faa0ad5 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -73,6 +73,8 @@ import { ArticleDesign, type ArticleFormat } from './articleFormat'; import type { EditionId } from './edition'; import { getLargestImageSize } from './image'; +import { LoopVideoInArticle } from '../components/LoopVideoInArticle.importable'; + type Props = { format: ArticleFormat; element: FEElement; @@ -349,6 +351,7 @@ export const renderElement = ({ ); case 'model.dotcomrendering.pageElements.GuVideoBlockElement': + console.log('RENDER_ELEMENT: pageElements.GuVideoBlockElement'); return ( ); case 'model.dotcomrendering.pageElements.MediaAtomBlockElement': - return ( - - ); + console.log('RENDER_ELEMENT: pageElements.MediaAtomBlockElement'); + /* + - 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 new attributes + - Note: we'll probably extend this functionality to handle new 'cenemagraph' videos + - These may use the looping video, or yet another new, video player + - But they will still be Media Atoms + + TESTING + - While waiting for the new parameters, we can test: + `2 + 2 === 5` - current behaviour - `VideoAtom` + `2 + 2 === 4` - display in the looping video player + */ + + if (2 + 2 === 5) { + return ( + <> + {element.posterImage?.[0]?.url && ( + + + + )} + + ); + } else { + return ( + + ); + } case 'model.dotcomrendering.pageElements.MiniProfilesBlockElement': return ( ; case 'model.dotcomrendering.pageElements.VideoFacebookBlockElement': + console.log( + 'RENDER_ELEMENT: pageElements.VideoFacebookBlockElement', + ); return ( ); case 'model.dotcomrendering.pageElements.VideoVimeoBlockElement': + console.log('RENDER_ELEMENT: pageElements.VideoVimeoBlockElement'); return ( ); case 'model.dotcomrendering.pageElements.VideoYoutubeBlockElement': + console.log( + 'RENDER_ELEMENT: pageElements.VideoYoutubeBlockElement', + ); return ( ); case 'model.dotcomrendering.pageElements.VineBlockElement': + console.log('RENDER_ELEMENT: pageElements.VineBlockElement'); return ( ; } }; diff --git a/dotcom-rendering/src/model/block-schema.json b/dotcom-rendering/src/model/block-schema.json index f365dd6bf08..ad7a4dd4b34 100644 --- a/dotcom-rendering/src/model/block-schema.json +++ b/dotcom-rendering/src/model/block-schema.json @@ -168,6 +168,9 @@ { "$ref": "#/definitions/ListBlockElement" }, + { + "$ref": "#/definitions/LoopVideoInArticleElement" + }, { "$ref": "#/definitions/MapBlockElement" }, @@ -2089,6 +2092,57 @@ "elements" ] }, + "LoopVideoInArticleElement": { + "type": "object", + "properties": { + "_type": { + "type": "string", + "const": "model.dotcomrendering.pageElements.MediaAtomBlockElement" + }, + "elementId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/VideoAssets" + } + }, + "posterImage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "width": { + "type": "number" + } + }, + "required": [ + "url", + "width" + ] + } + }, + "title": { + "type": "string" + }, + "duration": { + "type": "number" + } + }, + "required": [ + "_type", + "assets", + "elementId", + "id" + ] + }, "MapBlockElement": { "type": "object", "properties": { diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts index b87c95b4812..2a3c85a61bc 100644 --- a/dotcom-rendering/src/types/content.ts +++ b/dotcom-rendering/src/types/content.ts @@ -411,6 +411,19 @@ export interface ListBlockElement { elementId: string; } +// interface LoopVideoInArticleElement { +// _type: 'model.dotcomrendering.pageElements.MediaAtomBlockElement'; +// elementId: string; +// id: string; +// assets: VideoAssets[]; +// posterImage?: { +// url: string; +// width: number; +// }[]; +// title?: string; +// duration?: number; +// } + export interface MapBlockElement extends ThirdPartyEmbeddedContent { _type: 'model.dotcomrendering.pageElements.MapBlockElement'; elementId: string; @@ -802,6 +815,7 @@ export type FEElement = | KeyTakeawaysBlockElement | LinkBlockElement | ListBlockElement + | LoopVideoInArticleElement | MapBlockElement | MediaAtomBlockElement | MiniProfilesBlockElement From 54b261235ae06dabaefe3efc26283e79c11da068 Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Fri, 14 Nov 2025 11:37:43 +0000 Subject: [PATCH 02/13] Looping video in article - initial work underway --- dotcom-rendering/src/types/content.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts index 64e143222ed..353d0864eac 100644 --- a/dotcom-rendering/src/types/content.ts +++ b/dotcom-rendering/src/types/content.ts @@ -832,7 +832,6 @@ export type FEElement = | KeyTakeawaysBlockElement | LinkBlockElement | ListBlockElement - | LoopVideoInArticleElement | MapBlockElement | MediaAtomBlockElement | MiniProfilesBlockElement From 5e3cabaa15f9fe82b44a9b5a237858534d53f55e Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Fri, 14 Nov 2025 11:44:08 +0000 Subject: [PATCH 03/13] Looping video in article - initial work underway --- .../src/components/LoopVideoInArticle.importable.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx index fe0d5de7305..beb207b30ae 100644 --- a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx +++ b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx @@ -3,6 +3,7 @@ import type { ArticleFormat } from '../lib/articleFormat'; import { Caption } from './Caption'; import type { Props as CardPictureProps } from './CardPicture'; import type { Source } from '../lib/video'; +import type { SubtitleSize } from './LoopVideoPlayer'; type LoopVideoInArticleProps = { atomId: string; @@ -20,6 +21,8 @@ type LoopVideoInArticleProps = { sources: Source[]; uniqueId: string; width: number; + subtitleSource?: string; + subtitleSize: SubtitleSize; }; export const LoopVideoInArticle = ({ @@ -38,6 +41,8 @@ export const LoopVideoInArticle = ({ sources, uniqueId, width = 500, + subtitleSource, + subtitleSize, }: LoopVideoInArticleProps) => { console.log('LVIAimp - atomId', atomId); return ( @@ -55,6 +60,8 @@ export const LoopVideoInArticle = ({ fallbackImageAlt={fallbackImageAlt} fallbackImageAspectRatio={fallbackImageAspectRatio} linkTo={linkTo} + subtitleSource={subtitleSource} + subtitleSize={subtitleSize} /> {!!caption && ( Date: Fri, 14 Nov 2025 15:27:33 +0000 Subject: [PATCH 04/13] Looping video in article - initial work underway --- .../LoopVideoInArticle.importable.tsx | 2 +- .../src/components/LoopVideoPlayer.tsx | 6 +- dotcom-rendering/src/lib/renderElement.tsx | 11 ++- dotcom-rendering/src/types/content.ts | 81 ++++--------------- 4 files changed, 25 insertions(+), 75 deletions(-) diff --git a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx index beb207b30ae..5988251ee02 100644 --- a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx +++ b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx @@ -67,7 +67,7 @@ export const LoopVideoInArticle = ({ )} diff --git a/dotcom-rendering/src/components/LoopVideoPlayer.tsx b/dotcom-rendering/src/components/LoopVideoPlayer.tsx index e3ecc8a648b..62803de51bf 100644 --- a/dotcom-rendering/src/components/LoopVideoPlayer.tsx +++ b/dotcom-rendering/src/components/LoopVideoPlayer.tsx @@ -213,11 +213,7 @@ export const LoopVideoPlayer = forwardRef( ))} diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index 32df4a9ce4e..354c5ebc200 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -514,7 +514,13 @@ export const renderElement = ({ `2 + 2 === 4` - display in the looping video player */ - if (2 + 2 === 5) { + if (2 + 2 === 4) { + console.log('Loop: element.assets', element.assets); + const updatedSources = element.assets.map((a) => ({ + src: a.src || a.url, + mimeType: a.mimeType, + })); + console.log('Loop: updatedSources', updatedSources); return ( <> {element.posterImage?.[0]?.url && ( @@ -523,7 +529,7 @@ export const renderElement = ({ defer={{ until: 'visible' }} > ); } else { + console.log('Default: element.assets', element.assets); return ( Date: Fri, 14 Nov 2025 15:29:36 +0000 Subject: [PATCH 05/13] Looping video in article - initial work underway --- dotcom-rendering/src/components/LoopVideoPlayer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/components/LoopVideoPlayer.tsx b/dotcom-rendering/src/components/LoopVideoPlayer.tsx index 62803de51bf..d975e7bb45f 100644 --- a/dotcom-rendering/src/components/LoopVideoPlayer.tsx +++ b/dotcom-rendering/src/components/LoopVideoPlayer.tsx @@ -213,7 +213,7 @@ export const LoopVideoPlayer = forwardRef( ))} From 672c50b7a16bf5ab1a8d54682e3aa3ebceb54159 Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Fri, 14 Nov 2025 15:36:27 +0000 Subject: [PATCH 06/13] remove cosole.log --- dotcom-rendering/src/components/MainMedia.tsx | 1 - dotcom-rendering/src/types/content.ts | 56 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index a49333b7489..e797ea35623 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -110,7 +110,6 @@ export const MainMedia = ({ contentType, contentLayout, }: Props) => { - console.log('MainMedia component - elements', elements); return (
{elements.map((element, index) => ( diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts index 6115f4de9cc..bb944684a53 100644 --- a/dotcom-rendering/src/types/content.ts +++ b/dotcom-rendering/src/types/content.ts @@ -482,6 +482,23 @@ export interface InteractiveContentsBlockElement { endDocumentElementId?: string; } +export interface ProductBlockElement { + _type: 'model.dotcomrendering.pageElements.ProductBlockElement'; + elementId: string; + brandName: string; + starRating: ProductStarRating; + productName: string; + image?: ProductImage; + secondaryHeadingHtml: string; + primaryHeadingHtml: string; + customAttributes: ProductCustomAttribute[]; + content: FEElement[]; + h2Id?: string; + displayType: ProductDisplayType; + productCtas: ProductCta[]; + lowestPrice?: string; +} + interface ProfileAtomBlockElement { _type: 'model.dotcomrendering.pageElements.ProfileAtomBlockElement'; elementId: string; @@ -847,7 +864,8 @@ export type FEElement = | VineBlockElement | YoutubeBlockElement | WitnessTypeBlockElement - | CrosswordElement; + | CrosswordElement + | ProductBlockElement; // ------------------------------------- // Misc @@ -879,11 +897,47 @@ export interface ImageSource { srcSet: SrcSetItem[]; } +export type ProductDisplayType = + | 'InlineOnly' + | 'ProductCardOnly' + | 'InlineWithProductCard'; + +export type ProductCta = { + url: string; + text: string; + retailer: string; + price: string; +}; + +export type ProductCustomAttribute = { + name: string; + value: string; +}; + +export type ProductStarRating = + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | 'none-selected'; + export interface SrcSetItem { src: string; width: number; } +export interface ProductImage { + url: string; + caption: string; + credit: string; + alt: string; + displayCredit: boolean; + height: number; + width: number; +} + export interface Image { index: number; fields: { From aad6e2d6878877a6690979e6c5ab0cb889e20e3d Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Mon, 17 Nov 2025 15:04:26 +0000 Subject: [PATCH 07/13] Investigate why loop video not displaying in article --- .../src/components/LoopVideo.importable.tsx | 67 +++---------------- .../src/components/LoopVideoPlayer.tsx | 1 + 2 files changed, 12 insertions(+), 56 deletions(-) diff --git a/dotcom-rendering/src/components/LoopVideo.importable.tsx b/dotcom-rendering/src/components/LoopVideo.importable.tsx index 8871bf056a2..c90b646dab5 100644 --- a/dotcom-rendering/src/components/LoopVideo.importable.tsx +++ b/dotcom-rendering/src/components/LoopVideo.importable.tsx @@ -1,6 +1,5 @@ import { css } from '@emotion/react'; import { log, storage } from '@guardian/libs'; -import { space } from '@guardian/source/foundations'; import { SvgAudio, SvgAudioMute } from '@guardian/source/react-components'; import { useCallback, useEffect, useRef, useState } from 'react'; import { @@ -12,7 +11,6 @@ import { getZIndex } from '../lib/getZIndex'; import { generateImageURL } from '../lib/image'; import { useIsInView } from '../lib/useIsInView'; import { useShouldAdapt } from '../lib/useShouldAdapt'; -import { useSubtitles } from '../lib/useSubtitles'; import type { CustomPlayEventDetail, Source } from '../lib/video'; import { customLoopPlayAudioEventName, @@ -20,12 +18,8 @@ import { } from '../lib/video'; import { CardPicture, type Props as CardPictureProps } from './CardPicture'; import { useConfig } from './ConfigContext'; -import type { - PLAYER_STATES, - PlayerStates, - SubtitleSize, -} from './LoopVideoPlayer'; import { LoopVideoPlayer } from './LoopVideoPlayer'; +import type { PLAYER_STATES, PlayerStates } from './LoopVideoPlayer'; import { ophanTrackerWeb } from './YoutubeAtom/eventEmitters'; const videoContainerStyles = css` @@ -123,8 +117,6 @@ type Props = { fallbackImageAlt: CardPictureProps['alt']; fallbackImageAspectRatio: CardPictureProps['aspectRatio']; linkTo: string; - subtitleSource?: string; - subtitleSize: SubtitleSize; }; export const LoopVideo = ({ @@ -140,9 +132,8 @@ export const LoopVideo = ({ fallbackImageAlt, fallbackImageAspectRatio, linkTo, - subtitleSource, - subtitleSize, }: Props) => { + console.log('LVimp - atomId', atomId); const adapted = useShouldAdapt(); const { renderingTarget } = useConfig(); const vidRef = useRef(null); @@ -165,8 +156,6 @@ export const LoopVideo = ({ * want to pause the video if it has been in view. */ const [hasBeenInView, setHasBeenInView] = useState(false); - const [hasBeenPlayed, setHasBeenPlayed] = useState(false); - const [hasTrackedPlay, setHasTrackedPlay] = useState(false); const [devicePixelRatio, setDevicePixelRatio] = useState(1); @@ -177,12 +166,6 @@ export const LoopVideo = ({ threshold: VISIBILITY_THRESHOLD, }); - const activeCue = useSubtitles({ - video: vidRef.current, - playerState, - currentTime, - }); - const playVideo = useCallback(async () => { const video = vidRef.current; if (!video) return; @@ -196,7 +179,6 @@ export const LoopVideo = ({ .then(() => { // Autoplay succeeded dispatchOphanAttentionEvent('videoPlaying'); - setHasBeenPlayed(true); setPlayerState('PLAYING'); }) .catch((error: Error) => { @@ -273,6 +255,7 @@ export const LoopVideo = ({ * 2. Creates event listeners to control playback when there are multiple videos. */ useEffect(() => { + console.log('LVimp - useEffect - setup', uniqueId); setIsAutoplayAllowed(doesUserPermitAutoplay()); /** @@ -406,19 +389,6 @@ export const LoopVideo = ({ } }, [isInView, hasBeenInView, atomId, linkTo]); - /** - * Track the first successful video play in Ophan. - * - * This effect runs only after the video has actually started playing - * for the first time. This is to ensure we don't double-report the event. - */ - useEffect(() => { - if (!hasBeenPlayed || hasTrackedPlay) return; - - ophanTrackerWeb(atomId, 'loop')('play'); - setHasTrackedPlay(true); - }, [atomId, hasBeenPlayed, hasTrackedPlay]); - /** * Handle play/pause, when instigated by the browser. */ @@ -448,6 +418,14 @@ export const LoopVideo = ({ playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' || (hasPageBecomeActive && playerState === 'PAUSED_BY_BROWSER')) ) { + /** + * Check if the video has not been in view before tracking the play. + * This is so we only track the first play. + */ + if (!hasBeenInView) { + ophanTrackerWeb(atomId, 'loop')('play'); + } + setHasPageBecomeActive(false); void playVideo(); } @@ -506,25 +484,6 @@ export const LoopVideo = ({ return FallbackImageComponent; } - const handleLoadedMetadata = () => { - const video = vidRef.current; - if (!video) return; - - const track = video.textTracks[0]; - if (!track?.cues) return; - const pxFromBottom = space[3]; - const videoHeight = video.getBoundingClientRect().height; - const percentFromTop = - ((videoHeight - pxFromBottom) / videoHeight) * 100; - - for (const cue of Array.from(track.cues)) { - if (cue instanceof VTTCue) { - cue.snapToLines = false; - cue.line = percentFromTop; - } - } - }; - const handleLoadedData = () => { if (vidRef.current) { setHasAudio(doesVideoHaveAudio(vidRef.current)); @@ -664,7 +623,6 @@ export const LoopVideo = ({ isPlayable={isPlayable} playerState={playerState} isMuted={isMuted} - handleLoadedMetadata={handleLoadedMetadata} handleLoadedData={handleLoadedData} handleCanPlay={handleCanPlay} handlePlayPauseClick={handlePlayPauseClick} @@ -675,9 +633,6 @@ export const LoopVideo = ({ AudioIcon={hasAudio ? AudioIcon : null} preloadPartialData={preloadPartialData} showPlayIcon={showPlayIcon} - subtitleSource={subtitleSource} - subtitleSize={subtitleSize} - activeCue={activeCue} /> ); diff --git a/dotcom-rendering/src/components/LoopVideoPlayer.tsx b/dotcom-rendering/src/components/LoopVideoPlayer.tsx index d975e7bb45f..90e59cea1bf 100644 --- a/dotcom-rendering/src/components/LoopVideoPlayer.tsx +++ b/dotcom-rendering/src/components/LoopVideoPlayer.tsx @@ -169,6 +169,7 @@ export const LoopVideoPlayer = forwardRef( ref: React.ForwardedRef, ) => { const loopVideoId = `loop-video-${uniqueId}`; + console.log('LoopVideoPlayer'); return ( <> {/* eslint-disable-next-line jsx-a11y/media-has-caption -- Captions will be considered later. */} From 3d2050a1c48c358cb9ff00d9590559a2282c8ea2 Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Tue, 18 Nov 2025 15:53:32 +0000 Subject: [PATCH 08/13] Remove console.log --- .../src/components/LoopVideo.importable.tsx | 2 -- .../LoopVideoInArticle.importable.tsx | 1 - .../src/components/LoopVideoPlayer.tsx | 1 - dotcom-rendering/src/lib/renderElement.tsx | 22 +------------------ 4 files changed, 1 insertion(+), 25 deletions(-) diff --git a/dotcom-rendering/src/components/LoopVideo.importable.tsx b/dotcom-rendering/src/components/LoopVideo.importable.tsx index c90b646dab5..78f3da22803 100644 --- a/dotcom-rendering/src/components/LoopVideo.importable.tsx +++ b/dotcom-rendering/src/components/LoopVideo.importable.tsx @@ -133,7 +133,6 @@ export const LoopVideo = ({ fallbackImageAspectRatio, linkTo, }: Props) => { - console.log('LVimp - atomId', atomId); const adapted = useShouldAdapt(); const { renderingTarget } = useConfig(); const vidRef = useRef(null); @@ -255,7 +254,6 @@ export const LoopVideo = ({ * 2. Creates event listeners to control playback when there are multiple videos. */ useEffect(() => { - console.log('LVimp - useEffect - setup', uniqueId); setIsAutoplayAllowed(doesUserPermitAutoplay()); /** diff --git a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx index 5988251ee02..39a53c67944 100644 --- a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx +++ b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx @@ -44,7 +44,6 @@ export const LoopVideoInArticle = ({ subtitleSource, subtitleSize, }: LoopVideoInArticleProps) => { - console.log('LVIAimp - atomId', atomId); return ( <> , ) => { const loopVideoId = `loop-video-${uniqueId}`; - console.log('LoopVideoPlayer'); return ( <> {/* eslint-disable-next-line jsx-a11y/media-has-caption -- Captions will be considered later. */} diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index 354c5ebc200..bdf9447f5bd 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -360,7 +360,6 @@ export const renderElement = ({ ); case 'model.dotcomrendering.pageElements.GuVideoBlockElement': - console.log('RENDER_ELEMENT: pageElements.GuVideoBlockElement'); return ( ); case 'model.dotcomrendering.pageElements.MediaAtomBlockElement': - console.log('RENDER_ELEMENT: pageElements.MediaAtomBlockElement'); /* - MediaAtomBlockElement is used for self-hosted videos - Historically, these videos have been self-hosted for legal or sensitive reasons @@ -514,13 +512,11 @@ export const renderElement = ({ `2 + 2 === 4` - display in the looping video player */ - if (2 + 2 === 4) { - console.log('Loop: element.assets', element.assets); + if (2 + 2 === 5) { const updatedSources = element.assets.map((a) => ({ src: a.src || a.url, mimeType: a.mimeType, })); - console.log('Loop: updatedSources', updatedSources); return ( <> {element.posterImage?.[0]?.url && ( @@ -552,7 +548,6 @@ export const renderElement = ({ ); } else { - console.log('Default: element.assets', element.assets); return ( ; case 'model.dotcomrendering.pageElements.VideoFacebookBlockElement': - console.log( - 'RENDER_ELEMENT: pageElements.VideoFacebookBlockElement', - ); return ( ); case 'model.dotcomrendering.pageElements.VideoVimeoBlockElement': - console.log('RENDER_ELEMENT: pageElements.VideoVimeoBlockElement'); return ( ); case 'model.dotcomrendering.pageElements.VideoYoutubeBlockElement': - console.log( - 'RENDER_ELEMENT: pageElements.VideoYoutubeBlockElement', - ); return ( ); case 'model.dotcomrendering.pageElements.VineBlockElement': - console.log('RENDER_ELEMENT: pageElements.VineBlockElement'); return ( ; } }; From 3e683c983178f0efb2ff95b40d8543c95bb9f055 Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Tue, 18 Nov 2025 16:04:10 +0000 Subject: [PATCH 09/13] Reconcile with main branch code --- .../src/components/LoopVideo.importable.tsx | 65 ++++++++++++++++--- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/dotcom-rendering/src/components/LoopVideo.importable.tsx b/dotcom-rendering/src/components/LoopVideo.importable.tsx index 78f3da22803..8871bf056a2 100644 --- a/dotcom-rendering/src/components/LoopVideo.importable.tsx +++ b/dotcom-rendering/src/components/LoopVideo.importable.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/react'; import { log, storage } from '@guardian/libs'; +import { space } from '@guardian/source/foundations'; import { SvgAudio, SvgAudioMute } from '@guardian/source/react-components'; import { useCallback, useEffect, useRef, useState } from 'react'; import { @@ -11,6 +12,7 @@ import { getZIndex } from '../lib/getZIndex'; import { generateImageURL } from '../lib/image'; import { useIsInView } from '../lib/useIsInView'; import { useShouldAdapt } from '../lib/useShouldAdapt'; +import { useSubtitles } from '../lib/useSubtitles'; import type { CustomPlayEventDetail, Source } from '../lib/video'; import { customLoopPlayAudioEventName, @@ -18,8 +20,12 @@ import { } from '../lib/video'; import { CardPicture, type Props as CardPictureProps } from './CardPicture'; import { useConfig } from './ConfigContext'; +import type { + PLAYER_STATES, + PlayerStates, + SubtitleSize, +} from './LoopVideoPlayer'; import { LoopVideoPlayer } from './LoopVideoPlayer'; -import type { PLAYER_STATES, PlayerStates } from './LoopVideoPlayer'; import { ophanTrackerWeb } from './YoutubeAtom/eventEmitters'; const videoContainerStyles = css` @@ -117,6 +123,8 @@ type Props = { fallbackImageAlt: CardPictureProps['alt']; fallbackImageAspectRatio: CardPictureProps['aspectRatio']; linkTo: string; + subtitleSource?: string; + subtitleSize: SubtitleSize; }; export const LoopVideo = ({ @@ -132,6 +140,8 @@ export const LoopVideo = ({ fallbackImageAlt, fallbackImageAspectRatio, linkTo, + subtitleSource, + subtitleSize, }: Props) => { const adapted = useShouldAdapt(); const { renderingTarget } = useConfig(); @@ -155,6 +165,8 @@ export const LoopVideo = ({ * want to pause the video if it has been in view. */ const [hasBeenInView, setHasBeenInView] = useState(false); + const [hasBeenPlayed, setHasBeenPlayed] = useState(false); + const [hasTrackedPlay, setHasTrackedPlay] = useState(false); const [devicePixelRatio, setDevicePixelRatio] = useState(1); @@ -165,6 +177,12 @@ export const LoopVideo = ({ threshold: VISIBILITY_THRESHOLD, }); + const activeCue = useSubtitles({ + video: vidRef.current, + playerState, + currentTime, + }); + const playVideo = useCallback(async () => { const video = vidRef.current; if (!video) return; @@ -178,6 +196,7 @@ export const LoopVideo = ({ .then(() => { // Autoplay succeeded dispatchOphanAttentionEvent('videoPlaying'); + setHasBeenPlayed(true); setPlayerState('PLAYING'); }) .catch((error: Error) => { @@ -387,6 +406,19 @@ export const LoopVideo = ({ } }, [isInView, hasBeenInView, atomId, linkTo]); + /** + * Track the first successful video play in Ophan. + * + * This effect runs only after the video has actually started playing + * for the first time. This is to ensure we don't double-report the event. + */ + useEffect(() => { + if (!hasBeenPlayed || hasTrackedPlay) return; + + ophanTrackerWeb(atomId, 'loop')('play'); + setHasTrackedPlay(true); + }, [atomId, hasBeenPlayed, hasTrackedPlay]); + /** * Handle play/pause, when instigated by the browser. */ @@ -416,14 +448,6 @@ export const LoopVideo = ({ playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' || (hasPageBecomeActive && playerState === 'PAUSED_BY_BROWSER')) ) { - /** - * Check if the video has not been in view before tracking the play. - * This is so we only track the first play. - */ - if (!hasBeenInView) { - ophanTrackerWeb(atomId, 'loop')('play'); - } - setHasPageBecomeActive(false); void playVideo(); } @@ -482,6 +506,25 @@ export const LoopVideo = ({ return FallbackImageComponent; } + const handleLoadedMetadata = () => { + const video = vidRef.current; + if (!video) return; + + const track = video.textTracks[0]; + if (!track?.cues) return; + const pxFromBottom = space[3]; + const videoHeight = video.getBoundingClientRect().height; + const percentFromTop = + ((videoHeight - pxFromBottom) / videoHeight) * 100; + + for (const cue of Array.from(track.cues)) { + if (cue instanceof VTTCue) { + cue.snapToLines = false; + cue.line = percentFromTop; + } + } + }; + const handleLoadedData = () => { if (vidRef.current) { setHasAudio(doesVideoHaveAudio(vidRef.current)); @@ -621,6 +664,7 @@ export const LoopVideo = ({ isPlayable={isPlayable} playerState={playerState} isMuted={isMuted} + handleLoadedMetadata={handleLoadedMetadata} handleLoadedData={handleLoadedData} handleCanPlay={handleCanPlay} handlePlayPauseClick={handlePlayPauseClick} @@ -631,6 +675,9 @@ export const LoopVideo = ({ AudioIcon={hasAudio ? AudioIcon : null} preloadPartialData={preloadPartialData} showPlayIcon={showPlayIcon} + subtitleSource={subtitleSource} + subtitleSize={subtitleSize} + activeCue={activeCue} /> ); From 3af9a68f3b9a3fec0910e21c7d1d93c3982d79ea Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Tue, 18 Nov 2025 16:44:06 +0000 Subject: [PATCH 10/13] Update loop test to use videoPlayerFormat attribute --- dotcom-rendering/src/lib/renderElement.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index bdf9447f5bd..f3b646ae14e 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -501,18 +501,14 @@ export const renderElement = ({ - 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 new attributes - - Note: we'll probably extend this functionality to handle new 'cenemagraph' videos + - 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 - - TESTING - - While waiting for the new parameters, we can test: - `2 + 2 === 5` - current behaviour - `VideoAtom` - `2 + 2 === 4` - display in the looping video player */ - if (2 + 2 === 5) { + if (element.videoPlayerFormat === 'Loop') { const updatedSources = element.assets.map((a) => ({ src: a.src || a.url, mimeType: a.mimeType, From 503658cf73433a5ed1108e722744fb6ff47f975f Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Wed, 19 Nov 2025 16:49:30 +0000 Subject: [PATCH 11/13] Fix linting etc --- .../LoopVideoInArticle.importable.tsx | 55 +++++++++++++------ dotcom-rendering/src/components/VideoAtom.tsx | 11 ++-- dotcom-rendering/src/lib/renderElement.tsx | 22 ++++---- dotcom-rendering/src/types/content.ts | 10 +++- 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx index 39a53c67944..02cff3bffda 100644 --- a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx +++ b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx @@ -1,11 +1,13 @@ -import { LoopVideo } from './LoopVideo.importable'; 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 type { Source } from '../lib/video'; +import { LoopVideo } from './LoopVideo.importable'; import type { SubtitleSize } from './LoopVideoPlayer'; type LoopVideoInArticleProps = { + assets: VideoAssets[]; atomId: string; caption?: string; fallbackImage: CardPictureProps['mainImage']; @@ -18,14 +20,36 @@ type LoopVideoInArticleProps = { isMainMedia: boolean; linkTo: string; posterImage: string; - sources: Source[]; uniqueId: string; width: number; subtitleSource?: string; - subtitleSize: SubtitleSize; + subtitleSize?: string; +}; + +// 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 convertSubtitleSize = (val?: string): SubtitleSize => { + if (val == null) { + return 'small' as SubtitleSize; + } + if (['small', 'medium', 'large'].includes(val)) { + return val as SubtitleSize; + } + return 'small' as SubtitleSize; }; export const LoopVideoInArticle = ({ + assets, atomId, caption, fallbackImage, @@ -38,36 +62,35 @@ export const LoopVideoInArticle = ({ isMainMedia, linkTo, posterImage, - sources, + subtitleSize, + subtitleSource, uniqueId, width = 500, - subtitleSource, - subtitleSize, }: LoopVideoInArticleProps) => { return ( <> {!!caption && ( )} diff --git a/dotcom-rendering/src/components/VideoAtom.tsx b/dotcom-rendering/src/components/VideoAtom.tsx index d1a858f7e32..335804301ce 100644 --- a/dotcom-rendering/src/components/VideoAtom.tsx +++ b/dotcom-rendering/src/components/VideoAtom.tsx @@ -1,15 +1,16 @@ import type { ArticleFormat } from '../lib/articleFormat'; +import type { VideoAssets } from '../types/content'; import { Caption } from './Caption'; import { MaintainAspectRatio } from './MaintainAspectRatio'; -type AssetType = { - url: string; - mimeType?: string; -}; +// type AssetType = { +// url: string; +// mimeType?: string; +// }; interface Props { format: ArticleFormat; - assets: AssetType[]; + assets: VideoAssets[]; isMainMedia: boolean; poster?: string; caption?: string; diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index f3b646ae14e..f10cb3bc66a 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -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'; @@ -74,8 +75,6 @@ import { ArticleDesign, type ArticleFormat } from './articleFormat'; import type { EditionId } from './edition'; import { getLargestImageSize } from './image'; -import { LoopVideoInArticle } from '../components/LoopVideoInArticle.importable'; - type Props = { format: ArticleFormat; element: FEElement; @@ -508,27 +507,26 @@ export const renderElement = ({ - But they will still be Media Atoms */ - if (element.videoPlayerFormat === 'Loop') { - const updatedSources = element.assets.map((a) => ({ - src: a.src || a.url, - mimeType: a.mimeType, - })); + if (element?.videoPlayerFormat === 'Loop') { + const posterImageExists = !!element.posterImage?.[0]?.url; return ( <> - {element.posterImage?.[0]?.url && ( + {posterImageExists && ( )} diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts index bb944684a53..edcd044e98a 100644 --- a/dotcom-rendering/src/types/content.ts +++ b/dotcom-rendering/src/types/content.ts @@ -422,6 +422,9 @@ interface LoopVideoInArticleElement { }[]; title?: string; duration?: number; + videoPlayerFormat?: string; + subtitleSize?: string; + subtitleSource?: string; } export interface MapBlockElement extends ThirdPartyEmbeddedContent { @@ -447,6 +450,9 @@ interface MediaAtomBlockElement { }[]; title?: string; duration?: number; + videoPlayerFormat?: string; + subtitleSize?: string; + subtitleSource?: string; } export interface MultiImageBlockElement { @@ -953,8 +959,8 @@ export interface Image { url: string; } -interface VideoAssets { - url: string; +export interface VideoAssets { + url?: string; mimeType?: string; fields?: { source?: string; From aa2c34cd941dbab5c03fcced78b993122efb6cca Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Fri, 21 Nov 2025 11:37:34 +0000 Subject: [PATCH 12/13] Fixing remaining issues --- .../LoopVideoInArticle.importable.tsx | 21 +++---- dotcom-rendering/src/components/VideoAtom.tsx | 11 ++-- .../src/frontend/schemas/feArticle.json | 57 +------------------ dotcom-rendering/src/lib/renderElement.tsx | 4 +- dotcom-rendering/src/model/block-schema.json | 57 +------------------ dotcom-rendering/src/types/content.ts | 37 ++++++------ 6 files changed, 37 insertions(+), 150 deletions(-) diff --git a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx index 02cff3bffda..5295b989be7 100644 --- a/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx +++ b/dotcom-rendering/src/components/LoopVideoInArticle.importable.tsx @@ -22,8 +22,6 @@ type LoopVideoInArticleProps = { posterImage: string; uniqueId: string; width: number; - subtitleSource?: string; - subtitleSize?: string; }; // The looping video player types its `sources` attribute as `Sources` @@ -38,14 +36,11 @@ const convertAssetsToSources = (assets: VideoAssets[]): Source[] => { }); }; -const convertSubtitleSize = (val?: string): SubtitleSize => { - if (val == null) { - return 'small' as SubtitleSize; - } - if (['small', 'medium', 'large'].includes(val)) { - return val as SubtitleSize; - } - return 'small' as SubtitleSize; +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 = ({ @@ -62,8 +57,6 @@ export const LoopVideoInArticle = ({ isMainMedia, linkTo, posterImage, - subtitleSize, - subtitleSource, uniqueId, width = 500, }: LoopVideoInArticleProps) => { @@ -80,8 +73,8 @@ export const LoopVideoInArticle = ({ linkTo={linkTo} posterImage={posterImage} sources={convertAssetsToSources(assets)} - subtitleSize={convertSubtitleSize(subtitleSize)} - subtitleSource={subtitleSource} + subtitleSize={'small' as SubtitleSize} + subtitleSource={getSubtitleAsset(assets)} uniqueId={uniqueId} width={width} /> diff --git a/dotcom-rendering/src/components/VideoAtom.tsx b/dotcom-rendering/src/components/VideoAtom.tsx index 335804301ce..d1a858f7e32 100644 --- a/dotcom-rendering/src/components/VideoAtom.tsx +++ b/dotcom-rendering/src/components/VideoAtom.tsx @@ -1,16 +1,15 @@ import type { ArticleFormat } from '../lib/articleFormat'; -import type { VideoAssets } from '../types/content'; import { Caption } from './Caption'; import { MaintainAspectRatio } from './MaintainAspectRatio'; -// type AssetType = { -// url: string; -// mimeType?: string; -// }; +type AssetType = { + url: string; + mimeType?: string; +}; interface Props { format: ArticleFormat; - assets: VideoAssets[]; + assets: AssetType[]; isMainMedia: boolean; poster?: string; caption?: string; diff --git a/dotcom-rendering/src/frontend/schemas/feArticle.json b/dotcom-rendering/src/frontend/schemas/feArticle.json index 7ecb5c2e81a..e10713b4a91 100644 --- a/dotcom-rendering/src/frontend/schemas/feArticle.json +++ b/dotcom-rendering/src/frontend/schemas/feArticle.json @@ -680,9 +680,6 @@ { "$ref": "#/definitions/ListBlockElement" }, - { - "$ref": "#/definitions/LoopVideoInArticleElement" - }, { "$ref": "#/definitions/MapBlockElement" }, @@ -2607,57 +2604,6 @@ "elements" ] }, - "LoopVideoInArticleElement": { - "type": "object", - "properties": { - "_type": { - "type": "string", - "const": "model.dotcomrendering.pageElements.MediaAtomBlockElement" - }, - "elementId": { - "type": "string" - }, - "id": { - "type": "string" - }, - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/VideoAssets" - } - }, - "posterImage": { - "type": "array", - "items": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "width": { - "type": "number" - } - }, - "required": [ - "url", - "width" - ] - } - }, - "title": { - "type": "string" - }, - "duration": { - "type": "number" - } - }, - "required": [ - "_type", - "assets", - "elementId", - "id" - ] - }, "MapBlockElement": { "type": "object", "properties": { @@ -2752,6 +2698,9 @@ }, "duration": { "type": "number" + }, + "videoPlayerFormat": { + "type": "string" } }, "required": [ diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index f10cb3bc66a..5e0663641d5 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -519,7 +519,7 @@ export const renderElement = ({ )} diff --git a/dotcom-rendering/src/model/block-schema.json b/dotcom-rendering/src/model/block-schema.json index 85ab2258baf..3695040c4cf 100644 --- a/dotcom-rendering/src/model/block-schema.json +++ b/dotcom-rendering/src/model/block-schema.json @@ -168,9 +168,6 @@ { "$ref": "#/definitions/ListBlockElement" }, - { - "$ref": "#/definitions/LoopVideoInArticleElement" - }, { "$ref": "#/definitions/MapBlockElement" }, @@ -2095,57 +2092,6 @@ "elements" ] }, - "LoopVideoInArticleElement": { - "type": "object", - "properties": { - "_type": { - "type": "string", - "const": "model.dotcomrendering.pageElements.MediaAtomBlockElement" - }, - "elementId": { - "type": "string" - }, - "id": { - "type": "string" - }, - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/VideoAssets" - } - }, - "posterImage": { - "type": "array", - "items": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "width": { - "type": "number" - } - }, - "required": [ - "url", - "width" - ] - } - }, - "title": { - "type": "string" - }, - "duration": { - "type": "number" - } - }, - "required": [ - "_type", - "assets", - "elementId", - "id" - ] - }, "MapBlockElement": { "type": "object", "properties": { @@ -2240,6 +2186,9 @@ }, "duration": { "type": "number" + }, + "videoPlayerFormat": { + "type": "string" } }, "required": [ diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts index edcd044e98a..69422cc63a9 100644 --- a/dotcom-rendering/src/types/content.ts +++ b/dotcom-rendering/src/types/content.ts @@ -411,21 +411,21 @@ export interface ListBlockElement { elementId: string; } -interface LoopVideoInArticleElement { - _type: 'model.dotcomrendering.pageElements.MediaAtomBlockElement'; - elementId: string; - id: string; - assets: VideoAssets[]; - posterImage?: { - url: string; - width: number; - }[]; - title?: string; - duration?: number; - videoPlayerFormat?: string; - subtitleSize?: string; - subtitleSource?: string; -} +// interface LoopVideoInArticleElement { +// _type: 'model.dotcomrendering.pageElements.MediaAtomBlockElement'; +// elementId: string; +// id: string; +// assets: VideoAssets[]; +// posterImage?: { +// url: string; +// width: number; +// }[]; +// title?: string; +// duration?: number; +// videoPlayerFormat?: string; +// subtitleSize?: string; +// subtitleSource?: string; +// } export interface MapBlockElement extends ThirdPartyEmbeddedContent { _type: 'model.dotcomrendering.pageElements.MapBlockElement'; @@ -451,8 +451,8 @@ interface MediaAtomBlockElement { title?: string; duration?: number; videoPlayerFormat?: string; - subtitleSize?: string; - subtitleSource?: string; + // subtitleSize?: string; + // subtitleSource?: string; } export interface MultiImageBlockElement { @@ -838,7 +838,6 @@ export type FEElement = | KeyTakeawaysBlockElement | LinkBlockElement | ListBlockElement - | LoopVideoInArticleElement | MapBlockElement | MediaAtomBlockElement | MiniProfilesBlockElement @@ -960,7 +959,7 @@ export interface Image { } export interface VideoAssets { - url?: string; + url: string; mimeType?: string; fields?: { source?: string; From 41b8cffc7cb377c45633273ee9f48371583e64fe Mon Sep 17 00:00:00 2001 From: Rik Roots Date: Fri, 21 Nov 2025 11:39:56 +0000 Subject: [PATCH 13/13] Remove commented out lines --- dotcom-rendering/src/types/content.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts index 69422cc63a9..e3dc090487b 100644 --- a/dotcom-rendering/src/types/content.ts +++ b/dotcom-rendering/src/types/content.ts @@ -411,22 +411,6 @@ export interface ListBlockElement { elementId: string; } -// interface LoopVideoInArticleElement { -// _type: 'model.dotcomrendering.pageElements.MediaAtomBlockElement'; -// elementId: string; -// id: string; -// assets: VideoAssets[]; -// posterImage?: { -// url: string; -// width: number; -// }[]; -// title?: string; -// duration?: number; -// videoPlayerFormat?: string; -// subtitleSize?: string; -// subtitleSource?: string; -// } - export interface MapBlockElement extends ThirdPartyEmbeddedContent { _type: 'model.dotcomrendering.pageElements.MapBlockElement'; elementId: string; @@ -451,8 +435,6 @@ interface MediaAtomBlockElement { title?: string; duration?: number; videoPlayerFormat?: string; - // subtitleSize?: string; - // subtitleSource?: string; } export interface MultiImageBlockElement {