@@ -53,18 +53,22 @@ export const LoopVideo = ({
53
53
const vidRef = useRef < HTMLVideoElement > ( null ) ;
54
54
const [ isPlayable , setIsPlayable ] = useState ( false ) ;
55
55
const [ isMuted , setIsMuted ] = useState ( true ) ;
56
+ const [ showPlayIcon , setShowPlayIcon ] = useState ( false ) ;
57
+ const [ preloadPartialData , setPreloadPartialData ] = useState ( false ) ;
58
+ const [ posterImage , setPosterImage ] = useState < string | undefined > (
59
+ undefined ,
60
+ ) ;
56
61
const [ currentTime , setCurrentTime ] = useState ( 0 ) ;
57
62
const [ playerState , setPlayerState ] =
58
63
useState < ( typeof PLAYER_STATES ) [ number ] > ( 'NOT_STARTED' ) ;
59
64
60
- // The user indicates a preference for reduced motion: https://web.dev/articles/prefers-reduced-motion
61
- const [ prefersReducedMotion , setPrefersReducedMotion ] = useState <
62
- boolean | null
63
- > ( null ) ;
65
+ const [ isAutoplayAllowed , setIsAutoplayAllowed ] = useState < boolean | null > (
66
+ null ,
67
+ ) ;
64
68
65
69
/**
66
- * Keep a track of whether the video has been in view. We only want to
67
- * pause the video if it has been in view.
70
+ * Keep a track of whether the video has been in view. We only
71
+ * want to pause the video if it has been in view.
68
72
*/
69
73
const [ hasBeenInView , setHasBeenInView ] = useState ( false ) ;
70
74
@@ -77,13 +81,16 @@ export const LoopVideo = ({
77
81
* Setup.
78
82
*
79
83
* Register the users motion preferences.
80
- * Creates an event listener to ensure we don't play audio from multiple loops
84
+ * Creates event listeners to control playback when there are multiple videos.
81
85
*/
82
86
useEffect ( ( ) => {
87
+ /**
88
+ * The user indicates a preference for reduced motion: https://web.dev/articles/prefers-reduced-motion
89
+ */
83
90
const userPrefersReducedMotion = window . matchMedia (
84
91
'(prefers-reduced-motion: reduce)' ,
85
92
) . matches ;
86
- setPrefersReducedMotion ( userPrefersReducedMotion ) ;
93
+ setIsAutoplayAllowed ( ! userPrefersReducedMotion ) ;
87
94
88
95
/**
89
96
* Mutes the current video when another video is unmuted
@@ -131,11 +138,17 @@ export const LoopVideo = ({
131
138
} ;
132
139
} , [ uniqueId ] ) ;
133
140
141
+ useEffect ( ( ) => {
142
+ if ( isInView && ! hasBeenInView ) {
143
+ setHasBeenInView ( true ) ;
144
+ }
145
+ } , [ isInView , hasBeenInView ] ) ;
146
+
134
147
/**
135
148
* Autoplay the video when it comes into view.
136
149
*/
137
150
useEffect ( ( ) => {
138
- if ( ! vidRef . current || prefersReducedMotion ! == false ) {
151
+ if ( ! vidRef . current || isAutoplayAllowed = == false ) {
139
152
return ;
140
153
}
141
154
@@ -146,11 +159,9 @@ export const LoopVideo = ({
146
159
playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' )
147
160
) {
148
161
setPlayerState ( 'PLAYING' ) ;
149
- setHasBeenInView ( true ) ;
150
-
151
162
void vidRef . current . play ( ) ;
152
163
}
153
- } , [ isInView , isPlayable , playerState , prefersReducedMotion ] ) ;
164
+ } , [ isInView , isPlayable , playerState , isAutoplayAllowed ] ) ;
154
165
155
166
/**
156
167
* Stops playback when the video is scrolled out of view, resumes playbacks
@@ -159,7 +170,8 @@ export const LoopVideo = ({
159
170
useEffect ( ( ) => {
160
171
if ( ! vidRef . current || ! hasBeenInView ) return ;
161
172
162
- const isNoLongerInView = playerState === 'PLAYING' && ! isInView ;
173
+ const isNoLongerInView =
174
+ playerState === 'PLAYING' && isInView === false ;
163
175
if ( isNoLongerInView ) {
164
176
setPlayerState ( 'PAUSED_BY_INTERSECTION_OBSERVER' ) ;
165
177
void vidRef . current . pause ( ) ;
@@ -180,6 +192,44 @@ export const LoopVideo = ({
180
192
}
181
193
} , [ isInView , hasBeenInView , playerState ] ) ;
182
194
195
+ /**
196
+ * Show the play icon when the video is not playing, except for when it is scrolled
197
+ * out of view. In this case, the intersection observer will resume playback and
198
+ * having a play icon would falsely indicate a user action is required to resume playback.
199
+ */
200
+ useEffect ( ( ) => {
201
+ const shouldShowPlayIcon =
202
+ playerState === 'PAUSED_BY_USER' ||
203
+ ( ! isAutoplayAllowed && playerState === 'NOT_STARTED' ) ;
204
+
205
+ setShowPlayIcon ( shouldShowPlayIcon ) ;
206
+ } , [ playerState , isAutoplayAllowed ] ) ;
207
+
208
+ /**
209
+ * Show a poster image if a video does NOT play automatically. Otherwise, we do not need
210
+ * to download the image as the video will be autoplayed and the image will not be seen.
211
+ *
212
+ * If the video is partially in view (not enough to trigger autoplay) and hasn't yet been
213
+ * seen, we want to show the poster image to avoid showing a blank space.
214
+ */
215
+ useEffect ( ( ) => {
216
+ if (
217
+ isAutoplayAllowed === false ||
218
+ ( isInView === false && ! hasBeenInView )
219
+ ) {
220
+ setPosterImage ( thumbnailImage ) ;
221
+ }
222
+ } , [ isAutoplayAllowed , isInView , hasBeenInView , thumbnailImage ] ) ;
223
+
224
+ /**
225
+ * We almost always want to preload some of the video data. If a user has prefers-reduced-motion
226
+ * enabled, then the video will only be partially preloaded (metadata + small amount of video)
227
+ * when it comes into view.
228
+ */
229
+ useEffect ( ( ) => {
230
+ setPreloadPartialData ( isAutoplayAllowed === false || ! ! isInView ) ;
231
+ } , [ isAutoplayAllowed , isInView ] ) ;
232
+
183
233
if ( renderingTarget !== 'Web' ) return null ;
184
234
185
235
if ( adapted ) return fallbackImageComponent ;
@@ -188,7 +238,6 @@ export const LoopVideo = ({
188
238
if ( ! vidRef . current ) return ;
189
239
190
240
setPlayerState ( 'PLAYING' ) ;
191
- setHasBeenInView ( true ) ;
192
241
void vidRef . current . play ( ) ;
193
242
} ;
194
243
@@ -285,20 +334,6 @@ export const LoopVideo = ({
285
334
286
335
const AudioIcon = isMuted ? SvgAudioMute : SvgAudio ;
287
336
288
- // We only show a poster image when the user has indicated that they do
289
- // not want videos to play automatically, e.g. prefers reduced motion. Otherwise,
290
- // we do not need to download the image as the video will be autoplayed.
291
- const posterImage =
292
- ! ! prefersReducedMotion || isInView === false
293
- ? thumbnailImage
294
- : undefined ;
295
-
296
- const showPlayIcon =
297
- playerState === 'PAUSED_BY_USER' ||
298
- ( ! ! prefersReducedMotion && playerState === 'NOT_STARTED' ) ;
299
-
300
- const shouldPreloadData = ! ! isInView || prefersReducedMotion === false ;
301
-
302
337
return (
303
338
< div
304
339
ref = { setNode }
@@ -324,7 +359,7 @@ export const LoopVideo = ({
324
359
handleKeyDown = { handleKeyDown }
325
360
onError = { onError }
326
361
AudioIcon = { AudioIcon }
327
- shouldPreload = { shouldPreloadData }
362
+ preloadPartialData = { preloadPartialData }
328
363
showPlayIcon = { showPlayIcon }
329
364
/>
330
365
</ div >
0 commit comments