@@ -6,6 +6,7 @@ import { getZIndex } from '../lib/getZIndex';
6
6
import { useIsInView } from '../lib/useIsInView' ;
7
7
import { useShouldAdapt } from '../lib/useShouldAdapt' ;
8
8
import { useConfig } from './ConfigContext' ;
9
+ import type { PLAYER_STATES } from './LoopVideoPlayer' ;
9
10
import { LoopVideoPlayer } from './LoopVideoPlayer' ;
10
11
11
12
const videoContainerStyles = css `
@@ -20,7 +21,6 @@ type Props = {
20
21
height : number ;
21
22
thumbnailImage : string ;
22
23
fallbackImageComponent : JSX . Element ;
23
- hasAudio ?: boolean ;
24
24
} ;
25
25
26
26
export const LoopVideo = ( {
@@ -30,16 +30,21 @@ export const LoopVideo = ({
30
30
height,
31
31
thumbnailImage,
32
32
fallbackImageComponent,
33
- hasAudio = true ,
34
33
} : Props ) => {
35
34
const adapted = useShouldAdapt ( ) ;
36
35
const { renderingTarget } = useConfig ( ) ;
37
36
const vidRef = useRef < HTMLVideoElement > ( null ) ;
38
37
const [ isPlayable , setIsPlayable ] = useState ( false ) ;
39
- const [ isPlaying , setIsPlaying ] = useState ( false ) ;
40
38
const [ isMuted , setIsMuted ] = useState ( true ) ;
41
39
const [ currentTime , setCurrentTime ] = useState ( 0 ) ;
42
- const [ prefersReducedMotion , setPrefersReducedMotion ] = useState ( false ) ;
40
+ const [ playerState , setPlayerState ] =
41
+ useState < ( typeof PLAYER_STATES ) [ number ] > ( 'NOT_STARTED' ) ;
42
+
43
+ // The user indicates a preference for reduced motion: https://web.dev/articles/prefers-reduced-motion
44
+ const [ prefersReducedMotion , setPrefersReducedMotion ] = useState <
45
+ boolean | null
46
+ > ( null ) ;
47
+
43
48
/**
44
49
* Keep a track of whether the video has been in view. We only want to
45
50
* pause the video if it has been in view.
@@ -52,51 +57,83 @@ export const LoopVideo = ({
52
57
} ) ;
53
58
54
59
/**
55
- * Pause the video when the user scrolls past it .
60
+ * Register the users motion preferences .
56
61
*/
57
62
useEffect ( ( ) => {
58
- if ( ! vidRef . current ) return ;
63
+ const userPrefersReducedMotion = window . matchMedia (
64
+ '(prefers-reduced-motion: reduce)' ,
65
+ ) . matches ;
66
+ setPrefersReducedMotion ( userPrefersReducedMotion ) ;
67
+ } , [ ] ) ;
59
68
60
- if ( isInView ) {
61
- // We only autoplay the first time the video comes into view.
62
- if ( hasBeenInView ) return ;
69
+ /**
70
+ * Autoplays the video when it comes into view.
71
+ */
72
+ useEffect ( ( ) => {
73
+ if ( ! vidRef . current || playerState === 'PAUSED_BY_USER' ) return ;
63
74
64
- if ( window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ) {
65
- setPrefersReducedMotion ( true ) ;
75
+ if ( isInView && isPlayable && playerState !== 'PLAYING' ) {
76
+ if ( prefersReducedMotion !== false ) {
66
77
return ;
67
78
}
68
79
69
- setIsPlaying ( true ) ;
70
- void vidRef . current . play ( ) ;
71
-
80
+ setPlayerState ( 'PLAYING' ) ;
72
81
setHasBeenInView ( true ) ;
82
+
83
+ void vidRef . current . play ( ) ;
73
84
}
85
+ } , [ isInView , isPlayable , playerState , prefersReducedMotion ] ) ;
74
86
75
- if ( ! isInView && hasBeenInView && isPlayable && isPlaying ) {
76
- setIsPlaying ( false ) ;
87
+ /**
88
+ * Stops playback when the video is scrolled out of view, resumes playbacks
89
+ * when the video is back in the viewport.
90
+ */
91
+ useEffect ( ( ) => {
92
+ if ( ! vidRef . current || ! hasBeenInView ) return ;
93
+
94
+ const isNoLongerInView = playerState === 'PLAYING' && ! isInView ;
95
+ if ( isNoLongerInView ) {
96
+ setPlayerState ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
77
97
void vidRef . current . pause ( ) ;
98
+ setIsMuted ( true ) ;
99
+ }
100
+
101
+ // If a user action paused the video, they have indicated
102
+ // that they don't want to watch the video. Therefore, don't
103
+ // resume the video when it comes back in view
104
+ const isBackInView =
105
+ playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' && isInView ;
106
+ if ( isBackInView ) {
107
+ setPlayerState ( 'PLAYING' ) ;
108
+
109
+ void vidRef . current . play ( ) ;
78
110
}
79
- } , [ isInView , hasBeenInView , isPlayable , isPlaying ] ) ;
111
+ } , [ isInView , hasBeenInView , playerState ] ) ;
80
112
81
113
if ( renderingTarget !== 'Web' ) return null ;
82
114
83
115
if ( adapted ) return fallbackImageComponent ;
84
116
85
117
const playVideo = ( ) => {
86
118
if ( ! vidRef . current ) return ;
87
- setIsPlaying ( true ) ;
119
+
120
+ setPlayerState ( 'PLAYING' ) ;
121
+ setHasBeenInView ( true ) ;
88
122
void vidRef . current . play ( ) ;
89
123
} ;
90
124
91
125
const pauseVideo = ( ) => {
92
126
if ( ! vidRef . current ) return ;
93
- setIsPlaying ( false ) ;
127
+
128
+ setPlayerState ( 'PAUSED_BY_USER' ) ;
94
129
void vidRef . current . pause ( ) ;
95
130
} ;
96
131
97
132
const playPauseVideo = ( ) => {
98
- if ( isPlaying ) {
99
- pauseVideo ( ) ;
133
+ if ( playerState === 'PLAYING' ) {
134
+ if ( isInView ) {
135
+ pauseVideo ( ) ;
136
+ }
100
137
} else {
101
138
playVideo ( ) ;
102
139
}
@@ -166,6 +203,20 @@ export const LoopVideo = ({
166
203
167
204
const AudioIcon = isMuted ? SvgAudioMute : SvgAudio ;
168
205
206
+ // We only show a poster image when the user has indicated that they do
207
+ // not want videos to play automatically, e.g. prefers reduced motion. Otherwise,
208
+ // we do not need to download the image as the video will be autoplayed.
209
+ const posterImage =
210
+ ! ! prefersReducedMotion || isInView === false
211
+ ? thumbnailImage
212
+ : undefined ;
213
+
214
+ const showPlayIcon =
215
+ playerState === 'PAUSED_BY_USER' ||
216
+ ( ! ! prefersReducedMotion && playerState === 'NOT_STARTED' ) ;
217
+
218
+ const shouldPreloadData = ! ! isInView || prefersReducedMotion === false ;
219
+
169
220
return (
170
221
< div
171
222
ref = { setNode }
@@ -177,24 +228,23 @@ export const LoopVideo = ({
177
228
videoId = { videoId }
178
229
width = { width }
179
230
height = { height }
180
- hasAudio = { hasAudio }
231
+ posterImage = { posterImage }
181
232
fallbackImageComponent = { fallbackImageComponent }
182
233
currentTime = { currentTime }
183
234
setCurrentTime = { setCurrentTime }
184
235
ref = { vidRef }
185
236
isPlayable = { isPlayable }
186
237
setIsPlayable = { setIsPlayable }
187
- isPlaying = { isPlaying }
188
- setIsPlaying = { setIsPlaying }
238
+ playerState = { playerState }
239
+ setPlayerState = { setPlayerState }
189
240
isMuted = { isMuted }
190
241
setIsMuted = { setIsMuted }
191
242
handleClick = { handleClick }
192
243
handleKeyDown = { handleKeyDown }
193
244
onError = { onError }
194
245
AudioIcon = { AudioIcon }
195
- thumbnailImage = {
196
- prefersReducedMotion ? thumbnailImage : undefined
197
- }
246
+ shouldPreload = { shouldPreloadData }
247
+ showPlayIcon = { showPlayIcon }
198
248
/>
199
249
</ div >
200
250
) ;
0 commit comments