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