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,64 @@ 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+ setHasBeenInView ( true ) ;
111+ } ) ;
112+ }
113+ } , [ src , image ] ) ;
114+
115+ const pauseVideo = (
116+ reason : Extract <
117+ PlayerStates ,
118+ 'PAUSED_BY_USER' | 'PAUSED_BY_INTERSECTION_OBSERVER'
119+ > ,
120+ ) => {
121+ if ( ! vidRef . current ) return ;
122+
123+ if ( reason === 'PAUSED_BY_INTERSECTION_OBSERVER' ) {
124+ setIsMuted ( true ) ;
125+ }
126+
127+ setPlayerState ( reason ) ;
128+ void vidRef . current . pause ( ) ;
129+ } ;
130+
131+ const playPauseVideo = ( ) => {
132+ if ( playerState === 'PLAYING' ) {
133+ if ( isInView ) {
134+ pauseVideo ( 'PAUSED_BY_USER' ) ;
135+ }
136+ } else {
137+ void playVideo ( ) ;
138+ }
139+ } ;
140+
83141 /**
84142 * Setup.
85143 *
@@ -161,10 +219,9 @@ export const LoopVideo = ({
161219 ( playerState === 'NOT_STARTED' ||
162220 playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' )
163221 ) {
164- setPlayerState ( 'PLAYING' ) ;
165- void vidRef . current . play ( ) ;
222+ void playVideo ( ) ;
166223 }
167- } , [ isInView , isPlayable , playerState , isAutoplayAllowed ] ) ;
224+ } , [ isAutoplayAllowed , isInView , isPlayable , playerState , playVideo ] ) ;
168225
169226 /**
170227 * Stops playback when the video is scrolled out of view, resumes playbacks
@@ -176,9 +233,7 @@ export const LoopVideo = ({
176233 const isNoLongerInView =
177234 playerState === 'PLAYING' && isInView === false ;
178235 if ( isNoLongerInView ) {
179- setPlayerState ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
180- void vidRef . current . pause ( ) ;
181- setIsMuted ( true ) ;
236+ pauseVideo ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
182237 }
183238
184239 /**
@@ -189,11 +244,9 @@ export const LoopVideo = ({
189244 const isBackInView =
190245 playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' && isInView ;
191246 if ( isBackInView ) {
192- setPlayerState ( 'PLAYING' ) ;
193-
194- void vidRef . current . play ( ) ;
247+ void playVideo ( ) ;
195248 }
196- } , [ isInView , hasBeenInView , playerState ] ) ;
249+ } , [ isInView , hasBeenInView , playerState , playVideo ] ) ;
197250
198251 /**
199252 * Show the play icon when the video is not playing, except for when it is scrolled
@@ -237,30 +290,6 @@ export const LoopVideo = ({
237290
238291 if ( adapted ) return fallbackImageComponent ;
239292
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-
264293 const handlePlayPauseClick = ( event : React . SyntheticEvent ) => {
265294 event . preventDefault ( ) ;
266295 playPauseVideo ( ) ;
@@ -280,12 +309,18 @@ export const LoopVideo = ({
280309 }
281310 } ;
282311
312+ /**
313+ * If the video could not be loaded due to an error, report to
314+ * Sentry and log in the console.
315+ */
283316 const onError = ( ) => {
317+ const message = `Loop video could not be played. source: ${ src } ` ;
318+
284319 window . guardian . modules . sentry . reportError (
285- new Error ( `Loop video could not be played. source: ${ src } ` ) ,
320+ new Error ( message ) ,
286321 'loop-video' ,
287322 ) ;
288- log ( 'dotcom' , `Loop video could not be played. source: ${ src } ` ) ;
323+ log ( 'dotcom' , message ) ;
289324 } ;
290325
291326 const seekForward = ( ) => {
@@ -323,7 +358,7 @@ export const LoopVideo = ({
323358 playPauseVideo ( ) ;
324359 break ;
325360 case 'Escape' :
326- pauseVideo ( ) ;
361+ pauseVideo ( 'PAUSED_BY_USER' ) ;
327362 break ;
328363 case 'ArrowRight' :
329364 seekForward ( ) ;
0 commit comments