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..9906f7c6a48fdc87e8529b11c2ea9645c7160bc0 |
| --- /dev/null |
| +++ b/Source/core/html/AutoplayExperimentHelper.cpp |
| @@ -0,0 +1,325 @@ |
| +/* |
| + * Copyright (C) 2015 Google Inc. All rights reserved. |
| + * |
| + * Redistribution and use in source and binary forms, with or without |
| + * modification, are permitted provided that the following conditions are |
| + * met: |
| + * |
| + * * Redistributions of source code must retain the above copyright |
| + * notice, this list of conditions and the following disclaimer. |
| + * * Redistributions in binary form must reproduce the above |
| + * copyright notice, this list of conditions and the following disclaimer |
| + * in the documentation and/or other materials provided with the |
| + * distribution. |
| + * * Neither the name of Google Inc. nor the names of its |
| + * contributors may be used to endorse or promote products derived from |
| + * this software without specific prior written permission. |
| + * |
| + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| + */ |
| + |
| +#include "config.h" |
| +#include "core/html/AutoplayExperimentHelper.h" |
| + |
| +#include "core/dom/Document.h" |
| +#include "core/frame/Settings.h" |
| +#include "core/html/HTMLMediaElement.h" |
| +#include "platform/UserGestureIndicator.h" |
| +#include "platform/geometry/FloatRect.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; |
| + |
| +// Event listener that just informs the AutoplayExperimentHelper that a |
| +// scroll has happened. |
| +class AutoplayExperimentHelper::ScrollListener : public EventListener { |
| + public: |
| + ScrollListener(AutoplayExperimentHelper* helper) : EventListener(CPPEventListenerType), m_helper(helper) { } |
| + virtual bool operator==(const EventListener& them) |
| + { |
| + return &them == this; |
| + } |
| + |
| + void handleEvent(ExecutionContext*, Event*) override |
| + { |
| + if (m_helper) |
| + m_helper->notifyScrolled(); |
| + } |
| + |
| + private: |
| + AutoplayExperimentHelper* m_helper; |
|
philipj_slow
2015/08/13 10:15:40
It looks like the pointer is always passed and nev
liberato (no reviews please)
2015/09/01 06:54:19
indeed, but the listener has since been removed.
|
| +}; |
| + |
| + |
| +AutoplayExperimentHelper::AutoplayExperimentHelper(HTMLMediaElement& element) |
| + : m_element(element) |
| + , m_mode(ExperimentOff) |
| + , m_playPending(false) |
| + , m_viewportTimer(this, &AutoplayExperimentHelper::viewportTimerFired) |
| +{ |
| + if (document().settings()) { |
| + const String& autoplayMode = document().settings()->autoplayExperimentMode(); |
|
ojan
2015/08/11 02:45:21
Did you also want to measure the case Philip sugge
ojan
2015/08/11 02:49:34
Also, I don't see anything for tab visibility. So,
liberato (no reviews please)
2015/09/01 06:54:19
added a check for page()->visibilityState() to isI
philipj_slow
2015/09/02 09:31:47
What about the experiment to only require page vis
philipj_slow
2015/09/04 08:44:08
I haven't seen the other CL yet, so just a gentle
|
| + if (autoplayMode.contains("enabled")) { |
| + // Autoplay with no gesture requirement. |
| + m_mode |= ExperimentEnabled; |
| + } |
| + if (autoplayMode.contains("-ifviewport")) { |
| + // Override gesture requirement only if the player is within the |
| + // current viewport. |
| + m_mode |= ExperimentIfViewport; |
| + } |
| + if (autoplayMode.contains("-ifmuted")) { |
| + // Override gesture requirement only if the media is muted or has |
| + // no audio track. |
| + m_mode |= ExperimentIfMuted; |
| + } |
| + if (autoplayMode.contains("-ifmobile")) { |
| + // Override gesture requirement only if the page is optimized |
| + // for mobile. |
| + m_mode |= ExperimentIfMobile; |
| + } |
| + if (autoplayMode.contains("-playmuted")) { |
|
ojan
2015/08/11 02:45:21
For good measure, add a comment explaining this on
liberato (no reviews please)
2015/09/01 06:54:19
done, though the docs are with the enums in Autopl
|
| + m_mode |= ExperimentPlayMuted; |
| + } |
| + |
| + if (m_mode != ExperimentOff) { |
| + WTF_LOG(Media, "HTMLMediaElement: autoplay experiment set to '%s' (%d)", |
| + autoplayMode.ascii().data(), m_mode); |
| + } |
| + } |
| +} |
| + |
| +AutoplayExperimentHelper::~AutoplayExperimentHelper() |
| +{ |
| + clearEventListenerIfNeeded(); |
| +} |
| + |
| +void AutoplayExperimentHelper::onReadyToPlay() |
| +{ |
| + // 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(GesturelessPlaybackStartedByLoad); |
| + else |
| + installEventListenerIfNeeded(); |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::onPlayMethodCalled() |
| +{ |
| + // 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(GesturelessPlaybackStartedByPlayMethod); |
| + } else { |
| + // Wait for viewport visibility. |
| + installEventListenerIfNeeded(); |
| + } |
| + } |
| + |
| + } else if (m_element.isUserGestureRequiredForPlay()) { |
| + clearEventListenerIfNeeded(); |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::onPauseMethodCalled() |
| +{ |
| + // Don't try to autoplay, if we would have. |
| + m_playPending = false; |
| + clearEventListenerIfNeeded(); |
| +} |
| + |
| +void AutoplayExperimentHelper::onMuteChanged() |
| +{ |
| + // 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 (!(m_mode & ExperimentIfViewport)) |
| + return; |
| + |
| + if (document().domWindow() && !m_scrollListener) { |
| + m_scrollListener = adoptRef(new ScrollListener(this)); |
| + document().domWindow()->addEventListener("scroll", m_scrollListener, false); |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::clearEventListenerIfNeeded() |
| +{ |
| + if (m_scrollListener) { |
| + LocalDOMWindow* domWindow = document().domWindow(); |
| + if (domWindow) { |
| + domWindow->removeEventListener("scroll", m_scrollListener, false); |
| + } |
| + // Either way, clear our ref. |
| + m_scrollListener.clear(); |
| + } |
| +} |
| + |
| +void AutoplayExperimentHelper::notifyScrolled() |
| +{ |
| + // Reset the timer to indicate that scrolling has happened |
| + // recently, and might still be ongoing. |
| + m_viewportTimer.startOneShot(viewportTimerPollDelay, FROM_HERE); |
| +} |
| + |
| +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. |
| + 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 (!(m_mode & ExperimentIfViewport)) |
| + return true; |
| + |
| + // Check if we're in the viewport. |
| + const LocalDOMWindow* domWindow = document().domWindow(); |
| + if (!domWindow) |
| + return false; |
| + |
| + FloatRect us(m_element.offsetLeft(), m_element.offsetTop(), m_element.clientWidth(), m_element.clientHeight()); |
| + FloatRect screen(domWindow->scrollX(), domWindow->scrollY(), domWindow->innerWidth(), domWindow->innerHeight()); |
| + |
| + return screen.contains(us); |
|
ojan
2015/08/11 02:45:21
What if the video is larger than the screen or par
liberato (no reviews please)
2015/09/01 06:54:19
for those videos that don't show controls, or use
|
| +} |
| + |
| +bool AutoplayExperimentHelper::maybeStartPlaying() |
| +{ |
| + // See if we're allowed to autoplay now. |
| + if (!isEligible() |
| + || !isInViewportIfNeeded()) { |
| + return false; |
| + } |
| + |
| + // Start playing! |
| + prepareToPlay(GesturelessPlaybackStartedByScroll); |
| + 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 == ExperimentOff) |
| + 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()) |
| + 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 ((m_mode & ExperimentIfMobile) |
| + && !document().viewportDescription().isLegacyViewportType()) |
| + return false; |
| + |
| + if (m_mode & ExperimentIfMuted) { |
| + // If media is muted, then autoplay when it comes into view. |
| + return m_element.fastHasAttribute(mutedAttr) || m_element.muted(); |
| + } |
| + |
| + // Autoplay when it comes into view (if needed), maybe muted. |
| + return true; |
| +} |
| + |
| +void AutoplayExperimentHelper::muteIfNeeded() |
| +{ |
| + if (m_mode & ExperimentPlayMuted && !m_element.muted()) { |
| + // This will call onMuteChanged(), which we really don't want |
| + // to do anything, since we're called when trying to play. If |
| + // the element is still marked as eligible, then we'll probably |
|
philipj_slow
2015/08/13 10:15:40
setMuted returns early if there's no change, so I
liberato (no reviews please)
2015/09/01 06:54:20
infinite recursion: true, though it's not obvious
|
| + // recurse indefinitely. |
| + ASSERT(!isEligible()); |
| + 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 |
| + // 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(); |
| +} |
| + |
| +} |