1
1
import { css } from '@emotion/react' ;
2
- import { log } from '@guardian/libs' ;
2
+ import { log , storage } 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,11 +80,68 @@ 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
*
86
- * Register the users motion preferences.
87
- * Creates event listeners to control playback when there are multiple videos.
143
+ * 1. Register the user's motion preferences.
144
+ * 2. Creates event listeners to control playback when there are multiple videos.
88
145
*/
89
146
useEffect ( ( ) => {
90
147
/**
@@ -93,7 +150,19 @@ export const LoopVideo = ({
93
150
const userPrefersReducedMotion = window . matchMedia (
94
151
'(prefers-reduced-motion: reduce)' ,
95
152
) . matches ;
96
- setIsAutoplayAllowed ( ! userPrefersReducedMotion ) ;
153
+
154
+ /**
155
+ * The user indicates a preference for no flashing elements.
156
+ * `flashingPreference` is `null` if no preference exists and
157
+ * explicitly `false` when the reader has said they don't want flashing.
158
+ */
159
+ const flashingPreferences = storage . local . get (
160
+ 'gu.prefs.accessibility.flashing-elements' ,
161
+ ) ;
162
+
163
+ setIsAutoplayAllowed (
164
+ ! userPrefersReducedMotion && flashingPreferences !== false ,
165
+ ) ;
97
166
98
167
/**
99
168
* Mutes the current video when another video is unmuted
@@ -113,7 +182,7 @@ export const LoopVideo = ({
113
182
} ;
114
183
115
184
/**
116
- * Mute the current video when a Youtube video is played
185
+ * Mute the current video when a YouTube video is played
117
186
* Triggered by the CustomEvent in YoutubeAtomPlayer.
118
187
*/
119
188
const handleCustomPlayYoutubeEvent = ( ) => {
@@ -161,10 +230,9 @@ export const LoopVideo = ({
161
230
( playerState === 'NOT_STARTED' ||
162
231
playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' )
163
232
) {
164
- setPlayerState ( 'PLAYING' ) ;
165
- void vidRef . current . play ( ) ;
233
+ void playVideo ( ) ;
166
234
}
167
- } , [ isInView , isPlayable , playerState , isAutoplayAllowed ] ) ;
235
+ } , [ isAutoplayAllowed , isInView , isPlayable , playerState , playVideo ] ) ;
168
236
169
237
/**
170
238
* Stops playback when the video is scrolled out of view, resumes playbacks
@@ -176,9 +244,7 @@ export const LoopVideo = ({
176
244
const isNoLongerInView =
177
245
playerState === 'PLAYING' && isInView === false ;
178
246
if ( isNoLongerInView ) {
179
- setPlayerState ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
180
- void vidRef . current . pause ( ) ;
181
- setIsMuted ( true ) ;
247
+ pauseVideo ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
182
248
}
183
249
184
250
/**
@@ -189,11 +255,9 @@ export const LoopVideo = ({
189
255
const isBackInView =
190
256
playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' && isInView ;
191
257
if ( isBackInView ) {
192
- setPlayerState ( 'PLAYING' ) ;
193
-
194
- void vidRef . current . play ( ) ;
258
+ void playVideo ( ) ;
195
259
}
196
- } , [ isInView , hasBeenInView , playerState ] ) ;
260
+ } , [ isInView , hasBeenInView , playerState , playVideo ] ) ;
197
261
198
262
/**
199
263
* Show the play icon when the video is not playing, except for when it is scrolled
@@ -237,30 +301,6 @@ export const LoopVideo = ({
237
301
238
302
if ( adapted ) return fallbackImageComponent ;
239
303
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
304
const handlePlayPauseClick = ( event : React . SyntheticEvent ) => {
265
305
event . preventDefault ( ) ;
266
306
playPauseVideo ( ) ;
@@ -280,12 +320,18 @@ export const LoopVideo = ({
280
320
}
281
321
} ;
282
322
323
+ /**
324
+ * If the video could not be loaded due to an error, report to
325
+ * Sentry and log in the console.
326
+ */
283
327
const onError = ( ) => {
328
+ const message = `Loop video could not be played. source: ${ src } ` ;
329
+
284
330
window . guardian . modules . sentry . reportError (
285
- new Error ( `Loop video could not be played. source: ${ src } ` ) ,
331
+ new Error ( message ) ,
286
332
'loop-video' ,
287
333
) ;
288
- log ( 'dotcom' , `Loop video could not be played. source: ${ src } ` ) ;
334
+ log ( 'dotcom' , message ) ;
289
335
} ;
290
336
291
337
const seekForward = ( ) => {
@@ -323,7 +369,7 @@ export const LoopVideo = ({
323
369
playPauseVideo ( ) ;
324
370
break ;
325
371
case 'Escape' :
326
- pauseVideo ( ) ;
372
+ pauseVideo ( 'PAUSED_BY_USER' ) ;
327
373
break ;
328
374
case 'ArrowRight' :
329
375
seekForward ( ) ;
0 commit comments