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)
36
37#include "DOMRect.h"
38#include "DOMTokenList.h"
39#include "ElementChildIterator.h"
40#include "HTMLDivElement.h"
41#include "HTMLParserIdioms.h"
42#include "Logging.h"
43#include "RenderElement.h"
44#include "VTTCue.h"
45#include "VTTScanner.h"
46#include "WebVTTParser.h"
47#include <wtf/MathExtras.h>
48
49namespace WebCore {
50
51// The default values are defined within the WebVTT Regions Spec.
52// https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/region.html
53
54// Default region line-height (vh units)
55static const float lineHeight = 5.33;
56
57// Default scrolling animation time period (s).
58static const Seconds scrollTime { 433_ms };
59
60VTTRegion::VTTRegion(ScriptExecutionContext& context)
61 : ContextDestructionObserver(&context)
62 , m_id(emptyString())
63 , m_scrollTimer(*this, &VTTRegion::scrollTimerFired)
64{
65}
66
67VTTRegion::~VTTRegion() = default;
68
69void VTTRegion::setTrack(TextTrack* track)
70{
71 m_track = track;
72}
73
74void VTTRegion::setId(const String& id)
75{
76 m_id = id;
77}
78
79ExceptionOr<void> VTTRegion::setWidth(double value)
80{
81 if (!(value >= 0 && value <= 100))
82 return Exception { IndexSizeError };
83 m_width = value;
84 return { };
85}
86
87ExceptionOr<void> VTTRegion::setLines(int value)
88{
89 if (value < 0)
90 return Exception { IndexSizeError };
91 m_lines = value;
92 return { };
93}
94
95ExceptionOr<void> VTTRegion::setRegionAnchorX(double value)
96{
97 if (!(value >= 0 && value <= 100))
98 return Exception { IndexSizeError };
99 m_regionAnchor.setX(value);
100 return { };
101}
102
103ExceptionOr<void> VTTRegion::setRegionAnchorY(double value)
104{
105 if (!(value >= 0 && value <= 100))
106 return Exception { IndexSizeError };
107 m_regionAnchor.setY(value);
108 return { };
109}
110
111ExceptionOr<void> VTTRegion::setViewportAnchorX(double value)
112{
113 if (!(value >= 0 && value <= 100))
114 return Exception { IndexSizeError };
115 m_viewportAnchor.setX(value);
116 return { };
117}
118
119ExceptionOr<void> VTTRegion::setViewportAnchorY(double value)
120{
121 if (!(value >= 0 && value <= 100))
122 return Exception { IndexSizeError };
123 m_viewportAnchor.setY(value);
124 return { };
125}
126
127static const AtomicString& upKeyword()
128{
129 static NeverDestroyed<const AtomicString> upKeyword("up", AtomicString::ConstructFromLiteral);
130 return upKeyword;
131}
132
133const AtomicString& VTTRegion::scroll() const
134{
135 return m_scroll ? upKeyword() : emptyAtom();
136}
137
138ExceptionOr<void> VTTRegion::setScroll(const AtomicString& value)
139{
140 if (value.isEmpty()) {
141 m_scroll = false;
142 return { };
143 }
144 if (value == upKeyword()) {
145 m_scroll = true;
146 return { };
147 }
148 return Exception { SyntaxError };
149}
150
151void VTTRegion::updateParametersFromRegion(const VTTRegion& other)
152{
153 m_lines = other.m_lines;
154 m_width = other.m_width;
155 m_regionAnchor = other.m_regionAnchor;
156 m_viewportAnchor = other.m_viewportAnchor;
157 m_scroll = other.m_scroll;
158}
159
160void VTTRegion::setRegionSettings(const String& inputString)
161{
162 m_settings = inputString;
163 VTTScanner input(inputString);
164
165 while (!input.isAtEnd()) {
166 input.skipWhile<WebVTTParser::isValidSettingDelimiter>();
167 if (input.isAtEnd())
168 break;
169
170 // Scan the name part.
171 RegionSetting name = scanSettingName(input);
172
173 // Verify that we're looking at a ':'.
174 if (name == None || !input.scan(':')) {
175 input.skipUntil<isHTMLSpace<UChar>>();
176 continue;
177 }
178
179 // Scan the value part.
180 parseSettingValue(name, input);
181 }
182}
183
184VTTRegion::RegionSetting VTTRegion::scanSettingName(VTTScanner& input)
185{
186 if (input.scan("id"))
187 return Id;
188 if (input.scan("lines"))
189 return Lines;
190 if (input.scan("width"))
191 return Width;
192 if (input.scan("viewportanchor"))
193 return ViewportAnchor;
194 if (input.scan("regionanchor"))
195 return RegionAnchor;
196 if (input.scan("scroll"))
197 return Scroll;
198
199 return None;
200}
201
202static inline bool parsedEntireRun(const VTTScanner& input, const VTTScanner::Run& run)
203{
204 return input.isAt(run.end());
205}
206
207void VTTRegion::parseSettingValue(RegionSetting setting, VTTScanner& input)
208{
209 VTTScanner::Run valueRun = input.collectUntil<isHTMLSpace<UChar>>();
210
211 switch (setting) {
212 case Id: {
213 String stringValue = input.extractString(valueRun);
214 if (stringValue.find("-->") == notFound)
215 m_id = stringValue;
216 break;
217 }
218 case Width: {
219 float floatWidth;
220 if (WebVTTParser::parseFloatPercentageValue(input, floatWidth) && parsedEntireRun(input, valueRun))
221 m_width = floatWidth;
222 else
223 LOG(Media, "VTTRegion::parseSettingValue, invalid Width");
224 break;
225 }
226 case Lines: {
227 int number;
228 if (input.scanDigits(number) && parsedEntireRun(input, valueRun))
229 m_lines = number;
230 else
231 LOG(Media, "VTTRegion::parseSettingValue, invalid Height");
232 break;
233 }
234 case RegionAnchor: {
235 FloatPoint anchor;
236 if (WebVTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun))
237 m_regionAnchor = anchor;
238 else
239 LOG(Media, "VTTRegion::parseSettingValue, invalid RegionAnchor");
240 break;
241 }
242 case ViewportAnchor: {
243 FloatPoint anchor;
244 if (WebVTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun))
245 m_viewportAnchor = anchor;
246 else
247 LOG(Media, "VTTRegion::parseSettingValue, invalid ViewportAnchor");
248 break;
249 }
250 case Scroll:
251 if (input.scanRun(valueRun, upKeyword()))
252 m_scroll = true;
253 else
254 LOG(Media, "VTTRegion::parseSettingValue, invalid Scroll");
255 break;
256 case None:
257 break;
258 }
259
260 input.skipRun(valueRun);
261}
262
263const AtomicString& VTTRegion::textTrackCueContainerScrollingClass()
264{
265 static NeverDestroyed<const AtomicString> trackRegionCueContainerScrollingClass("scrolling", AtomicString::ConstructFromLiteral);
266
267 return trackRegionCueContainerScrollingClass;
268}
269
270const AtomicString& VTTRegion::textTrackCueContainerShadowPseudoId()
271{
272 static NeverDestroyed<const AtomicString> trackRegionCueContainerPseudoId("-webkit-media-text-track-region-container", AtomicString::ConstructFromLiteral);
273
274 return trackRegionCueContainerPseudoId;
275}
276
277const AtomicString& VTTRegion::textTrackRegionShadowPseudoId()
278{
279 static NeverDestroyed<const AtomicString> trackRegionShadowPseudoId("-webkit-media-text-track-region", AtomicString::ConstructFromLiteral);
280
281 return trackRegionShadowPseudoId;
282}
283
284void VTTRegion::appendTextTrackCueBox(Ref<VTTCueBox>&& displayBox)
285{
286 ASSERT(m_cueContainer);
287
288 if (m_cueContainer->contains(displayBox.ptr()))
289 return;
290
291 m_cueContainer->appendChild(displayBox);
292 displayLastTextTrackCueBox();
293}
294
295void VTTRegion::displayLastTextTrackCueBox()
296{
297 ASSERT(m_cueContainer);
298
299 // The container needs to be rendered, if it is not empty and the region is not currently scrolling.
300 if (!m_cueContainer->renderer() || !m_cueContainer->hasChildNodes() || m_scrollTimer.isActive())
301 return;
302
303 // If it's a scrolling region, add the scrolling class.
304 if (isScrollingRegion())
305 m_cueContainer->classList().add(textTrackCueContainerScrollingClass());
306
307 float regionBottom = m_regionDisplayTree->getBoundingClientRect()->bottom();
308
309 // Find first cue that is not entirely displayed and scroll it upwards.
310 for (auto& child : childrenOfType<Element>(*m_cueContainer)) {
311 auto rect = child.getBoundingClientRect();
312 float childTop = rect->top();
313 float childBottom = rect->bottom();
314
315 if (regionBottom >= childBottom)
316 continue;
317
318 float height = childBottom - childTop;
319
320 m_currentTop -= std::min(height, childBottom - regionBottom);
321 m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX);
322
323 startTimer();
324 break;
325 }
326}
327
328void VTTRegion::willRemoveTextTrackCueBox(VTTCueBox* box)
329{
330 LOG(Media, "VTTRegion::willRemoveTextTrackCueBox");
331 ASSERT(m_cueContainer->contains(box));
332
333 double boxHeight = box->getBoundingClientRect()->bottom() - box->getBoundingClientRect()->top();
334
335 m_cueContainer->classList().remove(textTrackCueContainerScrollingClass());
336
337 m_currentTop += boxHeight;
338 m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX);
339}
340
341HTMLDivElement& VTTRegion::getDisplayTree()
342{
343 if (!m_regionDisplayTree) {
344 m_regionDisplayTree = HTMLDivElement::create(downcast<Document>(*m_scriptExecutionContext));
345 prepareRegionDisplayTree();
346 }
347
348 return *m_regionDisplayTree;
349}
350
351void VTTRegion::prepareRegionDisplayTree()
352{
353 ASSERT(m_regionDisplayTree);
354
355 // 7.2 Prepare region CSS boxes
356
357 // FIXME: Change the code below to use viewport units when
358 // http://crbug/244618 is fixed.
359
360 // Let regionWidth be the text track region width.
361 // Let width be 'regionWidth vw' ('vw' is a CSS unit)
362 m_regionDisplayTree->setInlineStyleProperty(CSSPropertyWidth, m_width, CSSPrimitiveValue::CSS_PERCENTAGE);
363
364 // Let lineHeight be '0.0533vh' ('vh' is a CSS unit) and regionHeight be
365 // the text track region height. Let height be 'lineHeight' multiplied
366 // by regionHeight.
367 double height = lineHeight * m_lines;
368 m_regionDisplayTree->setInlineStyleProperty(CSSPropertyHeight, height, CSSPrimitiveValue::CSS_VH);
369
370 // Let viewportAnchorX be the x dimension of the text track region viewport
371 // anchor and regionAnchorX be the x dimension of the text track region
372 // anchor. Let leftOffset be regionAnchorX multiplied by width divided by
373 // 100.0. Let left be leftOffset subtracted from 'viewportAnchorX vw'.
374 double leftOffset = m_regionAnchor.x() * m_width / 100;
375 m_regionDisplayTree->setInlineStyleProperty(CSSPropertyLeft, m_viewportAnchor.x() - leftOffset, CSSPrimitiveValue::CSS_PERCENTAGE);
376
377 // Let viewportAnchorY be the y dimension of the text track region viewport
378 // anchor and regionAnchorY be the y dimension of the text track region
379 // anchor. Let topOffset be regionAnchorY multiplied by height divided by
380 // 100.0. Let top be topOffset subtracted from 'viewportAnchorY vh'.
381 double topOffset = m_regionAnchor.y() * height / 100;
382 m_regionDisplayTree->setInlineStyleProperty(CSSPropertyTop, m_viewportAnchor.y() - topOffset, CSSPrimitiveValue::CSS_PERCENTAGE);
383
384 // The cue container is used to wrap the cues and it is the object which is
385 // gradually scrolled out as multiple cues are appended to the region.
386 m_cueContainer = HTMLDivElement::create(downcast<Document>(*m_scriptExecutionContext));
387 m_cueContainer->setInlineStyleProperty(CSSPropertyTop, 0.0f, CSSPrimitiveValue::CSS_PX);
388
389 m_cueContainer->setPseudo(textTrackCueContainerShadowPseudoId());
390 m_regionDisplayTree->appendChild(*m_cueContainer);
391
392 // 7.5 Every WebVTT region object is initialised with the following CSS
393 m_regionDisplayTree->setPseudo(textTrackRegionShadowPseudoId());
394}
395
396void VTTRegion::startTimer()
397{
398 LOG(Media, "VTTRegion::startTimer");
399
400 if (m_scrollTimer.isActive())
401 return;
402
403 Seconds duration = isScrollingRegion() ? scrollTime : 0_s;
404 m_scrollTimer.startOneShot(duration);
405}
406
407void VTTRegion::stopTimer()
408{
409 LOG(Media, "VTTRegion::stopTimer");
410
411 if (m_scrollTimer.isActive())
412 m_scrollTimer.stop();
413}
414
415void VTTRegion::scrollTimerFired()
416{
417 LOG(Media, "VTTRegion::scrollTimerFired");
418
419 stopTimer();
420 displayLastTextTrackCueBox();
421}
422
423} // namespace WebCore
424
425#endif
426