Skip to content

Commit 38959a1

Browse files
authored
Merge pull request #14201 from guardian/doml/loop-video-no-autoplay
Log when autoplay fails
2 parents 7cfed81 + 5977b3c commit 38959a1

File tree

3 files changed

+78
-42
lines changed

3 files changed

+78
-42
lines changed

dotcom-rendering/src/components/FeatureCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@ export type Props = {
327327
collectionId: number;
328328
isNewsletter?: boolean;
329329
/**
330-
* An immersive feature card variant. It dictates that the card has a full width background image on all breakpoints. It also dictates the the card change aspect ratio to 5:3 on desktop and 4:5 on mobile.
331-
*
330+
* An immersive feature card variant. It dictates that the card has a full width background image on
331+
* all breakpoints. It also dictates the the card change aspect ratio to 5:3 on desktop and 4:5 on mobile.
332332
*/
333333
isImmersive?: boolean;
334334
showVideo?: boolean;

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

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { css } from '@emotion/react';
22
import { log } from '@guardian/libs';
33
import { SvgAudio, SvgAudioMute } from '@guardian/source/react-components';
4-
import { useEffect, useRef, useState } from 'react';
4+
import { useCallback, useEffect, useRef, useState } from 'react';
55
import { submitClickComponentEvent } from '../client/ophan/ophan';
66
import { getZIndex } from '../lib/getZIndex';
77
import { useIsInView } from '../lib/useIsInView';
@@ -12,7 +12,7 @@ import {
1212
customYoutubePlayEventName,
1313
} from '../lib/video';
1414
import { useConfig } from './ConfigContext';
15-
import type { PLAYER_STATES } from './LoopVideoPlayer';
15+
import type { PLAYER_STATES, PlayerStates } from './LoopVideoPlayer';
1616
import { LoopVideoPlayer } from './LoopVideoPlayer';
1717

1818
const videoContainerStyles = css`
@@ -80,6 +80,63 @@ export const LoopVideo = ({
8080
threshold: 0.5,
8181
});
8282

83+
const playVideo = useCallback(async () => {
84+
if (!vidRef.current) return;
85+
86+
/** https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Autoplay#example_handling_play_failures */
87+
const startPlayPromise = vidRef.current.play();
88+
89+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- In earlier versions of the HTML specification, play() didn't return a value
90+
if (startPlayPromise !== undefined) {
91+
await startPlayPromise
92+
.catch((error) => {
93+
// Autoplay failed
94+
const message = `Autoplay failure for loop video. Source: ${src} could not be played. Error: ${error}`;
95+
if (error instanceof Error) {
96+
window.guardian.modules.sentry.reportError(
97+
new Error(message),
98+
'loop-video',
99+
);
100+
}
101+
102+
log('dotcom', message);
103+
104+
setPosterImage(image);
105+
setShowPlayIcon(true);
106+
})
107+
.then(() => {
108+
// Autoplay succeeded
109+
setPlayerState('PLAYING');
110+
});
111+
}
112+
}, [src, image]);
113+
114+
const pauseVideo = (
115+
reason: Extract<
116+
PlayerStates,
117+
'PAUSED_BY_USER' | 'PAUSED_BY_INTERSECTION_OBSERVER'
118+
>,
119+
) => {
120+
if (!vidRef.current) return;
121+
122+
if (reason === 'PAUSED_BY_INTERSECTION_OBSERVER') {
123+
setIsMuted(true);
124+
}
125+
126+
setPlayerState(reason);
127+
void vidRef.current.pause();
128+
};
129+
130+
const playPauseVideo = () => {
131+
if (playerState === 'PLAYING') {
132+
if (isInView) {
133+
pauseVideo('PAUSED_BY_USER');
134+
}
135+
} else {
136+
void playVideo();
137+
}
138+
};
139+
83140
/**
84141
* Setup.
85142
*
@@ -161,10 +218,9 @@ export const LoopVideo = ({
161218
(playerState === 'NOT_STARTED' ||
162219
playerState === 'PAUSED_BY_INTERSECTION_OBSERVER')
163220
) {
164-
setPlayerState('PLAYING');
165-
void vidRef.current.play();
221+
void playVideo();
166222
}
167-
}, [isInView, isPlayable, playerState, isAutoplayAllowed]);
223+
}, [isAutoplayAllowed, isInView, isPlayable, playerState, playVideo]);
168224

169225
/**
170226
* Stops playback when the video is scrolled out of view, resumes playbacks
@@ -176,9 +232,7 @@ export const LoopVideo = ({
176232
const isNoLongerInView =
177233
playerState === 'PLAYING' && isInView === false;
178234
if (isNoLongerInView) {
179-
setPlayerState('PAUSED_BY_INTERSECTION_OBSERVER');
180-
void vidRef.current.pause();
181-
setIsMuted(true);
235+
pauseVideo('PAUSED_BY_INTERSECTION_OBSERVER');
182236
}
183237

184238
/**
@@ -189,11 +243,9 @@ export const LoopVideo = ({
189243
const isBackInView =
190244
playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' && isInView;
191245
if (isBackInView) {
192-
setPlayerState('PLAYING');
193-
194-
void vidRef.current.play();
246+
void playVideo();
195247
}
196-
}, [isInView, hasBeenInView, playerState]);
248+
}, [isInView, hasBeenInView, playerState, playVideo]);
197249

198250
/**
199251
* Show the play icon when the video is not playing, except for when it is scrolled
@@ -237,30 +289,6 @@ export const LoopVideo = ({
237289

238290
if (adapted) return fallbackImageComponent;
239291

240-
const playVideo = () => {
241-
if (!vidRef.current) return;
242-
243-
setPlayerState('PLAYING');
244-
void vidRef.current.play();
245-
};
246-
247-
const pauseVideo = () => {
248-
if (!vidRef.current) return;
249-
250-
setPlayerState('PAUSED_BY_USER');
251-
void vidRef.current.pause();
252-
};
253-
254-
const playPauseVideo = () => {
255-
if (playerState === 'PLAYING') {
256-
if (isInView) {
257-
pauseVideo();
258-
}
259-
} else {
260-
playVideo();
261-
}
262-
};
263-
264292
const handlePlayPauseClick = (event: React.SyntheticEvent) => {
265293
event.preventDefault();
266294
playPauseVideo();
@@ -280,12 +308,18 @@ export const LoopVideo = ({
280308
}
281309
};
282310

311+
/**
312+
* If the video could not be loaded due to an error, report to
313+
* Sentry and log in the console.
314+
*/
283315
const onError = () => {
316+
const message = `Loop video could not be played. source: ${src}`;
317+
284318
window.guardian.modules.sentry.reportError(
285-
new Error(`Loop video could not be played. source: ${src}`),
319+
new Error(message),
286320
'loop-video',
287321
);
288-
log('dotcom', `Loop video could not be played. source: ${src}`);
322+
log('dotcom', message);
289323
};
290324

291325
const seekForward = () => {
@@ -323,7 +357,7 @@ export const LoopVideo = ({
323357
playPauseVideo();
324358
break;
325359
case 'Escape':
326-
pauseVideo();
360+
pauseVideo('PAUSED_BY_USER');
327361
break;
328362
case 'ArrowRight':
329363
seekForward();

dotcom-rendering/src/components/LoopVideoPlayer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export const PLAYER_STATES = [
5858
'PAUSED_BY_INTERSECTION_OBSERVER',
5959
] as const;
6060

61+
export type PlayerStates = (typeof PLAYER_STATES)[number];
62+
6163
type Props = {
6264
src: string;
6365
atomId: string;
@@ -67,7 +69,7 @@ type Props = {
6769
fallbackImageComponent: JSX.Element;
6870
isPlayable: boolean;
6971
setIsPlayable: Dispatch<SetStateAction<boolean>>;
70-
playerState: (typeof PLAYER_STATES)[number];
72+
playerState: PlayerStates;
7173
currentTime: number;
7274
setCurrentTime: Dispatch<SetStateAction<number>>;
7375
isMuted: boolean;

0 commit comments

Comments
 (0)