Chromium Code Reviews| Index: Source/core/html/HTMLMediaElement.cpp |
| diff --git a/Source/core/html/HTMLMediaElement.cpp b/Source/core/html/HTMLMediaElement.cpp |
| index 63eda2639e62a01ca9a5fd97b8bdfc5a2bd153f8..8e6d794926774cdf05d00fe808238dddb486e2d5 100644 |
| --- a/Source/core/html/HTMLMediaElement.cpp |
| +++ b/Source/core/html/HTMLMediaElement.cpp |
| @@ -126,6 +126,10 @@ 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 long do we wait after a scroll event before deciding that no more |
| +// scroll events are going to arrive? |
| +static const double viewportTimerPollDelay = 0.3; |
| + |
| using namespace HTMLNames; |
| typedef WillBeHeapHashSet<RawPtrWillBeWeakMember<HTMLMediaElement>> WeakMediaElementSet; |
| @@ -251,25 +255,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 +286,24 @@ WebMimeRegistry::SupportsType HTMLMediaElement::supportsType(const ContentType& |
| URLRegistry* HTMLMediaElement::s_mediaStreamRegistry = 0; |
| +class HTMLMediaElement::AutoplayExperimentScrollListener : public EventListener { |
|
dglazkov
2015/08/06 20:55:23
Is this the right approach? Adding Ojan, the TL fo
liberato (no reviews please)
2015/09/01 06:54:19
Done.
|
| + public: |
| + AutoplayExperimentScrollListener(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->notifyScrolled(); |
| + } |
| + |
| + private: |
| + HTMLMediaElement* m_element; |
| +}; |
| + |
| void HTMLMediaElement::setMediaStreamRegistry(URLRegistry* registry) |
| { |
| ASSERT(!s_mediaStreamRegistry); |
| @@ -364,9 +368,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_autoplayViewportTimer(this, &HTMLMediaElement::viewportTimerFired) |
| + , m_autoplayLastScrollX(std::numeric_limits<double>::quiet_NaN()) |
| + , m_autoplayLastScrollY(std::numeric_limits<double>::quiet_NaN()) |
| + , m_autoplayViewportTimerSpan(0) |
| { |
| #if ENABLE(OILPAN) |
| ThreadState::current()->registerPreFinalizer(this); |
| @@ -375,8 +386,39 @@ 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.contains("enabled")) { |
|
dglazkov
2015/08/06 20:55:23
See my note above about not using strings for pass
|
| + // Autoplay with no gesture requirement. |
| + m_autoplayExperimentMode |= ExperimentEnabled; |
| + } |
| + if (autoplayMode.contains("-ifviewport")) { |
| + // Override gesture requirement only if the player is within the |
| + // current viewport. |
| + m_autoplayExperimentMode |= ExperimentIfViewport; |
| + } |
| + 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 +427,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 |
| @@ -694,12 +739,37 @@ String HTMLMediaElement::canPlayType(const String& mimeType, const String& keySy |
| return canPlay; |
| } |
| +void HTMLMediaElement::recordMetricsIfStopping() |
| +{ |
| + // If not playing, then nothing to record. |
| + if (!m_playing) |
| + return; |
| + |
| + const bool bailout = isBailout(); |
| + |
| + // Record that play was stopped. We don't care if it was autoplay, |
| + // play(), or the user manually started it. |
| + recordAutoplayMetric(AnyPlaybackStopped); |
| + if (bailout) |
| + recordAutoplayMetric(AnyPlaybackBailout); |
| + |
| + // If this was a gestureless play, then record that separately. |
| + // These cover attr and play() gestureless starts. |
| + if (m_initialPlayWithoutUserGestures) { |
| + m_initialPlayWithoutUserGestures = false; |
| + |
| + recordAutoplayMetric(AutoplayStopped); |
| + |
| + if (bailout) |
| + recordAutoplayMetric(AutoplayBailout); |
| + } |
| +} |
| + |
| void HTMLMediaElement::load() |
| { |
| WTF_LOG(Media, "HTMLMediaElement::load(%p)", this); |
| - if (m_initialPlayWithoutUserGestures && m_playing) |
| - gesturelessInitialPlayHalted(); |
| + recordMetricsIfStopping(); |
| if (UserGestureIndicator::processingUserGesture() && m_userGestureRequiredForPlay) { |
| recordAutoplayMetric(AutoplayEnabledThroughLoad); |
| @@ -1555,11 +1625,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 (autoplayExperimentIsInViewportIfNeeded()) { |
| + autoplayExperimentPrepareToPlay(GesturelessPlaybackStartedByLoad); |
| + // Will play below. |
| + } else { |
| + // Wait for viewport checks to pass. |
| + autoplayExperimentInstallEventListenerIfNeeded(); |
| + } |
| + } |
| + |
| + if (!m_userGestureRequiredForPlay) { |
| + m_paused = false; |
| + invalidateCachedTime(); |
| + scheduleEvent(EventTypeNames::play); |
| + scheduleEvent(EventTypeNames::playing); |
| + } |
| } |
| } |
| @@ -1960,9 +2044,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 (autoplayExperimentIsInViewportIfNeeded()) { |
| + // Override the gesture and play. |
| + autoplayExperimentPrepareToPlay(GesturelessPlaybackStartedByPlayMethod); |
| + } else { |
| + // Wait for viewport visibility. |
| + autoplayExperimentInstallEventListenerIfNeeded(); |
| + } |
| + } |
| + |
| if (m_userGestureRequiredForPlay) { |
| + recordAutoplayMetric(PlayMethodFailed); |
| 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 +2076,7 @@ void HTMLMediaElement::play() |
| if (m_autoplayMediaCounted) |
| recordAutoplayMetric(AutoplayManualStart); |
| m_userGestureRequiredForPlay = false; |
| + autoplayExperimentClearEventListenerIfNeeded(); |
| } |
| playInternal(); |
| @@ -1997,6 +2103,7 @@ void HTMLMediaElement::playInternal() |
| m_paused = false; |
| invalidateCachedTime(); |
| scheduleEvent(EventTypeNames::play); |
| + recordAutoplayMetric(AnyPlaybackStarted); |
| if (m_readyState <= HAVE_CURRENT_DATA) |
| scheduleEvent(EventTypeNames::waiting); |
| @@ -2020,21 +2127,13 @@ void HTMLMediaElement::autoplayMediaEncountered() |
| } |
| } |
| -void HTMLMediaElement::gesturelessInitialPlayHalted() |
| +bool HTMLMediaElement::isBailout() const |
| { |
| - ASSERT(m_initialPlayWithoutUserGestures); |
| - m_initialPlayWithoutUserGestures = false; |
| - |
| - recordAutoplayMetric(AutoplayStopped); |
| - |
| // 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) |
| - recordAutoplayMetric(AutoplayBailout); |
| - } |
| + const double playedTime = currentTime(); |
| + const double progress = playedTime / duration(); |
| + return (playedTime < 60) && (progress < 0.5); |
| } |
| void HTMLMediaElement::pause() |
| @@ -2044,11 +2143,14 @@ 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) { |
| - if (m_initialPlayWithoutUserGestures) |
| - gesturelessInitialPlayHalted(); |
| + recordMetricsIfStopping(); |
| m_paused = true; |
| scheduleTimeupdateEvent(false); |
| @@ -2141,6 +2243,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 |
| + // we just needed 'mute' to autoplay. |
| + if (!autoplayExperimentIsEligible()) { |
| + autoplayExperimentClearEventListenerIfNeeded(); |
| + } else { |
| + // Try to play. If we can't, then install a listener. |
| + if (!autoplayExperimentMaybeStartPlaying()) |
| + autoplayExperimentInstallEventListenerIfNeeded(); |
| + } |
| + |
| updateVolume(); |
| scheduleEvent(EventTypeNames::volumechange); |
| @@ -3111,8 +3225,7 @@ void HTMLMediaElement::stop() |
| { |
| WTF_LOG(Media, "HTMLMediaElement::stop(%p)", this); |
| - if (m_playing && m_initialPlayWithoutUserGestures) |
| - gesturelessInitialPlayHalted(); |
| + recordMetricsIfStopping(); |
| // Close the async event queue so that no events are enqueued by userCancelledLoad. |
| cancelPendingEventsAndCallbacks(); |
| @@ -3789,4 +3902,144 @@ DEFINE_TRACE(HTMLMediaElement::AudioSourceProviderImpl) |
| } |
| #endif |
| +void HTMLMediaElement::autoplayExperimentInstallEventListenerIfNeeded() |
| +{ |
| + // If we don't require that the player is in the viewport, then we don't |
| + // need the listener. |
| + if (!(m_autoplayExperimentMode & ExperimentIfViewport)) |
| + return; |
| + |
| + if (document().domWindow() && !m_autoplayExperimentScrollListener) { |
| + m_autoplayExperimentScrollListener = adoptRef(new AutoplayExperimentScrollListener(this)); |
| + document().domWindow()->addEventListener("scroll", m_autoplayExperimentScrollListener, false); |
|
dglazkov
2015/08/06 20:55:23
Ojan, PTAL.
|
| + } |
| +} |
| + |
| +void HTMLMediaElement::autoplayExperimentClearEventListenerIfNeeded() |
| +{ |
| + if (m_autoplayExperimentScrollListener) { |
| + LocalDOMWindow* domWindow = document().domWindow(); |
| + if (domWindow) { |
| + domWindow->removeEventListener("scroll", m_autoplayExperimentScrollListener, false); |
| + } |
| + // Either way, clear our ref. |
| + m_autoplayExperimentScrollListener.clear(); |
| + } |
| +} |
| + |
| +void HTMLMediaElement::notifyScrolled() |
| +{ |
| + // Reset the timer to indicate that scrolling has happened |
| + // recently, and might still be ongoing. |
| + m_autoplayViewportTimer.startOneShot(viewportTimerPollDelay, FROM_HERE); |
| +} |
| + |
| +void HTMLMediaElement::viewportTimerFired(Timer<HTMLMediaElement>*) |
| +{ |
| + // Sufficient time has passed since the last scroll that we'll |
| + // treat it as the end of scroll. Autoplay if we should. |
| + autoplayExperimentMaybeStartPlaying(); |
| +} |
| + |
| +bool HTMLMediaElement::autoplayExperimentIsInViewportIfNeeded() |
| +{ |
| + // 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_autoplayExperimentMode & ExperimentIfViewport)) |
| + return true; |
| + |
| + // Check if we're in the viewport. |
| + const LocalDOMWindow* domWindow = document().domWindow(); |
| + if (!domWindow) |
| + return false; |
| + |
| + FloatRect us(offsetLeft(), offsetTop(), clientWidth(), clientHeight()); |
|
dglazkov
2015/08/06 20:55:23
This will force layout, and something we should av
liberato (no reviews please)
2015/08/07 14:33:15
one alternative is checking layoutObject()->needsL
|
| + FloatRect screen(domWindow->scrollX(), domWindow->scrollY(), domWindow->innerWidth(), domWindow->innerHeight()); |
| + |
| + return screen.contains(us); |
| +} |
| + |
| +bool HTMLMediaElement::autoplayExperimentMaybeStartPlaying() |
| +{ |
| + if (!autoplayExperimentIsEligible() |
| + || !autoplayExperimentIsInViewportIfNeeded()) { |
| + return false; |
| + } |
| + |
| + // Start playing! |
| + autoplayExperimentPrepareToPlay(GesturelessPlaybackStartedByScroll); |
| + 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; |
| +} |
| + |
| } |