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
fullscreenIsSupported(const Document & document)44 static bool fullscreenIsSupported(const Document& document)
45 {
46 return !document.settings() || document.settings()->fullscreenSupported();
47 }
48
MediaControls(HTMLMediaElement & mediaElement)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
create(HTMLMediaElement & mediaElement)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
initializeControls()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
reset()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
show()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
mediaElementFocused()221 void MediaControls::mediaElementFocused()
222 {
223 show();
224 resetHideMediaControlsTimer();
225 }
226
hide()227 void MediaControls::hide()
228 {
229 m_panel->setIsDisplayed(false);
230 m_panel->hide();
231 if (m_overlayPlayButton)
232 m_overlayPlayButton->hide();
233 }
234
makeOpaque()235 void MediaControls::makeOpaque()
236 {
237 m_panel->makeOpaque();
238 }
239
makeTransparent()240 void MediaControls::makeTransparent()
241 {
242 m_panel->makeTransparent();
243 }
244
shouldHideMediaControls(unsigned behaviorFlags) const245 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
playbackStarted()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
playbackProgressed()279 void MediaControls::playbackProgressed()
280 {
281 m_timeline->setPosition(mediaElement().currentTime());
282 updateCurrentTimeDisplay();
283
284 if (shouldHideMediaControls())
285 makeTransparent();
286 }
287
playbackStopped()288 void MediaControls::playbackStopped()
289 {
290 updatePlayState();
291 m_timeline->setPosition(mediaElement().currentTime());
292 updateCurrentTimeDisplay();
293 makeOpaque();
294
295 stopHideMediaControlsTimer();
296 }
297
updatePlayState()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
beginScrubbing()308 void MediaControls::beginScrubbing()
309 {
310 if (!mediaElement().togglePlayStateWillPlay()) {
311 m_isPausedForScrubbing = true;
312 mediaElement().togglePlayState();
313 }
314 }
315
endScrubbing()316 void MediaControls::endScrubbing()
317 {
318 if (m_isPausedForScrubbing) {
319 m_isPausedForScrubbing = false;
320 if (mediaElement().togglePlayStateWillPlay())
321 mediaElement().togglePlayState();
322 }
323 }
324
updateCurrentTimeDisplay()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
updateVolume()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
changedClosedCaptionsVisibility()353 void MediaControls::changedClosedCaptionsVisibility()
354 {
355 m_toggleClosedCaptionsButton->updateDisplayType();
356 }
357
refreshClosedCaptionsButtonVisibility()358 void MediaControls::refreshClosedCaptionsButtonVisibility()
359 {
360 if (mediaElement().hasClosedCaptions())
361 m_toggleClosedCaptionsButton->show();
362 else
363 m_toggleClosedCaptionsButton->hide();
364 }
365
textTracksChanged()366 void MediaControls::textTracksChanged()
367 {
368 refreshClosedCaptionsButtonVisibility();
369 }
370
refreshCastButtonVisibility()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
showOverlayCastButton()396 void MediaControls::showOverlayCastButton()
397 {
398 m_overlayCastButton->show();
399 resetHideMediaControlsTimer();
400 }
401
enteredFullscreen()402 void MediaControls::enteredFullscreen()
403 {
404 m_fullScreenButton->setIsFullscreen(true);
405 stopHideMediaControlsTimer();
406 startHideMediaControlsTimer();
407 }
408
exitedFullscreen()409 void MediaControls::exitedFullscreen()
410 {
411 m_fullScreenButton->setIsFullscreen(false);
412 stopHideMediaControlsTimer();
413 startHideMediaControlsTimer();
414 }
415
startedCasting()416 void MediaControls::startedCasting()
417 {
418 m_castButton->setIsPlayingRemotely(true);
419 m_overlayCastButton->setIsPlayingRemotely(true);
420 }
421
stoppedCasting()422 void MediaControls::stoppedCasting()
423 {
424 m_castButton->setIsPlayingRemotely(false);
425 m_overlayCastButton->setIsPlayingRemotely(false);
426 }
427
defaultEventHandler(Event * event)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
hideMediaControlsTimerFired(Timer<MediaControls> *)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
startHideMediaControlsTimer()481 void MediaControls::startHideMediaControlsTimer()
482 {
483 m_hideMediaControlsTimer.startOneShot(timeWithoutMouseMovementBeforeHidingMediaControls, FROM_HERE);
484 }
485
stopHideMediaControlsTimer()486 void MediaControls::stopHideMediaControlsTimer()
487 {
488 m_hideMediaControlsTimer.stop();
489 }
490
resetHideMediaControlsTimer()491 void MediaControls::resetHideMediaControlsTimer()
492 {
493 stopHideMediaControlsTimer();
494 if (!mediaElement().paused())
495 startHideMediaControlsTimer();
496 }
497
498
shadowPseudoId() const499 const AtomicString& MediaControls::shadowPseudoId() const
500 {
501 DEFINE_STATIC_LOCAL(AtomicString, id, ("-webkit-media-controls", AtomicString::ConstructFromLiteral));
502 return id;
503 }
504
containsRelatedTarget(Event * event)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
createTextTrackDisplay()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
showTextTrackDisplay()530 void MediaControls::showTextTrackDisplay()
531 {
532 if (!m_textDisplayContainer)
533 createTextTrackDisplay();
534 m_textDisplayContainer->show();
535 }
536
hideTextTrackDisplay()537 void MediaControls::hideTextTrackDisplay()
538 {
539 if (!m_textDisplayContainer)
540 createTextTrackDisplay();
541 m_textDisplayContainer->hide();
542 }
543
updateTextTrackDisplay()544 void MediaControls::updateTextTrackDisplay()
545 {
546 if (!m_textDisplayContainer)
547 createTextTrackDisplay();
548
549 m_textDisplayContainer->updateDisplay();
550 }
551
trace(Visitor * visitor)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