| OLD | NEW |
| (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 | |
| OLD | NEW |