Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(736)

Side by Side Diff: Source/core/html/AutoplayExperimentHelper.cpp

Issue 1329853004: Include viewport visibility checks for autoplay experiment. (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@autoplay_step1
Patch Set: rebased. Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "config.h" 5 #include "config.h"
6 #include "core/html/AutoplayExperimentHelper.h" 6 #include "core/html/AutoplayExperimentHelper.h"
7 7
8 #include "core/dom/Document.h" 8 #include "core/dom/Document.h"
9 #include "core/frame/Settings.h" 9 #include "core/frame/Settings.h"
10 #include "core/html/HTMLMediaElement.h" 10 #include "core/html/HTMLMediaElement.h"
11 #include "core/layout/LayoutBox.h" 11 #include "core/layout/LayoutBox.h"
12 #include "core/layout/LayoutObject.h" 12 #include "core/layout/LayoutObject.h"
13 #include "core/layout/LayoutVideo.h" 13 #include "core/layout/LayoutVideo.h"
14 #include "core/layout/LayoutView.h" 14 #include "core/layout/LayoutView.h"
15 #include "core/page/Page.h" 15 #include "core/page/Page.h"
16 #include "platform/Logging.h" 16 #include "platform/Logging.h"
17 #include "platform/UserGestureIndicator.h" 17 #include "platform/UserGestureIndicator.h"
18 #include "platform/geometry/IntRect.h" 18 #include "platform/geometry/IntRect.h"
19 19
20 namespace blink { 20 namespace blink {
21 21
22 using namespace HTMLNames; 22 using namespace HTMLNames;
23 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
24 AutoplayExperimentHelper::AutoplayExperimentHelper(HTMLMediaElement& element) 28 AutoplayExperimentHelper::AutoplayExperimentHelper(HTMLMediaElement& element)
25 : m_element(element) 29 : m_element(element)
26 , m_mode(AutoplayExperimentConfig::Mode::Off) 30 , m_mode(AutoplayExperimentConfig::Mode::Off)
27 , m_playPending(false) 31 , m_playPending(false)
32 , m_registeredWithView(false)
33 , m_wasInViewport(false)
34 , m_lastLocationUpdateTime(-std::numeric_limits<double>::infinity())
35 , m_viewportTimer(this, &AutoplayExperimentHelper::viewportTimerFired)
28 { 36 {
29 if (document().settings()) { 37 if (document().settings()) {
30 m_mode = AutoplayExperimentConfig::fromString(document().settings()->aut oplayExperimentMode()); 38 m_mode = AutoplayExperimentConfig::fromString(document().settings()->aut oplayExperimentMode());
31 39
32 if (m_mode != AutoplayExperimentConfig::Mode::Off) { 40 if (m_mode != AutoplayExperimentConfig::Mode::Off) {
33 WTF_LOG(Media, "HTMLMediaElement: autoplay experiment set to %d", 41 WTF_LOG(Media, "HTMLMediaElement: autoplay experiment set to %d",
34 m_mode); 42 m_mode);
35 } 43 }
36 } 44 }
37 } 45 }
38 46
39 AutoplayExperimentHelper::~AutoplayExperimentHelper() 47 AutoplayExperimentHelper::~AutoplayExperimentHelper()
40 { 48 {
49 unregisterForPositionUpdatesIfNeeded();
41 } 50 }
42 51
43 void AutoplayExperimentHelper::becameReadyToPlay() 52 void AutoplayExperimentHelper::becameReadyToPlay()
44 { 53 {
45 // Assuming that we're eligible to override the user gesture requirement, 54 // Assuming that we're eligible to override the user gesture requirement,
46 // then play. 55 // either play if we meet the visibility checks, or install a listener
56 // to wait for them to pass.
47 if (isEligible()) { 57 if (isEligible()) {
48 prepareToPlay(GesturelessPlaybackStartedByAutoplayFlagImmediately); 58 if (meetsVisibilityRequirements())
59 prepareToPlay(GesturelessPlaybackStartedByAutoplayFlagImmediately);
60 else
61 registerForPositionUpdatesIfNeeded();
49 } 62 }
50 } 63 }
51 64
52 void AutoplayExperimentHelper::playMethodCalled() 65 void AutoplayExperimentHelper::playMethodCalled()
53 { 66 {
54 // Set the pending state, even if the play isn't going to be pending. 67 // Set the pending state, even if the play isn't going to be pending.
55 // Eligibility can change if, for example, the mute status changes. 68 // Eligibility can change if, for example, the mute status changes.
56 // Having this set is okay. 69 // Having this set is okay.
57 m_playPending = true; 70 m_playPending = true;
58 71
59 if (!UserGestureIndicator::processingUserGesture()) { 72 if (!UserGestureIndicator::processingUserGesture()) {
60 73
61 if (isEligible()) { 74 if (isEligible()) {
62 // Remember that userGestureRequiredForPlay is required for 75 // Remember that userGestureRequiredForPlay is required for
63 // us to be eligible for the experiment. 76 // us to be eligible for the experiment.
64 // We are able to override the gesture requirement now, so 77 // If we are able to override the gesture requirement now, then
65 // do so. 78 // do so. Otherwise, install an event listener if we need one.
66 prepareToPlay(GesturelessPlaybackStartedByPlayMethodImmediately); 79 if (meetsVisibilityRequirements()) {
80 // Override the gesture and play.
81 prepareToPlay(GesturelessPlaybackStartedByPlayMethodImmediately) ;
82 } else {
83 // Wait for viewport visibility.
philipj_slow 2015/09/21 15:02:04 I guess it actually waits for either page visiblit
liberato (no reviews please) 2015/09/23 06:14:57 it doesn't register for page visibility because th
philipj_slow 2015/09/25 15:32:27 Acknowledged.
84 registerForPositionUpdatesIfNeeded();
85 }
67 } 86 }
68 87
88 } else if (m_element.isUserGestureRequiredForPlay()) {
89 unregisterForPositionUpdatesIfNeeded();
69 } 90 }
70 } 91 }
71 92
72 void AutoplayExperimentHelper::pauseMethodCalled() 93 void AutoplayExperimentHelper::pauseMethodCalled()
73 { 94 {
74 // Don't try to autoplay, if we would have. 95 // Don't try to autoplay, if we would have.
75 m_playPending = false; 96 m_playPending = false;
97 unregisterForPositionUpdatesIfNeeded();
76 } 98 }
77 99
78 void AutoplayExperimentHelper::mutedChanged() 100 void AutoplayExperimentHelper::mutedChanged()
79 { 101 {
80 // In other words, start playing if we just needed 'mute' to autoplay. 102 // If we are no longer eligible for the autoplay experiment, then also
103 // quit listening for events. If we are eligible, and if we should be
104 // playing, then start playing. In other words, start playing if
105 // we just needed 'mute' to autoplay.
106 if (!isEligible()) {
107 unregisterForPositionUpdatesIfNeeded();
108 } else {
109 // Try to play. If we can't, then install a listener.
110 if (!maybeStartPlaying())
111 registerForPositionUpdatesIfNeeded();
112 }
113 }
114
115 void AutoplayExperimentHelper::registerForPositionUpdatesIfNeeded()
116 {
117 // If we don't require that the player is in the viewport, then we don't
118 // need the listener.
119 if (!enabled(AutoplayExperimentConfig::Mode::IfViewport)
120 && !enabled(AutoplayExperimentConfig::Mode::IfPageVisible))
121 return;
122
123 LayoutObject* layoutObject = m_element.layoutObject();
124 if (layoutObject && layoutObject->isMedia()) {
125 LayoutMedia* layoutMedia = static_cast<LayoutMedia*>(layoutObject);
126 layoutMedia->setRequestPositionUpdates(true);
127 m_registeredWithView = true;
philipj_slow 2015/09/21 15:02:05 Here we're registering with the layout object, so
liberato (no reviews please) 2015/09/23 06:14:56 Done.
128 }
129 }
130
131 void AutoplayExperimentHelper::unregisterForPositionUpdatesIfNeeded()
132 {
133 if (m_registeredWithView) {
134 LayoutObject* obj = m_element.layoutObject();
135 if (obj && obj->isMedia()) {
136 LayoutMedia* layoutMedia = (LayoutMedia*)obj;
philipj_slow 2015/09/21 15:02:04 static_cast
liberato (no reviews please) 2015/09/23 06:14:56 Done.
137 layoutMedia->setRequestPositionUpdates(false);
138 m_registeredWithView = false;
139 }
140 }
141 }
142
143 void AutoplayExperimentHelper::positionChanged()
144 {
145 // Something, maybe position, has changed. If applicable, start a
146 // timer to look for the end of a scroll operation.
147 // Don't do much work here.
148 // Also note that we are called quite often, including when the
149 // page becomes visible. That's why we don't bother to register
150 // for page visibility changes explicitly.
151
152 LocationState curLocation(m_element);
153 const bool inViewport = meetsVisibilityRequirements(curLocation);
154
155 // If the location has changed, then record the time of the last change.
156 if (m_lastLocation != curLocation) {
157 m_lastLocationUpdateTime = monotonicallyIncreasingTime();
158 m_lastLocation = curLocation;
159 }
160
161 // If we have transitioned to be visible, then start the timer.
162 if (inViewport && !m_wasInViewport) {
163 // We have transitioned from not visible to visible. Reset the timer
164 // to check if we should start autoplay. We do this to avoid
165 // resetting the timer too often in this callback.
166 m_viewportTimer.startOneShot(viewportTimerPollDelay, FROM_HERE);
167 }
168 m_wasInViewport = inViewport;
169 }
170
171 void AutoplayExperimentHelper::triggerAutoplayViewportCheck()
172 {
173 // This method is for testing.
174
175 // If we're registered to receive updates on position, pretend that we did
176 // receive one.
177 if (m_registeredWithView) {
178 positionChanged();
179 }
180
181 // Make sure that the timer is actually active. If not, then
182 // positionChanged wasn't called yet.
183 if (!m_viewportTimer.isActive()) {
184 return;
philipj_slow 2015/09/21 15:02:04 Is this code path reached and if so when *is* posi
liberato (no reviews please) 2015/09/23 06:14:56 actually, the more i think about it, we don't want
philipj_slow 2015/09/25 15:32:27 I see this block was removed.
185 }
186
187 // Make sure that the last update appears to be sufficiently far in the
188 // past to allow the test to succeed.
philipj_slow 2015/09/21 15:02:04 I think "the test" here refers to an if-statement
liberato (no reviews please) 2015/09/23 06:14:56 Done.
189 m_lastLocationUpdateTime = monotonicallyIncreasingTime() - viewportTimerPoll Delay - 1;
190 viewportTimerFired(nullptr);
191 }
192
193 void AutoplayExperimentHelper::viewportTimerFired(Timer<AutoplayExperimentHelper >*)
194 {
195 double now = monotonicallyIncreasingTime();
196 double delta = now - m_lastLocationUpdateTime;
197 if (delta < viewportTimerPollDelay) {
198 // If we are not visible, then skip the timer. It will be started
199 // again if we become visible again.
200 if (m_wasInViewport)
201 m_viewportTimer.startOneShot(viewportTimerPollDelay - delta, FROM_HE RE);
202
203 return;
204 }
205
206 // Sufficient time has passed since the last scroll that we'll
207 // treat it as the end of scroll. Autoplay if we should.
81 maybeStartPlaying(); 208 maybeStartPlaying();
82 } 209 }
83 210
211 bool AutoplayExperimentHelper::meetsVisibilityRequirements(const LocationState& location) const
212 {
213 if (enabled(AutoplayExperimentConfig::Mode::IfPageVisible)
214 && !location.isPageVisible())
215 return false;
216
217 return !enabled(AutoplayExperimentConfig::Mode::IfViewport)
218 || location.isInViewport();
219 }
220
221 bool AutoplayExperimentHelper::meetsVisibilityRequirements() const
222 {
223 // We could check for eligibility here, but we skip it. Some of our
224 // callers need to do it separately, and we don't want to check more
philipj_slow 2015/09/21 15:02:05 Are there some callers that don't check it, and if
liberato (no reviews please) 2015/09/23 06:14:56 mVR() is called quite often during visibility chec
philipj_slow 2015/09/25 15:32:27 OK, I read "We could check for eligibility here, b
225 // than we need to.
226
227 // If visibility isn't required, then it's visible enough.
228 if (!enabled(AutoplayExperimentConfig::Mode::IfViewport)
229 && !enabled(AutoplayExperimentConfig::Mode::IfPageVisible))
230 return true;
231
232 LocationState location(m_element);
233 return meetsVisibilityRequirements(location);
234 }
235
84 bool AutoplayExperimentHelper::maybeStartPlaying() 236 bool AutoplayExperimentHelper::maybeStartPlaying()
85 { 237 {
86 // See if we're allowed to autoplay now. 238 // See if we're allowed to autoplay now.
87 if (!isEligible()) { 239 if (!isEligible() || !meetsVisibilityRequirements()) {
88 return false; 240 return false;
89 } 241 }
90 242
91 // Start playing! 243 // Start playing!
92 prepareToPlay(m_element.shouldAutoplay() 244 prepareToPlay(m_element.shouldAutoplay()
93 ? GesturelessPlaybackStartedByAutoplayFlagAfterScroll 245 ? GesturelessPlaybackStartedByAutoplayFlagAfterScroll
94 : GesturelessPlaybackStartedByPlayMethodAfterScroll); 246 : GesturelessPlaybackStartedByPlayMethodAfterScroll);
95 m_element.playInternal(); 247 m_element.playInternal();
96 248
97 return true; 249 return true;
(...skipping 18 matching lines...) Expand all
116 268
117 if (!enabled(AutoplayExperimentConfig::Mode::ForAudio) 269 if (!enabled(AutoplayExperimentConfig::Mode::ForAudio)
118 && isHTMLAudioElement(m_element)) 270 && isHTMLAudioElement(m_element))
119 return false; 271 return false;
120 272
121 // If nobody has requested playback, either by the autoplay attribute or 273 // If nobody has requested playback, either by the autoplay attribute or
122 // a play() call, then do nothing. 274 // a play() call, then do nothing.
123 if (!m_playPending && !m_element.shouldAutoplay()) 275 if (!m_playPending && !m_element.shouldAutoplay())
124 return false; 276 return false;
125 277
126 // If the video is already playing, then do nothing. Note that there
127 // is not a path where a user gesture is required but the video is
128 // playing. However, we check for completeness.
129 if (!m_element.paused())
130 return false;
131
132 // Note that the viewport test always returns false on desktop, which is 278 // Note that the viewport test always returns false on desktop, which is
133 // why video-autoplay-experiment.html doesn't check -ifmobile . 279 // why video-autoplay-experiment.html doesn't check -ifmobile .
134 if (enabled(AutoplayExperimentConfig::Mode::IfMobile) 280 if (enabled(AutoplayExperimentConfig::Mode::IfMobile)
135 && !document().viewportDescription().isLegacyViewportType()) 281 && !document().viewportDescription().isLegacyViewportType())
136 return false; 282 return false;
137 283
138 // If media is muted, then autoplay when it comes into view. 284 // If we require muted media and this is muted, then it is eligible.
139 if (enabled(AutoplayExperimentConfig::Mode::IfMuted)) 285 if (enabled(AutoplayExperimentConfig::Mode::IfMuted))
140 return m_element.muted(); 286 return m_element.muted();
141 287
142 // Autoplay when it comes into view (if needed), maybe muted. 288 // Element is eligible for gesture override, maybe muted.
143 return true; 289 return true;
144 } 290 }
145 291
146 void AutoplayExperimentHelper::muteIfNeeded() 292 void AutoplayExperimentHelper::muteIfNeeded()
147 { 293 {
148 if (enabled(AutoplayExperimentConfig::Mode::PlayMuted)) { 294 if (enabled(AutoplayExperimentConfig::Mode::PlayMuted)) {
149 ASSERT(!isEligible()); 295 ASSERT(!isEligible());
150 // If we are actually changing the muted state, then this will call 296 // If we are actually changing the muted state, then this will call
151 // mutedChanged(). If isEligible(), then mutedChanged() will try 297 // mutedChanged(). If isEligible(), then mutedChanged() will try
152 // to start playback, which we should not do here. 298 // to start playback, which we should not do here.
153 m_element.setMuted(true); 299 m_element.setMuted(true);
154 } 300 }
155 } 301 }
156 302
157 void AutoplayExperimentHelper::prepareToPlay(AutoplayMetrics metric) 303 void AutoplayExperimentHelper::prepareToPlay(AutoplayMetrics metric)
158 { 304 {
159 m_element.recordAutoplayMetric(metric); 305 m_element.recordAutoplayMetric(metric);
160 306
161 // This also causes !isEligible, so that we don't allow autoplay more than 307 // This also causes !isEligible, so that we don't allow autoplay more than
162 // once. Be sure to do this before muteIfNeeded(). 308 // once. Be sure to do this before muteIfNeeded().
163 m_element.removeUserGestureRequirement(); 309 m_element.removeUserGestureRequirement();
164 310
311 unregisterForPositionUpdatesIfNeeded();
165 muteIfNeeded(); 312 muteIfNeeded();
166 313
167 // Record that this autoplayed without a user gesture. This is normally 314 // Record that this autoplayed without a user gesture. This is normally
168 // set when we discover an autoplay attribute, but we include all cases 315 // set when we discover an autoplay attribute, but we include all cases
169 // where playback started without a user gesture, e.g., play(). 316 // where playback started without a user gesture, e.g., play().
170 m_element.setInitialPlayWithoutUserGestures(true); 317 m_element.setInitialPlayWithoutUserGestures(true);
171 318
172 // Do not actually start playback here. 319 // Do not actually start playback here.
173 } 320 }
174 321
175 Document& AutoplayExperimentHelper::document() const 322 Document& AutoplayExperimentHelper::document() const
176 { 323 {
177 return m_element.document(); 324 return m_element.document();
178 } 325 }
179 326
327 AutoplayExperimentHelper::LocationState::LocationState(Element& element)
328 : m_valid(false)
329 {
330 const LocalDOMWindow* domWindow = element.document().domWindow();
331 if (!domWindow)
332 return;
333
334 // Get the page visibility.
335 Frame* frame = domWindow->frame();
336 if (!frame)
337 return;
338
339 Page* page = frame->page();
340 if (!page)
341 return;
342
343 if (!element.layoutObject())
philipj_slow 2015/09/21 15:02:04 Should this updateLayoutIgnorePendingStylesheets()
liberato (no reviews please) 2015/09/23 06:14:56 i don't think that we need to update the layout he
philipj_slow 2015/09/25 15:32:27 Acknowledged.
344 return;
345
346 const LayoutBox* elementBox = element.layoutObject()->enclosingBox();
347 if (!elementBox)
348 return;
349
350 float zoom = elementBox->style()->effectiveZoom();
351 IntRect us(elementBox->offsetLeft().toInt()
352 , elementBox->offsetTop().toInt()
353 , elementBox->clientWidth().toInt()
354 , elementBox->clientHeight().toInt());
355 IntRect screen(domWindow->scrollX()*zoom, domWindow->scrollY()*zoom, domWind ow->innerWidth()*zoom, domWindow->innerHeight()*zoom);
356
357 m_visibilityState = page->visibilityState();
358 m_element = us;
359 m_screen = screen;
360 m_valid = true;
180 } 361 }
362
363 bool AutoplayExperimentHelper::LocationState::isInViewport() const
364 {
365 // Check if we're in the viewport. If the element is bigger than the
366 // viewport, then be happy if it covers it.
philipj_slow 2015/09/21 15:02:04 This all seems pretty elaborate, why not just retu
liberato (no reviews please) 2015/09/23 06:14:56 "contained in" just seemed nicer than "intersects
philipj_slow 2015/09/25 15:32:27 It seems like it could be a bit annoying in cases
367 if (!m_valid)
368 return false;
369
370 IntRect element = m_element;
371 // If element completely fills the screen, then truncate it to exactly
372 // match the screen. Any element that is wider just has to cover.
373 if (element.x() <= m_screen.x()
374 && element.x() + element.width() >= m_screen.x() + m_screen.width()) {
375 element.setX(m_screen.x());
376 element.setWidth(m_screen.width());
377 }
378
379 if (element.y() <= m_screen.y()
380 && element.y() + element.height() >= m_screen.y() + m_screen.height()) {
381 element.setY(m_screen.y());
382 element.setHeight(m_screen.height());
383 }
384
385 // See if elemet is contained by the screen.
philipj_slow 2015/09/21 15:02:04 Typo if this line doesn't go *poof*
liberato (no reviews please) 2015/09/23 06:14:56 poof.
386 return m_screen.contains(element);
387 }
388
389 bool AutoplayExperimentHelper::LocationState::isPageVisible() const
390 {
391 // Check if we're in the viewport. If the element is bigger than the
philipj_slow 2015/09/21 15:02:05 This comment is copied from the above and isn't ac
liberato (no reviews please) 2015/09/23 06:14:56 it doesn't add much over the single line of code,
392 // viewport, then be happy if it covers it.
393 return m_valid && m_visibilityState == PageVisibilityStateVisible;
394 }
395
396 bool AutoplayExperimentHelper::LocationState::operator==(const LocationState& th em) const
397 {
398 // If either state is not valid, then they are not equal.
399 return m_valid && them.valid()
400 && m_visibilityState == them.visibilityState()
401 && m_screen == them.screen()
402 && m_element == them.element();
403 }
404
405 bool AutoplayExperimentHelper::LocationState::operator!=(const LocationState& th em) const
406 {
407 return !((*this) == them);
philipj_slow 2015/09/21 15:02:04 Don't need () around *this I think.
liberato (no reviews please) 2015/09/23 06:14:56 Done.
408 }
409
410 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698