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

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

Issue 1179223002: Implement autoplay gesture override experiment. (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: Rebased. Created 5 years, 5 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
« no previous file with comments | « Source/core/html/HTMLMediaElement.h ('k') | Source/core/testing/Internals.h » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: Source/core/html/HTMLMediaElement.cpp
diff --git a/Source/core/html/HTMLMediaElement.cpp b/Source/core/html/HTMLMediaElement.cpp
index 47e62a65b6ab2a764e5c27369bedde49c86df3c5..28983344f3f6bd5b8a5703bb5d6a4efcf3927748 100644
--- a/Source/core/html/HTMLMediaElement.cpp
+++ b/Source/core/html/HTMLMediaElement.cpp
@@ -128,6 +128,12 @@ static const char* boolString(bool val)
// URL protocol used to signal that the media source API is being used.
static const char mediaSourceBlobProtocol[] = "blob";
+// How often do we poll for scrolling stopped during a visibility check?
+static const double visibilityTimerPollDelay = 0.5;
+// How long do we repeat visibility checks? We will poll once every PollDelay.
+// Note that we will stop checking if we don't detect scrolling, also.
+static const double visibilityCheckDuration = 5;
+
using namespace HTMLNames;
typedef WillBeHeapHashSet<RawPtrWillBeWeakMember<HTMLMediaElement>> WeakMediaElementSet;
@@ -253,25 +259,7 @@ static bool canLoadURL(const KURL& url, const ContentType& contentType, const St
return false;
}
-// These values are used for a histogram. Do not reorder.
-enum AutoplayMetrics {
- // Media element with autoplay seen.
- AutoplayMediaFound = 0,
- // Autoplay enabled and user stopped media play at any point.
- AutoplayStopped = 1,
- // Autoplay enabled but user bailed out on media play early.
- AutoplayBailout = 2,
- // Autoplay disabled but user manually started media.
- AutoplayManualStart = 3,
- // Autoplay was (re)enabled through a user-gesture triggered load()
- AutoplayEnabledThroughLoad = 4,
- // Autoplay disabled by sandbox flags.
- AutoplayDisabledBySandbox = 5,
- // This enum value must be last.
- NumberOfAutoplayMetrics,
-};
-
-static void recordAutoplayMetric(AutoplayMetrics metric)
+void HTMLMediaElement::recordAutoplayMetric(AutoplayMetrics metric)
{
Platform::current()->histogramEnumeration("Blink.MediaElement.Autoplay", metric, NumberOfAutoplayMetrics);
}
@@ -302,6 +290,24 @@ WebMimeRegistry::SupportsType HTMLMediaElement::supportsType(const ContentType&
URLRegistry* HTMLMediaElement::s_mediaStreamRegistry = 0;
+class HTMLMediaElement::AutoplayExperimentTouchListener : public EventListener {
+ public:
+ AutoplayExperimentTouchListener(HTMLMediaElement* element) : EventListener(CPPEventListenerType), m_element(element) { }
+ virtual bool operator==(const EventListener& them)
+ {
+ return &them == this;
+ }
+
+ void handleEvent(ExecutionContext*, Event*) override
+ {
+ if (m_element)
+ m_element->beginPeriodicVisibilityCheck();
+ }
+
+ private:
+ HTMLMediaElement* m_element;
+};
+
void HTMLMediaElement::setMediaStreamRegistry(URLRegistry* registry)
{
ASSERT(!s_mediaStreamRegistry);
@@ -363,12 +369,19 @@ HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document& docum
, m_isFinalizing(false)
, m_initialPlayWithoutUserGestures(false)
, m_autoplayMediaCounted(false)
+ , m_autoplayExperimentPlayPending(false)
+ , m_autoplayExperimentStartedByExperiment(false)
+ , m_autoplayExperimentMode(ExperimentOff)
, m_audioTracks(AudioTrackList::create(*this))
, m_videoTracks(VideoTrackList::create(*this))
, m_textTracks(nullptr)
#if ENABLE(WEB_AUDIO)
, m_audioSourceNode(nullptr)
#endif
+ , m_autoplayVisibilityTimer(this, &HTMLMediaElement::visibilityTimerFired)
+ , m_autoplayLastScrollX(std::numeric_limits<double>::quiet_NaN())
+ , m_autoplayLastScrollY(std::numeric_limits<double>::quiet_NaN())
+ , m_autoplayVisibilityTimerSpan(0)
{
#if ENABLE(OILPAN)
ThreadState::current()->registerPreFinalizer(this);
@@ -377,8 +390,24 @@ HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document& docum
WTF_LOG(Media, "HTMLMediaElement::HTMLMediaElement(%p)", this);
- if (document.settings() && document.settings()->mediaPlaybackRequiresUserGesture())
- m_userGestureRequiredForPlay = true;
+ if (document.settings()) {
+ if (document.settings()->mediaPlaybackRequiresUserGesture())
+ m_userGestureRequiredForPlay = true;
+
+ const String& autoplayMode = document.settings()->autoplayExperimentMode();
+ if (autoplayMode == "always") {
+ m_autoplayExperimentMode = ExperimentAlways;
+ } else if (autoplayMode == "if-muted") {
+ m_autoplayExperimentMode = ExperimentIfMuted;
+ } else if (autoplayMode == "play-muted") {
+ m_autoplayExperimentMode = ExperimentPlayMuted;
+ }
+
+ if (m_autoplayExperimentMode != ExperimentOff) {
+ WTF_LOG(Media, "HTMLMediaElement: autoplay experiment set to '%s'",
+ autoplayMode.ascii().data());
+ }
+ }
setHasCustomStyleCallbacks();
addElementToDocumentMap(this, &document);
@@ -387,6 +416,9 @@ HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document& docum
HTMLMediaElement::~HTMLMediaElement()
{
WTF_LOG(Media, "HTMLMediaElement::~HTMLMediaElement(%p)", this);
+
+ autoplayExperimentClearEventListenerIfNeeded();
+
#if !ENABLE(OILPAN)
// HTMLMediaElement and m_asyncEventQueue always become unreachable
// together. So HTMLMediaElement and m_asyncEventQueue are destructed in
@@ -1557,11 +1589,25 @@ void HTMLMediaElement::setReadyState(ReadyState state)
if (document().isSandboxed(SandboxAutomaticFeatures)) {
recordAutoplayMetric(AutoplayDisabledBySandbox);
- } else if (!m_userGestureRequiredForPlay) {
- m_paused = false;
- invalidateCachedTime();
- scheduleEvent(EventTypeNames::play);
- scheduleEvent(EventTypeNames::playing);
+ } else {
+ // If the autoplay experiment says that it's okay to play now,
+ // then don't require a user gesture.
+ if (autoplayExperimentIsEligible()) {
+ if (autoplayExperimentIsVisible()) {
+ autoplayExperimentPrepareToPlay(AutoplayExperimentStartedByLoad);
+ // Will play below.
+ } else {
+ // Wait for visibility checks to pass.
+ autoplayExperimentInstallEventListenerIfNeeded();
+ }
+ }
+
+ if (!m_userGestureRequiredForPlay) {
+ m_paused = false;
+ invalidateCachedTime();
+ scheduleEvent(EventTypeNames::play);
+ scheduleEvent(EventTypeNames::playing);
+ }
}
}
@@ -1962,8 +2008,28 @@ void HTMLMediaElement::play()
{
WTF_LOG(Media, "HTMLMediaElement::play(%p)", this);
+ // 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_autoplayExperimentPlayPending = true;
+
if (!UserGestureIndicator::processingUserGesture()) {
autoplayMediaEncountered();
+
+ if (autoplayExperimentIsEligible()) {
+ // Remember that m_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 (autoplayExperimentIsVisible()) {
+ // Override the gesture and play.
+ autoplayExperimentPrepareToPlay(AutoplayExperimentStartedByPlay);
+ } else {
+ // Wait for visibility.
+ autoplayExperimentInstallEventListenerIfNeeded();
+ }
+ }
+
if (m_userGestureRequiredForPlay) {
String message = ExceptionMessages::failedToExecute("play", "HTMLMediaElement", "API can only be initiated by a user gesture.");
document().executionContext()->addConsoleMessage(ConsoleMessage::create(JSMessageSource, WarningMessageLevel, message));
@@ -1973,6 +2039,7 @@ void HTMLMediaElement::play()
if (m_autoplayMediaCounted)
recordAutoplayMetric(AutoplayManualStart);
m_userGestureRequiredForPlay = false;
+ autoplayExperimentClearEventListenerIfNeeded();
}
playInternal();
@@ -2029,13 +2096,21 @@ void HTMLMediaElement::gesturelessInitialPlayHalted()
recordAutoplayMetric(AutoplayStopped);
+ if (m_autoplayExperimentStartedByExperiment)
+ recordAutoplayMetric(AutoplayExperimentStopped);
+
// We count the user as having bailed-out on the video if they watched
// less than one minute and less than 50% of it.
double playedTime = currentTime();
if (playedTime < 60) {
double progress = playedTime / duration();
- if (progress < 0.5)
+ if (progress < 0.5) {
recordAutoplayMetric(AutoplayBailout);
+ // If this autoplay was started by the experiment, then record
+ // that separately.
+ if (m_autoplayExperimentStartedByExperiment)
+ recordAutoplayMetric(AutoplayExperimentBailout);
+ }
}
}
@@ -2046,6 +2121,10 @@ void HTMLMediaElement::pause()
if (m_networkState == NETWORK_EMPTY)
scheduleDelayedAction(LoadMediaResource);
+ // Don't try to autoplay, if we would have.
+ m_autoplayExperimentPlayPending = false;
+ autoplayExperimentClearEventListenerIfNeeded();
+
m_autoplaying = false;
if (!m_paused) {
@@ -2143,6 +2222,18 @@ void HTMLMediaElement::setMuted(bool muted)
m_muted = muted;
liberato (no reviews please) 2015/07/27 06:03:09 @philipj: you are correct that we don't stop JS fr
+ // 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 (!autoplayExperimentIsEligible()) {
+ autoplayExperimentClearEventListenerIfNeeded();
+ } else {
+ // Try to play. If we can't, then install a visibility listener.
+ if (!autoplayExperimentMaybeStartPlaying())
+ autoplayExperimentInstallEventListenerIfNeeded();
+ }
+
updateVolume();
scheduleEvent(EventTypeNames::volumechange);
@@ -3795,4 +3886,168 @@ DEFINE_TRACE(HTMLMediaElement::AudioSourceProviderImpl)
}
#endif
+void HTMLMediaElement::autoplayExperimentInstallEventListenerIfNeeded()
+{
+ if (document().domWindow() && !m_autoplayExperimentTouchListener) {
+ m_autoplayExperimentTouchListener = adoptRef(new AutoplayExperimentTouchListener(this));
+ // Listen for events that might show a user-initiated scroll. We
+ // don't try to catch programmatic scrolls right now.
+ document().domWindow()->addEventListener("touchend", m_autoplayExperimentTouchListener, false);
+ document().domWindow()->addEventListener("touchcancel", m_autoplayExperimentTouchListener, false);
+ }
+}
+
+void HTMLMediaElement::autoplayExperimentClearEventListenerIfNeeded()
+{
+ if (m_autoplayExperimentTouchListener) {
+ LocalDOMWindow* domWindow = document().domWindow();
+ if (domWindow) {
+ domWindow->removeEventListener("touchend", m_autoplayExperimentTouchListener, false);
+ domWindow->removeEventListener("touchcancel", m_autoplayExperimentTouchListener, false);
+ }
+ // Either way, clear our ref.
+ m_autoplayExperimentTouchListener.clear();
+ }
+}
+
+void HTMLMediaElement::beginPeriodicVisibilityCheck()
+{
+ // Note that a visibility check might already be in progress.
+ // Always reset the span of the checks to maximum.
+ m_autoplayVisibilityTimerSpan = visibilityCheckDuration;
+
+ // If the timer isn't active, then fire the timer immediately to reset
+ // it. We might just setOneShot(0).
+ if (!m_autoplayVisibilityTimer.isActive()) {
+ visibilityTimerFired(0);
+ }
+}
+
+void HTMLMediaElement::visibilityTimerFired(Timer<HTMLMediaElement>*)
+{
+ const LocalDOMWindow* domWindow = document().domWindow();
+ if (!domWindow)
+ return;
+
+ const int currentScrollX = domWindow->scrollX();
+ const int currentScrollY = domWindow->scrollY();
+
+ if (currentScrollX != m_autoplayLastScrollX
+ || currentScrollY != m_autoplayLastScrollY) {
+ // Still scrolling, so wait a bit more.
+ m_autoplayLastScrollX = currentScrollX;
+ m_autoplayLastScrollY = currentScrollY;
+
+ // Reset the timer to check again if we haven't tried for long enough.
+ m_autoplayVisibilityTimerSpan -= visibilityTimerPollDelay;
+ if (m_autoplayVisibilityTimerSpan >= 0) {
+ m_autoplayVisibilityTimer.startOneShot(visibilityTimerPollDelay, FROM_HERE);
+ }
+ } else {
+ // No longer scrolling, so check visibility and stop.
+ autoplayExperimentMaybeStartPlaying();
+ }
+}
+
+bool HTMLMediaElement::autoplayExperimentIsVisible()
+{
+ // 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.
+
+ // Autoplay is requested, and we're willing to override if the visibility
+ // requirements are met.
+
+ // Check visibility.
+ const LocalDOMWindow* domWindow = document().domWindow();
+ if (!domWindow)
+ return false;
+
+ FloatRect us(offsetLeft(), offsetTop(), clientWidth(), clientHeight());
+ FloatRect screen(domWindow->scrollX(), domWindow->scrollY(), domWindow->innerWidth(), domWindow->innerHeight());
+
+ return screen.contains(us);
+}
+
+bool HTMLMediaElement::autoplayExperimentMaybeStartPlaying()
+{
+ // Make sure that we're eligible and visible.
+ if (!autoplayExperimentIsEligible() || !autoplayExperimentIsVisible()) {
+ return false;
+ }
+
+ // Start playing!
+ autoplayExperimentPrepareToPlay(AutoplayExperimentStartedByScroll);
+ // Why are we always preparing? Just go!
+ playInternal();
+
+ return true;
+}
+
+bool HTMLMediaElement::autoplayExperimentIsEligible() 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_userGestureRequiredForPlay)
+ return false;
+
+ if (m_autoplayExperimentMode == ExperimentOff)
+ return false;
+
+ // If nobody has requested playback, either by the autoplay attribute or
+ // a play() call, then do nothing.
+ if (!m_autoplayExperimentPlayPending && !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_paused)
+ return false;
+
+ // Note that the viewport test always returns false on desktop. For
+ // tests, we allow an override.
+ const bool optimizedForMobile = document().viewportDescription().isLegacyViewportType()
+ || (document().settings() && document().settings()->overrideOptimizedForMobileCheck());
+
+ if (!optimizedForMobile)
+ return false;
+
+ if (m_autoplayExperimentMode == ExperimentIfMuted) {
+ // If media is muted, then autoplay when it comes into view.
+ return fastHasAttribute(mutedAttr) || m_muted;
+ }
+
+ // Autoplay when it comes into view, maybe muted.
+ return true;
+}
+
+void HTMLMediaElement::autoplayExperimentMuteIfNeeded()
+{
+ if (m_autoplayExperimentMode == ExperimentPlayMuted) {
+ m_muted = true;
+ updateVolume();
+ }
+}
+
+void HTMLMediaElement::autoplayExperimentPrepareToPlay(AutoplayMetrics metric)
+{
+ recordAutoplayMetric(metric);
+
+ // This also causes !autoplayExperimentIsEligible, so that we don't
+ // allow autoplay more than once.
+ m_userGestureRequiredForPlay = false;
+
+ m_autoplayExperimentStartedByExperiment = true;
+ autoplayExperimentClearEventListenerIfNeeded();
+ autoplayExperimentMuteIfNeeded();
+
+ // 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_initialPlayWithoutUserGestures = true;
+}
+
}
« no previous file with comments | « Source/core/html/HTMLMediaElement.h ('k') | Source/core/testing/Internals.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698