1/*
2 * Copyright (C) 2008 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 "MediaDocument.h"
28
29#if ENABLE(VIDEO)
30
31#include "Chrome.h"
32#include "ChromeClient.h"
33#include "DocumentLoader.h"
34#include "EventNames.h"
35#include "Frame.h"
36#include "FrameLoader.h"
37#include "FrameLoaderClient.h"
38#include "HTMLBodyElement.h"
39#include "HTMLEmbedElement.h"
40#include "HTMLHeadElement.h"
41#include "HTMLHtmlElement.h"
42#include "HTMLMetaElement.h"
43#include "HTMLNames.h"
44#include "HTMLSourceElement.h"
45#include "HTMLVideoElement.h"
46#include "KeyboardEvent.h"
47#include "NodeList.h"
48#include "Page.h"
49#include "RawDataDocumentParser.h"
50#include "RuntimeEnabledFeatures.h"
51#include "ScriptController.h"
52#include "ShadowRoot.h"
53#include "TypedElementDescendantIterator.h"
54#include <wtf/IsoMallocInlines.h>
55#include <wtf/text/StringBuilder.h>
56
57namespace WebCore {
58
59WTF_MAKE_ISO_ALLOCATED_IMPL(MediaDocument);
60
61using namespace HTMLNames;
62
63// FIXME: Share more code with PluginDocumentParser.
64class MediaDocumentParser final : public RawDataDocumentParser {
65public:
66 static Ref<MediaDocumentParser> create(MediaDocument& document)
67 {
68 return adoptRef(*new MediaDocumentParser(document));
69 }
70
71private:
72 MediaDocumentParser(MediaDocument& document)
73 : RawDataDocumentParser { document }
74 , m_outgoingReferrer { document.outgoingReferrer() }
75 {
76 }
77
78 void appendBytes(DocumentWriter&, const char*, size_t) final;
79 void createDocumentStructure();
80
81 HTMLMediaElement* m_mediaElement { nullptr };
82 String m_outgoingReferrer;
83};
84
85void MediaDocumentParser::createDocumentStructure()
86{
87 auto& document = *this->document();
88
89 auto rootElement = HTMLHtmlElement::create(document);
90 document.appendChild(rootElement);
91 document.setCSSTarget(rootElement.ptr());
92 rootElement->insertedByParser();
93
94 if (document.frame())
95 document.frame()->injectUserScripts(InjectAtDocumentStart);
96
97#if PLATFORM(IOS_FAMILY)
98 auto headElement = HTMLHeadElement::create(document);
99 rootElement->appendChild(headElement);
100
101 auto metaElement = HTMLMetaElement::create(document);
102 metaElement->setAttributeWithoutSynchronization(nameAttr, AtomicString("viewport", AtomicString::ConstructFromLiteral));
103 metaElement->setAttributeWithoutSynchronization(contentAttr, AtomicString("width=device-width,initial-scale=1", AtomicString::ConstructFromLiteral));
104 headElement->appendChild(metaElement);
105#endif
106
107 auto body = HTMLBodyElement::create(document);
108 rootElement->appendChild(body);
109
110 auto videoElement = HTMLVideoElement::create(document);
111 m_mediaElement = videoElement.ptr();
112 videoElement->setAttributeWithoutSynchronization(controlsAttr, emptyAtom());
113 videoElement->setAttributeWithoutSynchronization(autoplayAttr, emptyAtom());
114 videoElement->setAttributeWithoutSynchronization(srcAttr, document.url().string());
115 if (auto loader = makeRefPtr(document.loader()))
116 videoElement->setAttributeWithoutSynchronization(typeAttr, loader->responseMIMEType());
117
118 if (!RuntimeEnabledFeatures::sharedFeatures().modernMediaControlsEnabled()) {
119 StringBuilder elementStyle;
120 elementStyle.appendLiteral("max-width: 100%; max-height: 100%;");
121#if PLATFORM(IOS_FAMILY)
122 elementStyle.appendLiteral("width: 100%; height: auto;");
123#endif
124 videoElement->setAttribute(styleAttr, elementStyle.toString());
125 }
126
127 body->appendChild(videoElement);
128
129 RefPtr<Frame> frame = document.frame();
130 if (!frame)
131 return;
132
133 frame->loader().activeDocumentLoader()->setMainResourceDataBufferingPolicy(DataBufferingPolicy::DoNotBufferData);
134 frame->loader().setOutgoingReferrer(document.completeURL(m_outgoingReferrer));
135}
136
137void MediaDocumentParser::appendBytes(DocumentWriter&, const char*, size_t)
138{
139 if (m_mediaElement)
140 return;
141
142 createDocumentStructure();
143 finish();
144}
145
146MediaDocument::MediaDocument(Frame* frame, const URL& url)
147 : HTMLDocument(frame, url, MediaDocumentClass)
148 , m_replaceMediaElementTimer(*this, &MediaDocument::replaceMediaElementTimerFired)
149{
150 setCompatibilityMode(DocumentCompatibilityMode::QuirksMode);
151 lockCompatibilityMode();
152 if (frame)
153 m_outgoingReferrer = frame->loader().outgoingReferrer();
154}
155
156MediaDocument::~MediaDocument()
157{
158 ASSERT(!m_replaceMediaElementTimer.isActive());
159}
160
161Ref<DocumentParser> MediaDocument::createParser()
162{
163 return MediaDocumentParser::create(*this);
164}
165
166static inline HTMLVideoElement* descendantVideoElement(ContainerNode& node)
167{
168 if (is<HTMLVideoElement>(node))
169 return downcast<HTMLVideoElement>(&node);
170
171 return descendantsOfType<HTMLVideoElement>(node).first();
172}
173
174static inline HTMLVideoElement* ancestorVideoElement(Node* node)
175{
176 while (node && !is<HTMLVideoElement>(*node))
177 node = node->parentOrShadowHostNode();
178
179 return downcast<HTMLVideoElement>(node);
180}
181
182void MediaDocument::defaultEventHandler(Event& event)
183{
184 // Modern media controls have their own event handling to determine when to
185 // pause or resume playback.
186 if (RuntimeEnabledFeatures::sharedFeatures().modernMediaControlsEnabled())
187 return;
188
189 // Match the default Quicktime plugin behavior to allow
190 // clicking and double-clicking to pause and play the media.
191 if (!is<Node>(event.target()))
192 return;
193 auto& targetNode = downcast<Node>(*event.target());
194
195 if (auto video = makeRefPtr(ancestorVideoElement(&targetNode))) {
196 if (event.type() == eventNames().clickEvent) {
197 if (!video->canPlay()) {
198 video->pause();
199 event.setDefaultHandled();
200 }
201 } else if (event.type() == eventNames().dblclickEvent) {
202 if (video->canPlay()) {
203 video->play();
204 event.setDefaultHandled();
205 }
206 }
207 }
208
209 if (!is<ContainerNode>(targetNode))
210 return;
211 auto& targetContainer = downcast<ContainerNode>(targetNode);
212
213 if (event.type() == eventNames().keydownEvent && is<KeyboardEvent>(event)) {
214 auto video = makeRefPtr(descendantVideoElement(targetContainer));
215 if (!video)
216 return;
217
218 auto& keyboardEvent = downcast<KeyboardEvent>(event);
219 if (keyboardEvent.keyIdentifier() == "U+0020") { // space
220 if (video->paused()) {
221 if (video->canPlay())
222 video->play();
223 } else
224 video->pause();
225 keyboardEvent.setDefaultHandled();
226 }
227 }
228}
229
230void MediaDocument::mediaElementSawUnsupportedTracks()
231{
232 // The HTMLMediaElement was told it has something that the underlying
233 // MediaPlayer cannot handle so we should switch from <video> to <embed>
234 // and let the plugin handle this. Don't do it immediately as this
235 // function may be called directly from a media engine callback, and
236 // replaceChild will destroy the element, media player, and media engine.
237 m_replaceMediaElementTimer.startOneShot(0_s);
238}
239
240void MediaDocument::replaceMediaElementTimerFired()
241{
242 auto htmlBody = makeRefPtr(bodyOrFrameset());
243 if (!htmlBody)
244 return;
245
246 // Set body margin width and height to 0 as that is what a PluginDocument uses.
247 htmlBody->setAttributeWithoutSynchronization(marginwidthAttr, AtomicString("0", AtomicString::ConstructFromLiteral));
248 htmlBody->setAttributeWithoutSynchronization(marginheightAttr, AtomicString("0", AtomicString::ConstructFromLiteral));
249
250 if (auto videoElement = makeRefPtr(descendantVideoElement(*htmlBody))) {
251 auto embedElement = HTMLEmbedElement::create(*this);
252
253 embedElement->setAttributeWithoutSynchronization(widthAttr, AtomicString("100%", AtomicString::ConstructFromLiteral));
254 embedElement->setAttributeWithoutSynchronization(heightAttr, AtomicString("100%", AtomicString::ConstructFromLiteral));
255 embedElement->setAttributeWithoutSynchronization(nameAttr, AtomicString("plugin", AtomicString::ConstructFromLiteral));
256 embedElement->setAttributeWithoutSynchronization(srcAttr, url().string());
257
258 ASSERT(loader());
259 if (auto loader = makeRefPtr(this->loader()))
260 embedElement->setAttributeWithoutSynchronization(typeAttr, loader->writer().mimeType());
261
262 videoElement->parentNode()->replaceChild(embedElement, *videoElement);
263 }
264}
265
266void MediaDocument::mediaElementNaturalSizeChanged(const IntSize& newSize)
267{
268 if (ownerElement())
269 return;
270
271 if (newSize.isZero())
272 return;
273
274 if (page())
275 page()->chrome().client().imageOrMediaDocumentSizeChanged(newSize);
276}
277
278}
279
280#endif
281