| Index: third_party/WebKit/Source/core/html/AutoplayExperimentHelper.cpp
|
| diff --git a/third_party/WebKit/Source/core/html/AutoplayExperimentHelper.cpp b/third_party/WebKit/Source/core/html/AutoplayExperimentHelper.cpp
|
| index 3aa5811dc2c6c232404f8dea9fbf040b989ce8f1..efacfc60689c597d98367ad783bed465bbeead12 100644
|
| --- a/third_party/WebKit/Source/core/html/AutoplayExperimentHelper.cpp
|
| +++ b/third_party/WebKit/Source/core/html/AutoplayExperimentHelper.cpp
|
| @@ -6,6 +6,7 @@
|
| #include "core/html/AutoplayExperimentHelper.h"
|
|
|
| #include "core/dom/Document.h"
|
| +#include "core/frame/FrameView.h"
|
| #include "core/frame/Settings.h"
|
| #include "core/html/HTMLMediaElement.h"
|
| #include "core/layout/LayoutBox.h"
|
| @@ -21,15 +22,22 @@ namespace blink {
|
|
|
| using namespace HTMLNames;
|
|
|
| +// Seconds to wait after a video has stopped moving before playing it.
|
| +static const double kViewportTimerPollDelay = 0.5;
|
| +
|
| AutoplayExperimentHelper::AutoplayExperimentHelper(HTMLMediaElement& element)
|
| : m_element(element)
|
| - , m_mode(AutoplayExperimentConfig::Mode::Off)
|
| + , m_mode(Mode::ExperimentOff)
|
| , m_playPending(false)
|
| + , m_registeredWithLayoutObject(false)
|
| + , m_wasInViewport(false)
|
| + , m_lastLocationUpdateTime(-std::numeric_limits<double>::infinity())
|
| + , m_viewportTimer(this, &AutoplayExperimentHelper::viewportTimerFired)
|
| {
|
| if (document().settings()) {
|
| - m_mode = AutoplayExperimentConfig::fromString(document().settings()->autoplayExperimentMode());
|
| + m_mode = fromString(document().settings()->autoplayExperimentMode());
|
|
|
| - if (m_mode != AutoplayExperimentConfig::Mode::Off) {
|
| + if (m_mode != Mode::ExperimentOff) {
|
| WTF_LOG(Media, "HTMLMediaElement: autoplay experiment set to %d",
|
| m_mode);
|
| }
|
| @@ -38,14 +46,19 @@ AutoplayExperimentHelper::AutoplayExperimentHelper(HTMLMediaElement& element)
|
|
|
| AutoplayExperimentHelper::~AutoplayExperimentHelper()
|
| {
|
| + unregisterForPositionUpdatesIfNeeded();
|
| }
|
|
|
| void AutoplayExperimentHelper::becameReadyToPlay()
|
| {
|
| // Assuming that we're eligible to override the user gesture requirement,
|
| - // then play.
|
| + // either play if we meet the visibility checks, or install a listener
|
| + // to wait for them to pass.
|
| if (isEligible()) {
|
| - prepareToPlay(GesturelessPlaybackStartedByAutoplayFlagImmediately);
|
| + if (meetsVisibilityRequirements())
|
| + prepareToPlay(GesturelessPlaybackStartedByAutoplayFlagImmediately);
|
| + else
|
| + registerForPositionUpdatesIfNeeded();
|
| }
|
| }
|
|
|
| @@ -61,11 +74,19 @@ void AutoplayExperimentHelper::playMethodCalled()
|
| if (isEligible()) {
|
| // Remember that userGestureRequiredForPlay is required for
|
| // us to be eligible for the experiment.
|
| - // We are able to override the gesture requirement now, so
|
| - // do so.
|
| - prepareToPlay(GesturelessPlaybackStartedByPlayMethodImmediately);
|
| + // If we are able to override the gesture requirement now, then
|
| + // do so. Otherwise, install an event listener if we need one.
|
| + if (meetsVisibilityRequirements()) {
|
| + // Override the gesture and play.
|
| + prepareToPlay(GesturelessPlaybackStartedByPlayMethodImmediately);
|
| + } else {
|
| + // Wait for viewport visibility.
|
| + registerForPositionUpdatesIfNeeded();
|
| + }
|
| }
|
|
|
| + } else if (m_element.isUserGestureRequiredForPlay()) {
|
| + unregisterForPositionUpdatesIfNeeded();
|
| }
|
| }
|
|
|
| @@ -73,18 +94,163 @@ void AutoplayExperimentHelper::pauseMethodCalled()
|
| {
|
| // Don't try to autoplay, if we would have.
|
| m_playPending = false;
|
| + unregisterForPositionUpdatesIfNeeded();
|
| }
|
|
|
| void AutoplayExperimentHelper::mutedChanged()
|
| {
|
| - // In other words, start playing if we just needed 'mute' to autoplay.
|
| + // If we are no longer eligible for the autoplay experiment, then also
|
| + // quit listening for events. If we are eligible, and if we should be
|
| + // playing, then start playing. In other words, start playing if
|
| + // we just needed 'mute' to autoplay.
|
| + if (!isEligible()) {
|
| + unregisterForPositionUpdatesIfNeeded();
|
| + } else {
|
| + // Try to play. If we can't, then install a listener.
|
| + if (!maybeStartPlaying())
|
| + registerForPositionUpdatesIfNeeded();
|
| + }
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::registerForPositionUpdatesIfNeeded()
|
| +{
|
| + // If we don't require that the player is in the viewport, then we don't
|
| + // need the listener.
|
| + if (!enabled(IfViewport)) {
|
| + if (!enabled(IfPageVisible))
|
| + return;
|
| + }
|
| +
|
| + if (LayoutObject* layoutObject = m_element.layoutObject()) {
|
| + LayoutMedia* layoutMedia = toLayoutMedia(layoutObject);
|
| + layoutMedia->setRequestPositionUpdates(true);
|
| + }
|
| +
|
| + // Set this unconditionally, in case we have no layout object yet.
|
| + m_registeredWithLayoutObject = true;
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::unregisterForPositionUpdatesIfNeeded()
|
| +{
|
| + if (m_registeredWithLayoutObject) {
|
| + if (LayoutObject* obj = m_element.layoutObject()) {
|
| + LayoutMedia* layoutMedia = toLayoutMedia(obj);
|
| + layoutMedia->setRequestPositionUpdates(false);
|
| + }
|
| + }
|
| +
|
| + // Clear this unconditionally so that we don't re-register if we didn't
|
| + // have a LayoutObject now, but get one later.
|
| + m_registeredWithLayoutObject = false;
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::positionChanged(const IntRect& visibleRect)
|
| +{
|
| + // Something, maybe position, has changed. If applicable, start a
|
| + // timer to look for the end of a scroll operation.
|
| + // Don't do much work here.
|
| + // Also note that we are called quite often, including when the
|
| + // page becomes visible. That's why we don't bother to register
|
| + // for page visibility changes explicitly.
|
| +
|
| + m_lastVisibleRect = visibleRect;
|
| +
|
| + if (!m_element.layoutObject())
|
| + return;
|
| +
|
| + IntRect currentLocation = m_element.layoutObject()->absoluteBoundingBoxRect();
|
| + bool inViewport = meetsVisibilityRequirements();
|
| +
|
| + if (m_lastLocation != currentLocation) {
|
| + m_lastLocationUpdateTime = monotonicallyIncreasingTime();
|
| + m_lastLocation = currentLocation;
|
| + }
|
| +
|
| + if (inViewport && !m_wasInViewport) {
|
| + // Only reset the timer when we transition from not visible to
|
| + // visible, because resetting the timer isn't cheap.
|
| + m_viewportTimer.startOneShot(kViewportTimerPollDelay, BLINK_FROM_HERE);
|
| + }
|
| + m_wasInViewport = inViewport;
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::updatePositionNotificationRegistration()
|
| +{
|
| + if (m_registeredWithLayoutObject) {
|
| + LayoutMedia* layoutMedia = toLayoutMedia(m_element.layoutObject());
|
| + layoutMedia->setRequestPositionUpdates(true);
|
| + }
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::triggerAutoplayViewportCheckForTesting()
|
| +{
|
| + FrameView* view = document().view();
|
| + if (view)
|
| + positionChanged(view->rootFrameToContents(view->computeVisibleArea()));
|
| +
|
| + // Make sure that the last update appears to be sufficiently far in the
|
| + // past to appear that scrolling has stopped by now in viewportTimerFired.
|
| + m_lastLocationUpdateTime = monotonicallyIncreasingTime() - kViewportTimerPollDelay - 1;
|
| + viewportTimerFired(nullptr);
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::viewportTimerFired(Timer<AutoplayExperimentHelper>*)
|
| +{
|
| + double now = monotonicallyIncreasingTime();
|
| + double delta = now - m_lastLocationUpdateTime;
|
| + if (delta < kViewportTimerPollDelay) {
|
| + // If we are not visible, then skip the timer. It will be started
|
| + // again if we become visible again.
|
| + if (m_wasInViewport)
|
| + m_viewportTimer.startOneShot(kViewportTimerPollDelay - delta, BLINK_FROM_HERE);
|
| +
|
| + return;
|
| + }
|
| +
|
| + // Sufficient time has passed since the last scroll that we'll
|
| + // treat it as the end of scroll. Autoplay if we should.
|
| maybeStartPlaying();
|
| }
|
|
|
| +bool AutoplayExperimentHelper::meetsVisibilityRequirements() const
|
| +{
|
| + if (enabled(IfPageVisible)
|
| + && m_element.document().pageVisibilityState() != PageVisibilityStateVisible)
|
| + return false;
|
| +
|
| + if (!enabled(IfViewport))
|
| + return true;
|
| +
|
| + if (m_lastVisibleRect.isEmpty())
|
| + return false;
|
| +
|
| + LayoutObject* layoutObject = m_element.layoutObject();
|
| + if (!layoutObject)
|
| + return false;
|
| +
|
| + IntRect currentLocation = layoutObject->absoluteBoundingBoxRect();
|
| +
|
| + // If element completely fills the screen, then truncate it to exactly
|
| + // match the screen. Any element that is wider just has to cover.
|
| + if (currentLocation.x() <= m_lastVisibleRect.x()
|
| + && currentLocation.x() + currentLocation.width() >= m_lastVisibleRect.x() + m_lastVisibleRect.width()) {
|
| + currentLocation.setX(m_lastVisibleRect.x());
|
| + currentLocation.setWidth(m_lastVisibleRect.width());
|
| + }
|
| +
|
| + if (currentLocation.y() <= m_lastVisibleRect.y()
|
| + && currentLocation.y() + currentLocation.height() >= m_lastVisibleRect.y() + m_lastVisibleRect.height()) {
|
| + currentLocation.setY(m_lastVisibleRect.y());
|
| + currentLocation.setHeight(m_lastVisibleRect.height());
|
| + }
|
| +
|
| + return m_lastVisibleRect.contains(currentLocation);
|
| +}
|
| +
|
| bool AutoplayExperimentHelper::maybeStartPlaying()
|
| {
|
| // See if we're allowed to autoplay now.
|
| - if (!isEligible()) {
|
| + if (!isEligible() || !meetsVisibilityRequirements()) {
|
| return false;
|
| }
|
|
|
| @@ -99,6 +265,9 @@ bool AutoplayExperimentHelper::maybeStartPlaying()
|
|
|
| bool AutoplayExperimentHelper::isEligible() const
|
| {
|
| + if (m_mode == Mode::ExperimentOff)
|
| + return false;
|
| +
|
| // If no user gesture is required, then the experiment doesn't apply.
|
| // This is what prevents us from starting playback more than once.
|
| // Since this flag is never set to true once it's cleared, it will block
|
| @@ -106,46 +275,38 @@ bool AutoplayExperimentHelper::isEligible() const
|
| if (!m_element.isUserGestureRequiredForPlay())
|
| return false;
|
|
|
| - if (m_mode == AutoplayExperimentConfig::Mode::Off)
|
| - return false;
|
| -
|
| // Make sure that this is an element of the right type.
|
| - if (!enabled(AutoplayExperimentConfig::Mode::ForVideo)
|
| + if (!enabled(ForVideo)
|
| && isHTMLVideoElement(m_element))
|
| return false;
|
|
|
| - if (!enabled(AutoplayExperimentConfig::Mode::ForAudio)
|
| + if (!enabled(ForAudio)
|
| && isHTMLAudioElement(m_element))
|
| return false;
|
|
|
| // If nobody has requested playback, either by the autoplay attribute or
|
| // a play() call, then do nothing.
|
| - if (!m_playPending && !m_element.shouldAutoplay())
|
| - return false;
|
|
|
| - // If the video is already playing, then do nothing. Note that there
|
| - // is not a path where a user gesture is required but the video is
|
| - // playing. However, we check for completeness.
|
| - if (!m_element.paused())
|
| + if (!m_playPending && !m_element.shouldAutoplay())
|
| return false;
|
|
|
| // Note that the viewport test always returns false on desktop, which is
|
| // why video-autoplay-experiment.html doesn't check -ifmobile .
|
| - if (enabled(AutoplayExperimentConfig::Mode::IfMobile)
|
| + if (enabled(IfMobile)
|
| && !document().viewportDescription().isLegacyViewportType())
|
| return false;
|
|
|
| - // If media is muted, then autoplay when it comes into view.
|
| - if (enabled(AutoplayExperimentConfig::Mode::IfMuted))
|
| + // If we require muted media and this is muted, then it is eligible.
|
| + if (enabled(IfMuted))
|
| return m_element.muted();
|
|
|
| - // Autoplay when it comes into view (if needed), maybe muted.
|
| + // Element is eligible for gesture override, maybe muted.
|
| return true;
|
| }
|
|
|
| void AutoplayExperimentHelper::muteIfNeeded()
|
| {
|
| - if (enabled(AutoplayExperimentConfig::Mode::PlayMuted)) {
|
| + if (enabled(PlayMuted)) {
|
| ASSERT(!isEligible());
|
| // If we are actually changing the muted state, then this will call
|
| // mutedChanged(). If isEligible(), then mutedChanged() will try
|
| @@ -162,6 +323,7 @@ void AutoplayExperimentHelper::prepareToPlay(AutoplayMetrics metric)
|
| // once. Be sure to do this before muteIfNeeded().
|
| m_element.removeUserGestureRequirement();
|
|
|
| + unregisterForPositionUpdatesIfNeeded();
|
| muteIfNeeded();
|
|
|
| // Record that this autoplayed without a user gesture. This is normally
|
| @@ -177,4 +339,25 @@ Document& AutoplayExperimentHelper::document() const
|
| return m_element.document();
|
| }
|
|
|
| +AutoplayExperimentHelper::Mode AutoplayExperimentHelper::fromString(const String& mode)
|
| +{
|
| + Mode value = ExperimentOff;
|
| + if (mode.contains("-forvideo"))
|
| + value |= ForVideo;
|
| + if (mode.contains("-foraudio"))
|
| + value |= ForAudio;
|
| + if (mode.contains("-ifpagevisible"))
|
| + value |= IfPageVisible;
|
| + if (mode.contains("-ifviewport"))
|
| + value |= IfViewport;
|
| + if (mode.contains("-ifmuted"))
|
| + value |= IfMuted;
|
| + if (mode.contains("-ifmobile"))
|
| + value |= IfMobile;
|
| + if (mode.contains("-playmuted"))
|
| + value |= PlayMuted;
|
| +
|
| + return value;
|
| +}
|
| +
|
| }
|
|
|