Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 /* | |
| 2 * Copyright (C) 2015 Google Inc. All rights reserved. | |
| 3 * | |
| 4 * Redistribution and use in source and binary forms, with or without | |
| 5 * modification, are permitted provided that the following conditions are | |
| 6 * met: | |
| 7 * | |
| 8 * * Redistributions of source code must retain the above copyright | |
| 9 * notice, this list of conditions and the following disclaimer. | |
| 10 * * Redistributions in binary form must reproduce the above | |
| 11 * copyright notice, this list of conditions and the following disclaimer | |
| 12 * in the documentation and/or other materials provided with the | |
| 13 * distribution. | |
| 14 * * Neither the name of Google Inc. nor the names of its | |
| 15 * contributors may be used to endorse or promote products derived from | |
| 16 * this software without specific prior written permission. | |
| 17 * | |
| 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 29 */ | |
| 30 | |
| 31 #include "config.h" | |
| 32 #include "core/html/AutoplayExperimentHelper.h" | |
| 33 | |
| 34 #include "core/dom/Document.h" | |
| 35 #include "core/frame/Settings.h" | |
| 36 #include "core/html/HTMLMediaElement.h" | |
| 37 #include "platform/UserGestureIndicator.h" | |
| 38 #include "platform/geometry/FloatRect.h" | |
| 39 | |
| 40 namespace blink { | |
| 41 | |
| 42 using namespace HTMLNames; | |
| 43 | |
| 44 // How long do we wait after a scroll event before deciding that no more | |
| 45 // scroll events are going to arrive? | |
| 46 static const double viewportTimerPollDelay = 0.5; | |
| 47 | |
| 48 // Event listener that just informs the AutoplayExperimentHelper that a | |
| 49 // scroll has happened. | |
| 50 class AutoplayExperimentHelper::ScrollListener : public EventListener { | |
| 51 public: | |
| 52 ScrollListener(AutoplayExperimentHelper* helper) : EventListener(CPPEven tListenerType), m_helper(helper) { } | |
| 53 virtual bool operator==(const EventListener& them) | |
| 54 { | |
| 55 return &them == this; | |
| 56 } | |
| 57 | |
| 58 void handleEvent(ExecutionContext*, Event*) override | |
| 59 { | |
| 60 if (m_helper) | |
| 61 m_helper->notifyScrolled(); | |
| 62 } | |
| 63 | |
| 64 private: | |
| 65 AutoplayExperimentHelper* m_helper; | |
|
philipj_slow
2015/08/13 10:15:40
It looks like the pointer is always passed and nev
liberato (no reviews please)
2015/09/01 06:54:19
indeed, but the listener has since been removed.
| |
| 66 }; | |
| 67 | |
| 68 | |
| 69 AutoplayExperimentHelper::AutoplayExperimentHelper(HTMLMediaElement& element) | |
| 70 : m_element(element) | |
| 71 , m_mode(ExperimentOff) | |
| 72 , m_playPending(false) | |
| 73 , m_viewportTimer(this, &AutoplayExperimentHelper::viewportTimerFired) | |
| 74 { | |
| 75 if (document().settings()) { | |
| 76 const String& autoplayMode = document().settings()->autoplayExperimentMo de(); | |
|
ojan
2015/08/11 02:45:21
Did you also want to measure the case Philip sugge
ojan
2015/08/11 02:49:34
Also, I don't see anything for tab visibility. So,
liberato (no reviews please)
2015/09/01 06:54:19
added a check for page()->visibilityState() to isI
philipj_slow
2015/09/02 09:31:47
What about the experiment to only require page vis
philipj_slow
2015/09/04 08:44:08
I haven't seen the other CL yet, so just a gentle
| |
| 77 if (autoplayMode.contains("enabled")) { | |
| 78 // Autoplay with no gesture requirement. | |
| 79 m_mode |= ExperimentEnabled; | |
| 80 } | |
| 81 if (autoplayMode.contains("-ifviewport")) { | |
| 82 // Override gesture requirement only if the player is within the | |
| 83 // current viewport. | |
| 84 m_mode |= ExperimentIfViewport; | |
| 85 } | |
| 86 if (autoplayMode.contains("-ifmuted")) { | |
| 87 // Override gesture requirement only if the media is muted or has | |
| 88 // no audio track. | |
| 89 m_mode |= ExperimentIfMuted; | |
| 90 } | |
| 91 if (autoplayMode.contains("-ifmobile")) { | |
| 92 // Override gesture requirement only if the page is optimized | |
| 93 // for mobile. | |
| 94 m_mode |= ExperimentIfMobile; | |
| 95 } | |
| 96 if (autoplayMode.contains("-playmuted")) { | |
|
ojan
2015/08/11 02:45:21
For good measure, add a comment explaining this on
liberato (no reviews please)
2015/09/01 06:54:19
done, though the docs are with the enums in Autopl
| |
| 97 m_mode |= ExperimentPlayMuted; | |
| 98 } | |
| 99 | |
| 100 if (m_mode != ExperimentOff) { | |
| 101 WTF_LOG(Media, "HTMLMediaElement: autoplay experiment set to '%s' (% d)", | |
| 102 autoplayMode.ascii().data(), m_mode); | |
| 103 } | |
| 104 } | |
| 105 } | |
| 106 | |
| 107 AutoplayExperimentHelper::~AutoplayExperimentHelper() | |
| 108 { | |
| 109 clearEventListenerIfNeeded(); | |
| 110 } | |
| 111 | |
| 112 void AutoplayExperimentHelper::onReadyToPlay() | |
| 113 { | |
| 114 // Assuming that we're eligible to override the user gesture requirement, | |
| 115 // either play if we meet the visibility checks, or install a listener | |
| 116 // to wait for them to pass. | |
| 117 if (isEligible()) { | |
| 118 if (isInViewportIfNeeded()) | |
| 119 prepareToPlay(GesturelessPlaybackStartedByLoad); | |
| 120 else | |
| 121 installEventListenerIfNeeded(); | |
| 122 } | |
| 123 } | |
| 124 | |
| 125 void AutoplayExperimentHelper::onPlayMethodCalled() | |
| 126 { | |
| 127 // Set the pending state, even if the play isn't going to be pending. | |
| 128 // Eligibility can change if, for example, the mute status changes. | |
| 129 // Having this set is okay. | |
| 130 m_playPending = true; | |
| 131 | |
| 132 if (!UserGestureIndicator::processingUserGesture()) { | |
| 133 | |
| 134 if (isEligible()) { | |
| 135 // Remember that userGestureRequiredForPlay is required for | |
| 136 // us to be eligible for the experiment. | |
| 137 // If we are able to override the gesture requirement now, then | |
| 138 // do so. Otherwise, install an event listener if we need one. | |
| 139 if (isInViewportIfNeeded()) { | |
| 140 // Override the gesture and play. | |
| 141 prepareToPlay(GesturelessPlaybackStartedByPlayMethod); | |
| 142 } else { | |
| 143 // Wait for viewport visibility. | |
| 144 installEventListenerIfNeeded(); | |
| 145 } | |
| 146 } | |
| 147 | |
| 148 } else if (m_element.isUserGestureRequiredForPlay()) { | |
| 149 clearEventListenerIfNeeded(); | |
| 150 } | |
| 151 } | |
| 152 | |
| 153 void AutoplayExperimentHelper::onPauseMethodCalled() | |
| 154 { | |
| 155 // Don't try to autoplay, if we would have. | |
| 156 m_playPending = false; | |
| 157 clearEventListenerIfNeeded(); | |
| 158 } | |
| 159 | |
| 160 void AutoplayExperimentHelper::onMuteChanged() | |
| 161 { | |
| 162 // If we are no longer eligible for the autoplay experiment, then also | |
| 163 // quit listening for events. If we are eligible, and if we should be | |
| 164 // playing, then start playing. In other words, start playing if | |
| 165 // we just needed 'mute' to autoplay. | |
| 166 if (!isEligible()) { | |
| 167 clearEventListenerIfNeeded(); | |
| 168 } else { | |
| 169 // Try to play. If we can't, then install a listener. | |
| 170 if (!maybeStartPlaying()) | |
| 171 installEventListenerIfNeeded(); | |
| 172 } | |
| 173 } | |
| 174 | |
| 175 void AutoplayExperimentHelper::installEventListenerIfNeeded() | |
| 176 { | |
| 177 // If we don't require that the player is in the viewport, then we don't | |
| 178 // need the listener. | |
| 179 if (!(m_mode & ExperimentIfViewport)) | |
| 180 return; | |
| 181 | |
| 182 if (document().domWindow() && !m_scrollListener) { | |
| 183 m_scrollListener = adoptRef(new ScrollListener(this)); | |
| 184 document().domWindow()->addEventListener("scroll", m_scrollListener, fal se); | |
| 185 } | |
| 186 } | |
| 187 | |
| 188 void AutoplayExperimentHelper::clearEventListenerIfNeeded() | |
| 189 { | |
| 190 if (m_scrollListener) { | |
| 191 LocalDOMWindow* domWindow = document().domWindow(); | |
| 192 if (domWindow) { | |
| 193 domWindow->removeEventListener("scroll", m_scrollListener, false); | |
| 194 } | |
| 195 // Either way, clear our ref. | |
| 196 m_scrollListener.clear(); | |
| 197 } | |
| 198 } | |
| 199 | |
| 200 void AutoplayExperimentHelper::notifyScrolled() | |
| 201 { | |
| 202 // Reset the timer to indicate that scrolling has happened | |
| 203 // recently, and might still be ongoing. | |
| 204 m_viewportTimer.startOneShot(viewportTimerPollDelay, FROM_HERE); | |
| 205 } | |
| 206 | |
| 207 void AutoplayExperimentHelper::viewportTimerFired(Timer<AutoplayExperimentHelper >*) | |
| 208 { | |
| 209 // Sufficient time has passed since the last scroll that we'll | |
| 210 // treat it as the end of scroll. Autoplay if we should. | |
| 211 maybeStartPlaying(); | |
| 212 } | |
| 213 | |
| 214 bool AutoplayExperimentHelper::isInViewportIfNeeded() | |
| 215 { | |
| 216 // We could check for eligibility here, but we skip it. Some of our | |
| 217 // callers need to do it separately, and we don't want to check more | |
| 218 // than we need to. | |
| 219 // Also remember that page visibility is assumed for clank. | |
| 220 | |
| 221 // If viewport visibility isn't required, then it's visible enough. | |
| 222 if (!(m_mode & ExperimentIfViewport)) | |
| 223 return true; | |
| 224 | |
| 225 // Check if we're in the viewport. | |
| 226 const LocalDOMWindow* domWindow = document().domWindow(); | |
| 227 if (!domWindow) | |
| 228 return false; | |
| 229 | |
| 230 FloatRect us(m_element.offsetLeft(), m_element.offsetTop(), m_element.client Width(), m_element.clientHeight()); | |
| 231 FloatRect screen(domWindow->scrollX(), domWindow->scrollY(), domWindow->inne rWidth(), domWindow->innerHeight()); | |
| 232 | |
| 233 return screen.contains(us); | |
|
ojan
2015/08/11 02:45:21
What if the video is larger than the screen or par
liberato (no reviews please)
2015/09/01 06:54:19
for those videos that don't show controls, or use
| |
| 234 } | |
| 235 | |
| 236 bool AutoplayExperimentHelper::maybeStartPlaying() | |
| 237 { | |
| 238 // See if we're allowed to autoplay now. | |
| 239 if (!isEligible() | |
| 240 || !isInViewportIfNeeded()) { | |
| 241 return false; | |
| 242 } | |
| 243 | |
| 244 // Start playing! | |
| 245 prepareToPlay(GesturelessPlaybackStartedByScroll); | |
| 246 m_element.playInternal(); | |
| 247 | |
| 248 return true; | |
| 249 } | |
| 250 | |
| 251 bool AutoplayExperimentHelper::isEligible() const | |
| 252 { | |
| 253 // If no user gesture is required, then the experiment doesn't apply. | |
| 254 // This is what prevents us from starting playback more than once. | |
| 255 // Since this flag is never set to true once it's cleared, it will block | |
| 256 // the autoplay experiment forever. | |
| 257 if (!m_element.isUserGestureRequiredForPlay()) | |
| 258 return false; | |
| 259 | |
| 260 if (m_mode == ExperimentOff) | |
| 261 return false; | |
| 262 | |
| 263 // If nobody has requested playback, either by the autoplay attribute or | |
| 264 // a play() call, then do nothing. | |
| 265 if (!m_playPending && !m_element.autoplay()) | |
| 266 return false; | |
| 267 | |
| 268 // If the video is already playing, then do nothing. Note that there | |
| 269 // is not a path where a user gesture is required but the video is | |
| 270 // playing. However, we check for completeness. | |
| 271 if (!m_element.paused()) | |
| 272 return false; | |
| 273 | |
| 274 // Note that the viewport test always returns false on desktop, which is | |
| 275 // why video-autoplay-experiment.html doesn't check -ifmobile . | |
| 276 if ((m_mode & ExperimentIfMobile) | |
| 277 && !document().viewportDescription().isLegacyViewportType()) | |
| 278 return false; | |
| 279 | |
| 280 if (m_mode & ExperimentIfMuted) { | |
| 281 // If media is muted, then autoplay when it comes into view. | |
| 282 return m_element.fastHasAttribute(mutedAttr) || m_element.muted(); | |
| 283 } | |
| 284 | |
| 285 // Autoplay when it comes into view (if needed), maybe muted. | |
| 286 return true; | |
| 287 } | |
| 288 | |
| 289 void AutoplayExperimentHelper::muteIfNeeded() | |
| 290 { | |
| 291 if (m_mode & ExperimentPlayMuted && !m_element.muted()) { | |
| 292 // This will call onMuteChanged(), which we really don't want | |
| 293 // to do anything, since we're called when trying to play. If | |
| 294 // the element is still marked as eligible, then we'll probably | |
|
philipj_slow
2015/08/13 10:15:40
setMuted returns early if there's no change, so I
liberato (no reviews please)
2015/09/01 06:54:20
infinite recursion: true, though it's not obvious
| |
| 295 // recurse indefinitely. | |
| 296 ASSERT(!isEligible()); | |
| 297 m_element.setMuted(true); | |
| 298 } | |
| 299 } | |
| 300 | |
| 301 void AutoplayExperimentHelper::prepareToPlay(AutoplayMetrics metric) | |
| 302 { | |
| 303 m_element.recordAutoplayMetric(metric); | |
| 304 | |
| 305 // This also causes !isEligible, so that we don't alow autoplay more than | |
| 306 // once. Be sure to do this before muteIfNeeded(). | |
| 307 m_element.removeUserGestureRequirement(); | |
| 308 | |
| 309 clearEventListenerIfNeeded(); | |
| 310 muteIfNeeded(); | |
| 311 | |
| 312 // Record that this autoplayed without a user gesture. This is normally | |
| 313 // set when we discover an autoplay attribute, but we include all cases | |
| 314 // where playback started without a user gesture, e.g., play(). | |
| 315 m_element.setInitialPlayWithoutUserGestures(true); | |
| 316 | |
| 317 // Do not actually start playback here. | |
| 318 } | |
| 319 | |
| 320 Document& AutoplayExperimentHelper::document() const | |
| 321 { | |
| 322 return m_element.document(); | |
| 323 } | |
| 324 | |
| 325 } | |
| OLD | NEW |