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

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

Issue 2510353004: Deprecating AutoplayExperimentHelper (Closed)
Patch Set: rebased Created 4 years 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
(Empty)
1 // Copyright 2016 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 "core/html/AutoplayExperimentHelper.h"
6
7 #include "core/dom/Document.h"
8 #include "core/frame/Settings.h"
9 #include "core/html/HTMLMediaElement.h"
10 #include "core/page/Page.h"
11 #include "platform/UserGestureIndicator.h"
12 #include "platform/geometry/IntRect.h"
13
14 namespace blink {
15
16 using namespace HTMLNames;
17
18 // Seconds to wait after a video has stopped moving before playing it.
19 static const double kViewportTimerPollDelay = 0.5;
20
21 AutoplayExperimentHelper::AutoplayExperimentHelper(Client* client)
22 : m_client(client),
23 m_mode(Mode::ExperimentOff),
24 m_playPending(false),
25 m_registeredWithLayoutObject(false),
26 m_wasInViewport(false),
27 m_autoplayMediaEncountered(false),
28 m_playbackStartedMetricRecorded(false),
29 m_waitingForAutoplayPlaybackStop(false),
30 m_recordedElement(false),
31 m_lastLocationUpdateTime(-std::numeric_limits<double>::infinity()),
32 m_viewportTimer(this, &AutoplayExperimentHelper::viewportTimerFired),
33 m_autoplayDeferredMetric(GesturelessPlaybackNotOverridden) {
34 m_mode = fromString(this->client().autoplayExperimentMode());
35
36 DVLOG_IF(3, isExperimentEnabled()) << "autoplay experiment set to " << m_mode;
37 }
38
39 AutoplayExperimentHelper::~AutoplayExperimentHelper() {}
40
41 void AutoplayExperimentHelper::becameReadyToPlay() {
42 // Assuming that we're eligible to override the user gesture requirement,
43 // either play if we meet the visibility checks, or install a listener
44 // to wait for them to pass. We do not actually start playback; our
45 // caller must do that.
46 autoplayMediaEncountered();
47
48 if (isEligible()) {
49 if (meetsVisibilityRequirements())
50 prepareToAutoplay(GesturelessPlaybackStartedByAutoplayFlagImmediately);
51 else
52 registerForPositionUpdatesIfNeeded();
53 }
54 }
55
56 void AutoplayExperimentHelper::playMethodCalled() {
57 // If a play is already pending, then do nothing. We're already trying
58 // to play. Similarly, do nothing if we're already playing.
59 if (m_playPending || !m_client->paused())
60 return;
61
62 if (!UserGestureIndicator::utilizeUserGesture()) {
63 autoplayMediaEncountered();
64
65 // Check for eligibility, but don't worry if playback is currently
66 // pending. If we're still not eligible, then this play() will fail.
67 if (isEligible(IgnorePendingPlayback)) {
68 m_playPending = true;
69
70 // If we are able to override the gesture requirement now, then
71 // do so. Otherwise, install an event listener if we need one.
72 // We do not actually start playback; play() will do that.
73 if (meetsVisibilityRequirements()) {
74 // Override the gesture and assume that play() will succeed.
75 prepareToAutoplay(GesturelessPlaybackStartedByPlayMethodImmediately);
76 } else {
77 // Wait for viewport visibility.
78 // TODO(liberato): if the autoplay is allowed soon enough, then
79 // it should still record *Immediately. Otherwise, we end up
80 // here before the first layout sometimes, when the item is
81 // visible but we just don't know that yet.
82 registerForPositionUpdatesIfNeeded();
83 }
84 }
85 } else if (isLockedPendingUserGesture()) {
86 // If this media tried to autoplay, and we haven't played it yet, then
87 // record that the user provided the gesture to start it the first time.
88 if (m_autoplayMediaEncountered && !m_playbackStartedMetricRecorded)
89 recordAutoplayMetric(AutoplayManualStart);
90 // Don't let future gestureless playbacks affect metrics.
91 m_autoplayMediaEncountered = true;
92 m_playbackStartedMetricRecorded = true;
93 m_playPending = false;
94
95 unregisterForPositionUpdatesIfNeeded();
96 }
97 }
98
99 void AutoplayExperimentHelper::pauseMethodCalled() {
100 // Don't try to autoplay, if we would have.
101 m_playPending = false;
102 unregisterForPositionUpdatesIfNeeded();
103 }
104
105 void AutoplayExperimentHelper::loadMethodCalled() {
106 if (isLockedPendingUserGesture() &&
107 UserGestureIndicator::utilizeUserGesture()) {
108 recordAutoplayMetric(AutoplayEnabledThroughLoad);
109 unlockUserGesture(GesturelessPlaybackEnabledByLoad);
110 }
111 }
112
113 void AutoplayExperimentHelper::mutedChanged() {
114 // Mute changes are always allowed if this is unlocked.
115 if (!client().isLockedPendingUserGesture())
116 return;
117
118 // Changes with a user gesture are okay.
119 if (UserGestureIndicator::utilizeUserGesture())
120 return;
121
122 // If the mute state has changed to 'muted', then it's okay.
123 if (client().muted())
124 return;
125
126 // If nothing is playing, then changes are okay too.
127 if (client().paused())
128 return;
129
130 // Trying to unmute without a user gesture.
131
132 // If we don't care about muted state, then it's okay.
133 if (!enabled(IfMuted) && !(client().isCrossOrigin() && enabled(OrMuted)))
134 return;
135
136 // Unmuting isn't allowed, so pause.
137 client().pauseInternal();
138 }
139
140 void AutoplayExperimentHelper::registerForPositionUpdatesIfNeeded() {
141 // If we don't require that the player is in the viewport, then we don't
142 // need the listener.
143 if (!requiresViewportVisibility()) {
144 if (!enabled(IfPageVisible))
145 return;
146 }
147
148 m_client->setRequestPositionUpdates(true);
149
150 // Set this unconditionally, in case we have no layout object yet.
151 m_registeredWithLayoutObject = true;
152 }
153
154 void AutoplayExperimentHelper::unregisterForPositionUpdatesIfNeeded() {
155 if (m_registeredWithLayoutObject)
156 m_client->setRequestPositionUpdates(false);
157
158 // Clear this unconditionally so that we don't re-register if we didn't
159 // have a LayoutObject now, but get one later.
160 m_registeredWithLayoutObject = false;
161 }
162
163 void AutoplayExperimentHelper::positionChanged(const IntRect& visibleRect) {
164 // Something, maybe position, has changed. If applicable, start a
165 // timer to look for the end of a scroll operation.
166 // Don't do much work here.
167 // Also note that we are called quite often, including when the
168 // page becomes visible. That's why we don't bother to register
169 // for page visibility changes explicitly.
170 if (visibleRect.isEmpty())
171 return;
172
173 m_lastVisibleRect = visibleRect;
174
175 IntRect currentLocation = client().absoluteBoundingBoxRect();
176 if (currentLocation.isEmpty())
177 return;
178
179 bool inViewport = meetsVisibilityRequirements();
180
181 if (m_lastLocation != currentLocation) {
182 m_lastLocationUpdateTime = monotonicallyIncreasingTime();
183 m_lastLocation = currentLocation;
184 }
185
186 if (inViewport && !m_wasInViewport) {
187 // Only reset the timer when we transition from not visible to
188 // visible, because resetting the timer isn't cheap.
189 m_viewportTimer.startOneShot(kViewportTimerPollDelay, BLINK_FROM_HERE);
190 }
191 m_wasInViewport = inViewport;
192 }
193
194 void AutoplayExperimentHelper::updatePositionNotificationRegistration() {
195 if (m_registeredWithLayoutObject)
196 m_client->setRequestPositionUpdates(true);
197 }
198
199 void AutoplayExperimentHelper::triggerAutoplayViewportCheckForTesting() {
200 // Make sure that the last update appears to be sufficiently far in the
201 // past to appear that scrolling has stopped by now in viewportTimerFired.
202 m_lastLocationUpdateTime =
203 monotonicallyIncreasingTime() - kViewportTimerPollDelay - 1;
204 viewportTimerFired(nullptr);
205 }
206
207 void AutoplayExperimentHelper::viewportTimerFired(TimerBase*) {
208 double now = monotonicallyIncreasingTime();
209 double delta = now - m_lastLocationUpdateTime;
210 if (delta < kViewportTimerPollDelay) {
211 // If we are not visible, then skip the timer. It will be started
212 // again if we become visible again.
213 if (m_wasInViewport)
214 m_viewportTimer.startOneShot(kViewportTimerPollDelay - delta,
215 BLINK_FROM_HERE);
216
217 return;
218 }
219
220 // Sufficient time has passed since the last scroll that we'll
221 // treat it as the end of scroll. Autoplay if we should.
222 maybeStartPlaying();
223 }
224
225 bool AutoplayExperimentHelper::meetsVisibilityRequirements() const {
226 if (enabled(IfPageVisible) &&
227 client().pageVisibilityState() != PageVisibilityStateVisible)
228 return false;
229
230 if (!requiresViewportVisibility())
231 return true;
232
233 if (m_lastVisibleRect.isEmpty())
234 return false;
235
236 IntRect currentLocation = client().absoluteBoundingBoxRect();
237 if (currentLocation.isEmpty())
238 return false;
239
240 // In partial-viewport mode, we require only 1x1 area.
241 if (enabled(IfPartialViewport)) {
242 return m_lastVisibleRect.intersects(currentLocation);
243 }
244
245 // Element must be completely visible, or as much as fits.
246 // If element completely fills the screen, then truncate it to exactly
247 // match the screen. Any element that is wider just has to cover.
248 if (currentLocation.x() <= m_lastVisibleRect.x() &&
249 currentLocation.x() + currentLocation.width() >=
250 m_lastVisibleRect.x() + m_lastVisibleRect.width()) {
251 currentLocation.setX(m_lastVisibleRect.x());
252 currentLocation.setWidth(m_lastVisibleRect.width());
253 }
254
255 if (currentLocation.y() <= m_lastVisibleRect.y() &&
256 currentLocation.y() + currentLocation.height() >=
257 m_lastVisibleRect.y() + m_lastVisibleRect.height()) {
258 currentLocation.setY(m_lastVisibleRect.y());
259 currentLocation.setHeight(m_lastVisibleRect.height());
260 }
261
262 return m_lastVisibleRect.contains(currentLocation);
263 }
264
265 bool AutoplayExperimentHelper::maybeStartPlaying() {
266 // See if we're allowed to autoplay now.
267 if (!isGestureRequirementOverridden())
268 return false;
269
270 // Start playing!
271 prepareToAutoplay(client().shouldAutoplay()
272 ? GesturelessPlaybackStartedByAutoplayFlagAfterScroll
273 : GesturelessPlaybackStartedByPlayMethodAfterScroll);
274
275 // Record that this played without a user gesture.
276 // This should rarely actually do anything. Usually, playMethodCalled()
277 // and becameReadyToPlay will handle it, but toggling muted state can,
278 // in some cases, also trigger autoplay if the autoplay attribute is set
279 // after the media is ready to play.
280 autoplayMediaEncountered();
281
282 client().playInternal();
283
284 return true;
285 }
286
287 bool AutoplayExperimentHelper::isGestureRequirementOverridden() const {
288 return isEligible() && meetsVisibilityRequirements();
289 }
290
291 bool AutoplayExperimentHelper::isPlaybackDeferred() const {
292 return m_playPending;
293 }
294
295 bool AutoplayExperimentHelper::isEligible(EligibilityMode mode) const {
296 if (m_mode == Mode::ExperimentOff)
297 return false;
298
299 // If autoplay is disabled, no one is eligible.
300 if (!client().isAutoplayAllowedPerSettings())
301 return false;
302
303 // If no user gesture is required, then the experiment doesn't apply.
304 // This is what prevents us from starting playback more than once.
305 // Since this flag is never set to true once it's cleared, it will block
306 // the autoplay experiment forever.
307 if (!isLockedPendingUserGesture())
308 return false;
309
310 // Make sure that this is an element of the right type.
311 if (!enabled(ForVideo) && client().isHTMLVideoElement())
312 return false;
313
314 if (!enabled(ForAudio) && client().isHTMLAudioElement())
315 return false;
316
317 // If nobody has requested playback, either by the autoplay attribute or
318 // a play() call, then do nothing.
319
320 if (mode != IgnorePendingPlayback && !m_playPending &&
321 !client().shouldAutoplay())
322 return false;
323
324 // Note that the viewport test always returns false on desktop, which is
325 // why video-autoplay-experiment.html doesn't check -ifmobile .
326 if (enabled(IfMobile) && !client().isLegacyViewportType())
327 return false;
328
329 // If we require same-origin, then check the origin.
330 if (enabled(IfSameOrigin) && client().isCrossOrigin()) {
331 // We're cross-origin, so block unless it's muted content and OrMuted
332 // is enabled. For good measure, we also block all audio elements.
333 if (client().isHTMLAudioElement() || !client().muted() ||
334 !enabled(OrMuted)) {
335 return false;
336 }
337 }
338
339 // If we require muted media and this is muted, then it is eligible.
340 if (enabled(IfMuted))
341 return client().muted();
342
343 // Element is eligible for gesture override, maybe muted.
344 return true;
345 }
346
347 void AutoplayExperimentHelper::muteIfNeeded() {
348 if (enabled(PlayMuted))
349 client().setMuted(true);
350 }
351
352 void AutoplayExperimentHelper::unlockUserGesture(AutoplayMetrics metric) {
353 // Note that this could be moved back into HTMLMediaElement fairly easily.
354 // It's only here so that we can record the reason, and we can hide the
355 // ordering between unlocking and recording from the element this way.
356 if (!client().isLockedPendingUserGesture())
357 return;
358
359 setDeferredOverrideReason(metric);
360 client().unlockUserGesture();
361 }
362
363 void AutoplayExperimentHelper::setDeferredOverrideReason(
364 AutoplayMetrics metric) {
365 // If the player is unlocked, then we don't care about any later reason.
366 if (!client().isLockedPendingUserGesture())
367 return;
368
369 m_autoplayDeferredMetric = metric;
370 }
371
372 void AutoplayExperimentHelper::prepareToAutoplay(AutoplayMetrics metric) {
373 // This also causes !isEligible, so that we don't allow autoplay more than
374 // once. Be sure to do this before muteIfNeeded().
375 // Also note that, at this point, we know that we're goint to start
376 // playback. However, we still don't record the metric here. Instead,
377 // we let playbackStarted() do that later.
378 setDeferredOverrideReason(metric);
379
380 // Don't bother to call autoplayMediaEncountered, since whoever initiates
381 // playback has do it anyway, in case we don't allow autoplay.
382
383 unregisterForPositionUpdatesIfNeeded();
384 muteIfNeeded();
385
386 // Do not actually start playback here.
387 }
388
389 AutoplayExperimentHelper::Mode AutoplayExperimentHelper::fromString(
390 const String& mode) {
391 Mode value = ExperimentOff;
392 if (mode.contains("-forvideo"))
393 value |= ForVideo;
394 if (mode.contains("-foraudio"))
395 value |= ForAudio;
396 if (mode.contains("-ifpagevisible"))
397 value |= IfPageVisible;
398 if (mode.contains("-ifviewport"))
399 value |= IfViewport;
400 if (mode.contains("-ifpartialviewport"))
401 value |= IfPartialViewport;
402 if (mode.contains("-ifmuted"))
403 value |= IfMuted;
404 if (mode.contains("-ifmobile"))
405 value |= IfMobile;
406 if (mode.contains("-ifsameorigin"))
407 value |= IfSameOrigin;
408 if (mode.contains("-ormuted"))
409 value |= OrMuted;
410 if (mode.contains("-playmuted"))
411 value |= PlayMuted;
412
413 return value;
414 }
415
416 void AutoplayExperimentHelper::autoplayMediaEncountered() {
417 if (!m_autoplayMediaEncountered) {
418 m_autoplayMediaEncountered = true;
419 recordAutoplayMetric(AutoplayMediaFound);
420 }
421 }
422
423 bool AutoplayExperimentHelper::isLockedPendingUserGesture() const {
424 return client().isLockedPendingUserGesture();
425 }
426
427 void AutoplayExperimentHelper::playbackStarted() {
428 recordAutoplayMetric(AnyPlaybackStarted);
429
430 // Forget about our most recent visibility check. If another override is
431 // requested, then we'll have to refresh it. That way, we don't need to
432 // keep it up to date in the interim.
433 m_lastVisibleRect = IntRect();
434 m_wasInViewport = false;
435
436 // Any pending play is now playing.
437 m_playPending = false;
438
439 if (m_playbackStartedMetricRecorded)
440 return;
441
442 // Whether we record anything or not, we only want to record metrics for
443 // the initial playback.
444 m_playbackStartedMetricRecorded = true;
445
446 // If this is a gestureless start, then record why it was allowed.
447 if (m_autoplayMediaEncountered) {
448 m_waitingForAutoplayPlaybackStop = true;
449 recordAutoplayMetric(m_autoplayDeferredMetric);
450 }
451 }
452
453 void AutoplayExperimentHelper::playbackStopped() {
454 const bool ended = client().ended();
455 const bool bailout = isBailout();
456
457 // Record that play was paused. We don't care if it was autoplay,
458 // play(), or the user manually started it.
459 recordAutoplayMetric(ended ? AnyPlaybackComplete : AnyPlaybackPaused);
460 if (bailout)
461 recordAutoplayMetric(AnyPlaybackBailout);
462
463 // If this was a gestureless play, then record that separately.
464 // These cover attr and play() gestureless starts.
465 if (m_waitingForAutoplayPlaybackStop) {
466 m_waitingForAutoplayPlaybackStop = false;
467
468 recordAutoplayMetric(ended ? AutoplayComplete : AutoplayPaused);
469
470 if (bailout)
471 recordAutoplayMetric(AutoplayBailout);
472 }
473 }
474
475 void AutoplayExperimentHelper::recordAutoplayMetric(AutoplayMetrics metric) {
476 client().recordAutoplayMetric(metric);
477 }
478
479 bool AutoplayExperimentHelper::isBailout() const {
480 // We count the user as having bailed-out on the video if they watched
481 // less than one minute and less than 50% of it.
482 const double playedTime = client().currentTime();
483 const double progress = playedTime / client().duration();
484 return (playedTime < 60) && (progress < 0.5);
485 }
486
487 void AutoplayExperimentHelper::recordSandboxFailure() {
488 // We record autoplayMediaEncountered here because we know
489 // that the autoplay attempt will fail.
490 autoplayMediaEncountered();
491 recordAutoplayMetric(AutoplayDisabledBySandbox);
492 }
493
494 void AutoplayExperimentHelper::loadingStarted() {
495 if (m_recordedElement)
496 return;
497
498 m_recordedElement = true;
499 recordAutoplayMetric(client().isHTMLVideoElement() ? AnyVideoElement
500 : AnyAudioElement);
501 }
502
503 bool AutoplayExperimentHelper::requiresViewportVisibility() const {
504 return client().isHTMLVideoElement() &&
505 (enabled(IfViewport) || enabled(IfPartialViewport));
506 }
507
508 bool AutoplayExperimentHelper::isExperimentEnabled() {
509 return m_mode != Mode::ExperimentOff;
510 }
511
512 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698