@@ -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,48 +57,81 @@ 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
+ } , [ ] ) ;
68
+
69
+ /**
70
+ * Autoplays the video when it comes into view.
71
+ */
72
+ useEffect ( ( ) => {
73
+ if ( ! vidRef . current || playerState === 'PAUSED_BY_USER' ) return ;
59
74
60
- if ( isInView ) {
61
- if ( window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ) {
62
- setPrefersReducedMotion ( true ) ;
75
+ if ( isInView && isPlayable && playerState !== 'PLAYING' ) {
76
+ if ( prefersReducedMotion !== false ) {
63
77
return ;
64
78
}
65
79
66
- setIsPlaying ( true ) ;
67
- void vidRef . current . play ( ) ;
68
-
80
+ setPlayerState ( 'PLAYING' ) ;
69
81
setHasBeenInView ( true ) ;
82
+
83
+ void vidRef . current . play ( ) ;
70
84
}
85
+ } , [ isInView , isPlayable , playerState , prefersReducedMotion ] ) ;
71
86
72
- if ( ! isInView && hasBeenInView && isPlayable && isPlaying ) {
73
- 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' ) ;
74
97
void vidRef . current . pause ( ) ;
75
98
}
76
- } , [ isInView , hasBeenInView , isPlayable , isPlaying ] ) ;
99
+
100
+ // If a user action paused the video, they have indicated
101
+ // that they don't want to watch the video. Therefore, don't
102
+ // resume the video when it comes back in view
103
+ const isBackInView =
104
+ playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' && isInView ;
105
+ if ( isBackInView ) {
106
+ setPlayerState ( 'PLAYING' ) ;
107
+ void vidRef . current . play ( ) ;
108
+ }
109
+ } , [ isInView , hasBeenInView , playerState ] ) ;
77
110
78
111
if ( renderingTarget !== 'Web' ) return null ;
79
112
80
113
if ( adapted ) return fallbackImageComponent ;
81
114
82
115
const playVideo = ( ) => {
83
116
if ( ! vidRef . current ) return ;
84
- setIsPlaying ( true ) ;
117
+
118
+ setPlayerState ( 'PLAYING' ) ;
119
+ setHasBeenInView ( true ) ;
85
120
void vidRef . current . play ( ) ;
86
121
} ;
87
122
88
123
const pauseVideo = ( ) => {
89
124
if ( ! vidRef . current ) return ;
90
- setIsPlaying ( false ) ;
125
+
126
+ setPlayerState ( 'PAUSED_BY_USER' ) ;
91
127
void vidRef . current . pause ( ) ;
92
128
} ;
93
129
94
130
const playPauseVideo = ( ) => {
95
- if ( isPlaying ) {
96
- pauseVideo ( ) ;
131
+ if ( playerState === 'PLAYING' ) {
132
+ if ( isInView ) {
133
+ pauseVideo ( ) ;
134
+ }
97
135
} else {
98
136
playVideo ( ) ;
99
137
}
@@ -163,6 +201,20 @@ export const LoopVideo = ({
163
201
164
202
const AudioIcon = isMuted ? SvgAudioMute : SvgAudio ;
165
203
204
+ // We only show a poster image when the user has indicated that they do
205
+ // not want videos to play automatically, e.g. prefers reduced motion. Otherwise,
206
+ // we do not need to download the image as the video will be autoplayed.
207
+ const posterImage =
208
+ ! ! prefersReducedMotion || isInView === false
209
+ ? thumbnailImage
210
+ : undefined ;
211
+
212
+ const showPlayIcon =
213
+ playerState === 'PAUSED_BY_USER' ||
214
+ ( ! ! prefersReducedMotion && playerState === 'NOT_STARTED' ) ;
215
+
216
+ const shouldPreloadData = ! ! isInView || prefersReducedMotion === false ;
217
+
166
218
return (
167
219
< div
168
220
ref = { setNode }
@@ -174,24 +226,23 @@ export const LoopVideo = ({
174
226
videoId = { videoId }
175
227
width = { width }
176
228
height = { height }
177
- hasAudio = { hasAudio }
229
+ posterImage = { posterImage }
178
230
fallbackImageComponent = { fallbackImageComponent }
179
231
currentTime = { currentTime }
180
232
setCurrentTime = { setCurrentTime }
181
233
ref = { vidRef }
182
234
isPlayable = { isPlayable }
183
235
setIsPlayable = { setIsPlayable }
184
- isPlaying = { isPlaying }
185
- setIsPlaying = { setIsPlaying }
236
+ playerState = { playerState }
237
+ setPlayerState = { setPlayerState }
186
238
isMuted = { isMuted }
187
239
setIsMuted = { setIsMuted }
188
240
handleClick = { handleClick }
189
241
handleKeyDown = { handleKeyDown }
190
242
onError = { onError }
191
243
AudioIcon = { AudioIcon }
192
- thumbnailImage = {
193
- prefersReducedMotion ? thumbnailImage : undefined
194
- }
244
+ shouldPreload = { shouldPreloadData }
245
+ showPlayIcon = { showPlayIcon }
195
246
/>
196
247
</ div >
197
248
) ;
0 commit comments