OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "config.h" | |
6 #include "core/html/AutoplayExperimentHelper.h" | |
7 | |
8 #include "core/dom/Document.h" | |
9 #include "core/frame/Settings.h" | |
10 #include "core/html/HTMLMediaElement.h" | |
11 #include "core/layout/LayoutBox.h" | |
12 #include "core/layout/LayoutObject.h" | |
13 #include "core/layout/LayoutVideo.h" | |
14 #include "core/layout/LayoutView.h" | |
15 #include "core/page/Page.h" | |
16 #include "platform/Logging.h" | |
17 #include "platform/UserGestureIndicator.h" | |
18 #include "platform/geometry/IntRect.h" | |
19 | |
20 namespace blink { | |
21 | |
22 using namespace HTMLNames; | |
23 | |
24 // How long do we wait after a scroll event before deciding that no more | |
25 // scroll events are going to arrive? | |
26 static const double viewportTimerPollDelay = 0.5; | |
27 | |
28 AutoplayExperimentHelper::AutoplayExperimentHelper(HTMLMediaElement& element) | |
29 : m_element(element) | |
30 , m_mode(AutoplayExperimentConfig::Mode::Off) | |
31 , m_playPending(false) | |
32 , m_viewportTimer(this, &AutoplayExperimentHelper::viewportTimerFired) | |
33 , m_registeredWithView(false) | |
34 { | |
35 if (document().settings()) { | |
36 m_mode = AutoplayExperimentConfig::fromString(document().settings()->aut oplayExperimentMode()); | |
37 | |
38 if (m_mode != AutoplayExperimentConfig::Mode::Off) { | |
39 WTF_LOG(Media, "HTMLMediaElement: autoplay experiment set to %d", | |
40 m_mode); | |
41 } | |
42 } | |
43 } | |
44 | |
45 AutoplayExperimentHelper::~AutoplayExperimentHelper() | |
46 { | |
47 clearEventListenerIfNeeded(); | |
48 } | |
49 | |
50 void AutoplayExperimentHelper::becameReadyToPlay() | |
51 { | |
52 // Assuming that we're eligible to override the user gesture requirement, | |
53 // either play if we meet the visibility checks, or install a listener | |
54 // to wait for them to pass. | |
55 if (isEligible()) { | |
56 if (isInViewportIfNeeded()) | |
57 prepareToPlay(GesturelessPlaybackStartedByAutoplayFlagImmediately); | |
58 else | |
59 installEventListenerIfNeeded(); | |
60 } | |
61 } | |
62 | |
63 void AutoplayExperimentHelper::playMethodCalled() | |
64 { | |
65 // Set the pending state, even if the play isn't going to be pending. | |
66 // Eligibility can change if, for example, the mute status changes. | |
67 // Having this set is okay. | |
68 m_playPending = true; | |
69 | |
70 if (!UserGestureIndicator::processingUserGesture()) { | |
71 | |
72 if (isEligible()) { | |
73 // Remember that userGestureRequiredForPlay is required for | |
74 // us to be eligible for the experiment. | |
75 // If we are able to override the gesture requirement now, then | |
76 // do so. Otherwise, install an event listener if we need one. | |
77 if (isInViewportIfNeeded()) { | |
78 // Override the gesture and play. | |
79 prepareToPlay(GesturelessPlaybackStartedByPlayMethodImmediately) ; | |
80 } else { | |
81 // Wait for viewport visibility. | |
82 installEventListenerIfNeeded(); | |
83 } | |
84 } | |
85 | |
86 } else if (m_element.isUserGestureRequiredForPlay()) { | |
87 clearEventListenerIfNeeded(); | |
88 } | |
89 } | |
90 | |
91 void AutoplayExperimentHelper::pauseMethodCalled() | |
92 { | |
93 // Don't try to autoplay, if we would have. | |
94 m_playPending = false; | |
95 clearEventListenerIfNeeded(); | |
96 } | |
97 | |
98 void AutoplayExperimentHelper::mutedChanged() | |
99 { | |
100 // If we are no longer eligible for the autoplay experiment, then also | |
101 // quit listening for events. If we are eligible, and if we should be | |
102 // playing, then start playing. In other words, start playing if | |
103 // we just needed 'mute' to autoplay. | |
104 if (!isEligible()) { | |
105 clearEventListenerIfNeeded(); | |
106 } else { | |
107 // Try to play. If we can't, then install a listener. | |
108 if (!maybeStartPlaying()) | |
109 installEventListenerIfNeeded(); | |
110 } | |
111 } | |
112 | |
113 void AutoplayExperimentHelper::installEventListenerIfNeeded() | |
114 { | |
115 // If we don't require that the player is in the viewport, then we don't | |
116 // need the listener. | |
117 if (!enabled(AutoplayExperimentConfig::Mode::IfViewport)) | |
118 return; | |
119 | |
120 LayoutObject* layoutObject = m_element.layoutObject(); | |
121 if (layoutObject && layoutObject->isVideo()) { | |
122 LayoutVideo* layoutVideo = (LayoutVideo*)layoutObject; | |
philipj_slow
2015/09/02 09:24:11
Use static_cast
liberato (no reviews please)
2015/09/04 06:49:45
Done.
| |
123 layoutVideo->setRequestPositionUpdates(true); | |
124 // TODO(liberato): do we really need to keep track of this? it's | |
philipj_slow
2015/09/02 09:24:11
Is this TODO to be fixed before landing? Who knows
liberato (no reviews please)
2015/09/04 06:49:46
yes, it will. i wanted to put it out for CL with
philipj_slow
2015/09/04 08:42:29
OK, I see this will be done in a separate CL.
| |
125 // only to make clearEventListener() faster. | |
126 m_registeredWithView = true; | |
127 } | |
128 } | |
129 | |
130 void AutoplayExperimentHelper::clearEventListenerIfNeeded() | |
131 { | |
132 if (m_registeredWithView) { | |
133 LayoutObject* obj = m_element.layoutObject(); | |
134 if (obj && obj->isVideo()) { | |
135 LayoutVideo* video = (LayoutVideo*)obj; | |
philipj_slow
2015/09/02 09:24:11
static_cast
liberato (no reviews please)
2015/09/04 06:49:45
thanks, forgot what year it is.
| |
136 video->setRequestPositionUpdates(false); | |
137 m_registeredWithView = false; | |
138 } | |
139 } | |
140 } | |
141 | |
142 void AutoplayExperimentHelper::positionChanged() | |
143 { | |
144 // Reset the timer to indicate that scrolling has happened | |
145 // recently, and might still be ongoing. | |
146 // Also note that we are called quite often, including when the | |
147 // page becomes visible. That's why we don't bother to register | |
148 // for page visibility changes explicitly. | |
149 | |
150 // Since we're called very often, even if our visibility hasn't changed, | |
151 // make sure that we only reset the timer if something has moved. | |
152 // Otherwise, we will reset the timer every time a video frame plays | |
153 // anywhere, or the mouse moves, etc. | |
154 // We may want to lower the frequency of this via another timer, so that | |
155 // we do no work here. | |
156 LocationState curLocation(m_element); | |
157 if (curLocation != m_lastLocation) { | |
158 m_viewportTimer.startOneShot(viewportTimerPollDelay, FROM_HERE); | |
159 m_lastLocation = curLocation; | |
160 } | |
161 } | |
162 | |
163 void AutoplayExperimentHelper::triggerAutoplayViewportCheck() | |
164 { | |
165 viewportTimerFired(0); | |
philipj_slow
2015/09/02 09:24:11
nullptr
liberato (no reviews please)
2015/09/04 06:49:46
Done.
| |
166 } | |
167 | |
168 void AutoplayExperimentHelper::viewportTimerFired(Timer<AutoplayExperimentHelper >*) | |
169 { | |
170 // Sufficient time has passed since the last scroll that we'll | |
171 // treat it as the end of scroll. Autoplay if we should. | |
philipj_slow
2015/09/02 09:24:12
I didn't follow the discussion around this very cl
liberato (no reviews please)
2015/09/04 06:49:45
no, unfortunately.
philipj_slow
2015/09/04 08:42:29
Acknowledged.
| |
172 maybeStartPlaying(); | |
173 } | |
174 | |
175 bool AutoplayExperimentHelper::isInViewportIfNeeded() | |
176 { | |
177 // We could check for eligibility here, but we skip it. Some of our | |
178 // callers need to do it separately, and we don't want to check more | |
179 // than we need to. | |
180 // Also remember that page visibility is assumed for clank. | |
181 | |
182 // If viewport visibility isn't required, then it's visible enough. | |
183 if (!enabled(AutoplayExperimentConfig::Mode::IfViewport)) | |
184 return true; | |
185 | |
186 return LocationState(m_element).isInViewport(); | |
187 } | |
188 | |
189 bool AutoplayExperimentHelper::maybeStartPlaying() | |
190 { | |
191 // See if we're allowed to autoplay now. | |
192 if (!isEligible() | |
philipj_slow
2015/09/02 09:24:11
A bit much line breaking here, it shouldn't be ver
liberato (no reviews please)
2015/09/04 06:49:46
Done.
| |
193 || !isInViewportIfNeeded()) { | |
194 return false; | |
195 } | |
196 | |
197 // Start playing! | |
198 prepareToPlay(m_element.autoplay() | |
philipj_slow
2015/09/02 09:24:11
autoplay() checks the content attribute, but there
liberato (no reviews please)
2015/09/04 06:49:45
good point. i'll add this for clarity, but i thin
philipj_slow
2015/09/04 08:42:29
It should be possible to clear the autoplaying fla
| |
199 ? GesturelessPlaybackStartedByAutoplayFlagAfterScroll | |
200 : GesturelessPlaybackStartedByPlayMethodAfterScroll); | |
201 m_element.playInternal(); | |
202 | |
203 return true; | |
204 } | |
205 | |
206 bool AutoplayExperimentHelper::isEligible() const | |
207 { | |
208 // If no user gesture is required, then the experiment doesn't apply. | |
209 // This is what prevents us from starting playback more than once. | |
210 // Since this flag is never set to true once it's cleared, it will block | |
211 // the autoplay experiment forever. | |
212 if (!m_element.isUserGestureRequiredForPlay()) | |
213 return false; | |
214 | |
215 if (m_mode == AutoplayExperimentConfig::Mode::Off) | |
216 return false; | |
217 | |
218 // If nobody has requested playback, either by the autoplay attribute or | |
219 // a play() call, then do nothing. | |
220 if (!m_playPending && !m_element.autoplay()) | |
philipj_slow
2015/09/02 09:24:11
shouldAutoplay() here too. To write a test for the
liberato (no reviews please)
2015/09/04 06:49:46
Done, including test.
| |
221 return false; | |
222 | |
223 // If the video is already playing, then do nothing. Note that there | |
224 // is not a path where a user gesture is required but the video is | |
225 // playing. However, we check for completeness. | |
226 if (!m_element.paused()) | |
227 return false; | |
228 | |
229 // Note that the viewport test always returns false on desktop, which is | |
230 // why video-autoplay-experiment.html doesn't check -ifmobile . | |
231 if (enabled(AutoplayExperimentConfig::Mode::IfMobile) | |
232 && !document().viewportDescription().isLegacyViewportType()) | |
233 return false; | |
234 | |
235 if (enabled(AutoplayExperimentConfig::Mode::IfMuted)) { | |
236 // If media is muted, then autoplay when it comes into view. | |
philipj_slow
2015/09/02 09:24:11
Move this outside the if and remove {} for consist
liberato (no reviews please)
2015/09/04 06:49:45
Done.
| |
237 return m_element.fastHasAttribute(mutedAttr) || m_element.muted(); | |
238 } | |
239 | |
240 // Autoplay when it comes into view (if needed), maybe muted. | |
241 return true; | |
242 } | |
243 | |
244 void AutoplayExperimentHelper::muteIfNeeded() | |
245 { | |
246 if (enabled(AutoplayExperimentConfig::Mode::PlayMuted)) { | |
247 ASSERT(!isEligible()); | |
248 // If we are actually changing the muted state, then this will call | |
249 // mutedChanged(). If isEligible(), then mutedChanged() will try | |
250 // to start playback, which we should not do here. | |
251 m_element.setMuted(true); | |
252 } | |
253 } | |
254 | |
255 void AutoplayExperimentHelper::prepareToPlay(AutoplayMetrics metric) | |
256 { | |
257 m_element.recordAutoplayMetric(metric); | |
258 | |
259 // This also causes !isEligible, so that we don't alow autoplay more than | |
philipj_slow
2015/09/02 09:24:11
s/alow/allow/
liberato (no reviews please)
2015/09/04 06:49:45
Done.
| |
260 // once. Be sure to do this before muteIfNeeded(). | |
261 m_element.removeUserGestureRequirement(); | |
262 | |
263 clearEventListenerIfNeeded(); | |
264 muteIfNeeded(); | |
265 | |
266 // Record that this autoplayed without a user gesture. This is normally | |
267 // set when we discover an autoplay attribute, but we include all cases | |
268 // where playback started without a user gesture, e.g., play(). | |
269 m_element.setInitialPlayWithoutUserGestures(true); | |
270 | |
271 // Do not actually start playback here. | |
272 } | |
273 | |
274 Document& AutoplayExperimentHelper::document() const | |
275 { | |
276 return m_element.document(); | |
277 } | |
278 | |
279 AutoplayExperimentHelper::LocationState::LocationState(Element& element) | |
280 : m_valid(false) | |
281 { | |
282 const LocalDOMWindow* domWindow = element.document().domWindow(); | |
283 if (!domWindow) | |
284 return; | |
285 | |
286 // Get the page visibility. | |
287 Frame* frame = domWindow->frame(); | |
288 if (!frame) | |
289 return; | |
290 | |
291 Page* page = frame->page(); | |
292 if (!page) | |
293 return; | |
294 | |
295 if (!element.layoutObject()) | |
296 return; | |
297 | |
298 const LayoutBox* elementBox = element.layoutObject()->enclosingBox(); | |
299 if (!elementBox) | |
300 return; | |
301 | |
302 float zoom = elementBox->style()->effectiveZoom(); | |
303 IntRect us(elementBox->offsetLeft().toInt() | |
304 , elementBox->offsetTop().toInt() | |
305 , elementBox->clientWidth().toInt() | |
306 , elementBox->clientHeight().toInt()); | |
307 IntRect screen(domWindow->scrollX()*zoom, domWindow->scrollY()*zoom, domWind ow->innerWidth()*zoom, domWindow->innerHeight()*zoom); | |
308 | |
309 m_visibilityState = page->visibilityState(); | |
310 m_element = us; | |
311 m_screen = screen; | |
312 m_valid = true; | |
313 } | |
314 | |
315 bool AutoplayExperimentHelper::LocationState::isInViewport() | |
316 { | |
317 // Check if we're in the viewport. | |
318 return m_valid | |
319 && m_visibilityState == PageVisibilityStateVisible | |
320 && m_screen.contains(m_element); | |
321 } | |
322 | |
323 bool AutoplayExperimentHelper::LocationState::operator==(const LocationState& th em) const | |
324 { | |
325 // If either state is not valid, then they are not equal. | |
326 return m_valid && them.valid() | |
327 && m_visibilityState == them.visibilityState() | |
328 && m_screen == them.screen() | |
329 && m_element == them.element(); | |
330 } | |
331 | |
332 bool AutoplayExperimentHelper::LocationState::operator!=(const LocationState& th em) const | |
333 { | |
334 return !((*this) == them); | |
335 } | |
336 | |
337 } | |
OLD | NEW |