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

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: removed TODO that was already done. 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/HTMLMediaElement.cpp
diff --git a/Source/core/html/HTMLMediaElement.cpp b/Source/core/html/HTMLMediaElement.cpp
index 63eda2639e62a01ca9a5fd97b8bdfc5a2bd153f8..467b3f153f72f588e718d1600a8f53ba6e7d0932 100644
--- a/Source/core/html/HTMLMediaElement.cpp
+++ b/Source/core/html/HTMLMediaElement.cpp
@@ -126,6 +126,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;
@@ -251,25 +257,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);
}
@@ -300,6 +288,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);
@@ -364,9 +370,16 @@ HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document& docum
, m_audioTracks(AudioTrackList::create(*this))
, m_videoTracks(VideoTrackList::create(*this))
, m_textTracks(nullptr)
+ , m_autoplayExperimentPlayPending(false)
+ , m_autoplayExperimentStartedByExperiment(false)
+ , m_autoplayExperimentMode(ExperimentOff)
#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);
@@ -375,8 +388,38 @@ HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document& docum
WTF_LOG(Media, "HTMLMediaElement::HTMLMediaElement(%p)", this);
- if (document.settings() && document.settings()->mediaPlaybackRequiresUserGesture())
philipj_slow 2015/08/05 10:03:11 Wow, this has actually been broken the whole time,
liberato (no reviews please) 2015/08/06 06:37:58 i'm glad you're one of the good guys.
- m_userGestureRequiredForPlay = true;
+ if (document.settings()) {
+ if (document.settings()->mediaPlaybackRequiresUserGesture())
+ m_userGestureRequiredForPlay = true;
+
+ const String& autoplayMode = document.settings()->autoplayExperimentMode();
+ if (autoplayMode.contains("enabled")) {
+ // Autoplay with no gesture requirement.
+ m_autoplayExperimentMode |= ExperimentEnabled;
+ }
+ if (autoplayMode.contains("-ifvisible")) {
+ // Override gesture requirement only if the player is visible.
+ m_autoplayExperimentMode |= ExperimentIfVisible;
+ }
+ if (autoplayMode.contains("-ifmuted")) {
+ // Override gesture requirement only if the media is muted or has
+ // no audio track.
+ m_autoplayExperimentMode |= ExperimentIfMuted;
+ }
+ if (autoplayMode.contains("-ifmobile")) {
+ // Override gesture requirement only if the page is optimized
+ // for mobile.
+ m_autoplayExperimentMode |= ExperimentIfMobile;
+ }
+ if (autoplayMode.contains("-playmuted")) {
+ m_autoplayExperimentMode |= ExperimentPlayMuted;
+ }
+
+ if (m_autoplayExperimentMode != ExperimentOff) {
+ WTF_LOG(Media, "HTMLMediaElement: autoplay experiment set to '%s' (%d)",
+ autoplayMode.ascii().data(), m_autoplayExperimentMode);
+ }
+ }
setHasCustomStyleCallbacks();
addElementToDocumentMap(this, &document);
@@ -385,6 +428,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
@@ -1555,11 +1601,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()) {
philipj_slow 2015/08/05 10:03:11 Perhaps rename this so that it's clear that it alw
liberato (no reviews please) 2015/08/06 06:37:57 Done.
+ autoplayExperimentPrepareToPlay(AutoplayExperimentStartedByLoad);
+ // Will play below.
+ } else {
+ // Wait for visibility checks to pass.
+ autoplayExperimentInstallEventListenerIfNeeded();
+ }
+ }
+
+ if (!m_userGestureRequiredForPlay) {
+ m_paused = false;
philipj_slow 2015/08/05 10:03:11 This branch was previously predicated on the sandb
liberato (no reviews please) 2015/08/06 06:37:57 it still is, though it's virtually impossible to t
+ invalidateCachedTime();
+ scheduleEvent(EventTypeNames::play);
+ scheduleEvent(EventTypeNames::playing);
+ }
}
}
@@ -1960,9 +2020,30 @@ 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) {
+ recordAutoplayMetric(AutoplayPlayFailed);
String message = ExceptionMessages::failedToExecute("play", "HTMLMediaElement", "API can only be initiated by a user gesture.");
document().executionContext()->addConsoleMessage(ConsoleMessage::create(JSMessageSource, WarningMessageLevel, message));
return;
@@ -1971,6 +2052,7 @@ void HTMLMediaElement::play()
if (m_autoplayMediaCounted)
recordAutoplayMetric(AutoplayManualStart);
m_userGestureRequiredForPlay = false;
+ autoplayExperimentClearEventListenerIfNeeded();
}
playInternal();
@@ -2032,8 +2114,9 @@ void HTMLMediaElement::gesturelessInitialPlayHalted()
double playedTime = currentTime();
if (playedTime < 60) {
double progress = playedTime / duration();
- if (progress < 0.5)
+ if (progress < 0.5) {
philipj_slow 2015/08/05 10:03:11 Why the added {}?
liberato (no reviews please) 2015/08/06 06:37:57 whoops, thanks -- i had more code in there and for
recordAutoplayMetric(AutoplayBailout);
+ }
}
}
@@ -2044,6 +2127,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) {
@@ -2141,6 +2228,18 @@ void HTMLMediaElement::setMuted(bool muted)
m_muted = muted;
+ // 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
philipj_slow 2015/08/05 10:03:11 Really start playing by setting the muted attribut
liberato (no reviews please) 2015/08/06 06:37:57 there was interest in this from one of the origina
philipj_slow 2015/08/13 09:27:53 Acknowledged.
+ // 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);
@@ -3789,4 +3888,175 @@ DEFINE_TRACE(HTMLMediaElement::AudioSourceProviderImpl)
}
#endif
+void HTMLMediaElement::autoplayExperimentInstallEventListenerIfNeeded()
+{
+ // If we don't require visibility, then we don't need the listener.
+ if (!(m_autoplayExperimentMode & ExperimentIfVisible))
+ return;
+
+ 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.
+
+ // If visibility isn't required, then it's visible enough.
+ if (!(m_autoplayExperimentMode & ExperimentIfVisible))
+ return true;
+
+ // Autoplay is requested, and we're willing to override if the visibility
+ // requirements are met.
+
+ // Check visibility.
philipj_slow 2015/08/05 10:03:11 A page visibility check is missing, this ought to
liberato (no reviews please) 2015/08/06 06:37:57 i don't think it'll ever play unless in the foregr
philipj_slow 2015/08/13 09:27:53 What is it that would prevent playback when not in
liberato (no reviews please) 2015/09/01 06:54:19 Done.
+ 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!
philipj_slow 2015/08/05 10:03:12 Is this a question to the reviewer, or a rhetorica
liberato (no reviews please) 2015/08/06 06:37:57 random movie quotes make reviewing more fun! but
philipj_slow 2015/08/13 09:27:53 Ah, Spaceballs :)
+ 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, which is
+ // why video-autoplay-experiment.html doesn't check -ifmobile .
+ if (m_autoplayExperimentMode & ExperimentIfMobile) {
+ if (!document().viewportDescription().isLegacyViewportType())
+ 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 (if needed), 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;
+}
+
}

Powered by Google App Engine
This is Rietveld 408576698