OLD | NEW |
| (Empty) |
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 "core/html/shadow/MediaControls.h" | |
28 | |
29 #include "bindings/core/v8/ExceptionState.h" | |
30 #include "core/dom/ClientRect.h" | |
31 #include "core/dom/Fullscreen.h" | |
32 #include "core/dom/ResizeObserver.h" | |
33 #include "core/dom/ResizeObserverCallback.h" | |
34 #include "core/dom/ResizeObserverEntry.h" | |
35 #include "core/dom/TaskRunnerHelper.h" | |
36 #include "core/events/MouseEvent.h" | |
37 #include "core/frame/Settings.h" | |
38 #include "core/frame/UseCounter.h" | |
39 #include "core/html/HTMLMediaElement.h" | |
40 #include "core/html/HTMLVideoElement.h" | |
41 #include "core/html/media/HTMLMediaElementControlsList.h" | |
42 #include "core/html/shadow/MediaControlsMediaEventListener.h" | |
43 #include "core/html/shadow/MediaControlsOrientationLockDelegate.h" | |
44 #include "core/html/shadow/MediaControlsWindowEventListener.h" | |
45 #include "core/html/track/TextTrackContainer.h" | |
46 #include "core/html/track/TextTrackList.h" | |
47 #include "core/layout/LayoutObject.h" | |
48 #include "core/layout/LayoutTheme.h" | |
49 #include "platform/EventDispatchForbiddenScope.h" | |
50 | |
51 namespace blink { | |
52 | |
53 namespace { | |
54 | |
55 // TODO(steimel): should have better solution than hard-coding pixel values. | |
56 // Defined in core/css/mediaControls.css, core/css/mediaControlsAndroid.css, | |
57 // and core/paint/MediaControlsPainter.cpp. | |
58 constexpr int kOverlayPlayButtonWidth = 48; | |
59 constexpr int kOverlayPlayButtonHeight = 48; | |
60 constexpr int kOverlayBottomMargin = 10; | |
61 constexpr int kAndroidMediaPanelHeight = 48; | |
62 | |
63 constexpr int kMinWidthForOverlayPlayButton = kOverlayPlayButtonWidth; | |
64 constexpr int kMinHeightForOverlayPlayButton = kOverlayPlayButtonHeight + | |
65 kAndroidMediaPanelHeight + | |
66 (2 * kOverlayBottomMargin); | |
67 | |
68 } // anonymous namespace | |
69 | |
70 // If you change this value, then also update the corresponding value in | |
71 // LayoutTests/media/media-controls.js. | |
72 static const double timeWithoutMouseMovementBeforeHidingMediaControls = 3; | |
73 | |
74 static bool shouldShowFullscreenButton(const HTMLMediaElement& mediaElement) { | |
75 // Unconditionally allow the user to exit fullscreen if we are in it | |
76 // now. Especially on android, when we might not yet know if | |
77 // fullscreen is supported, we sometimes guess incorrectly and show | |
78 // the button earlier, and we don't want to remove it here if the | |
79 // user chose to enter fullscreen. crbug.com/500732 . | |
80 if (mediaElement.isFullscreen()) | |
81 return true; | |
82 | |
83 if (!mediaElement.isHTMLVideoElement()) | |
84 return false; | |
85 | |
86 if (!mediaElement.hasVideo()) | |
87 return false; | |
88 | |
89 if (!Fullscreen::fullscreenEnabled(mediaElement.document())) | |
90 return false; | |
91 | |
92 if (mediaElement.controlsListInternal()->shouldHideFullscreen()) { | |
93 UseCounter::count(mediaElement.document(), | |
94 UseCounter::HTMLMediaElementControlsListNoFullscreen); | |
95 return false; | |
96 } | |
97 | |
98 return true; | |
99 } | |
100 | |
101 static bool shouldShowCastButton(HTMLMediaElement& mediaElement) { | |
102 if (mediaElement.fastHasAttribute(HTMLNames::disableremoteplaybackAttr)) | |
103 return false; | |
104 | |
105 // Explicitly do not show cast button when the mediaControlsEnabled setting is | |
106 // false to make sure the overlay does not appear. | |
107 Document& document = mediaElement.document(); | |
108 if (document.settings() && !document.settings()->getMediaControlsEnabled()) | |
109 return false; | |
110 | |
111 // The page disabled the button via the attribute. | |
112 if (mediaElement.controlsListInternal()->shouldHideRemotePlayback()) { | |
113 UseCounter::count(mediaElement.document(), | |
114 UseCounter::HTMLMediaElementControlsListNoRemotePlayback); | |
115 return false; | |
116 } | |
117 | |
118 return mediaElement.hasRemoteRoutes(); | |
119 } | |
120 | |
121 static bool preferHiddenVolumeControls(const Document& document) { | |
122 return !document.settings() || | |
123 document.settings()->getPreferHiddenVolumeControls(); | |
124 } | |
125 | |
126 class MediaControls::BatchedControlUpdate { | |
127 WTF_MAKE_NONCOPYABLE(BatchedControlUpdate); | |
128 STACK_ALLOCATED(); | |
129 | |
130 public: | |
131 explicit BatchedControlUpdate(MediaControls* controls) | |
132 : m_controls(controls) { | |
133 DCHECK(isMainThread()); | |
134 DCHECK_GE(s_batchDepth, 0); | |
135 ++s_batchDepth; | |
136 } | |
137 ~BatchedControlUpdate() { | |
138 DCHECK(isMainThread()); | |
139 DCHECK_GT(s_batchDepth, 0); | |
140 if (!(--s_batchDepth)) | |
141 m_controls->computeWhichControlsFit(); | |
142 } | |
143 | |
144 private: | |
145 Member<MediaControls> m_controls; | |
146 static int s_batchDepth; | |
147 }; | |
148 | |
149 // Count of number open batches for controls visibility. | |
150 int MediaControls::BatchedControlUpdate::s_batchDepth = 0; | |
151 | |
152 class MediaControls::MediaControlsResizeObserverCallback final | |
153 : public ResizeObserverCallback { | |
154 public: | |
155 explicit MediaControlsResizeObserverCallback(MediaControls* controls) | |
156 : m_controls(controls) { | |
157 DCHECK(controls); | |
158 } | |
159 ~MediaControlsResizeObserverCallback() override = default; | |
160 | |
161 void handleEvent(const HeapVector<Member<ResizeObserverEntry>>& entries, | |
162 ResizeObserver* observer) override { | |
163 DCHECK_EQ(1u, entries.size()); | |
164 DCHECK_EQ(entries[0]->target(), m_controls->m_mediaElement); | |
165 m_controls->notifyElementSizeChanged(entries[0]->contentRect()); | |
166 } | |
167 | |
168 DEFINE_INLINE_TRACE() { | |
169 visitor->trace(m_controls); | |
170 ResizeObserverCallback::trace(visitor); | |
171 } | |
172 | |
173 private: | |
174 Member<MediaControls> m_controls; | |
175 }; | |
176 | |
177 MediaControls::MediaControls(HTMLMediaElement& mediaElement) | |
178 : HTMLDivElement(mediaElement.document()), | |
179 m_mediaElement(&mediaElement), | |
180 m_overlayEnclosure(nullptr), | |
181 m_overlayPlayButton(nullptr), | |
182 m_overlayCastButton(nullptr), | |
183 m_enclosure(nullptr), | |
184 m_panel(nullptr), | |
185 m_playButton(nullptr), | |
186 m_timeline(nullptr), | |
187 m_currentTimeDisplay(nullptr), | |
188 m_durationDisplay(nullptr), | |
189 m_muteButton(nullptr), | |
190 m_volumeSlider(nullptr), | |
191 m_toggleClosedCaptionsButton(nullptr), | |
192 m_textTrackList(nullptr), | |
193 m_overflowList(nullptr), | |
194 m_castButton(nullptr), | |
195 m_fullscreenButton(nullptr), | |
196 m_downloadButton(nullptr), | |
197 m_mediaEventListener(new MediaControlsMediaEventListener(this)), | |
198 m_windowEventListener(MediaControlsWindowEventListener::create( | |
199 this, | |
200 WTF::bind(&MediaControls::hideAllMenus, wrapWeakPersistent(this)))), | |
201 m_orientationLockDelegate(nullptr), | |
202 m_hideMediaControlsTimer(TaskRunnerHelper::get(TaskType::UnspecedTimer, | |
203 &mediaElement.document()), | |
204 this, | |
205 &MediaControls::hideMediaControlsTimerFired), | |
206 m_hideTimerBehaviorFlags(IgnoreNone), | |
207 m_isMouseOverControls(false), | |
208 m_isPausedForScrubbing(false), | |
209 m_resizeObserver(ResizeObserver::create( | |
210 mediaElement.document(), | |
211 new MediaControlsResizeObserverCallback(this))), | |
212 m_elementSizeChangedTimer(TaskRunnerHelper::get(TaskType::UnspecedTimer, | |
213 &mediaElement.document()), | |
214 this, | |
215 &MediaControls::elementSizeChangedTimerFired), | |
216 m_keepShowingUntilTimerFires(false) { | |
217 m_resizeObserver->observe(m_mediaElement); | |
218 } | |
219 | |
220 MediaControls* MediaControls::create(HTMLMediaElement& mediaElement, | |
221 ShadowRoot& shadowRoot) { | |
222 MediaControls* controls = new MediaControls(mediaElement); | |
223 controls->setShadowPseudoId(AtomicString("-webkit-media-controls")); | |
224 controls->initializeControls(); | |
225 controls->reset(); | |
226 | |
227 // Initialize the orientation lock when going fullscreen feature. | |
228 if (RuntimeEnabledFeatures::videoFullscreenOrientationLockEnabled() && | |
229 mediaElement.isHTMLVideoElement()) { | |
230 controls->m_orientationLockDelegate = | |
231 new MediaControlsOrientationLockDelegate( | |
232 toHTMLVideoElement(mediaElement)); | |
233 } | |
234 | |
235 shadowRoot.appendChild(controls); | |
236 return controls; | |
237 } | |
238 | |
239 // The media controls DOM structure looks like: | |
240 // | |
241 // MediaControls | |
242 // (-webkit-media-controls) | |
243 // +-MediaControlOverlayEnclosureElement | |
244 // | (-webkit-media-controls-overlay-enclosure) | |
245 // | +-MediaControlOverlayPlayButtonElement | |
246 // | | (-webkit-media-controls-overlay-play-button) | |
247 // | | {if mediaControlsOverlayPlayButtonEnabled} | |
248 // | \-MediaControlCastButtonElement | |
249 // | (-internal-media-controls-overlay-cast-button) | |
250 // \-MediaControlPanelEnclosureElement | |
251 // | (-webkit-media-controls-enclosure) | |
252 // \-MediaControlPanelElement | |
253 // | (-webkit-media-controls-panel) | |
254 // +-MediaControlPlayButtonElement | |
255 // | (-webkit-media-controls-play-button) | |
256 // +-MediaControlCurrentTimeDisplayElement | |
257 // | (-webkit-media-controls-current-time-display) | |
258 // +-MediaControlTimeRemainingDisplayElement | |
259 // | (-webkit-media-controls-time-remaining-display) | |
260 // +-MediaControlTimelineElement | |
261 // | (-webkit-media-controls-timeline) | |
262 // +-MediaControlMuteButtonElement | |
263 // | (-webkit-media-controls-mute-button) | |
264 // +-MediaControlVolumeSliderElement | |
265 // | (-webkit-media-controls-volume-slider) | |
266 // +-MediaControlFullscreenButtonElement | |
267 // | (-webkit-media-controls-fullscreen-button) | |
268 // +-MediaControlDownloadButtonElement | |
269 // | (-internal-media-controls-download-button) | |
270 // +-MediaControlToggleClosedCaptionsButtonElement | |
271 // | (-webkit-media-controls-toggle-closed-captions-button) | |
272 // \-MediaControlCastButtonElement | |
273 // (-internal-media-controls-cast-button) | |
274 // +-MediaControlTextTrackListElement | |
275 // | (-internal-media-controls-text-track-list) | |
276 // | {for each renderable text track} | |
277 // \-MediaControlTextTrackListItem | |
278 // | (-internal-media-controls-text-track-list-item) | |
279 // +-MediaControlTextTrackListItemInput | |
280 // | (-internal-media-controls-text-track-list-item-input) | |
281 // +-MediaControlTextTrackListItemCaptions | |
282 // | (-internal-media-controls-text-track-list-kind-captions) | |
283 // +-MediaControlTextTrackListItemSubtitles | |
284 // (-internal-media-controls-text-track-list-kind-subtitles) | |
285 void MediaControls::initializeControls() { | |
286 MediaControlOverlayEnclosureElement* overlayEnclosure = | |
287 MediaControlOverlayEnclosureElement::create(*this); | |
288 | |
289 if (RuntimeEnabledFeatures::mediaControlsOverlayPlayButtonEnabled()) { | |
290 MediaControlOverlayPlayButtonElement* overlayPlayButton = | |
291 MediaControlOverlayPlayButtonElement::create(*this); | |
292 m_overlayPlayButton = overlayPlayButton; | |
293 overlayEnclosure->appendChild(overlayPlayButton); | |
294 } | |
295 | |
296 MediaControlCastButtonElement* overlayCastButton = | |
297 MediaControlCastButtonElement::create(*this, true); | |
298 m_overlayCastButton = overlayCastButton; | |
299 overlayEnclosure->appendChild(overlayCastButton); | |
300 | |
301 m_overlayEnclosure = overlayEnclosure; | |
302 appendChild(overlayEnclosure); | |
303 | |
304 // Create an enclosing element for the panel so we can visually offset the | |
305 // controls correctly. | |
306 MediaControlPanelEnclosureElement* enclosure = | |
307 MediaControlPanelEnclosureElement::create(*this); | |
308 | |
309 MediaControlPanelElement* panel = MediaControlPanelElement::create(*this); | |
310 | |
311 MediaControlPlayButtonElement* playButton = | |
312 MediaControlPlayButtonElement::create(*this); | |
313 m_playButton = playButton; | |
314 panel->appendChild(playButton); | |
315 | |
316 MediaControlCurrentTimeDisplayElement* currentTimeDisplay = | |
317 MediaControlCurrentTimeDisplayElement::create(*this); | |
318 m_currentTimeDisplay = currentTimeDisplay; | |
319 m_currentTimeDisplay->setIsWanted(true); | |
320 panel->appendChild(currentTimeDisplay); | |
321 | |
322 MediaControlTimeRemainingDisplayElement* durationDisplay = | |
323 MediaControlTimeRemainingDisplayElement::create(*this); | |
324 m_durationDisplay = durationDisplay; | |
325 panel->appendChild(durationDisplay); | |
326 | |
327 MediaControlTimelineElement* timeline = | |
328 MediaControlTimelineElement::create(*this); | |
329 m_timeline = timeline; | |
330 panel->appendChild(timeline); | |
331 | |
332 MediaControlMuteButtonElement* muteButton = | |
333 MediaControlMuteButtonElement::create(*this); | |
334 m_muteButton = muteButton; | |
335 panel->appendChild(muteButton); | |
336 | |
337 MediaControlVolumeSliderElement* slider = | |
338 MediaControlVolumeSliderElement::create(*this); | |
339 m_volumeSlider = slider; | |
340 panel->appendChild(slider); | |
341 if (preferHiddenVolumeControls(document())) | |
342 m_volumeSlider->setIsWanted(false); | |
343 | |
344 MediaControlFullscreenButtonElement* fullscreenButton = | |
345 MediaControlFullscreenButtonElement::create(*this); | |
346 m_fullscreenButton = fullscreenButton; | |
347 panel->appendChild(fullscreenButton); | |
348 | |
349 MediaControlDownloadButtonElement* downloadButton = | |
350 MediaControlDownloadButtonElement::create(*this); | |
351 m_downloadButton = downloadButton; | |
352 panel->appendChild(downloadButton); | |
353 | |
354 MediaControlCastButtonElement* castButton = | |
355 MediaControlCastButtonElement::create(*this, false); | |
356 m_castButton = castButton; | |
357 panel->appendChild(castButton); | |
358 | |
359 MediaControlToggleClosedCaptionsButtonElement* toggleClosedCaptionsButton = | |
360 MediaControlToggleClosedCaptionsButtonElement::create(*this); | |
361 m_toggleClosedCaptionsButton = toggleClosedCaptionsButton; | |
362 panel->appendChild(toggleClosedCaptionsButton); | |
363 | |
364 m_panel = panel; | |
365 enclosure->appendChild(panel); | |
366 | |
367 m_enclosure = enclosure; | |
368 appendChild(enclosure); | |
369 | |
370 MediaControlTextTrackListElement* textTrackList = | |
371 MediaControlTextTrackListElement::create(*this); | |
372 m_textTrackList = textTrackList; | |
373 appendChild(textTrackList); | |
374 | |
375 MediaControlOverflowMenuButtonElement* overflowMenu = | |
376 MediaControlOverflowMenuButtonElement::create(*this); | |
377 m_overflowMenu = overflowMenu; | |
378 panel->appendChild(overflowMenu); | |
379 | |
380 MediaControlOverflowMenuListElement* overflowList = | |
381 MediaControlOverflowMenuListElement::create(*this); | |
382 m_overflowList = overflowList; | |
383 appendChild(overflowList); | |
384 | |
385 // The order in which we append elements to the overflow list is significant | |
386 // because it determines how the elements show up in the overflow menu | |
387 // relative to each other. The first item appended appears at the top of the | |
388 // overflow menu. | |
389 m_overflowList->appendChild(m_playButton->createOverflowElement( | |
390 *this, MediaControlPlayButtonElement::create(*this))); | |
391 m_overflowList->appendChild(m_fullscreenButton->createOverflowElement( | |
392 *this, MediaControlFullscreenButtonElement::create(*this))); | |
393 m_overflowList->appendChild(m_downloadButton->createOverflowElement( | |
394 *this, MediaControlDownloadButtonElement::create(*this))); | |
395 m_overflowList->appendChild(m_muteButton->createOverflowElement( | |
396 *this, MediaControlMuteButtonElement::create(*this))); | |
397 m_overflowList->appendChild(m_castButton->createOverflowElement( | |
398 *this, MediaControlCastButtonElement::create(*this, false))); | |
399 m_overflowList->appendChild( | |
400 m_toggleClosedCaptionsButton->createOverflowElement( | |
401 *this, MediaControlToggleClosedCaptionsButtonElement::create(*this))); | |
402 } | |
403 | |
404 Node::InsertionNotificationRequest MediaControls::insertedInto( | |
405 ContainerNode* root) { | |
406 if (!mediaElement().isConnected()) | |
407 return HTMLDivElement::insertedInto(root); | |
408 | |
409 // TODO(mlamouri): we should show the controls instead of having | |
410 // HTMLMediaElement do it. | |
411 | |
412 // m_windowEventListener doesn't need to be re-attached as it's only needed | |
413 // when a menu is visible. | |
414 m_mediaEventListener->attach(); | |
415 if (m_orientationLockDelegate) | |
416 m_orientationLockDelegate->attach(); | |
417 | |
418 if (!m_resizeObserver) { | |
419 m_resizeObserver = | |
420 ResizeObserver::create(m_mediaElement->document(), | |
421 new MediaControlsResizeObserverCallback(this)); | |
422 m_resizeObserver->observe(m_mediaElement); | |
423 } | |
424 | |
425 return HTMLDivElement::insertedInto(root); | |
426 } | |
427 | |
428 void MediaControls::removedFrom(ContainerNode*) { | |
429 DCHECK(!mediaElement().isConnected()); | |
430 | |
431 // TODO(mlamouri): we hide show the controls instead of having | |
432 // HTMLMediaElement do it. | |
433 | |
434 m_windowEventListener->stop(); | |
435 m_mediaEventListener->detach(); | |
436 if (m_orientationLockDelegate) | |
437 m_orientationLockDelegate->detach(); | |
438 | |
439 m_resizeObserver.clear(); | |
440 } | |
441 | |
442 void MediaControls::reset() { | |
443 EventDispatchForbiddenScope::AllowUserAgentEvents allowEventsInShadow; | |
444 BatchedControlUpdate batch(this); | |
445 | |
446 const double duration = mediaElement().duration(); | |
447 m_durationDisplay->setTextContent( | |
448 LayoutTheme::theme().formatMediaControlsTime(duration)); | |
449 m_durationDisplay->setCurrentValue(duration); | |
450 | |
451 // Show everything that we might hide. | |
452 // If we don't have a duration, then mark it to be hidden. For the | |
453 // old UI case, want / don't want is the same as show / hide since | |
454 // it is never marked as not fitting. | |
455 m_durationDisplay->setIsWanted(std::isfinite(duration)); | |
456 m_currentTimeDisplay->setIsWanted(true); | |
457 m_timeline->setIsWanted(true); | |
458 | |
459 // If the player has entered an error state, force it into the paused state. | |
460 if (mediaElement().error()) | |
461 mediaElement().pause(); | |
462 | |
463 updatePlayState(); | |
464 | |
465 updateCurrentTimeDisplay(); | |
466 | |
467 m_timeline->setDuration(duration); | |
468 m_timeline->setPosition(mediaElement().currentTime()); | |
469 | |
470 onVolumeChange(); | |
471 onTextTracksAddedOrRemoved(); | |
472 | |
473 onControlsListUpdated(); | |
474 } | |
475 | |
476 void MediaControls::onControlsListUpdated() { | |
477 BatchedControlUpdate batch(this); | |
478 | |
479 m_fullscreenButton->setIsWanted(shouldShowFullscreenButton(mediaElement())); | |
480 | |
481 refreshCastButtonVisibilityWithoutUpdate(); | |
482 | |
483 m_downloadButton->setIsWanted( | |
484 m_downloadButton->shouldDisplayDownloadButton()); | |
485 } | |
486 | |
487 LayoutObject* MediaControls::layoutObjectForTextTrackLayout() { | |
488 return m_panel->layoutObject(); | |
489 } | |
490 | |
491 void MediaControls::show() { | |
492 makeOpaque(); | |
493 m_panel->setIsWanted(true); | |
494 m_panel->setIsDisplayed(true); | |
495 if (m_overlayPlayButton) | |
496 m_overlayPlayButton->updateDisplayType(); | |
497 } | |
498 | |
499 void MediaControls::hide() { | |
500 m_panel->setIsWanted(false); | |
501 m_panel->setIsDisplayed(false); | |
502 if (m_overlayPlayButton) | |
503 m_overlayPlayButton->setIsWanted(false); | |
504 } | |
505 | |
506 bool MediaControls::isVisible() const { | |
507 return m_panel->isOpaque(); | |
508 } | |
509 | |
510 void MediaControls::makeOpaque() { | |
511 m_panel->makeOpaque(); | |
512 } | |
513 | |
514 void MediaControls::makeTransparent() { | |
515 m_panel->makeTransparent(); | |
516 } | |
517 | |
518 bool MediaControls::shouldHideMediaControls(unsigned behaviorFlags) const { | |
519 // Never hide for a media element without visual representation. | |
520 if (!mediaElement().isHTMLVideoElement() || !mediaElement().hasVideo() || | |
521 mediaElement().isPlayingRemotely()) { | |
522 return false; | |
523 } | |
524 | |
525 // Keep the controls visible as long as the timer is running. | |
526 const bool ignoreWaitForTimer = behaviorFlags & IgnoreWaitForTimer; | |
527 if (!ignoreWaitForTimer && m_keepShowingUntilTimerFires) | |
528 return false; | |
529 | |
530 // Don't hide if the mouse is over the controls. | |
531 const bool ignoreControlsHover = behaviorFlags & IgnoreControlsHover; | |
532 if (!ignoreControlsHover && m_panel->isHovered()) | |
533 return false; | |
534 | |
535 // Don't hide if the mouse is over the video area. | |
536 const bool ignoreVideoHover = behaviorFlags & IgnoreVideoHover; | |
537 if (!ignoreVideoHover && m_isMouseOverControls) | |
538 return false; | |
539 | |
540 // Don't hide if focus is on the HTMLMediaElement or within the | |
541 // controls/shadow tree. (Perform the checks separately to avoid going | |
542 // through all the potential ancestor hosts for the focused element.) | |
543 const bool ignoreFocus = behaviorFlags & IgnoreFocus; | |
544 if (!ignoreFocus && | |
545 (mediaElement().isFocused() || contains(document().focusedElement()))) { | |
546 return false; | |
547 } | |
548 | |
549 // Don't hide the media controls when a panel is showing. | |
550 if (m_textTrackList->isWanted() || m_overflowList->isWanted()) | |
551 return false; | |
552 | |
553 return true; | |
554 } | |
555 | |
556 void MediaControls::updatePlayState() { | |
557 if (m_isPausedForScrubbing) | |
558 return; | |
559 | |
560 if (m_overlayPlayButton) | |
561 m_overlayPlayButton->updateDisplayType(); | |
562 m_playButton->updateDisplayType(); | |
563 } | |
564 | |
565 void MediaControls::beginScrubbing() { | |
566 if (!mediaElement().paused()) { | |
567 m_isPausedForScrubbing = true; | |
568 mediaElement().pause(); | |
569 } | |
570 } | |
571 | |
572 void MediaControls::endScrubbing() { | |
573 if (m_isPausedForScrubbing) { | |
574 m_isPausedForScrubbing = false; | |
575 if (mediaElement().paused()) | |
576 mediaElement().play(); | |
577 } | |
578 } | |
579 | |
580 void MediaControls::updateCurrentTimeDisplay() { | |
581 double now = mediaElement().currentTime(); | |
582 double duration = mediaElement().duration(); | |
583 | |
584 // Allow the theme to format the time. | |
585 m_currentTimeDisplay->setInnerText( | |
586 LayoutTheme::theme().formatMediaControlsCurrentTime(now, duration), | |
587 IGNORE_EXCEPTION_FOR_TESTING); | |
588 m_currentTimeDisplay->setCurrentValue(now); | |
589 } | |
590 | |
591 void MediaControls::toggleTextTrackList() { | |
592 if (!mediaElement().hasClosedCaptions()) { | |
593 m_textTrackList->setVisible(false); | |
594 return; | |
595 } | |
596 | |
597 if (!m_textTrackList->isWanted()) | |
598 m_windowEventListener->start(); | |
599 | |
600 m_textTrackList->setVisible(!m_textTrackList->isWanted()); | |
601 } | |
602 | |
603 void MediaControls::showTextTrackAtIndex(unsigned indexToEnable) { | |
604 TextTrackList* trackList = mediaElement().textTracks(); | |
605 if (indexToEnable >= trackList->length()) | |
606 return; | |
607 TextTrack* track = trackList->anonymousIndexedGetter(indexToEnable); | |
608 if (track && track->canBeRendered()) | |
609 track->setMode(TextTrack::showingKeyword()); | |
610 } | |
611 | |
612 void MediaControls::disableShowingTextTracks() { | |
613 TextTrackList* trackList = mediaElement().textTracks(); | |
614 for (unsigned i = 0; i < trackList->length(); ++i) { | |
615 TextTrack* track = trackList->anonymousIndexedGetter(i); | |
616 if (track->mode() == TextTrack::showingKeyword()) | |
617 track->setMode(TextTrack::disabledKeyword()); | |
618 } | |
619 } | |
620 | |
621 void MediaControls::refreshCastButtonVisibility() { | |
622 refreshCastButtonVisibilityWithoutUpdate(); | |
623 BatchedControlUpdate batch(this); | |
624 } | |
625 | |
626 void MediaControls::refreshCastButtonVisibilityWithoutUpdate() { | |
627 if (!shouldShowCastButton(mediaElement())) { | |
628 m_castButton->setIsWanted(false); | |
629 m_overlayCastButton->setIsWanted(false); | |
630 return; | |
631 } | |
632 | |
633 // The reason for the autoplay test is that some pages (e.g. vimeo.com) have | |
634 // an autoplay background video, which doesn't autoplay on Chrome for Android | |
635 // (we prevent it) so starts paused. In such cases we don't want to | |
636 // automatically show the cast button, since it looks strange and is unlikely | |
637 // to correspond with anything the user wants to do. If a user does want to | |
638 // cast a paused autoplay video then they can still do so by touching or | |
639 // clicking on the video, which will cause the cast button to appear. | |
640 if (!mediaElement().shouldShowControls() && !mediaElement().autoplay() && | |
641 mediaElement().paused()) { | |
642 // Note that this is a case where we add the overlay cast button | |
643 // without wanting the panel cast button. We depend on the fact | |
644 // that computeWhichControlsFit() won't change overlay cast button | |
645 // visibility in the case where the cast button isn't wanted. | |
646 // We don't call compute...() here, but it will be called as | |
647 // non-cast changes (e.g., resize) occur. If the panel button | |
648 // is shown, however, compute...() will take control of the | |
649 // overlay cast button if it needs to hide it from the panel. | |
650 m_overlayCastButton->tryShowOverlay(); | |
651 m_castButton->setIsWanted(false); | |
652 } else if (mediaElement().shouldShowControls()) { | |
653 m_overlayCastButton->setIsWanted(false); | |
654 m_castButton->setIsWanted(true); | |
655 } | |
656 } | |
657 | |
658 void MediaControls::showOverlayCastButtonIfNeeded() { | |
659 if (mediaElement().shouldShowControls() || | |
660 !shouldShowCastButton(mediaElement())) | |
661 return; | |
662 | |
663 m_overlayCastButton->tryShowOverlay(); | |
664 resetHideMediaControlsTimer(); | |
665 } | |
666 | |
667 void MediaControls::enterFullscreen() { | |
668 Fullscreen::requestFullscreen(mediaElement()); | |
669 } | |
670 | |
671 void MediaControls::exitFullscreen() { | |
672 Fullscreen::exitFullscreen(document()); | |
673 } | |
674 | |
675 void MediaControls::startedCasting() { | |
676 m_castButton->setIsPlayingRemotely(true); | |
677 m_overlayCastButton->setIsPlayingRemotely(true); | |
678 } | |
679 | |
680 void MediaControls::stoppedCasting() { | |
681 m_castButton->setIsPlayingRemotely(false); | |
682 m_overlayCastButton->setIsPlayingRemotely(false); | |
683 } | |
684 | |
685 void MediaControls::defaultEventHandler(Event* event) { | |
686 HTMLDivElement::defaultEventHandler(event); | |
687 | |
688 // Do not handle events to not interfere with the rest of the page if no | |
689 // controls should be visible. | |
690 if (!mediaElement().shouldShowControls()) | |
691 return; | |
692 | |
693 // Add IgnoreControlsHover to m_hideTimerBehaviorFlags when we see a touch | |
694 // event, to allow the hide-timer to do the right thing when it fires. | |
695 // FIXME: Preferably we would only do this when we're actually handling the | |
696 // event here ourselves. | |
697 bool isTouchEvent = | |
698 event->isTouchEvent() || event->isGestureEvent() || | |
699 (event->isMouseEvent() && toMouseEvent(event)->fromTouch()); | |
700 m_hideTimerBehaviorFlags |= isTouchEvent ? IgnoreControlsHover : IgnoreNone; | |
701 | |
702 // Touch events are treated differently to avoid fake mouse events to trigger | |
703 // random behavior. The expect behaviour for touch is that a tap will show the | |
704 // controls and they will hide when the timer to hide fires. | |
705 if (isTouchEvent) { | |
706 if (event->type() != EventTypeNames::gesturetap) | |
707 return; | |
708 | |
709 if (!containsRelatedTarget(event)) { | |
710 if (!mediaElement().paused()) { | |
711 if (!isVisible()) { | |
712 makeOpaque(); | |
713 // When the panel switches from invisible to visible, we need to mark | |
714 // the event handled to avoid buttons below the tap to be activated. | |
715 event->setDefaultHandled(); | |
716 } | |
717 if (shouldHideMediaControls(IgnoreWaitForTimer)) { | |
718 m_keepShowingUntilTimerFires = true; | |
719 startHideMediaControlsTimer(); | |
720 } | |
721 } | |
722 } | |
723 | |
724 return; | |
725 } | |
726 | |
727 if (event->type() == EventTypeNames::mouseover) { | |
728 if (!containsRelatedTarget(event)) { | |
729 m_isMouseOverControls = true; | |
730 if (!mediaElement().paused()) { | |
731 makeOpaque(); | |
732 if (shouldHideMediaControls()) | |
733 startHideMediaControlsTimer(); | |
734 } | |
735 } | |
736 return; | |
737 } | |
738 | |
739 if (event->type() == EventTypeNames::mouseout) { | |
740 if (!containsRelatedTarget(event)) { | |
741 m_isMouseOverControls = false; | |
742 stopHideMediaControlsTimer(); | |
743 } | |
744 return; | |
745 } | |
746 | |
747 if (event->type() == EventTypeNames::mousemove) { | |
748 // When we get a mouse move, show the media controls, and start a timer | |
749 // that will hide the media controls after a 3 seconds without a mouse move. | |
750 makeOpaque(); | |
751 if (shouldHideMediaControls(IgnoreVideoHover)) | |
752 startHideMediaControlsTimer(); | |
753 return; | |
754 } | |
755 } | |
756 | |
757 void MediaControls::hideMediaControlsTimerFired(TimerBase*) { | |
758 unsigned behaviorFlags = | |
759 m_hideTimerBehaviorFlags | IgnoreFocus | IgnoreVideoHover; | |
760 m_hideTimerBehaviorFlags = IgnoreNone; | |
761 m_keepShowingUntilTimerFires = false; | |
762 | |
763 if (mediaElement().paused()) | |
764 return; | |
765 | |
766 if (!shouldHideMediaControls(behaviorFlags)) | |
767 return; | |
768 | |
769 makeTransparent(); | |
770 m_overlayCastButton->setIsWanted(false); | |
771 } | |
772 | |
773 void MediaControls::startHideMediaControlsTimer() { | |
774 m_hideMediaControlsTimer.startOneShot( | |
775 timeWithoutMouseMovementBeforeHidingMediaControls, BLINK_FROM_HERE); | |
776 } | |
777 | |
778 void MediaControls::stopHideMediaControlsTimer() { | |
779 m_keepShowingUntilTimerFires = false; | |
780 m_hideMediaControlsTimer.stop(); | |
781 } | |
782 | |
783 void MediaControls::resetHideMediaControlsTimer() { | |
784 stopHideMediaControlsTimer(); | |
785 if (!mediaElement().paused()) | |
786 startHideMediaControlsTimer(); | |
787 } | |
788 | |
789 bool MediaControls::containsRelatedTarget(Event* event) { | |
790 if (!event->isMouseEvent()) | |
791 return false; | |
792 EventTarget* relatedTarget = toMouseEvent(event)->relatedTarget(); | |
793 if (!relatedTarget) | |
794 return false; | |
795 return contains(relatedTarget->toNode()); | |
796 } | |
797 | |
798 void MediaControls::onVolumeChange() { | |
799 m_muteButton->updateDisplayType(); | |
800 m_volumeSlider->setVolume(mediaElement().muted() ? 0 | |
801 : mediaElement().volume()); | |
802 | |
803 // Update visibility of volume controls. | |
804 // TODO(mlamouri): it should not be part of the volumechange handling because | |
805 // it is using audio availability as input. | |
806 BatchedControlUpdate batch(this); | |
807 m_volumeSlider->setIsWanted(mediaElement().hasAudio() && | |
808 !preferHiddenVolumeControls(document())); | |
809 m_muteButton->setIsWanted(mediaElement().hasAudio()); | |
810 } | |
811 | |
812 void MediaControls::onFocusIn() { | |
813 if (!mediaElement().shouldShowControls()) | |
814 return; | |
815 | |
816 show(); | |
817 resetHideMediaControlsTimer(); | |
818 } | |
819 | |
820 void MediaControls::onTimeUpdate() { | |
821 m_timeline->setPosition(mediaElement().currentTime()); | |
822 updateCurrentTimeDisplay(); | |
823 | |
824 // 'timeupdate' might be called in a paused state. The controls should not | |
825 // become transparent in that case. | |
826 if (mediaElement().paused()) { | |
827 makeOpaque(); | |
828 return; | |
829 } | |
830 | |
831 if (isVisible() && shouldHideMediaControls()) | |
832 makeTransparent(); | |
833 } | |
834 | |
835 void MediaControls::onDurationChange() { | |
836 const double duration = mediaElement().duration(); | |
837 | |
838 // Update the displayed current time/duration. | |
839 m_durationDisplay->setTextContent( | |
840 LayoutTheme::theme().formatMediaControlsTime(duration)); | |
841 m_durationDisplay->setCurrentValue(duration); | |
842 updateCurrentTimeDisplay(); | |
843 | |
844 // Update the timeline (the UI with the seek marker). | |
845 m_timeline->setDuration(duration); | |
846 } | |
847 | |
848 void MediaControls::onPlay() { | |
849 updatePlayState(); | |
850 m_timeline->setPosition(mediaElement().currentTime()); | |
851 updateCurrentTimeDisplay(); | |
852 | |
853 startHideMediaControlsTimer(); | |
854 } | |
855 | |
856 void MediaControls::onPause() { | |
857 updatePlayState(); | |
858 m_timeline->setPosition(mediaElement().currentTime()); | |
859 updateCurrentTimeDisplay(); | |
860 makeOpaque(); | |
861 | |
862 stopHideMediaControlsTimer(); | |
863 } | |
864 | |
865 void MediaControls::onTextTracksAddedOrRemoved() { | |
866 m_toggleClosedCaptionsButton->setIsWanted(mediaElement().hasClosedCaptions()); | |
867 BatchedControlUpdate batch(this); | |
868 } | |
869 | |
870 void MediaControls::onTextTracksChanged() { | |
871 m_toggleClosedCaptionsButton->updateDisplayType(); | |
872 } | |
873 | |
874 void MediaControls::onError() { | |
875 // TODO(mlamouri): we should only change the aspects of the control that need | |
876 // to be changed. | |
877 reset(); | |
878 } | |
879 | |
880 void MediaControls::onLoadedMetadata() { | |
881 // TODO(mlamouri): we should only change the aspects of the control that need | |
882 // to be changed. | |
883 reset(); | |
884 } | |
885 | |
886 void MediaControls::onEnteredFullscreen() { | |
887 m_fullscreenButton->setIsFullscreen(true); | |
888 stopHideMediaControlsTimer(); | |
889 startHideMediaControlsTimer(); | |
890 } | |
891 | |
892 void MediaControls::onExitedFullscreen() { | |
893 m_fullscreenButton->setIsFullscreen(false); | |
894 stopHideMediaControlsTimer(); | |
895 startHideMediaControlsTimer(); | |
896 } | |
897 | |
898 void MediaControls::notifyElementSizeChanged(ClientRect* newSize) { | |
899 // Note that this code permits a bad frame on resize, since it is | |
900 // run after the relayout / paint happens. It would be great to improve | |
901 // this, but it would be even greater to move this code entirely to | |
902 // JS and fix it there. | |
903 | |
904 IntSize oldSize = m_size; | |
905 m_size.setWidth(newSize->width()); | |
906 m_size.setHeight(newSize->height()); | |
907 | |
908 // Adjust for effective zoom. | |
909 if (m_panel->layoutObject() && m_panel->layoutObject()->style()) { | |
910 m_size.setWidth(ceil(m_size.width() / | |
911 m_panel->layoutObject()->style()->effectiveZoom())); | |
912 m_size.setHeight(ceil(m_size.height() / | |
913 m_panel->layoutObject()->style()->effectiveZoom())); | |
914 } | |
915 | |
916 // Don't bother to do any work if this matches the most recent size. | |
917 if (oldSize != m_size) | |
918 m_elementSizeChangedTimer.startOneShot(0, BLINK_FROM_HERE); | |
919 } | |
920 | |
921 void MediaControls::elementSizeChangedTimerFired(TimerBase*) { | |
922 computeWhichControlsFit(); | |
923 } | |
924 | |
925 void MediaControls::computeWhichControlsFit() { | |
926 // Hide all controls that don't fit, and show the ones that do. | |
927 // This might be better suited for a layout, but since JS media controls | |
928 // won't benefit from that anwyay, we just do it here like JS will. | |
929 | |
930 // Controls that we'll hide / show, in order of decreasing priority. | |
931 MediaControlElement* elements[] = { | |
932 // Exclude m_overflowMenu; we handle it specially. | |
933 m_playButton.get(), | |
934 m_fullscreenButton.get(), | |
935 m_downloadButton.get(), | |
936 m_timeline.get(), | |
937 m_muteButton.get(), | |
938 m_volumeSlider.get(), | |
939 m_toggleClosedCaptionsButton.get(), | |
940 m_castButton.get(), | |
941 m_currentTimeDisplay.get(), | |
942 m_durationDisplay.get(), | |
943 }; | |
944 | |
945 // TODO(mlamouri): we need a more dynamic way to find out the width of an | |
946 // element. | |
947 const int sliderMargin = 36; // Sliders have 18px margin on each side. | |
948 | |
949 if (!m_size.width()) { | |
950 // No layout yet -- hide everything, then make them show up later. | |
951 // This prevents the wrong controls from being shown briefly | |
952 // immediately after the first layout and paint, but before we have | |
953 // a chance to revise them. | |
954 for (MediaControlElement* element : elements) { | |
955 if (element) | |
956 element->setDoesFit(false); | |
957 } | |
958 return; | |
959 } | |
960 | |
961 // Assume that all controls require 48px, unless we can get the computed | |
962 // style for a button. The minimumWidth is recorded and re-use for future | |
963 // MediaControls instances and future calls to this method given that at the | |
964 // moment the controls button width is per plataform. | |
965 // TODO(mlamouri): improve the mechanism without bandaid. | |
966 static int minimumWidth = 48; | |
967 if (m_playButton->layoutObject() && m_playButton->layoutObject()->style()) { | |
968 const ComputedStyle* style = m_playButton->layoutObject()->style(); | |
969 minimumWidth = ceil(style->width().pixels() / style->effectiveZoom()); | |
970 } else if (m_overflowMenu->layoutObject() && | |
971 m_overflowMenu->layoutObject()->style()) { | |
972 const ComputedStyle* style = m_overflowMenu->layoutObject()->style(); | |
973 minimumWidth = ceil(style->width().pixels() / style->effectiveZoom()); | |
974 } | |
975 | |
976 // Insert an overflow menu. However, if we see that the overflow menu | |
977 // doesn't end up containing at least two elements, we will not display it | |
978 // but instead make place for the first element that was dropped. | |
979 m_overflowMenu->setDoesFit(true); | |
980 m_overflowMenu->setIsWanted(true); | |
981 int usedWidth = minimumWidth; | |
982 | |
983 std::list<MediaControlElement*> overflowElements; | |
984 MediaControlElement* firstDisplacedElement = nullptr; | |
985 // For each control that fits, enable it in order of decreasing priority. | |
986 for (MediaControlElement* element : elements) { | |
987 if (!element) | |
988 continue; | |
989 int width = minimumWidth; | |
990 if ((element == m_timeline.get()) || (element == m_volumeSlider.get())) | |
991 width += sliderMargin; | |
992 element->shouldShowButtonInOverflowMenu(false); | |
993 if (element->isWanted()) { | |
994 if (usedWidth + width <= m_size.width()) { | |
995 element->setDoesFit(true); | |
996 usedWidth += width; | |
997 } else { | |
998 element->setDoesFit(false); | |
999 element->shouldShowButtonInOverflowMenu(true); | |
1000 if (element->hasOverflowButton()) | |
1001 overflowElements.push_front(element); | |
1002 // We want a way to access the first media element that was | |
1003 // removed. If we don't end up needing an overflow menu, we can | |
1004 // use the space the overflow menu would have taken up to | |
1005 // instead display that media element. | |
1006 if (!element->hasOverflowButton() && !firstDisplacedElement) | |
1007 firstDisplacedElement = element; | |
1008 } | |
1009 } | |
1010 } | |
1011 | |
1012 // If we don't have at least two overflow elements, we will not show the | |
1013 // overflow menu. | |
1014 if (overflowElements.empty()) { | |
1015 m_overflowMenu->setIsWanted(false); | |
1016 usedWidth -= minimumWidth; | |
1017 if (firstDisplacedElement) { | |
1018 int width = minimumWidth; | |
1019 if ((firstDisplacedElement == m_timeline.get()) || | |
1020 (firstDisplacedElement == m_volumeSlider.get())) | |
1021 width += sliderMargin; | |
1022 if (usedWidth + width <= m_size.width()) | |
1023 firstDisplacedElement->setDoesFit(true); | |
1024 } | |
1025 } else if (overflowElements.size() == 1) { | |
1026 m_overflowMenu->setIsWanted(false); | |
1027 overflowElements.front()->setDoesFit(true); | |
1028 } | |
1029 | |
1030 // Decide if the overlay play button fits. | |
1031 if (m_overlayPlayButton) { | |
1032 bool doesFit = m_size.width() >= kMinWidthForOverlayPlayButton && | |
1033 m_size.height() >= kMinHeightForOverlayPlayButton; | |
1034 m_overlayPlayButton->setDoesFit(doesFit); | |
1035 } | |
1036 } | |
1037 | |
1038 void MediaControls::invalidate(Element* element) { | |
1039 if (!element) | |
1040 return; | |
1041 | |
1042 if (LayoutObject* layoutObject = element->layoutObject()) | |
1043 layoutObject | |
1044 ->setShouldDoFullPaintInvalidationIncludingNonCompositingDescendants(); | |
1045 } | |
1046 | |
1047 void MediaControls::networkStateChanged() { | |
1048 invalidate(m_playButton); | |
1049 invalidate(m_overlayPlayButton); | |
1050 invalidate(m_muteButton); | |
1051 invalidate(m_fullscreenButton); | |
1052 invalidate(m_downloadButton); | |
1053 invalidate(m_timeline); | |
1054 invalidate(m_volumeSlider); | |
1055 | |
1056 // Update the display state of the download button in case we now have a | |
1057 // source or no longer have a source. | |
1058 m_downloadButton->setIsWanted( | |
1059 m_downloadButton->shouldDisplayDownloadButton()); | |
1060 } | |
1061 | |
1062 bool MediaControls::overflowMenuVisible() { | |
1063 return m_overflowList ? m_overflowList->isWanted() : false; | |
1064 } | |
1065 | |
1066 void MediaControls::toggleOverflowMenu() { | |
1067 DCHECK(m_overflowList); | |
1068 | |
1069 if (!m_overflowList->isWanted()) | |
1070 m_windowEventListener->start(); | |
1071 m_overflowList->setIsWanted(!m_overflowList->isWanted()); | |
1072 } | |
1073 | |
1074 void MediaControls::hideAllMenus() { | |
1075 m_windowEventListener->stop(); | |
1076 | |
1077 if (m_overflowList->isWanted()) | |
1078 m_overflowList->setIsWanted(false); | |
1079 if (m_textTrackList->isWanted()) | |
1080 m_textTrackList->setVisible(false); | |
1081 } | |
1082 | |
1083 DEFINE_TRACE(MediaControls) { | |
1084 visitor->trace(m_resizeObserver); | |
1085 visitor->trace(m_mediaElement); | |
1086 visitor->trace(m_panel); | |
1087 visitor->trace(m_overlayPlayButton); | |
1088 visitor->trace(m_overlayEnclosure); | |
1089 visitor->trace(m_playButton); | |
1090 visitor->trace(m_currentTimeDisplay); | |
1091 visitor->trace(m_timeline); | |
1092 visitor->trace(m_muteButton); | |
1093 visitor->trace(m_volumeSlider); | |
1094 visitor->trace(m_toggleClosedCaptionsButton); | |
1095 visitor->trace(m_fullscreenButton); | |
1096 visitor->trace(m_downloadButton); | |
1097 visitor->trace(m_durationDisplay); | |
1098 visitor->trace(m_enclosure); | |
1099 visitor->trace(m_textTrackList); | |
1100 visitor->trace(m_overflowMenu); | |
1101 visitor->trace(m_overflowList); | |
1102 visitor->trace(m_castButton); | |
1103 visitor->trace(m_overlayCastButton); | |
1104 visitor->trace(m_mediaEventListener); | |
1105 visitor->trace(m_windowEventListener); | |
1106 visitor->trace(m_orientationLockDelegate); | |
1107 HTMLDivElement::trace(visitor); | |
1108 } | |
1109 | |
1110 } // namespace blink | |
OLD | NEW |