1 /* 2 * Copyright (C) 2011, 2012 Apple Inc. All rights reserved. 3 * Copyright (C) 2011, 2012 Google Inc. All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 14 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY 15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR 18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27 #include "config.h" 28 #include "core/html/shadow/MediaControls.h" 29 30 #include "bindings/core/v8/ExceptionStatePlaceholder.h" 31 #include "core/dom/ClientRect.h" 32 #include "core/events/MouseEvent.h" 33 #include "core/frame/Settings.h" 34 #include "core/html/HTMLMediaElement.h" 35 #include "core/html/MediaController.h" 36 #include "core/rendering/RenderTheme.h" 37 38 namespace blink { 39 40 // If you change this value, then also update the corresponding value in 41 // LayoutTests/media/media-controls.js. 42 static const double timeWithoutMouseMovementBeforeHidingMediaControls = 3; 43 44 static bool fullscreenIsSupported(const Document& document) 45 { 46 return !document.settings() || document.settings()->fullscreenSupported(); 47 } 48 49 MediaControls::MediaControls(HTMLMediaElement& mediaElement) 50 : HTMLDivElement(mediaElement.document()) 51 , m_mediaElement(&mediaElement) 52 , m_panel(nullptr) 53 , m_textDisplayContainer(nullptr) 54 , m_overlayPlayButton(nullptr) 55 , m_overlayEnclosure(nullptr) 56 , m_playButton(nullptr) 57 , m_currentTimeDisplay(nullptr) 58 , m_timeline(nullptr) 59 , m_muteButton(nullptr) 60 , m_volumeSlider(nullptr) 61 , m_toggleClosedCaptionsButton(nullptr) 62 , m_fullScreenButton(nullptr) 63 , m_castButton(nullptr) 64 , m_overlayCastButton(nullptr) 65 , m_durationDisplay(nullptr) 66 , m_enclosure(nullptr) 67 , m_hideMediaControlsTimer(this, &MediaControls::hideMediaControlsTimerFired) 68 , m_isMouseOverControls(false) 69 , m_isPausedForScrubbing(false) 70 , m_wasLastEventTouch(false) 71 { 72 } 73 74 PassRefPtrWillBeRawPtr<MediaControls> MediaControls::create(HTMLMediaElement& mediaElement) 75 { 76 RefPtrWillBeRawPtr<MediaControls> controls = adoptRefWillBeNoop(new MediaControls(mediaElement)); 77 78 if (controls->initializeControls()) 79 return controls.release(); 80 81 return nullptr; 82 } 83 84 bool MediaControls::initializeControls() 85 { 86 TrackExceptionState exceptionState; 87 88 RefPtrWillBeRawPtr<MediaControlOverlayEnclosureElement> overlayEnclosure = MediaControlOverlayEnclosureElement::create(*this); 89 90 if (document().settings() && document().settings()->mediaControlsOverlayPlayButtonEnabled()) { 91 RefPtrWillBeRawPtr<MediaControlOverlayPlayButtonElement> overlayPlayButton = MediaControlOverlayPlayButtonElement::create(*this); 92 m_overlayPlayButton = overlayPlayButton.get(); 93 overlayEnclosure->appendChild(overlayPlayButton.release(), exceptionState); 94 if (exceptionState.hadException()) 95 return false; 96 } 97 98 RefPtrWillBeRawPtr<MediaControlCastButtonElement> overlayCastButton = MediaControlCastButtonElement::create(*this, true); 99 m_overlayCastButton = overlayCastButton.get(); 100 overlayEnclosure->appendChild(overlayCastButton.release(), exceptionState); 101 if (exceptionState.hadException()) 102 return false; 103 104 m_overlayEnclosure = overlayEnclosure.get(); 105 appendChild(overlayEnclosure.release(), exceptionState); 106 if (exceptionState.hadException()) 107 return false; 108 109 // Create an enclosing element for the panel so we can visually offset the controls correctly. 110 RefPtrWillBeRawPtr<MediaControlPanelEnclosureElement> enclosure = MediaControlPanelEnclosureElement::create(*this); 111 112 RefPtrWillBeRawPtr<MediaControlPanelElement> panel = MediaControlPanelElement::create(*this); 113 114 RefPtrWillBeRawPtr<MediaControlPlayButtonElement> playButton = MediaControlPlayButtonElement::create(*this); 115 m_playButton = playButton.get(); 116 panel->appendChild(playButton.release(), exceptionState); 117 if (exceptionState.hadException()) 118 return false; 119 120 RefPtrWillBeRawPtr<MediaControlTimelineElement> timeline = MediaControlTimelineElement::create(*this); 121 m_timeline = timeline.get(); 122 panel->appendChild(timeline.release(), exceptionState); 123 if (exceptionState.hadException()) 124 return false; 125 126 RefPtrWillBeRawPtr<MediaControlCurrentTimeDisplayElement> currentTimeDisplay = MediaControlCurrentTimeDisplayElement::create(*this); 127 m_currentTimeDisplay = currentTimeDisplay.get(); 128 m_currentTimeDisplay->hide(); 129 panel->appendChild(currentTimeDisplay.release(), exceptionState); 130 if (exceptionState.hadException()) 131 return false; 132 133 RefPtrWillBeRawPtr<MediaControlTimeRemainingDisplayElement> durationDisplay = MediaControlTimeRemainingDisplayElement::create(*this); 134 m_durationDisplay = durationDisplay.get(); 135 panel->appendChild(durationDisplay.release(), exceptionState); 136 if (exceptionState.hadException()) 137 return false; 138 139 RefPtrWillBeRawPtr<MediaControlMuteButtonElement> muteButton = MediaControlMuteButtonElement::create(*this); 140 m_muteButton = muteButton.get(); 141 panel->appendChild(muteButton.release(), exceptionState); 142 if (exceptionState.hadException()) 143 return false; 144 145 RefPtrWillBeRawPtr<MediaControlVolumeSliderElement> slider = MediaControlVolumeSliderElement::create(*this); 146 m_volumeSlider = slider.get(); 147 panel->appendChild(slider.release(), exceptionState); 148 if (exceptionState.hadException()) 149 return false; 150 151 RefPtrWillBeRawPtr<MediaControlToggleClosedCaptionsButtonElement> toggleClosedCaptionsButton = MediaControlToggleClosedCaptionsButtonElement::create(*this); 152 m_toggleClosedCaptionsButton = toggleClosedCaptionsButton.get(); 153 panel->appendChild(toggleClosedCaptionsButton.release(), exceptionState); 154 if (exceptionState.hadException()) 155 return false; 156 157 RefPtrWillBeRawPtr<MediaControlCastButtonElement> castButton = MediaControlCastButtonElement::create(*this, false); 158 m_castButton = castButton.get(); 159 panel->appendChild(castButton.release(), exceptionState); 160 if (exceptionState.hadException()) 161 return false; 162 163 RefPtrWillBeRawPtr<MediaControlFullscreenButtonElement> fullscreenButton = MediaControlFullscreenButtonElement::create(*this); 164 m_fullScreenButton = fullscreenButton.get(); 165 panel->appendChild(fullscreenButton.release(), exceptionState); 166 if (exceptionState.hadException()) 167 return false; 168 169 m_panel = panel.get(); 170 enclosure->appendChild(panel.release(), exceptionState); 171 if (exceptionState.hadException()) 172 return false; 173 174 m_enclosure = enclosure.get(); 175 appendChild(enclosure.release(), exceptionState); 176 if (exceptionState.hadException()) 177 return false; 178 179 return true; 180 } 181 182 void MediaControls::reset() 183 { 184 double duration = mediaElement().duration(); 185 m_durationDisplay->setInnerText(RenderTheme::theme().formatMediaControlsTime(duration), ASSERT_NO_EXCEPTION); 186 m_durationDisplay->setCurrentValue(duration); 187 188 updatePlayState(); 189 190 updateCurrentTimeDisplay(); 191 192 m_timeline->setDuration(duration); 193 m_timeline->setPosition(mediaElement().currentTime()); 194 195 if (!mediaElement().hasAudio()) 196 m_volumeSlider->hide(); 197 else 198 m_volumeSlider->show(); 199 updateVolume(); 200 201 refreshClosedCaptionsButtonVisibility(); 202 203 if (mediaElement().hasVideo() && fullscreenIsSupported(document())) 204 m_fullScreenButton->show(); 205 else 206 m_fullScreenButton->hide(); 207 208 refreshCastButtonVisibility(); 209 makeOpaque(); 210 } 211 212 void MediaControls::show() 213 { 214 makeOpaque(); 215 m_panel->setIsDisplayed(true); 216 m_panel->show(); 217 if (m_overlayPlayButton) 218 m_overlayPlayButton->updateDisplayType(); 219 } 220 221 void MediaControls::mediaElementFocused() 222 { 223 show(); 224 resetHideMediaControlsTimer(); 225 } 226 227 void MediaControls::hide() 228 { 229 m_panel->setIsDisplayed(false); 230 m_panel->hide(); 231 if (m_overlayPlayButton) 232 m_overlayPlayButton->hide(); 233 } 234 235 void MediaControls::makeOpaque() 236 { 237 m_panel->makeOpaque(); 238 } 239 240 void MediaControls::makeTransparent() 241 { 242 m_panel->makeTransparent(); 243 } 244 245 bool MediaControls::shouldHideMediaControls(unsigned behaviorFlags) const 246 { 247 // Never hide for a media element without visual representation. 248 if (!mediaElement().hasVideo() || mediaElement().isPlayingRemotely()) 249 return false; 250 // Don't hide if the mouse is over the controls. 251 const bool ignoreControlsHover = behaviorFlags & IgnoreControlsHover; 252 if (!ignoreControlsHover && m_panel->hovered()) 253 return false; 254 // Don't hide if the mouse is over the video area. 255 const bool ignoreVideoHover = behaviorFlags & IgnoreVideoHover; 256 if (!ignoreVideoHover && m_isMouseOverControls) 257 return false; 258 // Don't hide if focus is on the HTMLMediaElement or within the 259 // controls/shadow tree. (Perform the checks separately to avoid going 260 // through all the potential ancestor hosts for the focused element.) 261 const bool ignoreFocus = behaviorFlags & IgnoreFocus; 262 if (!ignoreFocus && (mediaElement().focused() || contains(document().focusedElement()))) 263 return false; 264 return true; 265 } 266 267 void MediaControls::playbackStarted() 268 { 269 m_currentTimeDisplay->show(); 270 m_durationDisplay->hide(); 271 272 updatePlayState(); 273 m_timeline->setPosition(mediaElement().currentTime()); 274 updateCurrentTimeDisplay(); 275 276 startHideMediaControlsTimer(); 277 } 278 279 void MediaControls::playbackProgressed() 280 { 281 m_timeline->setPosition(mediaElement().currentTime()); 282 updateCurrentTimeDisplay(); 283 284 if (shouldHideMediaControls()) 285 makeTransparent(); 286 } 287 288 void MediaControls::playbackStopped() 289 { 290 updatePlayState(); 291 m_timeline->setPosition(mediaElement().currentTime()); 292 updateCurrentTimeDisplay(); 293 makeOpaque(); 294 295 stopHideMediaControlsTimer(); 296 } 297 298 void MediaControls::updatePlayState() 299 { 300 if (m_isPausedForScrubbing) 301 return; 302 303 if (m_overlayPlayButton) 304 m_overlayPlayButton->updateDisplayType(); 305 m_playButton->updateDisplayType(); 306 } 307 308 void MediaControls::beginScrubbing() 309 { 310 if (!mediaElement().togglePlayStateWillPlay()) { 311 m_isPausedForScrubbing = true; 312 mediaElement().togglePlayState(); 313 } 314 } 315 316 void MediaControls::endScrubbing() 317 { 318 if (m_isPausedForScrubbing) { 319 m_isPausedForScrubbing = false; 320 if (mediaElement().togglePlayStateWillPlay()) 321 mediaElement().togglePlayState(); 322 } 323 } 324 325 void MediaControls::updateCurrentTimeDisplay() 326 { 327 double now = mediaElement().currentTime(); 328 double duration = mediaElement().duration(); 329 330 // After seek, hide duration display and show current time. 331 if (now > 0) { 332 m_currentTimeDisplay->show(); 333 m_durationDisplay->hide(); 334 } 335 336 // Allow the theme to format the time. 337 m_currentTimeDisplay->setInnerText(RenderTheme::theme().formatMediaControlsCurrentTime(now, duration), IGNORE_EXCEPTION); 338 m_currentTimeDisplay->setCurrentValue(now); 339 } 340 341 void MediaControls::updateVolume() 342 { 343 m_muteButton->updateDisplayType(); 344 if (m_muteButton->renderer()) 345 m_muteButton->renderer()->setShouldDoFullPaintInvalidation(true); 346 347 if (mediaElement().muted()) 348 m_volumeSlider->setVolume(0); 349 else 350 m_volumeSlider->setVolume(mediaElement().volume()); 351 } 352 353 void MediaControls::changedClosedCaptionsVisibility() 354 { 355 m_toggleClosedCaptionsButton->updateDisplayType(); 356 } 357 358 void MediaControls::refreshClosedCaptionsButtonVisibility() 359 { 360 if (mediaElement().hasClosedCaptions()) 361 m_toggleClosedCaptionsButton->show(); 362 else 363 m_toggleClosedCaptionsButton->hide(); 364 } 365 366 void MediaControls::textTracksChanged() 367 { 368 refreshClosedCaptionsButtonVisibility(); 369 } 370 371 void MediaControls::refreshCastButtonVisibility() 372 { 373 if (mediaElement().hasRemoteRoutes()) { 374 // The reason for the autoplay test is that some pages (e.g. vimeo.com) have an autoplay background video, which 375 // doesn't autoplay on Chrome for Android (we prevent it) so starts paused. In such cases we don't want to automatically 376 // show the cast button, since it looks strange and is unlikely to correspond with anything the user wants to do. 377 // If a user does want to cast a paused autoplay video then they can still do so by touching or clicking on the 378 // video, which will cause the cast button to appear. 379 if (!mediaElement().shouldShowControls() && !mediaElement().autoplay() && mediaElement().paused()) { 380 showOverlayCastButton(); 381 } else if (mediaElement().shouldShowControls()) { 382 m_overlayCastButton->hide(); 383 m_castButton->show(); 384 // Check that the cast button actually fits on the bar. 385 if (m_fullScreenButton->getBoundingClientRect()->right() > m_panel->getBoundingClientRect()->right()) { 386 m_castButton->hide(); 387 m_overlayCastButton->show(); 388 } 389 } 390 } else { 391 m_castButton->hide(); 392 m_overlayCastButton->hide(); 393 } 394 } 395 396 void MediaControls::showOverlayCastButton() 397 { 398 m_overlayCastButton->show(); 399 resetHideMediaControlsTimer(); 400 } 401 402 void MediaControls::enteredFullscreen() 403 { 404 m_fullScreenButton->setIsFullscreen(true); 405 stopHideMediaControlsTimer(); 406 startHideMediaControlsTimer(); 407 } 408 409 void MediaControls::exitedFullscreen() 410 { 411 m_fullScreenButton->setIsFullscreen(false); 412 stopHideMediaControlsTimer(); 413 startHideMediaControlsTimer(); 414 } 415 416 void MediaControls::startedCasting() 417 { 418 m_castButton->setIsPlayingRemotely(true); 419 m_overlayCastButton->setIsPlayingRemotely(true); 420 } 421 422 void MediaControls::stoppedCasting() 423 { 424 m_castButton->setIsPlayingRemotely(false); 425 m_overlayCastButton->setIsPlayingRemotely(false); 426 } 427 428 void MediaControls::defaultEventHandler(Event* event) 429 { 430 HTMLDivElement::defaultEventHandler(event); 431 m_wasLastEventTouch = event->isTouchEvent() || event->isGestureEvent() 432 || (event->isMouseEvent() && toMouseEvent(event)->fromTouch()); 433 434 if (event->type() == EventTypeNames::mouseover) { 435 if (!containsRelatedTarget(event)) { 436 m_isMouseOverControls = true; 437 if (!mediaElement().togglePlayStateWillPlay()) { 438 makeOpaque(); 439 if (shouldHideMediaControls()) 440 startHideMediaControlsTimer(); 441 } 442 } 443 return; 444 } 445 446 if (event->type() == EventTypeNames::mouseout) { 447 if (!containsRelatedTarget(event)) { 448 m_isMouseOverControls = false; 449 stopHideMediaControlsTimer(); 450 } 451 return; 452 } 453 454 if (event->type() == EventTypeNames::mousemove) { 455 // When we get a mouse move, show the media controls, and start a timer 456 // that will hide the media controls after a 3 seconds without a mouse move. 457 makeOpaque(); 458 refreshCastButtonVisibility(); 459 if (shouldHideMediaControls(IgnoreVideoHover)) 460 startHideMediaControlsTimer(); 461 return; 462 } 463 } 464 465 void MediaControls::hideMediaControlsTimerFired(Timer<MediaControls>*) 466 { 467 if (mediaElement().togglePlayStateWillPlay()) 468 return; 469 470 unsigned behaviorFlags = IgnoreFocus | IgnoreVideoHover; 471 if (m_wasLastEventTouch) { 472 behaviorFlags |= IgnoreControlsHover; 473 } 474 if (!shouldHideMediaControls(behaviorFlags)) 475 return; 476 477 makeTransparent(); 478 m_overlayCastButton->hide(); 479 } 480 481 void MediaControls::startHideMediaControlsTimer() 482 { 483 m_hideMediaControlsTimer.startOneShot(timeWithoutMouseMovementBeforeHidingMediaControls, FROM_HERE); 484 } 485 486 void MediaControls::stopHideMediaControlsTimer() 487 { 488 m_hideMediaControlsTimer.stop(); 489 } 490 491 void MediaControls::resetHideMediaControlsTimer() 492 { 493 stopHideMediaControlsTimer(); 494 if (!mediaElement().paused()) 495 startHideMediaControlsTimer(); 496 } 497 498 499 const AtomicString& MediaControls::shadowPseudoId() const 500 { 501 DEFINE_STATIC_LOCAL(AtomicString, id, ("-webkit-media-controls", AtomicString::ConstructFromLiteral)); 502 return id; 503 } 504 505 bool MediaControls::containsRelatedTarget(Event* event) 506 { 507 if (!event->isMouseEvent()) 508 return false; 509 EventTarget* relatedTarget = toMouseEvent(event)->relatedTarget(); 510 if (!relatedTarget) 511 return false; 512 return contains(relatedTarget->toNode()); 513 } 514 515 void MediaControls::createTextTrackDisplay() 516 { 517 if (m_textDisplayContainer) 518 return; 519 520 RefPtrWillBeRawPtr<MediaControlTextTrackContainerElement> textDisplayContainer = MediaControlTextTrackContainerElement::create(*this); 521 m_textDisplayContainer = textDisplayContainer.get(); 522 523 // Insert it before (behind) all other control elements. 524 if (m_overlayPlayButton) 525 m_overlayEnclosure->insertBefore(textDisplayContainer.release(), m_overlayPlayButton); 526 else 527 m_overlayEnclosure->insertBefore(textDisplayContainer.release(), m_overlayCastButton); 528 } 529 530 void MediaControls::showTextTrackDisplay() 531 { 532 if (!m_textDisplayContainer) 533 createTextTrackDisplay(); 534 m_textDisplayContainer->show(); 535 } 536 537 void MediaControls::hideTextTrackDisplay() 538 { 539 if (!m_textDisplayContainer) 540 createTextTrackDisplay(); 541 m_textDisplayContainer->hide(); 542 } 543 544 void MediaControls::updateTextTrackDisplay() 545 { 546 if (!m_textDisplayContainer) 547 createTextTrackDisplay(); 548 549 m_textDisplayContainer->updateDisplay(); 550 } 551 552 void MediaControls::trace(Visitor* visitor) 553 { 554 visitor->trace(m_mediaElement); 555 visitor->trace(m_panel); 556 visitor->trace(m_textDisplayContainer); 557 visitor->trace(m_overlayPlayButton); 558 visitor->trace(m_overlayEnclosure); 559 visitor->trace(m_playButton); 560 visitor->trace(m_currentTimeDisplay); 561 visitor->trace(m_timeline); 562 visitor->trace(m_muteButton); 563 visitor->trace(m_volumeSlider); 564 visitor->trace(m_toggleClosedCaptionsButton); 565 visitor->trace(m_fullScreenButton); 566 visitor->trace(m_durationDisplay); 567 visitor->trace(m_enclosure); 568 visitor->trace(m_castButton); 569 visitor->trace(m_overlayCastButton); 570 HTMLDivElement::trace(visitor); 571 } 572 573 } 574