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