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/events/MouseEvent.h"
32 #include "core/frame/Settings.h"
33 #include "core/html/HTMLMediaElement.h"
34 #include "core/html/MediaController.h"
35 #include "core/rendering/RenderTheme.h"
39 // If you change this value, then also update the corresponding value in
40 // LayoutTests/media/media-controls.js.
41 static const double timeWithoutMouseMovementBeforeHidingMediaControls = 3;
43 static bool fullscreenIsSupported(const Document& document)
45 return !document.settings() || document.settings()->fullscreenSupported();
48 MediaControls::MediaControls(HTMLMediaElement& mediaElement)
49 : HTMLDivElement(mediaElement.document())
50 , m_mediaElement(&mediaElement)
52 , m_textDisplayContainer(nullptr)
53 , m_overlayPlayButton(nullptr)
54 , m_overlayEnclosure(nullptr)
55 , m_playButton(nullptr)
56 , m_currentTimeDisplay(nullptr)
58 , m_muteButton(nullptr)
59 , m_volumeSlider(nullptr)
60 , m_toggleClosedCaptionsButton(nullptr)
61 , m_fullScreenButton(nullptr)
62 , m_castButton(nullptr)
63 , m_overlayCastButton(nullptr)
64 , m_durationDisplay(nullptr)
65 , m_enclosure(nullptr)
66 , m_hideMediaControlsTimer(this, &MediaControls::hideMediaControlsTimerFired)
67 , m_isMouseOverControls(false)
68 , m_isPausedForScrubbing(false)
69 , m_wasLastEventTouch(false)
73 PassRefPtrWillBeRawPtr<MediaControls> MediaControls::create(HTMLMediaElement& mediaElement)
75 RefPtrWillBeRawPtr<MediaControls> controls = adoptRefWillBeNoop(new MediaControls(mediaElement));
77 if (controls->initializeControls())
78 return controls.release();
83 bool MediaControls::initializeControls()
85 TrackExceptionState exceptionState;
87 RefPtrWillBeRawPtr<MediaControlOverlayEnclosureElement> overlayEnclosure = MediaControlOverlayEnclosureElement::create(*this);
89 if (document().settings() && document().settings()->mediaControlsOverlayPlayButtonEnabled()) {
90 RefPtrWillBeRawPtr<MediaControlOverlayPlayButtonElement> overlayPlayButton = MediaControlOverlayPlayButtonElement::create(*this);
91 m_overlayPlayButton = overlayPlayButton.get();
92 overlayEnclosure->appendChild(overlayPlayButton.release(), exceptionState);
93 if (exceptionState.hadException())
97 RefPtrWillBeRawPtr<MediaControlCastButtonElement> overlayCastButton = MediaControlCastButtonElement::create(*this, true);
98 m_overlayCastButton = overlayCastButton.get();
99 overlayEnclosure->appendChild(overlayCastButton.release(), exceptionState);
100 if (exceptionState.hadException())
103 m_overlayEnclosure = overlayEnclosure.get();
104 appendChild(overlayEnclosure.release(), exceptionState);
105 if (exceptionState.hadException())
108 // Create an enclosing element for the panel so we can visually offset the controls correctly.
109 RefPtrWillBeRawPtr<MediaControlPanelEnclosureElement> enclosure = MediaControlPanelEnclosureElement::create(*this);
111 RefPtrWillBeRawPtr<MediaControlPanelElement> panel = MediaControlPanelElement::create(*this);
113 RefPtrWillBeRawPtr<MediaControlPlayButtonElement> playButton = MediaControlPlayButtonElement::create(*this);
114 m_playButton = playButton.get();
115 panel->appendChild(playButton.release(), exceptionState);
116 if (exceptionState.hadException())
119 RefPtrWillBeRawPtr<MediaControlTimelineElement> timeline = MediaControlTimelineElement::create(*this);
120 m_timeline = timeline.get();
121 panel->appendChild(timeline.release(), exceptionState);
122 if (exceptionState.hadException())
125 RefPtrWillBeRawPtr<MediaControlCurrentTimeDisplayElement> currentTimeDisplay = MediaControlCurrentTimeDisplayElement::create(*this);
126 m_currentTimeDisplay = currentTimeDisplay.get();
127 m_currentTimeDisplay->hide();
128 panel->appendChild(currentTimeDisplay.release(), exceptionState);
129 if (exceptionState.hadException())
132 RefPtrWillBeRawPtr<MediaControlTimeRemainingDisplayElement> durationDisplay = MediaControlTimeRemainingDisplayElement::create(*this);
133 m_durationDisplay = durationDisplay.get();
134 panel->appendChild(durationDisplay.release(), exceptionState);
135 if (exceptionState.hadException())
138 RefPtrWillBeRawPtr<MediaControlMuteButtonElement> muteButton = MediaControlMuteButtonElement::create(*this);
139 m_muteButton = muteButton.get();
140 panel->appendChild(muteButton.release(), exceptionState);
141 if (exceptionState.hadException())
144 RefPtrWillBeRawPtr<MediaControlVolumeSliderElement> slider = MediaControlVolumeSliderElement::create(*this);
145 m_volumeSlider = slider.get();
146 panel->appendChild(slider.release(), exceptionState);
147 if (exceptionState.hadException())
150 RefPtrWillBeRawPtr<MediaControlToggleClosedCaptionsButtonElement> toggleClosedCaptionsButton = MediaControlToggleClosedCaptionsButtonElement::create(*this);
151 m_toggleClosedCaptionsButton = toggleClosedCaptionsButton.get();
152 panel->appendChild(toggleClosedCaptionsButton.release(), exceptionState);
153 if (exceptionState.hadException())
156 RefPtrWillBeRawPtr<MediaControlCastButtonElement> castButton = MediaControlCastButtonElement::create(*this, false);
157 m_castButton = castButton.get();
158 panel->appendChild(castButton.release(), exceptionState);
159 if (exceptionState.hadException())
162 RefPtrWillBeRawPtr<MediaControlFullscreenButtonElement> fullscreenButton = MediaControlFullscreenButtonElement::create(*this);
163 m_fullScreenButton = fullscreenButton.get();
164 panel->appendChild(fullscreenButton.release(), exceptionState);
165 if (exceptionState.hadException())
168 m_panel = panel.get();
169 enclosure->appendChild(panel.release(), exceptionState);
170 if (exceptionState.hadException())
173 m_enclosure = enclosure.get();
174 appendChild(enclosure.release(), exceptionState);
175 if (exceptionState.hadException())
181 void MediaControls::reset()
183 double duration = mediaElement().duration();
184 m_durationDisplay->setInnerText(RenderTheme::theme().formatMediaControlsTime(duration), ASSERT_NO_EXCEPTION);
185 m_durationDisplay->setCurrentValue(duration);
189 updateCurrentTimeDisplay();
191 m_timeline->setDuration(duration);
192 m_timeline->setPosition(mediaElement().currentTime());
194 if (!mediaElement().hasAudio())
195 m_volumeSlider->hide();
197 m_volumeSlider->show();
200 refreshClosedCaptionsButtonVisibility();
202 if (mediaElement().hasVideo() && fullscreenIsSupported(document()))
203 m_fullScreenButton->show();
205 m_fullScreenButton->hide();
207 refreshCastButtonVisibility();
211 void MediaControls::show()
214 m_panel->setIsDisplayed(true);
216 if (m_overlayPlayButton)
217 m_overlayPlayButton->updateDisplayType();
220 void MediaControls::mediaElementFocused()
223 resetHideMediaControlsTimer();
226 void MediaControls::hide()
228 m_panel->setIsDisplayed(false);
230 if (m_overlayPlayButton)
231 m_overlayPlayButton->hide();
234 void MediaControls::makeOpaque()
236 m_panel->makeOpaque();
239 void MediaControls::makeTransparent()
241 m_panel->makeTransparent();
244 bool MediaControls::shouldHideMediaControls(unsigned behaviorFlags) const
246 // Never hide for a media element without visual representation.
247 if (!mediaElement().hasVideo())
249 // Don't hide if the mouse is over the controls.
250 const bool ignoreControlsHover = behaviorFlags & IgnoreControlsHover;
251 if (!ignoreControlsHover && m_panel->hovered())
253 // Don't hide if the mouse is over the video area.
254 const bool ignoreVideoHover = behaviorFlags & IgnoreVideoHover;
255 if (!ignoreVideoHover && m_isMouseOverControls)
257 // Don't hide if focus is on the HTMLMediaElement or within the
258 // controls/shadow tree. (Perform the checks separately to avoid going
259 // through all the potential ancestor hosts for the focused element.)
260 const bool ignoreFocus = behaviorFlags & IgnoreFocus;
261 if (!ignoreFocus && (mediaElement().focused() || contains(document().focusedElement())))
266 void MediaControls::playbackStarted()
268 m_currentTimeDisplay->show();
269 m_durationDisplay->hide();
272 m_timeline->setPosition(mediaElement().currentTime());
273 updateCurrentTimeDisplay();
275 startHideMediaControlsTimer();
278 void MediaControls::playbackProgressed()
280 m_timeline->setPosition(mediaElement().currentTime());
281 updateCurrentTimeDisplay();
283 if (shouldHideMediaControls())
287 void MediaControls::playbackStopped()
290 m_timeline->setPosition(mediaElement().currentTime());
291 updateCurrentTimeDisplay();
294 stopHideMediaControlsTimer();
297 void MediaControls::updatePlayState()
299 if (m_isPausedForScrubbing)
302 if (m_overlayPlayButton)
303 m_overlayPlayButton->updateDisplayType();
304 m_playButton->updateDisplayType();
307 void MediaControls::beginScrubbing()
309 if (!mediaElement().togglePlayStateWillPlay()) {
310 m_isPausedForScrubbing = true;
311 mediaElement().togglePlayState();
315 void MediaControls::endScrubbing()
317 if (m_isPausedForScrubbing) {
318 m_isPausedForScrubbing = false;
319 if (mediaElement().togglePlayStateWillPlay())
320 mediaElement().togglePlayState();
324 void MediaControls::updateCurrentTimeDisplay()
326 double now = mediaElement().currentTime();
327 double duration = mediaElement().duration();
329 // After seek, hide duration display and show current time.
331 m_currentTimeDisplay->show();
332 m_durationDisplay->hide();
335 // Allow the theme to format the time.
336 m_currentTimeDisplay->setInnerText(RenderTheme::theme().formatMediaControlsCurrentTime(now, duration), IGNORE_EXCEPTION);
337 m_currentTimeDisplay->setCurrentValue(now);
340 void MediaControls::updateVolume()
342 m_muteButton->updateDisplayType();
343 if (m_muteButton->renderer())
344 m_muteButton->renderer()->setShouldDoFullPaintInvalidation(true);
346 if (mediaElement().muted())
347 m_volumeSlider->setVolume(0);
349 m_volumeSlider->setVolume(mediaElement().volume());
352 void MediaControls::changedClosedCaptionsVisibility()
354 m_toggleClosedCaptionsButton->updateDisplayType();
357 void MediaControls::refreshClosedCaptionsButtonVisibility()
359 if (mediaElement().hasClosedCaptions())
360 m_toggleClosedCaptionsButton->show();
362 m_toggleClosedCaptionsButton->hide();
365 void MediaControls::textTracksChanged()
367 refreshClosedCaptionsButtonVisibility();
370 void MediaControls::refreshCastButtonVisibility()
372 if (mediaElement().hasRemoteRoutes()) {
373 // The reason for the autoplay test is that some pages (e.g. vimeo.com) have an autoplay background video, which
374 // doesn't autoplay on Chrome for Android (we prevent it) so starts paused. In such cases we don't want to automatically
375 // show the cast button, since it looks strange and is unlikely to correspond with anything the user wants to do.
376 // If a user does want to cast a paused autoplay video then they can still do so by touching or clicking on the
377 // video, which will cause the cast button to appear.
378 if (!mediaElement().shouldShowControls() && !mediaElement().autoplay() && mediaElement().paused()) {
379 showOverlayCastButton();
380 } else if (mediaElement().shouldShowControls()) {
381 m_overlayCastButton->hide();
382 m_castButton->show();
385 m_castButton->hide();
386 m_overlayCastButton->hide();
390 void MediaControls::showOverlayCastButton()
392 m_overlayCastButton->show();
393 resetHideMediaControlsTimer();
396 void MediaControls::enteredFullscreen()
398 m_fullScreenButton->setIsFullscreen(true);
399 stopHideMediaControlsTimer();
400 startHideMediaControlsTimer();
403 void MediaControls::exitedFullscreen()
405 m_fullScreenButton->setIsFullscreen(false);
406 stopHideMediaControlsTimer();
407 startHideMediaControlsTimer();
410 void MediaControls::startedCasting()
412 m_castButton->setIsPlayingRemotely(true);
413 m_overlayCastButton->setIsPlayingRemotely(true);
416 void MediaControls::stoppedCasting()
418 m_castButton->setIsPlayingRemotely(false);
419 m_overlayCastButton->setIsPlayingRemotely(false);
422 void MediaControls::defaultEventHandler(Event* event)
424 HTMLDivElement::defaultEventHandler(event);
425 m_wasLastEventTouch = event->isTouchEvent() || event->isGestureEvent()
426 || (event->isMouseEvent() && toMouseEvent(event)->fromTouch());
428 if (event->type() == EventTypeNames::mouseover) {
429 if (!containsRelatedTarget(event)) {
430 m_isMouseOverControls = true;
431 if (!mediaElement().togglePlayStateWillPlay()) {
433 if (shouldHideMediaControls())
434 startHideMediaControlsTimer();
440 if (event->type() == EventTypeNames::mouseout) {
441 if (!containsRelatedTarget(event)) {
442 m_isMouseOverControls = false;
443 stopHideMediaControlsTimer();
448 if (event->type() == EventTypeNames::mousemove) {
449 // When we get a mouse move, show the media controls, and start a timer
450 // that will hide the media controls after a 3 seconds without a mouse move.
452 if (shouldHideMediaControls(IgnoreVideoHover))
453 startHideMediaControlsTimer();
458 void MediaControls::hideMediaControlsTimerFired(Timer<MediaControls>*)
460 if (mediaElement().togglePlayStateWillPlay())
463 unsigned behaviorFlags = IgnoreFocus | IgnoreVideoHover;
464 if (m_wasLastEventTouch) {
465 behaviorFlags |= IgnoreControlsHover;
467 if (!shouldHideMediaControls(behaviorFlags))
471 m_overlayCastButton->hide();
474 void MediaControls::startHideMediaControlsTimer()
476 m_hideMediaControlsTimer.startOneShot(timeWithoutMouseMovementBeforeHidingMediaControls, FROM_HERE);
479 void MediaControls::stopHideMediaControlsTimer()
481 m_hideMediaControlsTimer.stop();
484 void MediaControls::resetHideMediaControlsTimer()
486 stopHideMediaControlsTimer();
487 if (!mediaElement().paused())
488 startHideMediaControlsTimer();
492 const AtomicString& MediaControls::shadowPseudoId() const
494 DEFINE_STATIC_LOCAL(AtomicString, id, ("-webkit-media-controls", AtomicString::ConstructFromLiteral));
498 bool MediaControls::containsRelatedTarget(Event* event)
500 if (!event->isMouseEvent())
502 EventTarget* relatedTarget = toMouseEvent(event)->relatedTarget();
505 return contains(relatedTarget->toNode());
508 void MediaControls::createTextTrackDisplay()
510 if (m_textDisplayContainer)
513 RefPtrWillBeRawPtr<MediaControlTextTrackContainerElement> textDisplayContainer = MediaControlTextTrackContainerElement::create(*this);
514 m_textDisplayContainer = textDisplayContainer.get();
516 // Insert it before (behind) all other control elements.
517 if (m_overlayPlayButton)
518 m_overlayEnclosure->insertBefore(textDisplayContainer.release(), m_overlayPlayButton);
520 m_overlayEnclosure->insertBefore(textDisplayContainer.release(), m_overlayCastButton);
523 void MediaControls::showTextTrackDisplay()
525 if (!m_textDisplayContainer)
526 createTextTrackDisplay();
527 m_textDisplayContainer->show();
530 void MediaControls::hideTextTrackDisplay()
532 if (!m_textDisplayContainer)
533 createTextTrackDisplay();
534 m_textDisplayContainer->hide();
537 void MediaControls::updateTextTrackDisplay()
539 if (!m_textDisplayContainer)
540 createTextTrackDisplay();
542 m_textDisplayContainer->updateDisplay();
545 void MediaControls::trace(Visitor* visitor)
547 visitor->trace(m_mediaElement);
548 visitor->trace(m_panel);
549 visitor->trace(m_textDisplayContainer);
550 visitor->trace(m_overlayPlayButton);
551 visitor->trace(m_overlayEnclosure);
552 visitor->trace(m_playButton);
553 visitor->trace(m_currentTimeDisplay);
554 visitor->trace(m_timeline);
555 visitor->trace(m_muteButton);
556 visitor->trace(m_volumeSlider);
557 visitor->trace(m_toggleClosedCaptionsButton);
558 visitor->trace(m_fullScreenButton);
559 visitor->trace(m_durationDisplay);
560 visitor->trace(m_enclosure);
561 visitor->trace(m_castButton);
562 visitor->trace(m_overlayCastButton);
563 HTMLDivElement::trace(visitor);