2 * Copyright (C) 2011, 2012 Apple Inc. All rights reserved.
3 * Copyright (C) 2011, 2012 Google Inc. All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
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.
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.
28 #include "core/html/shadow/MediaControls.h"
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"
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;
44 static bool fullscreenIsSupported(const Document& document)
46 return !document.settings() || document.settings()->fullscreenSupported();
49 MediaControls::MediaControls(HTMLMediaElement& mediaElement)
50 : HTMLDivElement(mediaElement.document())
51 , m_mediaElement(&mediaElement)
53 , m_textDisplayContainer(nullptr)
54 , m_overlayPlayButton(nullptr)
55 , m_overlayEnclosure(nullptr)
56 , m_playButton(nullptr)
57 , m_currentTimeDisplay(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)
74 PassRefPtrWillBeRawPtr<MediaControls> MediaControls::create(HTMLMediaElement& mediaElement)
76 RefPtrWillBeRawPtr<MediaControls> controls = adoptRefWillBeNoop(new MediaControls(mediaElement));
78 if (controls->initializeControls())
79 return controls.release();
84 bool MediaControls::initializeControls()
86 TrackExceptionState exceptionState;
88 RefPtrWillBeRawPtr<MediaControlOverlayEnclosureElement> overlayEnclosure = MediaControlOverlayEnclosureElement::create(*this);
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())
98 RefPtrWillBeRawPtr<MediaControlCastButtonElement> overlayCastButton = MediaControlCastButtonElement::create(*this, true);
99 m_overlayCastButton = overlayCastButton.get();
100 overlayEnclosure->appendChild(overlayCastButton.release(), exceptionState);
101 if (exceptionState.hadException())
104 m_overlayEnclosure = overlayEnclosure.get();
105 appendChild(overlayEnclosure.release(), exceptionState);
106 if (exceptionState.hadException())
109 // Create an enclosing element for the panel so we can visually offset the controls correctly.
110 RefPtrWillBeRawPtr<MediaControlPanelEnclosureElement> enclosure = MediaControlPanelEnclosureElement::create(*this);
112 RefPtrWillBeRawPtr<MediaControlPanelElement> panel = MediaControlPanelElement::create(*this);
114 RefPtrWillBeRawPtr<MediaControlPlayButtonElement> playButton = MediaControlPlayButtonElement::create(*this);
115 m_playButton = playButton.get();
116 panel->appendChild(playButton.release(), exceptionState);
117 if (exceptionState.hadException())
120 RefPtrWillBeRawPtr<MediaControlTimelineElement> timeline = MediaControlTimelineElement::create(*this);
121 m_timeline = timeline.get();
122 panel->appendChild(timeline.release(), exceptionState);
123 if (exceptionState.hadException())
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())
133 RefPtrWillBeRawPtr<MediaControlTimeRemainingDisplayElement> durationDisplay = MediaControlTimeRemainingDisplayElement::create(*this);
134 m_durationDisplay = durationDisplay.get();
135 panel->appendChild(durationDisplay.release(), exceptionState);
136 if (exceptionState.hadException())
139 RefPtrWillBeRawPtr<MediaControlMuteButtonElement> muteButton = MediaControlMuteButtonElement::create(*this);
140 m_muteButton = muteButton.get();
141 panel->appendChild(muteButton.release(), exceptionState);
142 if (exceptionState.hadException())
145 RefPtrWillBeRawPtr<MediaControlVolumeSliderElement> slider = MediaControlVolumeSliderElement::create(*this);
146 m_volumeSlider = slider.get();
147 panel->appendChild(slider.release(), exceptionState);
148 if (exceptionState.hadException())
151 RefPtrWillBeRawPtr<MediaControlToggleClosedCaptionsButtonElement> toggleClosedCaptionsButton = MediaControlToggleClosedCaptionsButtonElement::create(*this);
152 m_toggleClosedCaptionsButton = toggleClosedCaptionsButton.get();
153 panel->appendChild(toggleClosedCaptionsButton.release(), exceptionState);
154 if (exceptionState.hadException())
157 RefPtrWillBeRawPtr<MediaControlCastButtonElement> castButton = MediaControlCastButtonElement::create(*this, false);
158 m_castButton = castButton.get();
159 panel->appendChild(castButton.release(), exceptionState);
160 if (exceptionState.hadException())
163 RefPtrWillBeRawPtr<MediaControlFullscreenButtonElement> fullscreenButton = MediaControlFullscreenButtonElement::create(*this);
164 m_fullScreenButton = fullscreenButton.get();
165 panel->appendChild(fullscreenButton.release(), exceptionState);
166 if (exceptionState.hadException())
169 m_panel = panel.get();
170 enclosure->appendChild(panel.release(), exceptionState);
171 if (exceptionState.hadException())
174 m_enclosure = enclosure.get();
175 appendChild(enclosure.release(), exceptionState);
176 if (exceptionState.hadException())
182 void MediaControls::reset()
184 double duration = mediaElement().duration();
185 m_durationDisplay->setInnerText(RenderTheme::theme().formatMediaControlsTime(duration), ASSERT_NO_EXCEPTION);
186 m_durationDisplay->setCurrentValue(duration);
190 updateCurrentTimeDisplay();
192 m_timeline->setDuration(duration);
193 m_timeline->setPosition(mediaElement().currentTime());
195 if (!mediaElement().hasAudio())
196 m_volumeSlider->hide();
198 m_volumeSlider->show();
201 refreshClosedCaptionsButtonVisibility();
203 if (mediaElement().hasVideo() && fullscreenIsSupported(document()))
204 m_fullScreenButton->show();
206 m_fullScreenButton->hide();
208 refreshCastButtonVisibility();
212 void MediaControls::show()
215 m_panel->setIsDisplayed(true);
217 if (m_overlayPlayButton)
218 m_overlayPlayButton->updateDisplayType();
221 void MediaControls::mediaElementFocused()
223 if (mediaElement().shouldShowControls()) {
225 resetHideMediaControlsTimer();
229 void MediaControls::hide()
231 m_panel->setIsDisplayed(false);
233 if (m_overlayPlayButton)
234 m_overlayPlayButton->hide();
237 void MediaControls::makeOpaque()
239 m_panel->makeOpaque();
242 void MediaControls::makeTransparent()
244 m_panel->makeTransparent();
247 bool MediaControls::shouldHideMediaControls(unsigned behaviorFlags) const
249 // Never hide for a media element without visual representation.
250 if (!mediaElement().hasVideo() || mediaElement().isPlayingRemotely())
252 // Don't hide if the mouse is over the controls.
253 const bool ignoreControlsHover = behaviorFlags & IgnoreControlsHover;
254 if (!ignoreControlsHover && m_panel->hovered())
256 // Don't hide if the mouse is over the video area.
257 const bool ignoreVideoHover = behaviorFlags & IgnoreVideoHover;
258 if (!ignoreVideoHover && m_isMouseOverControls)
260 // Don't hide if focus is on the HTMLMediaElement or within the
261 // controls/shadow tree. (Perform the checks separately to avoid going
262 // through all the potential ancestor hosts for the focused element.)
263 const bool ignoreFocus = behaviorFlags & IgnoreFocus;
264 if (!ignoreFocus && (mediaElement().focused() || contains(document().focusedElement())))
269 void MediaControls::playbackStarted()
271 m_currentTimeDisplay->show();
272 m_durationDisplay->hide();
275 m_timeline->setPosition(mediaElement().currentTime());
276 updateCurrentTimeDisplay();
278 startHideMediaControlsTimer();
281 void MediaControls::playbackProgressed()
283 m_timeline->setPosition(mediaElement().currentTime());
284 updateCurrentTimeDisplay();
286 if (shouldHideMediaControls())
290 void MediaControls::playbackStopped()
293 m_timeline->setPosition(mediaElement().currentTime());
294 updateCurrentTimeDisplay();
297 stopHideMediaControlsTimer();
300 void MediaControls::updatePlayState()
302 if (m_isPausedForScrubbing)
305 if (m_overlayPlayButton)
306 m_overlayPlayButton->updateDisplayType();
307 m_playButton->updateDisplayType();
310 void MediaControls::beginScrubbing()
312 if (!mediaElement().togglePlayStateWillPlay()) {
313 m_isPausedForScrubbing = true;
314 mediaElement().togglePlayState();
318 void MediaControls::endScrubbing()
320 if (m_isPausedForScrubbing) {
321 m_isPausedForScrubbing = false;
322 if (mediaElement().togglePlayStateWillPlay())
323 mediaElement().togglePlayState();
327 void MediaControls::updateCurrentTimeDisplay()
329 double now = mediaElement().currentTime();
330 double duration = mediaElement().duration();
332 // After seek, hide duration display and show current time.
334 m_currentTimeDisplay->show();
335 m_durationDisplay->hide();
338 // Allow the theme to format the time.
339 m_currentTimeDisplay->setInnerText(RenderTheme::theme().formatMediaControlsCurrentTime(now, duration), IGNORE_EXCEPTION);
340 m_currentTimeDisplay->setCurrentValue(now);
343 void MediaControls::updateVolume()
345 m_muteButton->updateDisplayType();
346 if (m_muteButton->renderer())
347 m_muteButton->renderer()->setShouldDoFullPaintInvalidation();
349 if (mediaElement().muted())
350 m_volumeSlider->setVolume(0);
352 m_volumeSlider->setVolume(mediaElement().volume());
355 void MediaControls::changedClosedCaptionsVisibility()
357 m_toggleClosedCaptionsButton->updateDisplayType();
360 void MediaControls::refreshClosedCaptionsButtonVisibility()
362 if (mediaElement().hasClosedCaptions())
363 m_toggleClosedCaptionsButton->show();
365 m_toggleClosedCaptionsButton->hide();
368 void MediaControls::textTracksChanged()
370 refreshClosedCaptionsButtonVisibility();
373 void MediaControls::refreshCastButtonVisibility()
375 if (mediaElement().hasRemoteRoutes()) {
376 // The reason for the autoplay test is that some pages (e.g. vimeo.com) have an autoplay background video, which
377 // doesn't autoplay on Chrome for Android (we prevent it) so starts paused. In such cases we don't want to automatically
378 // show the cast button, since it looks strange and is unlikely to correspond with anything the user wants to do.
379 // If a user does want to cast a paused autoplay video then they can still do so by touching or clicking on the
380 // video, which will cause the cast button to appear.
381 if (!mediaElement().shouldShowControls() && !mediaElement().autoplay() && mediaElement().paused()) {
382 showOverlayCastButton();
383 } else if (mediaElement().shouldShowControls()) {
384 m_overlayCastButton->hide();
385 m_castButton->show();
386 // Check that the cast button actually fits on the bar.
387 if (m_fullScreenButton->getBoundingClientRect()->right() > m_panel->getBoundingClientRect()->right()) {
388 m_castButton->hide();
389 m_overlayCastButton->show();
393 m_castButton->hide();
394 m_overlayCastButton->hide();
398 void MediaControls::showOverlayCastButton()
400 m_overlayCastButton->show();
401 resetHideMediaControlsTimer();
404 void MediaControls::enteredFullscreen()
406 m_fullScreenButton->setIsFullscreen(true);
407 stopHideMediaControlsTimer();
408 startHideMediaControlsTimer();
411 void MediaControls::exitedFullscreen()
413 m_fullScreenButton->setIsFullscreen(false);
414 stopHideMediaControlsTimer();
415 startHideMediaControlsTimer();
418 void MediaControls::startedCasting()
420 m_castButton->setIsPlayingRemotely(true);
421 m_overlayCastButton->setIsPlayingRemotely(true);
424 void MediaControls::stoppedCasting()
426 m_castButton->setIsPlayingRemotely(false);
427 m_overlayCastButton->setIsPlayingRemotely(false);
430 void MediaControls::defaultEventHandler(Event* event)
432 HTMLDivElement::defaultEventHandler(event);
433 m_wasLastEventTouch = event->isTouchEvent() || event->isGestureEvent()
434 || (event->isMouseEvent() && toMouseEvent(event)->fromTouch());
436 if (event->type() == EventTypeNames::mouseover) {
437 if (!containsRelatedTarget(event)) {
438 m_isMouseOverControls = true;
439 if (!mediaElement().togglePlayStateWillPlay()) {
441 if (shouldHideMediaControls())
442 startHideMediaControlsTimer();
448 if (event->type() == EventTypeNames::mouseout) {
449 if (!containsRelatedTarget(event)) {
450 m_isMouseOverControls = false;
451 stopHideMediaControlsTimer();
456 if (event->type() == EventTypeNames::mousemove) {
457 // When we get a mouse move, show the media controls, and start a timer
458 // that will hide the media controls after a 3 seconds without a mouse move.
460 refreshCastButtonVisibility();
461 if (shouldHideMediaControls(IgnoreVideoHover))
462 startHideMediaControlsTimer();
467 void MediaControls::hideMediaControlsTimerFired(Timer<MediaControls>*)
469 if (mediaElement().togglePlayStateWillPlay())
472 unsigned behaviorFlags = IgnoreFocus | IgnoreVideoHover;
473 if (m_wasLastEventTouch) {
474 behaviorFlags |= IgnoreControlsHover;
476 if (!shouldHideMediaControls(behaviorFlags))
480 m_overlayCastButton->hide();
483 void MediaControls::startHideMediaControlsTimer()
485 m_hideMediaControlsTimer.startOneShot(timeWithoutMouseMovementBeforeHidingMediaControls, FROM_HERE);
488 void MediaControls::stopHideMediaControlsTimer()
490 m_hideMediaControlsTimer.stop();
493 void MediaControls::resetHideMediaControlsTimer()
495 stopHideMediaControlsTimer();
496 if (!mediaElement().paused())
497 startHideMediaControlsTimer();
501 const AtomicString& MediaControls::shadowPseudoId() const
503 DEFINE_STATIC_LOCAL(AtomicString, id, ("-webkit-media-controls", AtomicString::ConstructFromLiteral));
507 bool MediaControls::containsRelatedTarget(Event* event)
509 if (!event->isMouseEvent())
511 EventTarget* relatedTarget = toMouseEvent(event)->relatedTarget();
514 return contains(relatedTarget->toNode());
517 void MediaControls::createTextTrackDisplay()
519 if (m_textDisplayContainer)
522 RefPtrWillBeRawPtr<MediaControlTextTrackContainerElement> textDisplayContainer = MediaControlTextTrackContainerElement::create(*this);
523 m_textDisplayContainer = textDisplayContainer.get();
525 // Insert it before (behind) all other control elements.
526 if (m_overlayPlayButton)
527 m_overlayEnclosure->insertBefore(textDisplayContainer.release(), m_overlayPlayButton);
529 m_overlayEnclosure->insertBefore(textDisplayContainer.release(), m_overlayCastButton);
532 void MediaControls::showTextTrackDisplay()
534 if (!m_textDisplayContainer)
535 createTextTrackDisplay();
536 m_textDisplayContainer->show();
539 void MediaControls::hideTextTrackDisplay()
541 if (!m_textDisplayContainer)
542 createTextTrackDisplay();
543 m_textDisplayContainer->hide();
546 void MediaControls::updateTextTrackDisplay()
548 if (!m_textDisplayContainer)
549 createTextTrackDisplay();
551 m_textDisplayContainer->updateDisplay();
554 void MediaControls::trace(Visitor* visitor)
556 visitor->trace(m_mediaElement);
557 visitor->trace(m_panel);
558 visitor->trace(m_textDisplayContainer);
559 visitor->trace(m_overlayPlayButton);
560 visitor->trace(m_overlayEnclosure);
561 visitor->trace(m_playButton);
562 visitor->trace(m_currentTimeDisplay);
563 visitor->trace(m_timeline);
564 visitor->trace(m_muteButton);
565 visitor->trace(m_volumeSlider);
566 visitor->trace(m_toggleClosedCaptionsButton);
567 visitor->trace(m_fullScreenButton);
568 visitor->trace(m_durationDisplay);
569 visitor->trace(m_enclosure);
570 visitor->trace(m_castButton);
571 visitor->trace(m_overlayCastButton);
572 HTMLDivElement::trace(visitor);