1 /*
2 * Copyright (C) 2013 Google 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 are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31 #include "third_party/blink/renderer/core/html/track/vtt/vtt_region.h"
32
33 #include "third_party/blink/public/platform/platform.h"
34 #include "third_party/blink/renderer/core/css/css_property_names.h"
35 #include "third_party/blink/renderer/core/dom/dom_token_list.h"
36 #include "third_party/blink/renderer/core/dom/element_traversal.h"
37 #include "third_party/blink/renderer/core/geometry/dom_rect.h"
38 #include "third_party/blink/renderer/core/html/html_div_element.h"
39 #include "third_party/blink/renderer/core/html/track/vtt/vtt_parser.h"
40 #include "third_party/blink/renderer/core/html/track/vtt/vtt_scanner.h"
41 #include "third_party/blink/renderer/platform/bindings/exception_messages.h"
42 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
43 #include "third_party/blink/renderer/platform/heap/heap.h"
44 #include "third_party/blink/renderer/platform/scheduler/public/thread.h"
45 #include "third_party/blink/renderer/platform/wtf/math_extras.h"
46
47 #define VTT_LOG_LEVEL 3
48
49 namespace blink {
50
51 namespace {
52 // The following values default values are defined within the WebVTT Regions
53 // Spec.
54 // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/region.html
55
56 // The region occupies by default 100% of the width of the video viewport.
57 constexpr double kDefaultRegionWidth = 100;
58
59 // The region has, by default, 3 lines of text.
60 constexpr int kDefaultHeightInLines = 3;
61
62 // The region and viewport are anchored in the bottom left corner.
63 constexpr double kDefaultAnchorPointX = 0;
64 constexpr double kDefaultAnchorPointY = 100;
65
66 // The region doesn't have scrolling text, by default.
67 constexpr bool kDefaultScroll = false;
68
69 // Default region line-height (vh units)
70 constexpr float kLineHeight = 5.33;
71
72 // Default scrolling animation time period (s).
73 constexpr base::TimeDelta kScrollTime = base::TimeDelta::FromMilliseconds(433);
74
IsNonPercentage(double value,const char * method,ExceptionState & exception_state)75 bool IsNonPercentage(double value,
76 const char* method,
77 ExceptionState& exception_state) {
78 if (value < 0 || value > 100) {
79 exception_state.ThrowDOMException(
80 DOMExceptionCode::kIndexSizeError,
81 ExceptionMessages::IndexOutsideRange(
82 "value", value, 0.0, ExceptionMessages::kInclusiveBound, 100.0,
83 ExceptionMessages::kInclusiveBound));
84 return true;
85 }
86 return false;
87 }
88
89 } // namespace
90
VTTRegion()91 VTTRegion::VTTRegion()
92 : id_(g_empty_string),
93 width_(kDefaultRegionWidth),
94 lines_(kDefaultHeightInLines),
95 region_anchor_(DoublePoint(kDefaultAnchorPointX, kDefaultAnchorPointY)),
96 viewport_anchor_(DoublePoint(kDefaultAnchorPointX, kDefaultAnchorPointY)),
97 scroll_(kDefaultScroll),
98 current_top_(0),
99 scroll_timer_(Thread::Current()->GetTaskRunner(),
100 this,
101 &VTTRegion::ScrollTimerFired) {}
102
103 VTTRegion::~VTTRegion() = default;
104
setId(const String & id)105 void VTTRegion::setId(const String& id) {
106 id_ = id;
107 }
108
setWidth(double value,ExceptionState & exception_state)109 void VTTRegion::setWidth(double value, ExceptionState& exception_state) {
110 if (IsNonPercentage(value, "width", exception_state))
111 return;
112
113 width_ = value;
114 }
115
setLines(unsigned value)116 void VTTRegion::setLines(unsigned value) {
117 lines_ = value;
118 }
119
setRegionAnchorX(double value,ExceptionState & exception_state)120 void VTTRegion::setRegionAnchorX(double value,
121 ExceptionState& exception_state) {
122 if (IsNonPercentage(value, "regionAnchorX", exception_state))
123 return;
124
125 region_anchor_.SetX(value);
126 }
127
setRegionAnchorY(double value,ExceptionState & exception_state)128 void VTTRegion::setRegionAnchorY(double value,
129 ExceptionState& exception_state) {
130 if (IsNonPercentage(value, "regionAnchorY", exception_state))
131 return;
132
133 region_anchor_.SetY(value);
134 }
135
setViewportAnchorX(double value,ExceptionState & exception_state)136 void VTTRegion::setViewportAnchorX(double value,
137 ExceptionState& exception_state) {
138 if (IsNonPercentage(value, "viewportAnchorX", exception_state))
139 return;
140
141 viewport_anchor_.SetX(value);
142 }
143
setViewportAnchorY(double value,ExceptionState & exception_state)144 void VTTRegion::setViewportAnchorY(double value,
145 ExceptionState& exception_state) {
146 if (IsNonPercentage(value, "viewportAnchorY", exception_state))
147 return;
148
149 viewport_anchor_.SetY(value);
150 }
151
scroll() const152 const AtomicString VTTRegion::scroll() const {
153 DEFINE_STATIC_LOCAL(const AtomicString, up_scroll_value_keyword, ("up"));
154 return scroll_ ? up_scroll_value_keyword : g_empty_atom;
155 }
156
setScroll(const AtomicString & value)157 void VTTRegion::setScroll(const AtomicString& value) {
158 DCHECK(value == "up" || value == g_empty_atom);
159 scroll_ = value != g_empty_atom;
160 }
161
SetRegionSettings(const String & input_string)162 void VTTRegion::SetRegionSettings(const String& input_string) {
163 VTTScanner input(input_string);
164
165 while (!input.IsAtEnd()) {
166 input.SkipWhile<VTTParser::IsASpace>();
167
168 if (input.IsAtEnd())
169 break;
170
171 // Scan the name part.
172 RegionSetting name = ScanSettingName(input);
173
174 // Verify that we're looking at a ':'.
175 if (name == kNone || !input.Scan(':')) {
176 input.SkipUntil<VTTParser::IsASpace>();
177 continue;
178 }
179
180 // Scan the value part.
181 ParseSettingValue(name, input);
182 }
183 }
184
ScanSettingName(VTTScanner & input)185 VTTRegion::RegionSetting VTTRegion::ScanSettingName(VTTScanner& input) {
186 if (input.Scan("id"))
187 return kId;
188 if (input.Scan("lines"))
189 return kLines;
190 if (input.Scan("width"))
191 return kWidth;
192 if (input.Scan("viewportanchor"))
193 return kViewportAnchor;
194 if (input.Scan("regionanchor"))
195 return kRegionAnchor;
196 if (input.Scan("scroll"))
197 return kScroll;
198
199 return kNone;
200 }
201
ParsedEntireRun(const VTTScanner & input,const VTTScanner::Run & run)202 static inline bool ParsedEntireRun(const VTTScanner& input,
203 const VTTScanner::Run& run) {
204 return input.IsAt(run.end());
205 }
206
ParseSettingValue(RegionSetting setting,VTTScanner & input)207 void VTTRegion::ParseSettingValue(RegionSetting setting, VTTScanner& input) {
208 DEFINE_STATIC_LOCAL(const AtomicString, scroll_up_value_keyword, ("up"));
209
210 VTTScanner::Run value_run = input.CollectUntil<VTTParser::IsASpace>();
211
212 switch (setting) {
213 case kId: {
214 String string_value = input.ExtractString(value_run);
215 if (string_value.Find("-->") == kNotFound)
216 id_ = string_value;
217 break;
218 }
219 case kWidth: {
220 double width;
221 if (VTTParser::ParsePercentageValue(input, width) &&
222 ParsedEntireRun(input, value_run))
223 width_ = width;
224 else
225 DVLOG(VTT_LOG_LEVEL) << "parseSettingValue, invalid Width";
226 break;
227 }
228 case kLines: {
229 unsigned number;
230 if (input.ScanDigits(number) && ParsedEntireRun(input, value_run))
231 lines_ = number;
232 else
233 DVLOG(VTT_LOG_LEVEL) << "parseSettingValue, invalid Lines";
234 break;
235 }
236 case kRegionAnchor: {
237 DoublePoint anchor;
238 if (VTTParser::ParsePercentageValuePair(input, ',', anchor) &&
239 ParsedEntireRun(input, value_run))
240 region_anchor_ = anchor;
241 else
242 DVLOG(VTT_LOG_LEVEL) << "parseSettingValue, invalid RegionAnchor";
243 break;
244 }
245 case kViewportAnchor: {
246 DoublePoint anchor;
247 if (VTTParser::ParsePercentageValuePair(input, ',', anchor) &&
248 ParsedEntireRun(input, value_run))
249 viewport_anchor_ = anchor;
250 else
251 DVLOG(VTT_LOG_LEVEL) << "parseSettingValue, invalid ViewportAnchor";
252 break;
253 }
254 case kScroll:
255 if (input.ScanRun(value_run, scroll_up_value_keyword))
256 scroll_ = true;
257 else
258 DVLOG(VTT_LOG_LEVEL) << "parseSettingValue, invalid Scroll";
259 break;
260 case kNone:
261 break;
262 }
263
264 input.SkipRun(value_run);
265 }
266
TextTrackCueContainerScrollingClass()267 const AtomicString& VTTRegion::TextTrackCueContainerScrollingClass() {
268 DEFINE_STATIC_LOCAL(const AtomicString,
269 track_region_cue_container_scrolling_class,
270 ("scrolling"));
271
272 return track_region_cue_container_scrolling_class;
273 }
274
GetDisplayTree(Document & document)275 HTMLDivElement* VTTRegion::GetDisplayTree(Document& document) {
276 if (!region_display_tree_) {
277 region_display_tree_ = MakeGarbageCollected<HTMLDivElement>(document);
278 PrepareRegionDisplayTree();
279 }
280
281 return region_display_tree_;
282 }
283
WillRemoveVTTCueBox(VTTCueBox * box)284 void VTTRegion::WillRemoveVTTCueBox(VTTCueBox* box) {
285 DVLOG(VTT_LOG_LEVEL) << "willRemoveVTTCueBox";
286 DCHECK(cue_container_->contains(box));
287
288 double box_height = box->getBoundingClientRect()->height();
289
290 cue_container_->classList().Remove(TextTrackCueContainerScrollingClass());
291
292 current_top_ += box_height;
293 cue_container_->SetInlineStyleProperty(CSSPropertyID::kTop, current_top_,
294 CSSPrimitiveValue::UnitType::kPixels);
295 }
296
AppendVTTCueBox(VTTCueBox * display_box)297 void VTTRegion::AppendVTTCueBox(VTTCueBox* display_box) {
298 DCHECK(cue_container_);
299
300 if (cue_container_->contains(display_box))
301 return;
302
303 cue_container_->AppendChild(display_box);
304 DisplayLastVTTCueBox();
305 }
306
DisplayLastVTTCueBox()307 void VTTRegion::DisplayLastVTTCueBox() {
308 DVLOG(VTT_LOG_LEVEL) << "displayLastVTTCueBox";
309 DCHECK(cue_container_);
310
311 // FIXME: This should not be causing recalc styles in a loop to set the "top"
312 // css property to move elements. We should just scroll the text track cues on
313 // the compositor with an animation.
314
315 if (scroll_timer_.IsActive())
316 return;
317
318 // If it's a scrolling region, add the scrolling class.
319 if (IsScrollingRegion())
320 cue_container_->classList().Add(TextTrackCueContainerScrollingClass());
321
322 double region_bottom =
323 region_display_tree_->getBoundingClientRect()->bottom();
324
325 // Find first cue that is not entirely displayed and scroll it upwards.
326 for (Element& child : ElementTraversal::ChildrenOf(*cue_container_)) {
327 DOMRect* client_rect = child.getBoundingClientRect();
328 double child_bottom = client_rect->bottom();
329
330 if (region_bottom >= child_bottom)
331 continue;
332
333 current_top_ -=
334 std::min(client_rect->height(), child_bottom - region_bottom);
335 cue_container_->SetInlineStyleProperty(
336 CSSPropertyID::kTop, current_top_,
337 CSSPrimitiveValue::UnitType::kPixels);
338
339 StartTimer();
340 break;
341 }
342 }
343
PrepareRegionDisplayTree()344 void VTTRegion::PrepareRegionDisplayTree() {
345 DCHECK(region_display_tree_);
346
347 // 7.2 Prepare region CSS boxes
348
349 // FIXME: Change the code below to use viewport units when
350 // http://crbug/244618 is fixed.
351
352 // Let regionWidth be the text track region width.
353 // Let width be 'regionWidth vw' ('vw' is a CSS unit)
354 region_display_tree_->SetInlineStyleProperty(
355 CSSPropertyID::kWidth, width_, CSSPrimitiveValue::UnitType::kPercentage);
356
357 // Let lineHeight be '0.0533vh' ('vh' is a CSS unit) and regionHeight be
358 // the text track region height. Let height be 'lineHeight' multiplied
359 // by regionHeight.
360 double height = kLineHeight * lines_;
361 region_display_tree_->SetInlineStyleProperty(
362 CSSPropertyID::kHeight, height,
363 CSSPrimitiveValue::UnitType::kViewportHeight);
364
365 // Let viewportAnchorX be the x dimension of the text track region viewport
366 // anchor and regionAnchorX be the x dimension of the text track region
367 // anchor. Let leftOffset be regionAnchorX multiplied by width divided by
368 // 100.0. Let left be leftOffset subtracted from 'viewportAnchorX vw'.
369 double left_offset = region_anchor_.X() * width_ / 100;
370 region_display_tree_->SetInlineStyleProperty(
371 CSSPropertyID::kLeft, viewport_anchor_.X() - left_offset,
372 CSSPrimitiveValue::UnitType::kPercentage);
373
374 // Let viewportAnchorY be the y dimension of the text track region viewport
375 // anchor and regionAnchorY be the y dimension of the text track region
376 // anchor. Let topOffset be regionAnchorY multiplied by height divided by
377 // 100.0. Let top be topOffset subtracted from 'viewportAnchorY vh'.
378 double top_offset = region_anchor_.Y() * height / 100;
379 region_display_tree_->SetInlineStyleProperty(
380 CSSPropertyID::kTop, viewport_anchor_.Y() - top_offset,
381 CSSPrimitiveValue::UnitType::kPercentage);
382
383 // The cue container is used to wrap the cues and it is the object which is
384 // gradually scrolled out as multiple cues are appended to the region.
385 cue_container_ =
386 MakeGarbageCollected<HTMLDivElement>(region_display_tree_->GetDocument());
387 cue_container_->SetInlineStyleProperty(CSSPropertyID::kTop, 0.0,
388 CSSPrimitiveValue::UnitType::kPixels);
389
390 cue_container_->SetShadowPseudoId(
391 AtomicString("-webkit-media-text-track-region-container"));
392 region_display_tree_->AppendChild(cue_container_);
393
394 // 7.5 Every WebVTT region object is initialised with the following CSS
395 region_display_tree_->SetShadowPseudoId(
396 AtomicString("-webkit-media-text-track-region"));
397 }
398
StartTimer()399 void VTTRegion::StartTimer() {
400 DVLOG(VTT_LOG_LEVEL) << "startTimer";
401
402 if (scroll_timer_.IsActive())
403 return;
404
405 base::TimeDelta duration =
406 IsScrollingRegion() ? kScrollTime : base::TimeDelta();
407 scroll_timer_.StartOneShot(duration, FROM_HERE);
408 }
409
StopTimer()410 void VTTRegion::StopTimer() {
411 DVLOG(VTT_LOG_LEVEL) << "stopTimer";
412 scroll_timer_.Stop();
413 }
414
ScrollTimerFired(TimerBase *)415 void VTTRegion::ScrollTimerFired(TimerBase*) {
416 DVLOG(VTT_LOG_LEVEL) << "scrollTimerFired";
417
418 StopTimer();
419 DisplayLastVTTCueBox();
420 }
421
Trace(Visitor * visitor)422 void VTTRegion::Trace(Visitor* visitor) {
423 visitor->Trace(cue_container_);
424 visitor->Trace(region_display_tree_);
425 ScriptWrappable::Trace(visitor);
426 }
427
428 } // namespace blink
429