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