#include "config.h"
#include "core/html/shadow/MediaControls.h"
-#include "bindings/v8/ExceptionStatePlaceholder.h"
-
-namespace WebCore {
-
-static const double timeWithoutMouseMovementBeforeHidingFullscreenControls = 3;
-
-MediaControls::MediaControls(Document& document)
- : HTMLDivElement(HTMLNames::divTag, document)
- , m_mediaController(0)
- , m_panel(0)
- , m_textDisplayContainer(0)
- , m_playButton(0)
- , m_currentTimeDisplay(0)
- , m_timeline(0)
- , m_panelMuteButton(0)
- , m_volumeSlider(0)
- , m_toggleClosedCaptionsButton(0)
- , m_fullScreenButton(0)
- , m_hideFullscreenControlsTimer(this, &MediaControls::hideFullscreenControlsTimerFired)
- , m_isFullscreen(false)
+#include "bindings/core/v8/ExceptionStatePlaceholder.h"
+#include "core/dom/ClientRect.h"
+#include "core/events/MouseEvent.h"
+#include "core/frame/Settings.h"
+#include "core/html/HTMLMediaElement.h"
+#include "core/html/MediaController.h"
+#include "core/rendering/RenderTheme.h"
+
+namespace blink {
+
+// If you change this value, then also update the corresponding value in
+// LayoutTests/media/media-controls.js.
+static const double timeWithoutMouseMovementBeforeHidingMediaControls = 3;
+
+static bool fullscreenIsSupported(const Document& document)
+{
+ return !document.settings() || document.settings()->fullscreenSupported();
+}
+
+MediaControls::MediaControls(HTMLMediaElement& mediaElement)
+ : HTMLDivElement(mediaElement.document())
+ , m_mediaElement(&mediaElement)
+ , m_panel(nullptr)
+ , m_textDisplayContainer(nullptr)
+ , m_overlayPlayButton(nullptr)
+ , m_overlayEnclosure(nullptr)
+ , m_playButton(nullptr)
+ , m_currentTimeDisplay(nullptr)
+ , m_timeline(nullptr)
+ , m_muteButton(nullptr)
+ , m_volumeSlider(nullptr)
+ , m_toggleClosedCaptionsButton(nullptr)
+ , m_fullScreenButton(nullptr)
+ , m_castButton(nullptr)
+ , m_overlayCastButton(nullptr)
+ , m_durationDisplay(nullptr)
+ , m_enclosure(nullptr)
+ , m_hideMediaControlsTimer(this, &MediaControls::hideMediaControlsTimerFired)
, m_isMouseOverControls(false)
+ , m_isPausedForScrubbing(false)
+ , m_wasLastEventTouch(false)
{
}
-void MediaControls::setMediaController(MediaControllerInterface* controller)
+PassRefPtrWillBeRawPtr<MediaControls> MediaControls::create(HTMLMediaElement& mediaElement)
{
- if (m_mediaController == controller)
- return;
- m_mediaController = controller;
+ RefPtrWillBeRawPtr<MediaControls> controls = adoptRefWillBeNoop(new MediaControls(mediaElement));
- if (m_panel)
- m_panel->setMediaController(controller);
- if (m_textDisplayContainer)
- m_textDisplayContainer->setMediaController(controller);
- if (m_playButton)
- m_playButton->setMediaController(controller);
- if (m_currentTimeDisplay)
- m_currentTimeDisplay->setMediaController(controller);
- if (m_timeline)
- m_timeline->setMediaController(controller);
- if (m_panelMuteButton)
- m_panelMuteButton->setMediaController(controller);
- if (m_volumeSlider)
- m_volumeSlider->setMediaController(controller);
- if (m_toggleClosedCaptionsButton)
- m_toggleClosedCaptionsButton->setMediaController(controller);
- if (m_fullScreenButton)
- m_fullScreenButton->setMediaController(controller);
+ if (controls->initializeControls())
+ return controls.release();
+
+ return nullptr;
}
-void MediaControls::reset()
+bool MediaControls::initializeControls()
{
- Page* page = document().page();
- if (!page)
- return;
-
- m_playButton->updateDisplayType();
+ TrackExceptionState exceptionState;
- updateCurrentTimeDisplay();
+ RefPtrWillBeRawPtr<MediaControlOverlayEnclosureElement> overlayEnclosure = MediaControlOverlayEnclosureElement::create(*this);
- double duration = m_mediaController->duration();
- if (std::isfinite(duration) || RenderTheme::theme().hasOwnDisabledStateHandlingFor(MediaSliderPart)) {
- m_timeline->setDuration(duration);
- m_timeline->setPosition(m_mediaController->currentTime());
+ if (document().settings() && document().settings()->mediaControlsOverlayPlayButtonEnabled()) {
+ RefPtrWillBeRawPtr<MediaControlOverlayPlayButtonElement> overlayPlayButton = MediaControlOverlayPlayButtonElement::create(*this);
+ m_overlayPlayButton = overlayPlayButton.get();
+ overlayEnclosure->appendChild(overlayPlayButton.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
}
- if (m_mediaController->hasAudio() || RenderTheme::theme().hasOwnDisabledStateHandlingFor(MediaMuteButtonPart))
- m_panelMuteButton->show();
- else
- m_panelMuteButton->hide();
-
- if (m_volumeSlider) {
- if (!m_mediaController->hasAudio())
- m_volumeSlider->hide();
- else {
- m_volumeSlider->show();
- m_volumeSlider->setVolume(m_mediaController->volume());
- }
- }
+ RefPtrWillBeRawPtr<MediaControlCastButtonElement> overlayCastButton = MediaControlCastButtonElement::create(*this, true);
+ m_overlayCastButton = overlayCastButton.get();
+ overlayEnclosure->appendChild(overlayCastButton.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
- refreshClosedCaptionsButtonVisibility();
+ m_overlayEnclosure = overlayEnclosure.get();
+ appendChild(overlayEnclosure.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
- if (m_fullScreenButton) {
- if (m_mediaController->supportsFullscreen() && m_mediaController->hasVideo())
- m_fullScreenButton->show();
- else
- m_fullScreenButton->hide();
- }
+ // Create an enclosing element for the panel so we can visually offset the controls correctly.
+ RefPtrWillBeRawPtr<MediaControlPanelEnclosureElement> enclosure = MediaControlPanelEnclosureElement::create(*this);
- makeOpaque();
+ RefPtrWillBeRawPtr<MediaControlPanelElement> panel = MediaControlPanelElement::create(*this);
+
+ RefPtrWillBeRawPtr<MediaControlPlayButtonElement> playButton = MediaControlPlayButtonElement::create(*this);
+ m_playButton = playButton.get();
+ panel->appendChild(playButton.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ RefPtrWillBeRawPtr<MediaControlTimelineElement> timeline = MediaControlTimelineElement::create(*this);
+ m_timeline = timeline.get();
+ panel->appendChild(timeline.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ RefPtrWillBeRawPtr<MediaControlCurrentTimeDisplayElement> currentTimeDisplay = MediaControlCurrentTimeDisplayElement::create(*this);
+ m_currentTimeDisplay = currentTimeDisplay.get();
+ m_currentTimeDisplay->hide();
+ panel->appendChild(currentTimeDisplay.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ RefPtrWillBeRawPtr<MediaControlTimeRemainingDisplayElement> durationDisplay = MediaControlTimeRemainingDisplayElement::create(*this);
+ m_durationDisplay = durationDisplay.get();
+ panel->appendChild(durationDisplay.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ RefPtrWillBeRawPtr<MediaControlMuteButtonElement> muteButton = MediaControlMuteButtonElement::create(*this);
+ m_muteButton = muteButton.get();
+ panel->appendChild(muteButton.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ RefPtrWillBeRawPtr<MediaControlVolumeSliderElement> slider = MediaControlVolumeSliderElement::create(*this);
+ m_volumeSlider = slider.get();
+ panel->appendChild(slider.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ RefPtrWillBeRawPtr<MediaControlToggleClosedCaptionsButtonElement> toggleClosedCaptionsButton = MediaControlToggleClosedCaptionsButtonElement::create(*this);
+ m_toggleClosedCaptionsButton = toggleClosedCaptionsButton.get();
+ panel->appendChild(toggleClosedCaptionsButton.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ RefPtrWillBeRawPtr<MediaControlCastButtonElement> castButton = MediaControlCastButtonElement::create(*this, false);
+ m_castButton = castButton.get();
+ panel->appendChild(castButton.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ RefPtrWillBeRawPtr<MediaControlFullscreenButtonElement> fullscreenButton = MediaControlFullscreenButtonElement::create(*this);
+ m_fullScreenButton = fullscreenButton.get();
+ panel->appendChild(fullscreenButton.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ m_panel = panel.get();
+ enclosure->appendChild(panel.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ m_enclosure = enclosure.get();
+ appendChild(enclosure.release(), exceptionState);
+ if (exceptionState.hadException())
+ return false;
+
+ return true;
}
-void MediaControls::reportedError()
+void MediaControls::reset()
{
- Page* page = document().page();
- if (!page)
- return;
+ double duration = mediaElement().duration();
+ m_durationDisplay->setInnerText(RenderTheme::theme().formatMediaControlsTime(duration), ASSERT_NO_EXCEPTION);
+ m_durationDisplay->setCurrentValue(duration);
+
+ updatePlayState();
- if (!RenderTheme::theme().hasOwnDisabledStateHandlingFor(MediaMuteButtonPart)) {
- m_panelMuteButton->hide();
+ updateCurrentTimeDisplay();
+
+ m_timeline->setDuration(duration);
+ m_timeline->setPosition(mediaElement().currentTime());
+
+ if (!mediaElement().hasAudio())
m_volumeSlider->hide();
- }
+ else
+ m_volumeSlider->show();
+ updateVolume();
- if (m_toggleClosedCaptionsButton && !RenderTheme::theme().hasOwnDisabledStateHandlingFor(MediaToggleClosedCaptionsButtonPart))
- m_toggleClosedCaptionsButton->hide();
+ refreshClosedCaptionsButtonVisibility();
- if (m_fullScreenButton && !RenderTheme::theme().hasOwnDisabledStateHandlingFor(MediaEnterFullscreenButtonPart))
+ if (mediaElement().hasVideo() && fullscreenIsSupported(document()))
+ m_fullScreenButton->show();
+ else
m_fullScreenButton->hide();
-}
-void MediaControls::loadedMetadata()
-{
- reset();
+ refreshCastButtonVisibility();
+ makeOpaque();
}
void MediaControls::show()
makeOpaque();
m_panel->setIsDisplayed(true);
m_panel->show();
+ if (m_overlayPlayButton)
+ m_overlayPlayButton->updateDisplayType();
+}
+
+void MediaControls::mediaElementFocused()
+{
+ show();
+ resetHideMediaControlsTimer();
}
void MediaControls::hide()
{
m_panel->setIsDisplayed(false);
m_panel->hide();
+ if (m_overlayPlayButton)
+ m_overlayPlayButton->hide();
}
void MediaControls::makeOpaque()
m_panel->makeTransparent();
}
-bool MediaControls::shouldHideControls()
-{
- return !m_panel->hovered();
-}
-
-void MediaControls::bufferingProgressed()
+bool MediaControls::shouldHideMediaControls(unsigned behaviorFlags) const
{
- // We only need to update buffering progress when paused, during normal
- // playback playbackProgressed() will take care of it.
- if (m_mediaController->paused())
- m_timeline->setPosition(m_mediaController->currentTime());
+ // Never hide for a media element without visual representation.
+ if (!mediaElement().hasVideo() || mediaElement().isPlayingRemotely())
+ return false;
+ // Don't hide if the mouse is over the controls.
+ const bool ignoreControlsHover = behaviorFlags & IgnoreControlsHover;
+ if (!ignoreControlsHover && m_panel->hovered())
+ return false;
+ // Don't hide if the mouse is over the video area.
+ const bool ignoreVideoHover = behaviorFlags & IgnoreVideoHover;
+ if (!ignoreVideoHover && m_isMouseOverControls)
+ return false;
+ // Don't hide if focus is on the HTMLMediaElement or within the
+ // controls/shadow tree. (Perform the checks separately to avoid going
+ // through all the potential ancestor hosts for the focused element.)
+ const bool ignoreFocus = behaviorFlags & IgnoreFocus;
+ if (!ignoreFocus && (mediaElement().focused() || contains(document().focusedElement())))
+ return false;
+ return true;
}
void MediaControls::playbackStarted()
{
- m_playButton->updateDisplayType();
- m_timeline->setPosition(m_mediaController->currentTime());
+ m_currentTimeDisplay->show();
+ m_durationDisplay->hide();
+
+ updatePlayState();
+ m_timeline->setPosition(mediaElement().currentTime());
updateCurrentTimeDisplay();
- if (m_isFullscreen)
- startHideFullscreenControlsTimer();
+ startHideMediaControlsTimer();
}
void MediaControls::playbackProgressed()
{
- m_timeline->setPosition(m_mediaController->currentTime());
+ m_timeline->setPosition(mediaElement().currentTime());
updateCurrentTimeDisplay();
- if (!m_isMouseOverControls && m_mediaController->hasVideo())
+ if (shouldHideMediaControls())
makeTransparent();
}
void MediaControls::playbackStopped()
{
- m_playButton->updateDisplayType();
- m_timeline->setPosition(m_mediaController->currentTime());
+ updatePlayState();
+ m_timeline->setPosition(mediaElement().currentTime());
updateCurrentTimeDisplay();
makeOpaque();
- stopHideFullscreenControlsTimer();
+ stopHideMediaControlsTimer();
}
-void MediaControls::showVolumeSlider()
+void MediaControls::updatePlayState()
{
- if (!m_mediaController->hasAudio())
+ if (m_isPausedForScrubbing)
return;
- m_volumeSlider->show();
+ if (m_overlayPlayButton)
+ m_overlayPlayButton->updateDisplayType();
+ m_playButton->updateDisplayType();
+}
+
+void MediaControls::beginScrubbing()
+{
+ if (!mediaElement().togglePlayStateWillPlay()) {
+ m_isPausedForScrubbing = true;
+ mediaElement().togglePlayState();
+ }
}
-void MediaControls::changedMute()
+void MediaControls::endScrubbing()
{
- m_panelMuteButton->changedMute();
+ if (m_isPausedForScrubbing) {
+ m_isPausedForScrubbing = false;
+ if (mediaElement().togglePlayStateWillPlay())
+ mediaElement().togglePlayState();
+ }
+}
+
+void MediaControls::updateCurrentTimeDisplay()
+{
+ double now = mediaElement().currentTime();
+ double duration = mediaElement().duration();
+
+ // After seek, hide duration display and show current time.
+ if (now > 0) {
+ m_currentTimeDisplay->show();
+ m_durationDisplay->hide();
+ }
+
+ // Allow the theme to format the time.
+ m_currentTimeDisplay->setInnerText(RenderTheme::theme().formatMediaControlsCurrentTime(now, duration), IGNORE_EXCEPTION);
+ m_currentTimeDisplay->setCurrentValue(now);
}
-void MediaControls::changedVolume()
+void MediaControls::updateVolume()
{
- if (m_volumeSlider)
- m_volumeSlider->setVolume(m_mediaController->volume());
- if (m_panelMuteButton && m_panelMuteButton->renderer())
- m_panelMuteButton->renderer()->repaint();
+ m_muteButton->updateDisplayType();
+ if (m_muteButton->renderer())
+ m_muteButton->renderer()->setShouldDoFullPaintInvalidation(true);
+
+ if (mediaElement().muted())
+ m_volumeSlider->setVolume(0);
+ else
+ m_volumeSlider->setVolume(mediaElement().volume());
}
void MediaControls::changedClosedCaptionsVisibility()
{
- if (m_toggleClosedCaptionsButton)
- m_toggleClosedCaptionsButton->updateDisplayType();
+ m_toggleClosedCaptionsButton->updateDisplayType();
}
void MediaControls::refreshClosedCaptionsButtonVisibility()
{
- if (!m_toggleClosedCaptionsButton)
- return;
-
- if (m_mediaController->hasClosedCaptions())
+ if (mediaElement().hasClosedCaptions())
m_toggleClosedCaptionsButton->show();
else
m_toggleClosedCaptionsButton->hide();
}
-void MediaControls::closedCaptionTracksChanged()
+void MediaControls::textTracksChanged()
{
refreshClosedCaptionsButtonVisibility();
}
+void MediaControls::refreshCastButtonVisibility()
+{
+ if (mediaElement().hasRemoteRoutes()) {
+ // The reason for the autoplay test is that some pages (e.g. vimeo.com) have an autoplay background video, which
+ // doesn't autoplay on Chrome for Android (we prevent it) so starts paused. In such cases we don't want to automatically
+ // show the cast button, since it looks strange and is unlikely to correspond with anything the user wants to do.
+ // If a user does want to cast a paused autoplay video then they can still do so by touching or clicking on the
+ // video, which will cause the cast button to appear.
+ if (!mediaElement().shouldShowControls() && !mediaElement().autoplay() && mediaElement().paused()) {
+ showOverlayCastButton();
+ } else if (mediaElement().shouldShowControls()) {
+ m_overlayCastButton->hide();
+ m_castButton->show();
+ // Check that the cast button actually fits on the bar.
+ if (m_fullScreenButton->getBoundingClientRect()->right() > m_panel->getBoundingClientRect()->right()) {
+ m_castButton->hide();
+ m_overlayCastButton->show();
+ }
+ }
+ } else {
+ m_castButton->hide();
+ m_overlayCastButton->hide();
+ }
+}
+
+void MediaControls::showOverlayCastButton()
+{
+ m_overlayCastButton->show();
+ resetHideMediaControlsTimer();
+}
+
void MediaControls::enteredFullscreen()
{
- m_isFullscreen = true;
m_fullScreenButton->setIsFullscreen(true);
- startHideFullscreenControlsTimer();
+ stopHideMediaControlsTimer();
+ startHideMediaControlsTimer();
}
void MediaControls::exitedFullscreen()
{
- m_isFullscreen = false;
m_fullScreenButton->setIsFullscreen(false);
- stopHideFullscreenControlsTimer();
+ stopHideMediaControlsTimer();
+ startHideMediaControlsTimer();
+}
+
+void MediaControls::startedCasting()
+{
+ m_castButton->setIsPlayingRemotely(true);
+ m_overlayCastButton->setIsPlayingRemotely(true);
+}
+
+void MediaControls::stoppedCasting()
+{
+ m_castButton->setIsPlayingRemotely(false);
+ m_overlayCastButton->setIsPlayingRemotely(false);
}
void MediaControls::defaultEventHandler(Event* event)
{
HTMLDivElement::defaultEventHandler(event);
+ m_wasLastEventTouch = event->isTouchEvent() || event->isGestureEvent()
+ || (event->isMouseEvent() && toMouseEvent(event)->fromTouch());
if (event->type() == EventTypeNames::mouseover) {
if (!containsRelatedTarget(event)) {
m_isMouseOverControls = true;
- if (!m_mediaController->canPlay()) {
+ if (!mediaElement().togglePlayStateWillPlay()) {
makeOpaque();
- if (shouldHideControls())
- startHideFullscreenControlsTimer();
+ if (shouldHideMediaControls())
+ startHideMediaControlsTimer();
}
}
return;
if (event->type() == EventTypeNames::mouseout) {
if (!containsRelatedTarget(event)) {
m_isMouseOverControls = false;
- stopHideFullscreenControlsTimer();
+ stopHideMediaControlsTimer();
}
return;
}
if (event->type() == EventTypeNames::mousemove) {
- if (m_isFullscreen) {
- // When we get a mouse move in fullscreen mode, show the media controls, and start a timer
- // that will hide the media controls after a 3 seconds without a mouse move.
- makeOpaque();
- if (shouldHideControls())
- startHideFullscreenControlsTimer();
- }
+ // When we get a mouse move, show the media controls, and start a timer
+ // that will hide the media controls after a 3 seconds without a mouse move.
+ makeOpaque();
+ refreshCastButtonVisibility();
+ if (shouldHideMediaControls(IgnoreVideoHover))
+ startHideMediaControlsTimer();
return;
}
}
-void MediaControls::hideFullscreenControlsTimerFired(Timer<MediaControls>*)
+void MediaControls::hideMediaControlsTimerFired(Timer<MediaControls>*)
{
- if (m_mediaController->paused())
+ if (mediaElement().togglePlayStateWillPlay())
return;
- if (!m_isFullscreen)
- return;
-
- if (!shouldHideControls())
+ unsigned behaviorFlags = IgnoreFocus | IgnoreVideoHover;
+ if (m_wasLastEventTouch) {
+ behaviorFlags |= IgnoreControlsHover;
+ }
+ if (!shouldHideMediaControls(behaviorFlags))
return;
makeTransparent();
+ m_overlayCastButton->hide();
}
-void MediaControls::startHideFullscreenControlsTimer()
+void MediaControls::startHideMediaControlsTimer()
{
- if (!m_isFullscreen)
- return;
-
- Page* page = document().page();
- if (!page)
- return;
+ m_hideMediaControlsTimer.startOneShot(timeWithoutMouseMovementBeforeHidingMediaControls, FROM_HERE);
+}
- m_hideFullscreenControlsTimer.startOneShot(timeWithoutMouseMovementBeforeHidingFullscreenControls);
+void MediaControls::stopHideMediaControlsTimer()
+{
+ m_hideMediaControlsTimer.stop();
}
-void MediaControls::stopHideFullscreenControlsTimer()
+void MediaControls::resetHideMediaControlsTimer()
{
- m_hideFullscreenControlsTimer.stop();
+ stopHideMediaControlsTimer();
+ if (!mediaElement().paused())
+ startHideMediaControlsTimer();
}
-const AtomicString& MediaControls::part() const
+
+const AtomicString& MediaControls::shadowPseudoId() const
{
- DEFINE_STATIC_LOCAL(AtomicString, id, ("-webkit-media-controls"));
+ DEFINE_STATIC_LOCAL(AtomicString, id, ("-webkit-media-controls", AtomicString::ConstructFromLiteral));
return id;
}
if (m_textDisplayContainer)
return;
- RefPtr<MediaControlTextTrackContainerElement> textDisplayContainer = MediaControlTextTrackContainerElement::create(document());
+ RefPtrWillBeRawPtr<MediaControlTextTrackContainerElement> textDisplayContainer = MediaControlTextTrackContainerElement::create(*this);
m_textDisplayContainer = textDisplayContainer.get();
- if (m_mediaController)
- m_textDisplayContainer->setMediaController(m_mediaController);
-
- // Insert it before the first controller element so it always displays behind the controls.
- insertBefore(textDisplayContainer.release(), m_panel, IGNORE_EXCEPTION);
+ // Insert it before (behind) all other control elements.
+ if (m_overlayPlayButton)
+ m_overlayEnclosure->insertBefore(textDisplayContainer.release(), m_overlayPlayButton);
+ else
+ m_overlayEnclosure->insertBefore(textDisplayContainer.release(), m_overlayCastButton);
}
void MediaControls::showTextTrackDisplay()
m_textDisplayContainer->updateDisplay();
}
+void MediaControls::trace(Visitor* visitor)
+{
+ visitor->trace(m_mediaElement);
+ visitor->trace(m_panel);
+ visitor->trace(m_textDisplayContainer);
+ visitor->trace(m_overlayPlayButton);
+ visitor->trace(m_overlayEnclosure);
+ visitor->trace(m_playButton);
+ visitor->trace(m_currentTimeDisplay);
+ visitor->trace(m_timeline);
+ visitor->trace(m_muteButton);
+ visitor->trace(m_volumeSlider);
+ visitor->trace(m_toggleClosedCaptionsButton);
+ visitor->trace(m_fullScreenButton);
+ visitor->trace(m_durationDisplay);
+ visitor->trace(m_enclosure);
+ visitor->trace(m_castButton);
+ visitor->trace(m_overlayCastButton);
+ HTMLDivElement::trace(visitor);
+}
+
}