Skip to content

Commit d39df4b

Browse files
authored
Merge branch 'main' into jm/feat-sign-in-gate-v2
2 parents cf8e58a + 100ec9d commit d39df4b

File tree

13 files changed

+184
-46
lines changed

13 files changed

+184
-46
lines changed

dotcom-rendering/fixtures/manual/trails.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,8 +613,12 @@ export const loopVideoCard: DCRFrontCard = {
613613
mainMedia: {
614614
type: 'LoopVideo',
615615
atomId: '3cb22b60-2c3f-48d6-8bce-38c956907cce',
616-
videoId:
617-
'https://uploads.guim.co.uk/2025%2F06%2F20%2Ftesting+only%2C+please+ignore--3cb22b60-2c3f-48d6-8bce-38c956907cce-3.mp4',
616+
sources: [
617+
{
618+
mimeType: 'video/mp4',
619+
src: 'https://uploads.guim.co.uk/2025%2F06%2F20%2Ftesting+only%2C+please+ignore--3cb22b60-2c3f-48d6-8bce-38c956907cce-3.mp4',
620+
},
621+
],
618622
duration: 0,
619623
width: 500,
620624
height: 400,

dotcom-rendering/src/components/Card/Card.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,7 @@ export const Card = ({
851851
defer={{ until: 'visible' }}
852852
>
853853
<LoopVideo
854-
src={media.mainMedia.videoId}
854+
sources={media.mainMedia.sources}
855855
atomId={media.mainMedia.atomId}
856856
uniqueId={uniqueId}
857857
height={media.mainMedia.height}

dotcom-rendering/src/components/LoopVideo.importable.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import { getZIndex } from '../lib/getZIndex';
1111
import { generateImageURL } from '../lib/image';
1212
import { useIsInView } from '../lib/useIsInView';
1313
import { useShouldAdapt } from '../lib/useShouldAdapt';
14-
import type { CustomPlayEventDetail } from '../lib/video';
14+
import type { CustomPlayEventDetail, Source } from '../lib/video';
1515
import {
1616
customLoopPlayAudioEventName,
1717
customYoutubePlayEventName,
1818
} from '../lib/video';
1919
import { CardPicture, type Props as CardPictureProps } from './CardPicture';
2020
import { useConfig } from './ConfigContext';
21-
import type { PLAYER_STATES, PlayerStates } from './LoopVideoPlayer';
2221
import { LoopVideoPlayer } from './LoopVideoPlayer';
22+
import type { PLAYER_STATES, PlayerStates } from './LoopVideoPlayer';
2323
import { ophanTrackerWeb } from './YoutubeAtom/eventEmitters';
2424

2525
const videoContainerStyles = css`
@@ -105,7 +105,7 @@ const doesVideoHaveAudio = (video: HTMLVideoElement): boolean => {
105105
};
106106

107107
type Props = {
108-
src: string;
108+
sources: Source[];
109109
atomId: string;
110110
uniqueId: string;
111111
height: number;
@@ -120,7 +120,7 @@ type Props = {
120120
};
121121

122122
export const LoopVideo = ({
123-
src,
123+
sources,
124124
atomId,
125125
uniqueId,
126126
height,
@@ -164,10 +164,11 @@ export const LoopVideo = ({
164164
});
165165

166166
const playVideo = useCallback(async () => {
167-
if (!vidRef.current) return;
167+
const video = vidRef.current;
168+
if (!video) return;
168169

169170
/** https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Autoplay#example_handling_play_failures */
170-
const startPlayPromise = vidRef.current.play();
171+
const startPlayPromise = video.play();
171172

172173
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- In earlier versions of the HTML specification, play() didn't return a value
173174
if (startPlayPromise !== undefined) {
@@ -179,12 +180,12 @@ export const LoopVideo = ({
179180
})
180181
.catch((error: Error) => {
181182
// Autoplay failed
182-
logAndReportError(src, error);
183+
logAndReportError(video.src, error);
183184
setShowPosterImage(true);
184185
setPlayerState('PAUSED_BY_BROWSER');
185186
});
186187
}
187-
}, [src]);
188+
}, []);
188189

189190
const pauseVideo = (
190191
reason: Extract<
@@ -529,7 +530,9 @@ export const LoopVideo = ({
529530
* Sentry and log in the console.
530531
*/
531532
const onError = () => {
532-
const message = `Loop video could not be played. source: ${src}`;
533+
const message = `Loop video could not be played. source: ${
534+
vidRef.current?.currentSrc ?? 'unknown'
535+
}`;
533536

534537
window.guardian.modules.sentry.reportError(
535538
new Error(message),
@@ -601,7 +604,7 @@ export const LoopVideo = ({
601604
data-component="gu-video-loop"
602605
>
603606
<LoopVideoPlayer
604-
src={src}
607+
sources={sources}
605608
atomId={atomId}
606609
uniqueId={uniqueId}
607610
width={width}

dotcom-rendering/src/components/LoopVideo.stories.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ type Story = StoryObj<typeof LoopVideo>;
2222
export const Default: Story = {
2323
name: 'Default',
2424
args: {
25-
src: 'https://uploads.guim.co.uk/2025%2F06%2F20%2Ftesting+only%2C+please+ignore--3cb22b60-2c3f-48d6-8bce-38c956907cce-3.mp4',
25+
sources: [
26+
{
27+
src: 'https://uploads.guim.co.uk/2025%2F06%2F20%2Ftesting+only%2C+please+ignore--3cb22b60-2c3f-48d6-8bce-38c956907cce-3.mp4',
28+
mimeType: 'video/mp4',
29+
},
30+
],
2631
uniqueId: 'test-video-1',
2732
atomId: 'test-atom-1',
2833
height: 720,
@@ -33,11 +38,33 @@ export const Default: Story = {
3338
},
3439
};
3540

41+
export const WithM3U8File: Story = {
42+
name: 'With M3U8 file',
43+
args: {
44+
...Default.args,
45+
sources: [
46+
{
47+
src: 'https://uploads.guimcode.co.uk/2025/09/01/Loop__Japan_fireball--ace3fcf6-1378-41db-9d21-f3fc07072ab2-1.10.m3u8',
48+
mimeType: 'application/x-mpegURL',
49+
},
50+
{
51+
src: 'https://uploads.guim.co.uk/2025%2F06%2F20%2Ftesting+only%2C+please+ignore--3cb22b60-2c3f-48d6-8bce-38c956907cce-3.mp4',
52+
mimeType: 'video/mp4',
53+
},
54+
],
55+
},
56+
};
57+
3658
export const Without5to4Ratio: Story = {
3759
name: 'Without 5:4 aspect ratio',
3860
args: {
3961
...Default.args,
40-
src: 'https://uploads.guim.co.uk/2024/10/01/241001HeleneLoop_2.mp4',
62+
sources: [
63+
{
64+
src: 'https://uploads.guim.co.uk/2024/10/01/241001HeleneLoop_2.mp4',
65+
mimeType: 'video/mp4',
66+
},
67+
],
4168
height: 1080,
4269
width: 1920,
4370
},

dotcom-rendering/src/components/LoopVideoPlayer.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
SyntheticEvent,
99
} from 'react';
1010
import { forwardRef } from 'react';
11+
import type { Source } from '../lib/video';
1112
import { palette } from '../palette';
1213
import { narrowPlayIconWidth, PlayIcon } from './Card/components/PlayIcon';
1314
import { LoopVideoProgressBar } from './LoopVideoProgressBar';
@@ -74,7 +75,7 @@ export const PLAYER_STATES = [
7475
export type PlayerStates = (typeof PLAYER_STATES)[number];
7576

7677
type Props = {
77-
src: string;
78+
sources: Source[];
7879
atomId: string;
7980
uniqueId: string;
8081
width: number;
@@ -105,7 +106,7 @@ type Props = {
105106
export const LoopVideoPlayer = forwardRef(
106107
(
107108
{
108-
src,
109+
sources,
109110
atomId,
110111
uniqueId,
111112
width,
@@ -170,9 +171,14 @@ export const LoopVideoPlayer = forwardRef(
170171
onKeyDown={handleKeyDown}
171172
onError={onError}
172173
>
173-
{/* Only mp4 is currently supported. Assumes the video file type is mp4. */}
174-
{/* The start time is set to 1ms so that Safari will autoplay the video */}
175-
<source src={`${src}#t=0.001`} type="video/mp4" />
174+
{sources.map((source) => (
175+
<source
176+
key={source.mimeType}
177+
/* The start time is set to 1ms so that Safari will autoplay the video */
178+
src={`${source.src}#t=0.001`}
179+
type={source.mimeType}
180+
/>
181+
))}
176182
{FallbackImageComponent}
177183
</video>
178184
{ref && 'current' in ref && ref.current && isPlayable && (
@@ -207,7 +213,7 @@ export const LoopVideoPlayer = forwardRef(
207213
>
208214
<div
209215
css={audioIconContainerStyles}
210-
data-testId={`${
216+
data-testid={`${
211217
isMuted ? 'unmute' : 'mute'
212218
}-icon`}
213219
>

dotcom-rendering/src/components/LoopVideoProgressBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const LoopVideoProgressBar = ({
4545

4646
/**
4747
* We achieve a smooth progress bar by using CSS transitions. Given that
48-
* onTimeUpdate firesevery 250ms or so, this means that the time on the
48+
* onTimeUpdate fires every 250ms or so, this means that the time on the
4949
* progress bar is always about 0.25s behind and begins 0.25s late.
5050
* Therefore, when calculating the progress percentage, we take 0.25s off the duration.
5151
*

dotcom-rendering/src/components/YoutubeBlockComponent.importable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,13 @@ export const YoutubeBlockComponent = ({
121121
// We need Video articles generated directly from Media Atom Maker
122122
// to always show their poster (16:9) image, but in other cases
123123
// use the override image (often supplied as 5:4 then cropped to 16:9)
124-
if (contentType && contentType.toLowerCase() === 'video') {
124+
if (contentType?.toLowerCase() === 'video') {
125125
return posterImage;
126126
}
127127

128128
// For Standard Articles with a Video atom for their main media
129129
// we need to display the poster image
130-
if (contentLayout && contentLayout.toLowerCase() === 'standardlayout') {
130+
if (contentLayout?.toLowerCase() === 'standardlayout') {
131131
return posterImage;
132132
}
133133

dotcom-rendering/src/frontend/schemas/feArticle.json

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5288,8 +5288,23 @@
52885288
"atomId": {
52895289
"type": "string"
52905290
},
5291-
"videoId": {
5292-
"type": "string"
5291+
"sources": {
5292+
"type": "array",
5293+
"items": {
5294+
"type": "object",
5295+
"properties": {
5296+
"src": {
5297+
"type": "string"
5298+
},
5299+
"mimeType": {
5300+
"$ref": "#/definitions/SupportedVideoFileType"
5301+
}
5302+
},
5303+
"required": [
5304+
"mimeType",
5305+
"src"
5306+
]
5307+
}
52935308
},
52945309
"height": {
52955310
"type": "number"
@@ -5308,13 +5323,21 @@
53085323
"atomId",
53095324
"duration",
53105325
"height",
5326+
"sources",
53115327
"type",
5312-
"videoId",
53135328
"width"
53145329
]
53155330
}
53165331
]
53175332
},
5333+
"SupportedVideoFileType": {
5334+
"enum": [
5335+
"application/vnd.apple.mpegurl",
5336+
"application/x-mpegURL",
5337+
"video/mp4"
5338+
],
5339+
"type": "string"
5340+
},
53185341
"Audio": {
53195342
"allOf": [
53205343
{

dotcom-rendering/src/frontend/schemas/feFront.json

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3922,8 +3922,23 @@
39223922
"atomId": {
39233923
"type": "string"
39243924
},
3925-
"videoId": {
3926-
"type": "string"
3925+
"sources": {
3926+
"type": "array",
3927+
"items": {
3928+
"type": "object",
3929+
"properties": {
3930+
"src": {
3931+
"type": "string"
3932+
},
3933+
"mimeType": {
3934+
"$ref": "#/definitions/SupportedVideoFileType"
3935+
}
3936+
},
3937+
"required": [
3938+
"mimeType",
3939+
"src"
3940+
]
3941+
}
39273942
},
39283943
"height": {
39293944
"type": "number"
@@ -3942,13 +3957,21 @@
39423957
"atomId",
39433958
"duration",
39443959
"height",
3960+
"sources",
39453961
"type",
3946-
"videoId",
39473962
"width"
39483963
]
39493964
}
39503965
]
39513966
},
3967+
"SupportedVideoFileType": {
3968+
"enum": [
3969+
"application/vnd.apple.mpegurl",
3970+
"application/x-mpegURL",
3971+
"video/mp4"
3972+
],
3973+
"type": "string"
3974+
},
39523975
"Audio": {
39533976
"allOf": [
39543977
{

dotcom-rendering/src/lib/video.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,19 @@ export type CustomPlayEventDetail = { uniqueId: string };
22

33
export const customLoopPlayAudioEventName = 'looping-video:play-with-audio';
44
export const customYoutubePlayEventName = 'youtube-video:play';
5+
6+
export type Source = {
7+
src: string;
8+
mimeType: SupportedVideoFileType;
9+
};
10+
11+
/**
12+
* Order is important here - the browser will use the first type it supports.
13+
*/
14+
export const supportedVideoFileTypes = [
15+
'application/x-mpegURL', // HLS format
16+
'application/vnd.apple.mpegurl', // Alternative HLS format
17+
'video/mp4', // MP4 format
18+
] as const;
19+
20+
export type SupportedVideoFileType = (typeof supportedVideoFileTypes)[number];

0 commit comments

Comments
 (0)