1 | /* |
2 | * Copyright (C) 2007, 2008, 2009, 2010 Apple Inc. All rights reserved. |
3 | * |
4 | * Redistribution and use in source and binary forms, with or without |
5 | * modification, are permitted provided that the following conditions |
6 | * are met: |
7 | * 1. Redistributions of source code must retain the above copyright |
8 | * notice, this list of conditions and the following disclaimer. |
9 | * 2. Redistributions in binary form must reproduce the above copyright |
10 | * notice, this list of conditions and the following disclaimer in the |
11 | * documentation and/or other materials provided with the distribution. |
12 | * |
13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
14 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
15 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
18 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
20 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
21 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
22 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
24 | */ |
25 | |
26 | #include "config.h" |
27 | #include "HTMLVideoElement.h" |
28 | |
29 | #if ENABLE(VIDEO) |
30 | |
31 | #include "CSSPropertyNames.h" |
32 | #include "Chrome.h" |
33 | #include "ChromeClient.h" |
34 | #include "Document.h" |
35 | #include "EventNames.h" |
36 | #include "Frame.h" |
37 | #include "HTMLImageLoader.h" |
38 | #include "HTMLNames.h" |
39 | #include "HTMLParserIdioms.h" |
40 | #include "Logging.h" |
41 | #include "Page.h" |
42 | #include "RenderImage.h" |
43 | #include "RenderVideo.h" |
44 | #include "ScriptController.h" |
45 | #include "Settings.h" |
46 | #include <wtf/IsoMallocInlines.h> |
47 | #include <wtf/text/TextStream.h> |
48 | |
49 | #if ENABLE(VIDEO_PRESENTATION_MODE) |
50 | #include "VideoFullscreenModel.h" |
51 | #endif |
52 | |
53 | namespace WebCore { |
54 | |
55 | WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLVideoElement); |
56 | |
57 | using namespace HTMLNames; |
58 | |
59 | inline HTMLVideoElement::HTMLVideoElement(const QualifiedName& tagName, Document& document, bool createdByParser) |
60 | : HTMLMediaElement(tagName, document, createdByParser) |
61 | { |
62 | ASSERT(hasTagName(videoTag)); |
63 | setHasCustomStyleResolveCallbacks(); |
64 | m_defaultPosterURL = document.settings().defaultVideoPosterURL(); |
65 | } |
66 | |
67 | Ref<HTMLVideoElement> HTMLVideoElement::create(const QualifiedName& tagName, Document& document, bool createdByParser) |
68 | { |
69 | auto videoElement = adoptRef(*new HTMLVideoElement(tagName, document, createdByParser)); |
70 | videoElement->finishInitialization(); |
71 | videoElement->suspendIfNeeded(); |
72 | return videoElement; |
73 | } |
74 | |
75 | Ref<HTMLVideoElement> HTMLVideoElement::create(Document& document) |
76 | { |
77 | return create(videoTag, document, false); |
78 | } |
79 | |
80 | bool HTMLVideoElement::rendererIsNeeded(const RenderStyle& style) |
81 | { |
82 | return HTMLElement::rendererIsNeeded(style); |
83 | } |
84 | |
85 | RenderPtr<RenderElement> HTMLVideoElement::createElementRenderer(RenderStyle&& style, const RenderTreePosition&) |
86 | { |
87 | return createRenderer<RenderVideo>(*this, WTFMove(style)); |
88 | } |
89 | |
90 | void HTMLVideoElement::didAttachRenderers() |
91 | { |
92 | HTMLMediaElement::didAttachRenderers(); |
93 | |
94 | updateDisplayState(); |
95 | if (shouldDisplayPosterImage()) { |
96 | if (!m_imageLoader) |
97 | m_imageLoader = std::make_unique<HTMLImageLoader>(*this); |
98 | m_imageLoader->updateFromElement(); |
99 | if (auto* renderer = this->renderer()) |
100 | renderer->imageResource().setCachedImage(m_imageLoader->image()); |
101 | } |
102 | } |
103 | |
104 | void HTMLVideoElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStyleProperties& style) |
105 | { |
106 | if (name == widthAttr) |
107 | addHTMLLengthToStyle(style, CSSPropertyWidth, value); |
108 | else if (name == heightAttr) |
109 | addHTMLLengthToStyle(style, CSSPropertyHeight, value); |
110 | else |
111 | HTMLMediaElement::collectStyleForPresentationAttribute(name, value, style); |
112 | } |
113 | |
114 | bool HTMLVideoElement::isPresentationAttribute(const QualifiedName& name) const |
115 | { |
116 | if (name == widthAttr || name == heightAttr) |
117 | return true; |
118 | return HTMLMediaElement::isPresentationAttribute(name); |
119 | } |
120 | |
121 | void HTMLVideoElement::parseAttribute(const QualifiedName& name, const AtomicString& value) |
122 | { |
123 | if (name == posterAttr) { |
124 | // Force a poster recalc by setting m_displayMode to Unknown directly before calling updateDisplayState. |
125 | HTMLMediaElement::setDisplayMode(Unknown); |
126 | updateDisplayState(); |
127 | |
128 | if (shouldDisplayPosterImage()) { |
129 | if (!m_imageLoader) |
130 | m_imageLoader = std::make_unique<HTMLImageLoader>(*this); |
131 | m_imageLoader->updateFromElementIgnoringPreviousError(); |
132 | } else { |
133 | if (auto* renderer = this->renderer()) |
134 | renderer->imageResource().setCachedImage(nullptr); |
135 | } |
136 | } |
137 | #if ENABLE(WIRELESS_PLAYBACK_TARGET) |
138 | else if (name == webkitwirelessvideoplaybackdisabledAttr) |
139 | mediaSession().setWirelessVideoPlaybackDisabled(true); |
140 | #endif |
141 | else { |
142 | HTMLMediaElement::parseAttribute(name, value); |
143 | |
144 | #if PLATFORM(IOS_FAMILY) && ENABLE(WIRELESS_PLAYBACK_TARGET) |
145 | if (name == webkitairplayAttr) { |
146 | bool disabled = false; |
147 | if (equalLettersIgnoringASCIICase(attributeWithoutSynchronization(HTMLNames::webkitairplayAttr), "deny" )) |
148 | disabled = true; |
149 | mediaSession().setWirelessVideoPlaybackDisabled(disabled); |
150 | } |
151 | #endif |
152 | } |
153 | |
154 | } |
155 | |
156 | bool HTMLVideoElement::supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenMode videoFullscreenMode) const |
157 | { |
158 | if (!player()) |
159 | return false; |
160 | |
161 | if (videoFullscreenMode == HTMLMediaElementEnums::VideoFullscreenModePictureInPicture) { |
162 | if (!mediaSession().allowsPictureInPicture()) |
163 | return false; |
164 | if (!player()->supportsPictureInPicture()) |
165 | return false; |
166 | } |
167 | |
168 | Page* page = document().page(); |
169 | if (!page) |
170 | return false; |
171 | |
172 | if (!player()->supportsFullscreen()) |
173 | return false; |
174 | |
175 | #if PLATFORM(IOS_FAMILY) |
176 | UNUSED_PARAM(videoFullscreenMode); |
177 | // Fullscreen implemented by player. |
178 | return true; |
179 | #else |
180 | #if ENABLE(FULLSCREEN_API) |
181 | // If the full screen API is enabled and is supported for the current element |
182 | // do not require that the player has a video track to enter full screen. |
183 | if (videoFullscreenMode == HTMLMediaElementEnums::VideoFullscreenModeStandard && page->chrome().client().supportsFullScreenForElement(*this, false)) |
184 | return true; |
185 | #endif |
186 | |
187 | if (!player()->hasVideo()) |
188 | return false; |
189 | |
190 | return page->chrome().client().supportsVideoFullscreen(videoFullscreenMode); |
191 | #endif // PLATFORM(IOS_FAMILY) |
192 | } |
193 | |
194 | |
195 | #if ENABLE(FULLSCREEN_API) && PLATFORM(IOS_FAMILY) |
196 | void HTMLVideoElement::webkitRequestFullscreen() |
197 | { |
198 | webkitSetPresentationMode(HTMLVideoElement::VideoPresentationMode::Fullscreen); |
199 | } |
200 | #endif |
201 | |
202 | unsigned HTMLVideoElement::videoWidth() const |
203 | { |
204 | if (!player()) |
205 | return 0; |
206 | return clampToUnsigned(player()->naturalSize().width()); |
207 | } |
208 | |
209 | unsigned HTMLVideoElement::videoHeight() const |
210 | { |
211 | if (!player()) |
212 | return 0; |
213 | return clampToUnsigned(player()->naturalSize().height()); |
214 | } |
215 | |
216 | void HTMLVideoElement::scheduleResizeEvent() |
217 | { |
218 | m_lastReportedVideoWidth = videoWidth(); |
219 | m_lastReportedVideoHeight = videoHeight(); |
220 | scheduleEvent(eventNames().resizeEvent); |
221 | } |
222 | |
223 | void HTMLVideoElement::scheduleResizeEventIfSizeChanged() |
224 | { |
225 | if (m_lastReportedVideoWidth == videoWidth() && m_lastReportedVideoHeight == videoHeight()) |
226 | return; |
227 | scheduleResizeEvent(); |
228 | } |
229 | |
230 | bool HTMLVideoElement::isURLAttribute(const Attribute& attribute) const |
231 | { |
232 | return attribute.name() == posterAttr || HTMLMediaElement::isURLAttribute(attribute); |
233 | } |
234 | |
235 | const AtomicString& HTMLVideoElement::imageSourceURL() const |
236 | { |
237 | const AtomicString& url = attributeWithoutSynchronization(posterAttr); |
238 | if (!stripLeadingAndTrailingHTMLSpaces(url).isEmpty()) |
239 | return url; |
240 | return m_defaultPosterURL; |
241 | } |
242 | |
243 | void HTMLVideoElement::setDisplayMode(DisplayMode mode) |
244 | { |
245 | DisplayMode oldMode = displayMode(); |
246 | URL poster = posterImageURL(); |
247 | |
248 | if (!poster.isEmpty()) { |
249 | // We have a poster path, but only show it until the user triggers display by playing or seeking and the |
250 | // media engine has something to display. |
251 | if (mode == Video) { |
252 | if (oldMode != Video && player()) |
253 | player()->prepareForRendering(); |
254 | if (!hasAvailableVideoFrame()) |
255 | mode = PosterWaitingForVideo; |
256 | } |
257 | } else if (oldMode != Video && player()) |
258 | player()->prepareForRendering(); |
259 | |
260 | HTMLMediaElement::setDisplayMode(mode); |
261 | |
262 | if (player() && player()->canLoadPoster()) { |
263 | bool canLoad = true; |
264 | if (!poster.isEmpty()) { |
265 | if (RefPtr<Frame> frame = document().frame()) |
266 | canLoad = frame->loader().willLoadMediaElementURL(poster, *this); |
267 | } |
268 | if (canLoad) |
269 | player()->setPoster(poster); |
270 | } |
271 | |
272 | if (auto* renderer = this->renderer()) { |
273 | if (displayMode() != oldMode) |
274 | renderer->updateFromElement(); |
275 | } |
276 | } |
277 | |
278 | void HTMLVideoElement::updateDisplayState() |
279 | { |
280 | if (posterImageURL().isEmpty()) |
281 | setDisplayMode(Video); |
282 | else if (displayMode() < Poster) |
283 | setDisplayMode(Poster); |
284 | } |
285 | |
286 | void HTMLVideoElement::paintCurrentFrameInContext(GraphicsContext& context, const FloatRect& destRect) |
287 | { |
288 | RefPtr<MediaPlayer> player = HTMLMediaElement::player(); |
289 | if (!player) |
290 | return; |
291 | |
292 | player->setVisible(true); // Make player visible or it won't draw. |
293 | player->paintCurrentFrameInContext(context, destRect); |
294 | } |
295 | |
296 | bool HTMLVideoElement::copyVideoTextureToPlatformTexture(GraphicsContext3D* context, Platform3DObject texture, GC3Denum target, GC3Dint level, GC3Denum internalFormat, GC3Denum format, GC3Denum type, bool premultiplyAlpha, bool flipY) |
297 | { |
298 | if (!player()) |
299 | return false; |
300 | return player()->copyVideoTextureToPlatformTexture(context, texture, target, level, internalFormat, format, type, premultiplyAlpha, flipY); |
301 | } |
302 | |
303 | bool HTMLVideoElement::hasAvailableVideoFrame() const |
304 | { |
305 | if (!player()) |
306 | return false; |
307 | |
308 | return player()->hasVideo() && player()->hasAvailableVideoFrame(); |
309 | } |
310 | |
311 | NativeImagePtr HTMLVideoElement::nativeImageForCurrentTime() |
312 | { |
313 | if (!player()) |
314 | return nullptr; |
315 | |
316 | return player()->nativeImageForCurrentTime(); |
317 | } |
318 | |
319 | ExceptionOr<void> HTMLVideoElement::webkitEnterFullscreen() |
320 | { |
321 | ALWAYS_LOG(LOGIDENTIFIER); |
322 | if (isFullscreen()) |
323 | return { }; |
324 | |
325 | // Generate an exception if this isn't called in response to a user gesture, or if the |
326 | // element does not support fullscreen. |
327 | if (!mediaSession().fullscreenPermitted() || !supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModeStandard)) |
328 | return Exception { InvalidStateError }; |
329 | |
330 | enterFullscreen(); |
331 | return { }; |
332 | } |
333 | |
334 | void HTMLVideoElement::webkitExitFullscreen() |
335 | { |
336 | ALWAYS_LOG(LOGIDENTIFIER); |
337 | if (isFullscreen()) |
338 | exitFullscreen(); |
339 | } |
340 | |
341 | bool HTMLVideoElement::webkitSupportsFullscreen() |
342 | { |
343 | return supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModeStandard); |
344 | } |
345 | |
346 | bool HTMLVideoElement::webkitDisplayingFullscreen() |
347 | { |
348 | return isFullscreen(); |
349 | } |
350 | |
351 | void HTMLVideoElement::ancestorWillEnterFullscreen() |
352 | { |
353 | #if PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE) |
354 | if (fullscreenMode() == VideoFullscreenModeNone) |
355 | return; |
356 | |
357 | // If this video element's presentation mode is not inline, but its ancestor |
358 | // is entering fullscreen, exit its current fullscreen mode. |
359 | exitToFullscreenModeWithoutAnimationIfPossible(fullscreenMode(), VideoFullscreenModeNone); |
360 | #endif |
361 | } |
362 | |
363 | #if ENABLE(WIRELESS_PLAYBACK_TARGET) |
364 | bool HTMLVideoElement::webkitWirelessVideoPlaybackDisabled() const |
365 | { |
366 | return mediaSession().wirelessVideoPlaybackDisabled(); |
367 | } |
368 | |
369 | void HTMLVideoElement::setWebkitWirelessVideoPlaybackDisabled(bool disabled) |
370 | { |
371 | setBooleanAttribute(webkitwirelessvideoplaybackdisabledAttr, disabled); |
372 | } |
373 | #endif |
374 | |
375 | void HTMLVideoElement::didMoveToNewDocument(Document& oldDocument, Document& newDocument) |
376 | { |
377 | if (m_imageLoader) |
378 | m_imageLoader->elementDidMoveToNewDocument(); |
379 | HTMLMediaElement::didMoveToNewDocument(oldDocument, newDocument); |
380 | } |
381 | |
382 | #if ENABLE(MEDIA_STATISTICS) |
383 | unsigned HTMLVideoElement::webkitDecodedFrameCount() const |
384 | { |
385 | if (!player()) |
386 | return 0; |
387 | |
388 | return player()->decodedFrameCount(); |
389 | } |
390 | |
391 | unsigned HTMLVideoElement::webkitDroppedFrameCount() const |
392 | { |
393 | if (!player()) |
394 | return 0; |
395 | |
396 | return player()->droppedFrameCount(); |
397 | } |
398 | #endif |
399 | |
400 | URL HTMLVideoElement::posterImageURL() const |
401 | { |
402 | String url = stripLeadingAndTrailingHTMLSpaces(imageSourceURL()); |
403 | if (url.isEmpty()) |
404 | return URL(); |
405 | return document().completeURL(url); |
406 | } |
407 | |
408 | #if ENABLE(VIDEO_PRESENTATION_MODE) |
409 | |
410 | bool HTMLVideoElement::webkitSupportsPresentationMode(VideoPresentationMode mode) const |
411 | { |
412 | if (mode == VideoPresentationMode::Fullscreen) |
413 | return supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModeStandard); |
414 | |
415 | if (mode == VideoPresentationMode::PictureInPicture) { |
416 | #if PLATFORM(COCOA) |
417 | if (!supportsPictureInPicture()) |
418 | return false; |
419 | #endif |
420 | |
421 | return supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModePictureInPicture); |
422 | } |
423 | |
424 | if (mode == VideoPresentationMode::Inline) |
425 | return !mediaSession().requiresFullscreenForVideoPlayback(); |
426 | |
427 | return false; |
428 | } |
429 | |
430 | static inline HTMLMediaElementEnums::VideoFullscreenMode toFullscreenMode(HTMLVideoElement::VideoPresentationMode mode) |
431 | { |
432 | switch (mode) { |
433 | case HTMLVideoElement::VideoPresentationMode::Fullscreen: |
434 | return HTMLMediaElementEnums::VideoFullscreenModeStandard; |
435 | case HTMLVideoElement::VideoPresentationMode::PictureInPicture: |
436 | return HTMLMediaElementEnums::VideoFullscreenModePictureInPicture; |
437 | case HTMLVideoElement::VideoPresentationMode::Inline: |
438 | return HTMLMediaElementEnums::VideoFullscreenModeNone; |
439 | } |
440 | ASSERT_NOT_REACHED(); |
441 | return HTMLMediaElementEnums::VideoFullscreenModeNone; |
442 | } |
443 | |
444 | void HTMLVideoElement::webkitSetPresentationMode(VideoPresentationMode mode) |
445 | { |
446 | ALWAYS_LOG(LOGIDENTIFIER, mode); |
447 | setFullscreenMode(toFullscreenMode(mode)); |
448 | } |
449 | |
450 | void HTMLVideoElement::setFullscreenMode(HTMLMediaElementEnums::VideoFullscreenMode mode) |
451 | { |
452 | if (mode == VideoFullscreenModeNone && isFullscreen()) { |
453 | exitFullscreen(); |
454 | return; |
455 | } |
456 | |
457 | if (!mediaSession().fullscreenPermitted() || !supportsFullscreen(mode)) |
458 | return; |
459 | |
460 | enterFullscreen(mode); |
461 | } |
462 | |
463 | static HTMLVideoElement::VideoPresentationMode toPresentationMode(HTMLMediaElementEnums::VideoFullscreenMode mode) |
464 | { |
465 | if (mode == HTMLMediaElementEnums::VideoFullscreenModeStandard) |
466 | return HTMLVideoElement::VideoPresentationMode::Fullscreen; |
467 | |
468 | if (mode & HTMLMediaElementEnums::VideoFullscreenModePictureInPicture) |
469 | return HTMLVideoElement::VideoPresentationMode::PictureInPicture; |
470 | |
471 | if (mode == HTMLMediaElementEnums::VideoFullscreenModeNone) |
472 | return HTMLVideoElement::VideoPresentationMode::Inline; |
473 | |
474 | ASSERT_NOT_REACHED(); |
475 | return HTMLVideoElement::VideoPresentationMode::Inline; |
476 | } |
477 | |
478 | auto HTMLVideoElement::webkitPresentationMode() const -> VideoPresentationMode |
479 | { |
480 | return toPresentationMode(fullscreenMode()); |
481 | } |
482 | |
483 | void HTMLVideoElement::fullscreenModeChanged(VideoFullscreenMode mode) |
484 | { |
485 | if (mode != fullscreenMode()) { |
486 | ALWAYS_LOG(LOGIDENTIFIER, "changed from " , fullscreenMode(), ", to " , mode); |
487 | scheduleEvent(eventNames().webkitpresentationmodechangedEvent); |
488 | } |
489 | |
490 | if (player()) |
491 | player()->setVideoFullscreenMode(mode); |
492 | |
493 | HTMLMediaElement::fullscreenModeChanged(mode); |
494 | } |
495 | |
496 | #endif |
497 | |
498 | #if PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE) |
499 | void HTMLVideoElement::exitToFullscreenModeWithoutAnimationIfPossible(HTMLMediaElementEnums::VideoFullscreenMode fromMode, HTMLMediaElementEnums::VideoFullscreenMode toMode) |
500 | { |
501 | if (document().page()->chrome().client().supportsVideoFullscreen(fromMode)) |
502 | document().page()->chrome().client().exitVideoFullscreenToModeWithoutAnimation(*this, toMode); |
503 | } |
504 | #endif |
505 | |
506 | } |
507 | |
508 | #endif |
509 | |