Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1009)

Unified Diff: Source/core/html/AutoplayExperimentHelper.cpp

Issue 1179223002: Implement autoplay gesture override experiment. (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: linker errors on win/mac... Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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);
+}
+
+}

Powered by Google App Engine
This is Rietveld 408576698