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(); |
+} |
+ |
+} |