1
1
import { css } from '@emotion/react' ;
2
2
import { log } from '@guardian/libs' ;
3
3
import { SvgAudio , SvgAudioMute } from '@guardian/source/react-components' ;
4
- import { useEffect , useRef , useState } from 'react' ;
4
+ import { useCallback , useEffect , useRef , useState } from 'react' ;
5
5
import { submitClickComponentEvent } from '../client/ophan/ophan' ;
6
6
import { getZIndex } from '../lib/getZIndex' ;
7
7
import { useIsInView } from '../lib/useIsInView' ;
@@ -12,7 +12,7 @@ import {
12
12
customYoutubePlayEventName ,
13
13
} from '../lib/video' ;
14
14
import { useConfig } from './ConfigContext' ;
15
- import type { PLAYER_STATES } from './LoopVideoPlayer' ;
15
+ import type { PLAYER_STATES , PlayerStates } from './LoopVideoPlayer' ;
16
16
import { LoopVideoPlayer } from './LoopVideoPlayer' ;
17
17
18
18
const videoContainerStyles = css `
@@ -80,6 +80,63 @@ export const LoopVideo = ({
80
80
threshold : 0.5 ,
81
81
} ) ;
82
82
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
+
83
140
/**
84
141
* Setup.
85
142
*
@@ -161,10 +218,9 @@ export const LoopVideo = ({
161
218
( playerState === 'NOT_STARTED' ||
162
219
playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' )
163
220
) {
164
- setPlayerState ( 'PLAYING' ) ;
165
- void vidRef . current . play ( ) ;
221
+ void playVideo ( ) ;
166
222
}
167
- } , [ isInView , isPlayable , playerState , isAutoplayAllowed ] ) ;
223
+ } , [ isAutoplayAllowed , isInView , isPlayable , playerState , playVideo ] ) ;
168
224
169
225
/**
170
226
* Stops playback when the video is scrolled out of view, resumes playbacks
@@ -176,9 +232,7 @@ export const LoopVideo = ({
176
232
const isNoLongerInView =
177
233
playerState === 'PLAYING' && isInView === false ;
178
234
if ( isNoLongerInView ) {
179
- setPlayerState ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
180
- void vidRef . current . pause ( ) ;
181
- setIsMuted ( true ) ;
235
+ pauseVideo ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
182
236
}
183
237
184
238
/**
@@ -189,11 +243,9 @@ export const LoopVideo = ({
189
243
const isBackInView =
190
244
playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' && isInView ;
191
245
if ( isBackInView ) {
192
- setPlayerState ( 'PLAYING' ) ;
193
-
194
- void vidRef . current . play ( ) ;
246
+ void playVideo ( ) ;
195
247
}
196
- } , [ isInView , hasBeenInView , playerState ] ) ;
248
+ } , [ isInView , hasBeenInView , playerState , playVideo ] ) ;
197
249
198
250
/**
199
251
* Show the play icon when the video is not playing, except for when it is scrolled
@@ -237,30 +289,6 @@ export const LoopVideo = ({
237
289
238
290
if ( adapted ) return fallbackImageComponent ;
239
291
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
-
264
292
const handlePlayPauseClick = ( event : React . SyntheticEvent ) => {
265
293
event . preventDefault ( ) ;
266
294
playPauseVideo ( ) ;
@@ -280,12 +308,18 @@ export const LoopVideo = ({
280
308
}
281
309
} ;
282
310
311
+ /**
312
+ * If the video could not be loaded due to an error, report to
313
+ * Sentry and log in the console.
314
+ */
283
315
const onError = ( ) => {
316
+ const message = `Loop video could not be played. source: ${ src } ` ;
317
+
284
318
window . guardian . modules . sentry . reportError (
285
- new Error ( `Loop video could not be played. source: ${ src } ` ) ,
319
+ new Error ( message ) ,
286
320
'loop-video' ,
287
321
) ;
288
- log ( 'dotcom' , `Loop video could not be played. source: ${ src } ` ) ;
322
+ log ( 'dotcom' , message ) ;
289
323
} ;
290
324
291
325
const seekForward = ( ) => {
@@ -323,7 +357,7 @@ export const LoopVideo = ({
323
357
playPauseVideo ( ) ;
324
358
break ;
325
359
case 'Escape' :
326
- pauseVideo ( ) ;
360
+ pauseVideo ( 'PAUSED_BY_USER' ) ;
327
361
break ;
328
362
case 'ArrowRight' :
329
363
seekForward ( ) ;
0 commit comments