11import { css } from '@emotion/react' ;
22import { log } from '@guardian/libs' ;
33import { SvgAudio , SvgAudioMute } from '@guardian/source/react-components' ;
4- import { useEffect , useRef , useState } from 'react' ;
4+ import { useCallback , useEffect , useRef , useState } from 'react' ;
55import { submitClickComponentEvent } from '../client/ophan/ophan' ;
66import { getZIndex } from '../lib/getZIndex' ;
77import { useIsInView } from '../lib/useIsInView' ;
@@ -12,7 +12,7 @@ import {
1212 customYoutubePlayEventName ,
1313} from '../lib/video' ;
1414import { useConfig } from './ConfigContext' ;
15- import type { PLAYER_STATES } from './LoopVideoPlayer' ;
15+ import type { PLAYER_STATES , PlayerStates } from './LoopVideoPlayer' ;
1616import { LoopVideoPlayer } from './LoopVideoPlayer' ;
1717
1818const videoContainerStyles = css `
@@ -80,6 +80,61 @@ 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+ window . guardian . modules . sentry . reportError (
96+ new Error ( message ) ,
97+ 'loop-video' ,
98+ ) ;
99+ log ( 'dotcom' , message ) ;
100+
101+ setPosterImage ( image ) ;
102+ setShowPlayIcon ( true ) ;
103+ } )
104+ . then ( ( ) => {
105+ // Autoplay succeeded
106+ setPlayerState ( 'PLAYING' ) ;
107+ setHasBeenInView ( true ) ;
108+ } ) ;
109+ }
110+ } , [ src , image ] ) ;
111+
112+ const pauseVideo = (
113+ reason : Extract <
114+ PlayerStates ,
115+ 'PAUSED_BY_USER' | 'PAUSED_BY_INTERSECTION_OBSERVER'
116+ > ,
117+ ) => {
118+ if ( ! vidRef . current ) return ;
119+
120+ if ( reason === 'PAUSED_BY_INTERSECTION_OBSERVER' ) {
121+ setIsMuted ( true ) ;
122+ }
123+
124+ setPlayerState ( reason ) ;
125+ void vidRef . current . pause ( ) ;
126+ } ;
127+
128+ const playPauseVideo = ( ) => {
129+ if ( playerState === 'PLAYING' ) {
130+ if ( isInView ) {
131+ pauseVideo ( 'PAUSED_BY_USER' ) ;
132+ }
133+ } else {
134+ void playVideo ( ) ;
135+ }
136+ } ;
137+
83138 /**
84139 * Setup.
85140 *
@@ -161,10 +216,9 @@ export const LoopVideo = ({
161216 ( playerState === 'NOT_STARTED' ||
162217 playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' )
163218 ) {
164- setPlayerState ( 'PLAYING' ) ;
165- void vidRef . current . play ( ) ;
219+ void playVideo ( ) ;
166220 }
167- } , [ isInView , isPlayable , playerState , isAutoplayAllowed ] ) ;
221+ } , [ isAutoplayAllowed , isInView , isPlayable , playerState , playVideo ] ) ;
168222
169223 /**
170224 * Stops playback when the video is scrolled out of view, resumes playbacks
@@ -176,9 +230,7 @@ export const LoopVideo = ({
176230 const isNoLongerInView =
177231 playerState === 'PLAYING' && isInView === false ;
178232 if ( isNoLongerInView ) {
179- setPlayerState ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
180- void vidRef . current . pause ( ) ;
181- setIsMuted ( true ) ;
233+ pauseVideo ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
182234 }
183235
184236 /**
@@ -189,11 +241,9 @@ export const LoopVideo = ({
189241 const isBackInView =
190242 playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' && isInView ;
191243 if ( isBackInView ) {
192- setPlayerState ( 'PLAYING' ) ;
193-
194- void vidRef . current . play ( ) ;
244+ void playVideo ( ) ;
195245 }
196- } , [ isInView , hasBeenInView , playerState ] ) ;
246+ } , [ isInView , hasBeenInView , playerState , playVideo ] ) ;
197247
198248 /**
199249 * Show the play icon when the video is not playing, except for when it is scrolled
@@ -237,30 +287,6 @@ export const LoopVideo = ({
237287
238288 if ( adapted ) return fallbackImageComponent ;
239289
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-
264290 const handlePlayPauseClick = ( event : React . SyntheticEvent ) => {
265291 event . preventDefault ( ) ;
266292 playPauseVideo ( ) ;
@@ -280,12 +306,15 @@ export const LoopVideo = ({
280306 }
281307 } ;
282308
309+ /**
310+ * If the video could not be loaded due to an error, report to
311+ * Sentry and log in the console.
312+ */
283313 const onError = ( ) => {
284- window . guardian . modules . sentry . reportError (
285- new Error ( `Loop video could not be played. source: ${ src } ` ) ,
286- 'loop-video' ,
287- ) ;
288- log ( 'dotcom' , `Loop video could not be played. source: ${ src } ` ) ;
314+ const message = `Loop video could not be played. source: ${ src } ` ;
315+
316+ window . guardian . modules . sentry . reportError ( new Error ( ) , 'loop-video' ) ;
317+ log ( 'dotcom' , message ) ;
289318 } ;
290319
291320 const seekForward = ( ) => {
@@ -323,7 +352,7 @@ export const LoopVideo = ({
323352 playPauseVideo ( ) ;
324353 break ;
325354 case 'Escape' :
326- pauseVideo ( ) ;
355+ pauseVideo ( 'PAUSED_BY_USER' ) ;
327356 break ;
328357 case 'ArrowRight' :
329358 seekForward ( ) ;
0 commit comments