07270c2b7bba46f74b53338e12d8056b842d0cd8
[WebKit-https.git] / Source / WebCore / html / track / VTTRegion.cpp
1 /*
2  * Copyright (C) 2013 Google Inc.  All rights reserved.
3  * Copyright (C) 2014 Apple 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 are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 #include "config.h"
33 #include "VTTRegion.h"
34
35 #if ENABLE(VIDEO_TRACK) && ENABLE(WEBVTT_REGIONS)
36
37 #include "ClientRect.h"
38 #include "DOMTokenList.h"
39 #include "ExceptionCodePlaceholder.h"
40 #include "HTMLDivElement.h"
41 #include "Logging.h"
42 #include "RenderElement.h"
43 #include "VTTCue.h"
44 #include "VTTScanner.h"
45 #include "WebVTTParser.h"
46 #include <wtf/MathExtras.h>
47
48 namespace WebCore {
49
50 // The following values default values are defined within the WebVTT Regions Spec.
51 // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/region.html
52
53 // The region occupies by default 100% of the width of the video viewport.
54 static const float defaultWidth = 100;
55
56 // The region has, by default, 3 lines of text.
57 static const long defaultHeightInLines = 3;
58
59 // The region and viewport are anchored in the bottom left corner.
60 static const float defaultAnchorPointX = 0;
61 static const float defaultAnchorPointY = 100;
62
63 // The region doesn't have scrolling text, by default.
64 static const bool defaultScroll = false;
65
66 // Default region line-height (vh units)
67 static const float lineHeight = 5.33;
68
69 // Default scrolling animation time period (s).
70 static const float scrollTime = 0.433;
71
72 VTTRegion::VTTRegion(ScriptExecutionContext& context)
73     : ContextDestructionObserver(&context)
74     , m_id(emptyString())
75     , m_width(defaultWidth)
76     , m_heightInLines(defaultHeightInLines)
77     , m_regionAnchor(FloatPoint(defaultAnchorPointX, defaultAnchorPointY))
78     , m_viewportAnchor(FloatPoint(defaultAnchorPointX, defaultAnchorPointY))
79     , m_scroll(defaultScroll)
80     , m_cueContainer(nullptr)
81     , m_regionDisplayTree(nullptr)
82     , m_track(nullptr)
83     , m_currentTop(0)
84     , m_scrollTimer(this, &VTTRegion::scrollTimerFired)
85 {
86 }
87
88 VTTRegion::~VTTRegion()
89 {
90 }
91
92 void VTTRegion::setTrack(TextTrack* track)
93 {
94     m_track = track;
95 }
96
97 void VTTRegion::setId(const String& id)
98 {
99     m_id = id;
100 }
101
102 void VTTRegion::setWidth(double value, ExceptionCode& ec)
103 {
104     if (std::isinf(value) || std::isnan(value)) {
105         ec = TypeError;
106         return;
107     }
108
109     if (value < 0 || value > 100) {
110         ec = INDEX_SIZE_ERR;
111         return;
112     }
113
114     m_width = value;
115 }
116
117 void VTTRegion::setHeight(long value, ExceptionCode& ec)
118 {
119     if (value < 0) {
120         ec = INDEX_SIZE_ERR;
121         return;
122     }
123
124     m_heightInLines = value;
125 }
126
127 void VTTRegion::setRegionAnchorX(double value, ExceptionCode& ec)
128 {
129     if (std::isinf(value) || std::isnan(value)) {
130         ec = TypeError;
131         return;
132     }
133
134     if (value < 0 || value > 100) {
135         ec = INDEX_SIZE_ERR;
136         return;
137     }
138
139     m_regionAnchor.setX(value);
140 }
141
142 void VTTRegion::setRegionAnchorY(double value, ExceptionCode& ec)
143 {
144     if (std::isinf(value) || std::isnan(value)) {
145         ec = TypeError;
146         return;
147     }
148
149     if (value < 0 || value > 100) {
150         ec = INDEX_SIZE_ERR;
151         return;
152     }
153
154     m_regionAnchor.setY(value);
155 }
156
157 void VTTRegion::setViewportAnchorX(double value, ExceptionCode& ec)
158 {
159     if (std::isinf(value) || std::isnan(value)) {
160         ec = TypeError;
161         return;
162     }
163
164     if (value < 0 || value > 100) {
165         ec = INDEX_SIZE_ERR;
166         return;
167     }
168
169     m_viewportAnchor.setX(value);
170 }
171
172 void VTTRegion::setViewportAnchorY(double value, ExceptionCode& ec)
173 {
174     if (std::isinf(value) || std::isnan(value)) {
175         ec = TypeError;
176         return;
177     }
178
179     if (value < 0 || value > 100) {
180         ec = INDEX_SIZE_ERR;
181         return;
182     }
183
184     m_viewportAnchor.setY(value);
185 }
186
187 const AtomicString VTTRegion::scroll() const
188 {
189     DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, upScrollValueKeyword, ("up", AtomicString::ConstructFromLiteral));
190
191     if (m_scroll)
192         return upScrollValueKeyword;
193
194     return "";
195 }
196
197 void VTTRegion::setScroll(const AtomicString& value, ExceptionCode& ec)
198 {
199     DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, upScrollValueKeyword, ("up", AtomicString::ConstructFromLiteral));
200
201     if (value != emptyString() && value != upScrollValueKeyword) {
202         ec = SYNTAX_ERR;
203         return;
204     }
205
206     m_scroll = value == upScrollValueKeyword;
207 }
208
209 void VTTRegion::updateParametersFromRegion(VTTRegion* region)
210 {
211     m_heightInLines = region->height();
212     m_width = region->width();
213
214     m_regionAnchor = FloatPoint(region->regionAnchorX(), region->regionAnchorY());
215     m_viewportAnchor = FloatPoint(region->viewportAnchorX(), region->viewportAnchorY());
216
217     setScroll(region->scroll(), ASSERT_NO_EXCEPTION);
218 }
219
220 void VTTRegion::setRegionSettings(const String& inputString)
221 {
222     m_settings = inputString;
223     VTTScanner input(inputString);
224
225     while (!input.isAtEnd()) {
226         input.skipWhile<WebVTTParser::isValidSettingDelimiter>();
227         if (input.isAtEnd())
228             break;
229
230         // Scan the name part.
231         RegionSetting name = scanSettingName(input);
232
233         // Verify that we're looking at a '='.
234         if (name == None || !input.scan('=')) {
235             input.skipUntil<WebVTTParser::isASpace>();
236             continue;
237         }
238
239         // Scan the value part.
240         parseSettingValue(name, input);
241     }
242 }
243
244 VTTRegion::RegionSetting VTTRegion::scanSettingName(VTTScanner& input)
245 {
246     if (input.scan("id"))
247         return Id;
248     if (input.scan("height"))
249         return Height;
250     if (input.scan("width"))
251         return Width;
252     if (input.scan("viewportanchor"))
253         return ViewportAnchor;
254     if (input.scan("regionanchor"))
255         return RegionAnchor;
256     if (input.scan("scroll"))
257         return Scroll;
258
259     return None;
260 }
261
262 static inline bool parsedEntireRun(const VTTScanner& input, const VTTScanner::Run& run)
263 {
264     return input.isAt(run.end()); 
265 }
266
267 void VTTRegion::parseSettingValue(RegionSetting setting, VTTScanner& input)
268 {
269     DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, scrollUpValueKeyword, ("up", AtomicString::ConstructFromLiteral));
270
271     VTTScanner::Run valueRun = input.collectUntil<WebVTTParser::isASpace>();
272
273     switch (setting) {
274     case Id: {
275         String stringValue = input.extractString(valueRun);
276         if (stringValue.find("-->") == notFound)
277             m_id = stringValue;
278         break;
279     }
280     case Width: {
281         float floatWidth;
282         if (WebVTTParser::parseFloatPercentageValue(input, floatWidth) && parsedEntireRun(input, valueRun))
283             m_width = floatWidth;
284         else
285             LOG(Media, "VTTRegion::parseSettingValue, invalid Width");
286         break;
287     }
288     case Height: {
289         int number;
290         if (input.scanDigits(number) && parsedEntireRun(input, valueRun))
291             m_heightInLines = number;
292         else
293             LOG(Media, "VTTRegion::parseSettingValue, invalid Height");
294         break;
295     }
296     case RegionAnchor: {
297         FloatPoint anchor;
298         if (WebVTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun))
299             m_regionAnchor = anchor;
300         else
301             LOG(Media, "VTTRegion::parseSettingValue, invalid RegionAnchor");
302         break;
303     }
304     case ViewportAnchor: {
305         FloatPoint anchor;
306         if (WebVTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun))
307             m_viewportAnchor = anchor;
308         else
309             LOG(Media, "VTTRegion::parseSettingValue, invalid ViewportAnchor");
310         break;
311     }
312     case Scroll:
313         if (input.scanRun(valueRun, scrollUpValueKeyword))
314             m_scroll = true;
315         else
316             LOG(Media, "VTTRegion::parseSettingValue, invalid Scroll");
317         break;
318     case None:
319         break;
320     }
321
322     input.skipRun(valueRun);
323 }
324
325 const AtomicString& VTTRegion::textTrackCueContainerScrollingClass()
326 {
327     DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, trackRegionCueContainerScrollingClass, ("scrolling", AtomicString::ConstructFromLiteral));
328
329     return trackRegionCueContainerScrollingClass;
330 }
331
332 const AtomicString& VTTRegion::textTrackCueContainerShadowPseudoId()
333 {
334     DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, trackRegionCueContainerPseudoId,
335         ("-webkit-media-text-track-region-container", AtomicString::ConstructFromLiteral));
336
337     return trackRegionCueContainerPseudoId;
338 }
339
340 const AtomicString& VTTRegion::textTrackRegionShadowPseudoId()
341 {
342     DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, trackRegionShadowPseudoId,
343         ("-webkit-media-text-track-region", AtomicString::ConstructFromLiteral));
344
345     return trackRegionShadowPseudoId;
346 }
347
348 void VTTRegion::appendTextTrackCueBox(PassRefPtr<VTTCueBox> displayBox)
349 {
350     ASSERT(m_cueContainer);
351
352     if (m_cueContainer->contains(displayBox.get()))
353         return;
354
355     m_cueContainer->appendChild(displayBox, ASSERT_NO_EXCEPTION);
356     displayLastTextTrackCueBox();
357 }
358
359 void VTTRegion::displayLastTextTrackCueBox()
360 {
361     ASSERT(m_cueContainer);
362
363     // The container needs to be rendered, if it is not empty and the region is not currently scrolling.
364     if (!m_cueContainer->renderer() || !m_cueContainer->childNodeCount() || m_scrollTimer.isActive())
365         return;
366
367     // If it's a scrolling region, add the scrolling class.
368     if (isScrollingRegion())
369         m_cueContainer->classList()->add(textTrackCueContainerScrollingClass(), IGNORE_EXCEPTION);
370
371     float regionBottom = m_regionDisplayTree->getBoundingClientRect()->bottom();
372
373     // Find first cue that is not entirely displayed and scroll it upwards.
374     for (size_t i = 0; i < m_cueContainer->childNodeCount() && !m_scrollTimer.isActive(); ++i) {
375         float childTop = static_cast<HTMLDivElement*>(m_cueContainer->childNode(i))->getBoundingClientRect()->top();
376         float childBottom = static_cast<HTMLDivElement*>(m_cueContainer->childNode(i))->getBoundingClientRect()->bottom();
377
378         if (regionBottom >= childBottom)
379             continue;
380
381         float height = childBottom - childTop;
382
383         m_currentTop -= std::min(height, childBottom - regionBottom);
384         m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX);
385
386         startTimer();
387     }
388 }
389
390 void VTTRegion::willRemoveTextTrackCueBox(VTTCueBox* box)
391 {
392     LOG(Media, "VTTRegion::willRemoveTextTrackCueBox");
393     ASSERT(m_cueContainer->contains(box));
394
395     double boxHeight = box->getBoundingClientRect()->bottom() - box->getBoundingClientRect()->top();
396
397     m_cueContainer->classList()->remove(textTrackCueContainerScrollingClass(), IGNORE_EXCEPTION);
398
399     m_currentTop += boxHeight;
400     m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX);
401 }
402
403 PassRefPtr<HTMLDivElement> VTTRegion::getDisplayTree()
404 {
405     if (!m_regionDisplayTree) {
406         m_regionDisplayTree = HTMLDivElement::create(*ownerDocument());
407         prepareRegionDisplayTree();
408     }
409
410     return m_regionDisplayTree;
411 }
412
413 void VTTRegion::prepareRegionDisplayTree()
414 {
415     ASSERT(m_regionDisplayTree);
416
417     // 7.2 Prepare region CSS boxes
418
419     // FIXME: Change the code below to use viewport units when
420     // http://crbug/244618 is fixed.
421
422     // Let regionWidth be the text track region width.
423     // Let width be 'regionWidth vw' ('vw' is a CSS unit)
424     m_regionDisplayTree->setInlineStyleProperty(CSSPropertyWidth, m_width, CSSPrimitiveValue::CSS_PERCENTAGE);
425
426     // Let lineHeight be '0.0533vh' ('vh' is a CSS unit) and regionHeight be
427     // the text track region height. Let height be 'lineHeight' multiplied
428     // by regionHeight.
429     double height = lineHeight * m_heightInLines;
430     m_regionDisplayTree->setInlineStyleProperty(CSSPropertyHeight, height, CSSPrimitiveValue::CSS_VH);
431
432     // Let viewportAnchorX be the x dimension of the text track region viewport
433     // anchor and regionAnchorX be the x dimension of the text track region
434     // anchor. Let leftOffset be regionAnchorX multiplied by width divided by
435     // 100.0. Let left be leftOffset subtracted from 'viewportAnchorX vw'.
436     double leftOffset = m_regionAnchor.x() * m_width / 100;
437     m_regionDisplayTree->setInlineStyleProperty(CSSPropertyLeft, m_viewportAnchor.x() - leftOffset, CSSPrimitiveValue::CSS_PERCENTAGE);
438
439     // Let viewportAnchorY be the y dimension of the text track region viewport
440     // anchor and regionAnchorY be the y dimension of the text track region
441     // anchor. Let topOffset be regionAnchorY multiplied by height divided by
442     // 100.0. Let top be topOffset subtracted from 'viewportAnchorY vh'.
443     double topOffset = m_regionAnchor.y() * height / 100;
444     m_regionDisplayTree->setInlineStyleProperty(CSSPropertyTop, m_viewportAnchor.y() - topOffset, CSSPrimitiveValue::CSS_PERCENTAGE);
445
446     // The cue container is used to wrap the cues and it is the object which is
447     // gradually scrolled out as multiple cues are appended to the region.
448     m_cueContainer = HTMLDivElement::create(*ownerDocument());
449     m_cueContainer->setInlineStyleProperty(CSSPropertyTop, 0.0f, CSSPrimitiveValue::CSS_PX);
450
451     m_cueContainer->setPseudo(textTrackCueContainerShadowPseudoId());
452     m_regionDisplayTree->appendChild(m_cueContainer);
453
454     // 7.5 Every WebVTT region object is initialised with the following CSS
455     m_regionDisplayTree->setPseudo(textTrackRegionShadowPseudoId());
456 }
457
458 void VTTRegion::startTimer()
459 {
460     LOG(Media, "VTTRegion::startTimer");
461
462     if (m_scrollTimer.isActive())
463         return;
464
465     double duration = isScrollingRegion() ? scrollTime : 0;
466     m_scrollTimer.startOneShot(duration);
467 }
468
469 void VTTRegion::stopTimer()
470 {
471     LOG(Media, "VTTRegion::stopTimer");
472
473     if (m_scrollTimer.isActive())
474         m_scrollTimer.stop();
475 }
476
477 void VTTRegion::scrollTimerFired(Timer<VTTRegion>*)
478 {
479     LOG(Media, "VTTRegion::scrollTimerFired");
480
481     stopTimer();
482     displayLastTextTrackCueBox();
483 }
484
485 } // namespace WebCore
486
487 #endif