Home | History | Annotate | Download | only in shadow
      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