| 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..9a0b6d8ffbbf3b519cdc527f097d91b3afda9c37 100644
|
| --- a/third_party/WebKit/Source/core/html/AutoplayExperimentHelper.cpp
|
| +++ b/third_party/WebKit/Source/core/html/AutoplayExperimentHelper.cpp
|
| @@ -21,10 +21,18 @@ namespace blink {
|
|
|
| using namespace HTMLNames;
|
|
|
| +// How long do we wait after a scroll event before deciding that no more
|
| +// scroll events are going to arrive?
|
| +static const double viewportTimerPollDelay = 0.5;
|
| +
|
| AutoplayExperimentHelper::AutoplayExperimentHelper(HTMLMediaElement& element)
|
| : m_element(element)
|
| , m_mode(AutoplayExperimentConfig::Mode::Off)
|
| , 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());
|
| @@ -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,143 @@ 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(AutoplayExperimentConfig::Mode::IfViewport)
|
| + && !enabled(AutoplayExperimentConfig::Mode::IfPageVisible))
|
| + return;
|
| +
|
| + LayoutObject* layoutObject = m_element.layoutObject();
|
| + if (layoutObject && layoutObject->isMedia()) {
|
| + LayoutMedia* layoutMedia = static_cast<LayoutMedia*>(layoutObject);
|
| + layoutMedia->setRequestPositionUpdates(true);
|
| + m_registeredWithLayoutObject = true;
|
| + }
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::unregisterForPositionUpdatesIfNeeded()
|
| +{
|
| + if (m_registeredWithLayoutObject) {
|
| + LayoutObject* obj = m_element.layoutObject();
|
| + if (obj && obj->isMedia()) {
|
| + LayoutMedia* layoutMedia = static_cast<LayoutMedia*>(obj);
|
| + layoutMedia->setRequestPositionUpdates(false);
|
| + m_registeredWithLayoutObject = false;
|
| + }
|
| + }
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::positionChanged()
|
| +{
|
| + // 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.
|
| +
|
| + LocationState curLocation(m_element);
|
| + const bool inViewport = meetsVisibilityRequirements(curLocation);
|
| +
|
| + // If the location has changed, then record the time of the last change.
|
| + if (m_lastLocation != curLocation) {
|
| + m_lastLocationUpdateTime = monotonicallyIncreasingTime();
|
| + m_lastLocation = curLocation;
|
| + }
|
| +
|
| + // If we have transitioned to be visible, then start the timer.
|
| + if (inViewport && !m_wasInViewport) {
|
| + // We have transitioned from not visible to visible. Reset the timer
|
| + // to check if we should start autoplay. We do this to avoid
|
| + // resetting the timer too often in this callback.
|
| + m_viewportTimer.startOneShot(viewportTimerPollDelay, FROM_HERE);
|
| + }
|
| + m_wasInViewport = inViewport;
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::triggerAutoplayViewportCheck()
|
| +{
|
| + // This method is for testing.
|
| +
|
| + // If we're registered to receive updates on position, pretend that we did
|
| + // receive one.
|
| + if (m_registeredWithLayoutObject) {
|
| + positionChanged();
|
| + }
|
| +
|
| + // 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() - viewportTimerPollDelay - 1;
|
| + viewportTimerFired(nullptr);
|
| +}
|
| +
|
| +void AutoplayExperimentHelper::viewportTimerFired(Timer<AutoplayExperimentHelper>*)
|
| +{
|
| + double now = monotonicallyIncreasingTime();
|
| + double delta = now - m_lastLocationUpdateTime;
|
| + if (delta < viewportTimerPollDelay) {
|
| + // 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(viewportTimerPollDelay - delta, 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 LocationState& location) const
|
| +{
|
| + if (enabled(AutoplayExperimentConfig::Mode::IfPageVisible)
|
| + && !location.isPageVisible())
|
| + return false;
|
| +
|
| + return !enabled(AutoplayExperimentConfig::Mode::IfViewport)
|
| + || location.isElementVisible();
|
| +}
|
| +
|
| +bool AutoplayExperimentHelper::meetsVisibilityRequirements() const
|
| +{
|
| + // We could check for eligibility here, but we skip it. Some of our
|
| + // callers need to do it separately, and we don't want to check more
|
| + // than we need to.
|
| +
|
| + // If visibility isn't required, then it's visible enough.
|
| + if (!enabled(AutoplayExperimentConfig::Mode::IfViewport)
|
| + && !enabled(AutoplayExperimentConfig::Mode::IfPageVisible))
|
| + return true;
|
| +
|
| + LocationState location(m_element);
|
| + return meetsVisibilityRequirements(location);
|
| +}
|
| +
|
| bool AutoplayExperimentHelper::maybeStartPlaying()
|
| {
|
| // See if we're allowed to autoplay now.
|
| - if (!isEligible()) {
|
| + if (!isEligible() || !meetsVisibilityRequirements()) {
|
| return false;
|
| }
|
|
|
| @@ -123,23 +269,17 @@ bool AutoplayExperimentHelper::isEligible() const
|
| 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())
|
| - 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)
|
| && !document().viewportDescription().isLegacyViewportType())
|
| return false;
|
|
|
| - // If media is muted, then autoplay when it comes into view.
|
| + // If we require muted media and this is muted, then it is eligible.
|
| if (enabled(AutoplayExperimentConfig::Mode::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;
|
| }
|
|
|
| @@ -162,6 +302,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 +318,91 @@ Document& AutoplayExperimentHelper::document() const
|
| return m_element.document();
|
| }
|
|
|
| +AutoplayExperimentHelper::LocationState::LocationState(Element& element)
|
| +{
|
| + // Default to empty rectangles. The screen is offset so that contains()
|
| + // returns false.
|
| + m_element = IntRect();
|
| + m_screen = IntRect(-1, -1, 0, 0);
|
| + ASSERT(!m_screen.contains(m_element));
|
| + m_visibilityState = PageVisibilityStateHidden;
|
| +
|
| + const LocalDOMWindow* domWindow = element.document().domWindow();
|
| + if (!domWindow)
|
| + return;
|
| +
|
| + // Get the page visibility.
|
| + Frame* frame = domWindow->frame();
|
| + if (!frame)
|
| + return;
|
| +
|
| + Page* page = frame->page();
|
| + if (!page)
|
| + return;
|
| +
|
| + m_visibilityState = page->visibilityState();
|
| +
|
| + if (!element.layoutObject())
|
| + return;
|
| +
|
| + const LayoutBox* elementBox = element.layoutObject()->enclosingBox();
|
| + if (!elementBox)
|
| + return;
|
| +
|
| + float zoom = elementBox->style()->effectiveZoom();
|
| + m_element = IntRect(elementBox->offsetLeft().toInt()
|
| + , elementBox->offsetTop().toInt()
|
| + , elementBox->clientWidth().toInt()
|
| + , elementBox->clientHeight().toInt());
|
| +
|
| + m_screen = IntRect(domWindow->scrollX()*zoom
|
| + , domWindow->scrollY()*zoom
|
| + , domWindow->innerWidth()*zoom
|
| + , domWindow->innerHeight()*zoom);
|
| +}
|
| +
|
| +bool AutoplayExperimentHelper::LocationState::isElementVisible() const
|
| +{
|
| + // Check if we're in the viewport. If the element is bigger than the
|
| + // viewport, then be happy if it covers it.
|
| +
|
| + IntRect element = m_element;
|
| + // If element completely fills the screen, then truncate it to exactly
|
| + // match the screen. Any element that is wider just has to cover.
|
| + if (element.x() <= m_screen.x()
|
| + && element.x() + element.width() >= m_screen.x() + m_screen.width()) {
|
| + element.setX(m_screen.x());
|
| + element.setWidth(m_screen.width());
|
| + }
|
| +
|
| + if (element.y() <= m_screen.y()
|
| + && element.y() + element.height() >= m_screen.y() + m_screen.height()) {
|
| + element.setY(m_screen.y());
|
| + element.setHeight(m_screen.height());
|
| + }
|
| +
|
| + return m_screen.contains(element);
|
| +}
|
| +
|
| +bool AutoplayExperimentHelper::LocationState::isPageVisible() const
|
| +{
|
| + return m_visibilityState == PageVisibilityStateVisible;
|
| +}
|
| +
|
| +bool AutoplayExperimentHelper::LocationState::operator==(const LocationState& them) const
|
| +{
|
| + // As a special case, if either is missing position information, then
|
| + // they are not equal.
|
| + return !m_element.isEmpty() && !them.element().isEmpty()
|
| + && !m_screen.isEmpty() && !them.screen().isEmpty()
|
| + && m_visibilityState == them.visibilityState()
|
| + && m_screen == them.screen()
|
| + && m_element == them.element();
|
| +}
|
| +
|
| +bool AutoplayExperimentHelper::LocationState::operator!=(const LocationState& them) const
|
| +{
|
| + return !(*this == them);
|
| +}
|
| +
|
| }
|
|
|