Chromium Code Reviews| Index: Source/core/html/AutoplayExperimentHelper.cpp |
| diff --git a/Source/core/html/AutoplayExperimentHelper.cpp b/Source/core/html/AutoplayExperimentHelper.cpp |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..03f1dc72dd676a7ef3eecd0c2b1117c5259b9bfe |
| --- /dev/null |
| +++ b/Source/core/html/AutoplayExperimentHelper.cpp |
| @@ -0,0 +1,337 @@ |
| +// Copyright 2015 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +#include "config.h" |
| +#include "core/html/AutoplayExperimentHelper.h" |
| + |
| +#include "core/dom/Document.h" |
| +#include "core/frame/Settings.h" |
| +#include "core/html/HTMLMediaElement.h" |
| +#include "core/layout/LayoutBox.h" |
| +#include "core/layout/LayoutObject.h" |
| +#include "core/layout/LayoutVideo.h" |
| +#include "core/layout/LayoutView.h" |
| +#include "core/page/Page.h" |
| +#include "platform/Logging.h" |
| +#include "platform/UserGestureIndicator.h" |
| +#include "platform/geometry/IntRect.h" |
| + |
| +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_viewportTimer(this, &AutoplayExperimentHelper::viewportTimerFired) |
| + , m_registeredWithView(false) |
| +{ |
| + if (document().settings()) { |
| + m_mode = AutoplayExperimentConfig::fromString(document().settings()->autoplayExperimentMode()); |
| + |
| + if (m_mode != AutoplayExperimentConfig::Mode::Off) { |
| + WTF_LOG(Media, "HTMLMediaElement: autoplay experiment set to %d", |
| + m_mode); |
| + } |
| + } |
| +} |
| + |
| +AutoplayExperimentHelper::~AutoplayExperimentHelper() |
| +{ |
| + clearEventListenerIfNeeded(); |
| +} |
| + |
| +void AutoplayExperimentHelper::becameReadyToPlay() |
| +{ |
| + // Assuming that we're eligible to override the user gesture requirement, |
| + // either play if we meet the visibility checks, or install a listener |
| + // to wait for them to pass. |
| + if (isEligible()) { |
| + if (isInViewportIfNeeded()) |
| + prepareToPlay(GesturelessPlaybackStartedByAutoplayFlagImmediately); |
| + else |
| + installEventListenerIfNeeded(); |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::playMethodCalled() |
| +{ |
| + // Set the pending state, even if the play isn't going to be pending. |
| + // Eligibility can change if, for example, the mute status changes. |
| + // Having this set is okay. |
| + m_playPending = true; |
| + |
| + if (!UserGestureIndicator::processingUserGesture()) { |
| + |
| + if (isEligible()) { |
| + // Remember that userGestureRequiredForPlay is required for |
| + // us to be eligible for the experiment. |
| + // If we are able to override the gesture requirement now, then |
| + // do so. Otherwise, install an event listener if we need one. |
| + if (isInViewportIfNeeded()) { |
| + // Override the gesture and play. |
| + prepareToPlay(GesturelessPlaybackStartedByPlayMethodImmediately); |
| + } else { |
| + // Wait for viewport visibility. |
| + installEventListenerIfNeeded(); |
| + } |
| + } |
| + |
| + } else if (m_element.isUserGestureRequiredForPlay()) { |
| + clearEventListenerIfNeeded(); |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::pauseMethodCalled() |
| +{ |
| + // Don't try to autoplay, if we would have. |
| + m_playPending = false; |
| + clearEventListenerIfNeeded(); |
| +} |
| + |
| +void AutoplayExperimentHelper::mutedChanged() |
| +{ |
| + // 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()) { |
| + clearEventListenerIfNeeded(); |
| + } else { |
| + // Try to play. If we can't, then install a listener. |
| + if (!maybeStartPlaying()) |
| + installEventListenerIfNeeded(); |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::installEventListenerIfNeeded() |
| +{ |
| + // If we don't require that the player is in the viewport, then we don't |
| + // need the listener. |
| + if (!enabled(AutoplayExperimentConfig::Mode::IfViewport)) |
| + return; |
| + |
| + LayoutObject* layoutObject = m_element.layoutObject(); |
| + if (layoutObject && layoutObject->isVideo()) { |
| + LayoutVideo* layoutVideo = (LayoutVideo*)layoutObject; |
|
philipj_slow
2015/09/02 09:24:11
Use static_cast
liberato (no reviews please)
2015/09/04 06:49:45
Done.
|
| + layoutVideo->setRequestPositionUpdates(true); |
| + // TODO(liberato): do we really need to keep track of this? it's |
|
philipj_slow
2015/09/02 09:24:11
Is this TODO to be fixed before landing? Who knows
liberato (no reviews please)
2015/09/04 06:49:46
yes, it will. i wanted to put it out for CL with
philipj_slow
2015/09/04 08:42:29
OK, I see this will be done in a separate CL.
|
| + // only to make clearEventListener() faster. |
| + m_registeredWithView = true; |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::clearEventListenerIfNeeded() |
| +{ |
| + if (m_registeredWithView) { |
| + LayoutObject* obj = m_element.layoutObject(); |
| + if (obj && obj->isVideo()) { |
| + LayoutVideo* video = (LayoutVideo*)obj; |
|
philipj_slow
2015/09/02 09:24:11
static_cast
liberato (no reviews please)
2015/09/04 06:49:45
thanks, forgot what year it is.
|
| + video->setRequestPositionUpdates(false); |
| + m_registeredWithView = false; |
| + } |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::positionChanged() |
| +{ |
| + // Reset the timer to indicate that scrolling has happened |
| + // recently, and might still be ongoing. |
| + // 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. |
| + |
| + // Since we're called very often, even if our visibility hasn't changed, |
| + // make sure that we only reset the timer if something has moved. |
| + // Otherwise, we will reset the timer every time a video frame plays |
| + // anywhere, or the mouse moves, etc. |
| + // We may want to lower the frequency of this via another timer, so that |
| + // we do no work here. |
| + LocationState curLocation(m_element); |
| + if (curLocation != m_lastLocation) { |
| + m_viewportTimer.startOneShot(viewportTimerPollDelay, FROM_HERE); |
| + m_lastLocation = curLocation; |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::triggerAutoplayViewportCheck() |
| +{ |
| + viewportTimerFired(0); |
|
philipj_slow
2015/09/02 09:24:11
nullptr
liberato (no reviews please)
2015/09/04 06:49:46
Done.
|
| +} |
| + |
| +void AutoplayExperimentHelper::viewportTimerFired(Timer<AutoplayExperimentHelper>*) |
| +{ |
| + // Sufficient time has passed since the last scroll that we'll |
| + // treat it as the end of scroll. Autoplay if we should. |
|
philipj_slow
2015/09/02 09:24:12
I didn't follow the discussion around this very cl
liberato (no reviews please)
2015/09/04 06:49:45
no, unfortunately.
philipj_slow
2015/09/04 08:42:29
Acknowledged.
|
| + maybeStartPlaying(); |
| +} |
| + |
| +bool AutoplayExperimentHelper::isInViewportIfNeeded() |
| +{ |
| + // 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. |
| + // Also remember that page visibility is assumed for clank. |
| + |
| + // If viewport visibility isn't required, then it's visible enough. |
| + if (!enabled(AutoplayExperimentConfig::Mode::IfViewport)) |
| + return true; |
| + |
| + return LocationState(m_element).isInViewport(); |
| +} |
| + |
| +bool AutoplayExperimentHelper::maybeStartPlaying() |
| +{ |
| + // See if we're allowed to autoplay now. |
| + if (!isEligible() |
|
philipj_slow
2015/09/02 09:24:11
A bit much line breaking here, it shouldn't be ver
liberato (no reviews please)
2015/09/04 06:49:46
Done.
|
| + || !isInViewportIfNeeded()) { |
| + return false; |
| + } |
| + |
| + // Start playing! |
| + prepareToPlay(m_element.autoplay() |
|
philipj_slow
2015/09/02 09:24:11
autoplay() checks the content attribute, but there
liberato (no reviews please)
2015/09/04 06:49:45
good point. i'll add this for clarity, but i thin
philipj_slow
2015/09/04 08:42:29
It should be possible to clear the autoplaying fla
|
| + ? GesturelessPlaybackStartedByAutoplayFlagAfterScroll |
| + : GesturelessPlaybackStartedByPlayMethodAfterScroll); |
| + m_element.playInternal(); |
| + |
| + return true; |
| +} |
| + |
| +bool AutoplayExperimentHelper::isEligible() const |
| +{ |
| + // 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 |
| + // the autoplay experiment forever. |
| + if (!m_element.isUserGestureRequiredForPlay()) |
| + return false; |
| + |
| + if (m_mode == AutoplayExperimentConfig::Mode::Off) |
| + return false; |
| + |
| + // If nobody has requested playback, either by the autoplay attribute or |
| + // a play() call, then do nothing. |
| + if (!m_playPending && !m_element.autoplay()) |
|
philipj_slow
2015/09/02 09:24:11
shouldAutoplay() here too. To write a test for the
liberato (no reviews please)
2015/09/04 06:49:46
Done, including test.
|
| + 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 (enabled(AutoplayExperimentConfig::Mode::IfMuted)) { |
| + // If media is muted, then autoplay when it comes into view. |
|
philipj_slow
2015/09/02 09:24:11
Move this outside the if and remove {} for consist
liberato (no reviews please)
2015/09/04 06:49:45
Done.
|
| + return m_element.fastHasAttribute(mutedAttr) || m_element.muted(); |
| + } |
| + |
| + // Autoplay when it comes into view (if needed), maybe muted. |
| + return true; |
| +} |
| + |
| +void AutoplayExperimentHelper::muteIfNeeded() |
| +{ |
| + if (enabled(AutoplayExperimentConfig::Mode::PlayMuted)) { |
| + ASSERT(!isEligible()); |
| + // If we are actually changing the muted state, then this will call |
| + // mutedChanged(). If isEligible(), then mutedChanged() will try |
| + // to start playback, which we should not do here. |
| + m_element.setMuted(true); |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::prepareToPlay(AutoplayMetrics metric) |
| +{ |
| + m_element.recordAutoplayMetric(metric); |
| + |
| + // This also causes !isEligible, so that we don't alow autoplay more than |
|
philipj_slow
2015/09/02 09:24:11
s/alow/allow/
liberato (no reviews please)
2015/09/04 06:49:45
Done.
|
| + // once. Be sure to do this before muteIfNeeded(). |
| + m_element.removeUserGestureRequirement(); |
| + |
| + clearEventListenerIfNeeded(); |
| + muteIfNeeded(); |
| + |
| + // Record that this autoplayed without a user gesture. This is normally |
| + // set when we discover an autoplay attribute, but we include all cases |
| + // where playback started without a user gesture, e.g., play(). |
| + m_element.setInitialPlayWithoutUserGestures(true); |
| + |
| + // Do not actually start playback here. |
| +} |
| + |
| +Document& AutoplayExperimentHelper::document() const |
| +{ |
| + return m_element.document(); |
| +} |
| + |
| +AutoplayExperimentHelper::LocationState::LocationState(Element& element) |
| + : m_valid(false) |
| +{ |
| + 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; |
| + |
| + if (!element.layoutObject()) |
| + return; |
| + |
| + const LayoutBox* elementBox = element.layoutObject()->enclosingBox(); |
| + if (!elementBox) |
| + return; |
| + |
| + float zoom = elementBox->style()->effectiveZoom(); |
| + IntRect us(elementBox->offsetLeft().toInt() |
| + , elementBox->offsetTop().toInt() |
| + , elementBox->clientWidth().toInt() |
| + , elementBox->clientHeight().toInt()); |
| + IntRect screen(domWindow->scrollX()*zoom, domWindow->scrollY()*zoom, domWindow->innerWidth()*zoom, domWindow->innerHeight()*zoom); |
| + |
| + m_visibilityState = page->visibilityState(); |
| + m_element = us; |
| + m_screen = screen; |
| + m_valid = true; |
| +} |
| + |
| +bool AutoplayExperimentHelper::LocationState::isInViewport() |
| +{ |
| + // Check if we're in the viewport. |
| + return m_valid |
| + && m_visibilityState == PageVisibilityStateVisible |
| + && m_screen.contains(m_element); |
| +} |
| + |
| +bool AutoplayExperimentHelper::LocationState::operator==(const LocationState& them) const |
| +{ |
| + // If either state is not valid, then they are not equal. |
| + return m_valid && them.valid() |
| + && m_visibilityState == them.visibilityState() |
| + && m_screen == them.screen() |
| + && m_element == them.element(); |
| +} |
| + |
| +bool AutoplayExperimentHelper::LocationState::operator!=(const LocationState& them) const |
| +{ |
| + return !((*this) == them); |
| +} |
| + |
| +} |