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 "config.h" 32 #include "core/html/track/vtt/VTTRegion.h" 33 34 #include "bindings/core/v8/ExceptionMessages.h" 35 #include "bindings/core/v8/ExceptionState.h" 36 #include "core/dom/ClientRect.h" 37 #include "core/dom/DOMTokenList.h" 38 #include "core/dom/ElementTraversal.h" 39 #include "core/html/HTMLDivElement.h" 40 #include "core/html/track/vtt/VTTParser.h" 41 #include "core/html/track/vtt/VTTScanner.h" 42 #include "core/rendering/RenderInline.h" 43 #include "core/rendering/RenderObject.h" 44 #include "platform/Logging.h" 45 #include "wtf/MathExtras.h" 46 #include "wtf/text/StringBuilder.h" 47 48 namespace blink { 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 static bool isNonPercentage(double value, const char* method, ExceptionState& exceptionState) 73 { 74 if (value < 0 || value > 100) { 75 exceptionState.throwDOMException(IndexSizeError, ExceptionMessages::indexOutsideRange("value", value, 0.0, ExceptionMessages::InclusiveBound, 100.0, ExceptionMessages::InclusiveBound)); 76 return true; 77 } 78 return false; 79 } 80 81 VTTRegion::VTTRegion() 82 : m_id(emptyString()) 83 , m_width(defaultWidth) 84 , m_heightInLines(defaultHeightInLines) 85 , m_regionAnchor(FloatPoint(defaultAnchorPointX, defaultAnchorPointY)) 86 , m_viewportAnchor(FloatPoint(defaultAnchorPointX, defaultAnchorPointY)) 87 , m_scroll(defaultScroll) 88 , m_track(nullptr) 89 , m_currentTop(0) 90 , m_scrollTimer(this, &VTTRegion::scrollTimerFired) 91 { 92 } 93 94 VTTRegion::~VTTRegion() 95 { 96 } 97 98 void VTTRegion::setTrack(TextTrack* track) 99 { 100 m_track = track; 101 } 102 103 void VTTRegion::setId(const String& id) 104 { 105 m_id = id; 106 } 107 108 void VTTRegion::setWidth(double value, ExceptionState& exceptionState) 109 { 110 if (isNonPercentage(value, "width", exceptionState)) 111 return; 112 113 m_width = value; 114 } 115 116 void VTTRegion::setHeight(long value, ExceptionState& exceptionState) 117 { 118 if (value < 0) { 119 exceptionState.throwDOMException(IndexSizeError, "The height provided (" + String::number(value) + ") is negative."); 120 return; 121 } 122 123 m_heightInLines = value; 124 } 125 126 void VTTRegion::setRegionAnchorX(double value, ExceptionState& exceptionState) 127 { 128 if (isNonPercentage(value, "regionAnchorX", exceptionState)) 129 return; 130 131 m_regionAnchor.setX(value); 132 } 133 134 void VTTRegion::setRegionAnchorY(double value, ExceptionState& exceptionState) 135 { 136 if (isNonPercentage(value, "regionAnchorY", exceptionState)) 137 return; 138 139 m_regionAnchor.setY(value); 140 } 141 142 void VTTRegion::setViewportAnchorX(double value, ExceptionState& exceptionState) 143 { 144 if (isNonPercentage(value, "viewportAnchorX", exceptionState)) 145 return; 146 147 m_viewportAnchor.setX(value); 148 } 149 150 void VTTRegion::setViewportAnchorY(double value, ExceptionState& exceptionState) 151 { 152 if (isNonPercentage(value, "viewportAnchorY", exceptionState)) 153 return; 154 155 m_viewportAnchor.setY(value); 156 } 157 158 const AtomicString VTTRegion::scroll() const 159 { 160 DEFINE_STATIC_LOCAL(const AtomicString, upScrollValueKeyword, ("up", AtomicString::ConstructFromLiteral)); 161 162 if (m_scroll) 163 return upScrollValueKeyword; 164 165 return ""; 166 } 167 168 void VTTRegion::setScroll(const AtomicString& value, ExceptionState& exceptionState) 169 { 170 DEFINE_STATIC_LOCAL(const AtomicString, upScrollValueKeyword, ("up", AtomicString::ConstructFromLiteral)); 171 172 if (value != emptyString() && value != upScrollValueKeyword) { 173 exceptionState.throwDOMException(SyntaxError, "The value provided ('" + value + "') is invalid. The 'scroll' property must be either the empty string, or 'up'."); 174 return; 175 } 176 177 m_scroll = value == upScrollValueKeyword; 178 } 179 180 void VTTRegion::updateParametersFromRegion(VTTRegion* region) 181 { 182 m_heightInLines = region->height(); 183 m_width = region->width(); 184 185 m_regionAnchor = FloatPoint(region->regionAnchorX(), region->regionAnchorY()); 186 m_viewportAnchor = FloatPoint(region->viewportAnchorX(), region->viewportAnchorY()); 187 188 setScroll(region->scroll(), ASSERT_NO_EXCEPTION); 189 } 190 191 void VTTRegion::setRegionSettings(const String& inputString) 192 { 193 m_settings = inputString; 194 195 VTTScanner input(inputString); 196 197 while (!input.isAtEnd()) { 198 input.skipWhile<VTTParser::isValidSettingDelimiter>(); 199 200 if (input.isAtEnd()) 201 break; 202 203 // Scan the name part. 204 RegionSetting name = scanSettingName(input); 205 206 // Verify that we're looking at a '='. 207 if (name == None || !input.scan('=')) { 208 input.skipUntil<VTTParser::isASpace>(); 209 continue; 210 } 211 212 // Scan the value part. 213 parseSettingValue(name, input); 214 } 215 } 216 217 VTTRegion::RegionSetting VTTRegion::scanSettingName(VTTScanner& input) 218 { 219 if (input.scan("id")) 220 return Id; 221 if (input.scan("height")) 222 return Height; 223 if (input.scan("width")) 224 return Width; 225 if (input.scan("viewportanchor")) 226 return ViewportAnchor; 227 if (input.scan("regionanchor")) 228 return RegionAnchor; 229 if (input.scan("scroll")) 230 return Scroll; 231 232 return None; 233 } 234 235 static inline bool parsedEntireRun(const VTTScanner& input, const VTTScanner::Run& run) 236 { 237 return input.isAt(run.end()); 238 } 239 240 void VTTRegion::parseSettingValue(RegionSetting setting, VTTScanner& input) 241 { 242 DEFINE_STATIC_LOCAL(const AtomicString, scrollUpValueKeyword, ("up", AtomicString::ConstructFromLiteral)); 243 244 VTTScanner::Run valueRun = input.collectUntil<VTTParser::isASpace>(); 245 246 switch (setting) { 247 case Id: { 248 String stringValue = input.extractString(valueRun); 249 if (stringValue.find("-->") == kNotFound) 250 m_id = stringValue; 251 break; 252 } 253 case Width: { 254 float floatWidth; 255 if (VTTParser::parseFloatPercentageValue(input, floatWidth) && parsedEntireRun(input, valueRun)) 256 m_width = floatWidth; 257 else 258 WTF_LOG(Media, "VTTRegion::parseSettingValue, invalid Width"); 259 break; 260 } 261 case Height: { 262 int number; 263 if (input.scanDigits(number) && parsedEntireRun(input, valueRun)) 264 m_heightInLines = number; 265 else 266 WTF_LOG(Media, "VTTRegion::parseSettingValue, invalid Height"); 267 break; 268 } 269 case RegionAnchor: { 270 FloatPoint anchor; 271 if (VTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun)) 272 m_regionAnchor = anchor; 273 else 274 WTF_LOG(Media, "VTTRegion::parseSettingValue, invalid RegionAnchor"); 275 break; 276 } 277 case ViewportAnchor: { 278 FloatPoint anchor; 279 if (VTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun)) 280 m_viewportAnchor = anchor; 281 else 282 WTF_LOG(Media, "VTTRegion::parseSettingValue, invalid ViewportAnchor"); 283 break; 284 } 285 case Scroll: 286 if (input.scanRun(valueRun, scrollUpValueKeyword)) 287 m_scroll = true; 288 else 289 WTF_LOG(Media, "VTTRegion::parseSettingValue, invalid Scroll"); 290 break; 291 case None: 292 break; 293 } 294 295 input.skipRun(valueRun); 296 } 297 298 const AtomicString& VTTRegion::textTrackCueContainerShadowPseudoId() 299 { 300 DEFINE_STATIC_LOCAL(const AtomicString, trackRegionCueContainerPseudoId, 301 ("-webkit-media-text-track-region-container", AtomicString::ConstructFromLiteral)); 302 303 return trackRegionCueContainerPseudoId; 304 } 305 306 const AtomicString& VTTRegion::textTrackCueContainerScrollingClass() 307 { 308 DEFINE_STATIC_LOCAL(const AtomicString, trackRegionCueContainerScrollingClass, 309 ("scrolling", AtomicString::ConstructFromLiteral)); 310 311 return trackRegionCueContainerScrollingClass; 312 } 313 314 const AtomicString& VTTRegion::textTrackRegionShadowPseudoId() 315 { 316 DEFINE_STATIC_LOCAL(const AtomicString, trackRegionShadowPseudoId, 317 ("-webkit-media-text-track-region", AtomicString::ConstructFromLiteral)); 318 319 return trackRegionShadowPseudoId; 320 } 321 322 PassRefPtrWillBeRawPtr<HTMLDivElement> VTTRegion::getDisplayTree(Document& document) 323 { 324 if (!m_regionDisplayTree) { 325 m_regionDisplayTree = HTMLDivElement::create(document); 326 prepareRegionDisplayTree(); 327 } 328 329 return m_regionDisplayTree; 330 } 331 332 void VTTRegion::willRemoveVTTCueBox(VTTCueBox* box) 333 { 334 WTF_LOG(Media, "VTTRegion::willRemoveVTTCueBox"); 335 ASSERT(m_cueContainer->contains(box)); 336 337 double boxHeight = box->getBoundingClientRect()->bottom() - box->getBoundingClientRect()->top(); 338 339 m_cueContainer->classList().remove(textTrackCueContainerScrollingClass(), ASSERT_NO_EXCEPTION); 340 341 m_currentTop += boxHeight; 342 m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX); 343 } 344 345 void VTTRegion::appendVTTCueBox(PassRefPtrWillBeRawPtr<VTTCueBox> displayBox) 346 { 347 ASSERT(m_cueContainer); 348 349 if (m_cueContainer->contains(displayBox.get())) 350 return; 351 352 m_cueContainer->appendChild(displayBox); 353 displayLastVTTCueBox(); 354 } 355 356 void VTTRegion::displayLastVTTCueBox() 357 { 358 WTF_LOG(Media, "VTTRegion::displayLastVTTCueBox"); 359 ASSERT(m_cueContainer); 360 361 // FIXME: This should not be causing recalc styles in a loop to set the "top" css 362 // property to move elements. We should just scroll the text track cues on the 363 // compositor with an animation. 364 365 if (m_scrollTimer.isActive()) 366 return; 367 368 // If it's a scrolling region, add the scrolling class. 369 if (isScrollingRegion()) 370 m_cueContainer->classList().add(textTrackCueContainerScrollingClass(), ASSERT_NO_EXCEPTION); 371 372 float regionBottom = m_regionDisplayTree->getBoundingClientRect()->bottom(); 373 374 // Find first cue that is not entirely displayed and scroll it upwards. 375 for (Element* child = ElementTraversal::firstChild(*m_cueContainer); child && !m_scrollTimer.isActive(); child = ElementTraversal::nextSibling(*child)) { 376 RefPtrWillBeRawPtr<ClientRect> clientRect = child->getBoundingClientRect(); 377 float childTop = clientRect->top(); 378 float childBottom = clientRect->bottom(); 379 380 if (regionBottom >= childBottom) 381 continue; 382 383 float height = childBottom - childTop; 384 385 m_currentTop -= std::min(height, childBottom - regionBottom); 386 m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX); 387 388 startTimer(); 389 } 390 } 391 392 void VTTRegion::prepareRegionDisplayTree() 393 { 394 ASSERT(m_regionDisplayTree); 395 396 // 7.2 Prepare region CSS boxes 397 398 // FIXME: Change the code below to use viewport units when 399 // http://crbug/244618 is fixed. 400 401 // Let regionWidth be the text track region width. 402 // Let width be 'regionWidth vw' ('vw' is a CSS unit) 403 m_regionDisplayTree->setInlineStyleProperty(CSSPropertyWidth, 404 m_width, CSSPrimitiveValue::CSS_PERCENTAGE); 405 406 // Let lineHeight be '0.0533vh' ('vh' is a CSS unit) and regionHeight be 407 // the text track region height. Let height be 'lineHeight' multiplied 408 // by regionHeight. 409 double height = lineHeight * m_heightInLines; 410 m_regionDisplayTree->setInlineStyleProperty(CSSPropertyHeight, 411 height, CSSPrimitiveValue::CSS_VH); 412 413 // Let viewportAnchorX be the x dimension of the text track region viewport 414 // anchor and regionAnchorX be the x dimension of the text track region 415 // anchor. Let leftOffset be regionAnchorX multiplied by width divided by 416 // 100.0. Let left be leftOffset subtracted from 'viewportAnchorX vw'. 417 double leftOffset = m_regionAnchor.x() * m_width / 100; 418 m_regionDisplayTree->setInlineStyleProperty(CSSPropertyLeft, 419 m_viewportAnchor.x() - leftOffset, 420 CSSPrimitiveValue::CSS_PERCENTAGE); 421 422 // Let viewportAnchorY be the y dimension of the text track region viewport 423 // anchor and regionAnchorY be the y dimension of the text track region 424 // anchor. Let topOffset be regionAnchorY multiplied by height divided by 425 // 100.0. Let top be topOffset subtracted from 'viewportAnchorY vh'. 426 double topOffset = m_regionAnchor.y() * height / 100; 427 m_regionDisplayTree->setInlineStyleProperty(CSSPropertyTop, 428 m_viewportAnchor.y() - topOffset, 429 CSSPrimitiveValue::CSS_PERCENTAGE); 430 431 // The cue container is used to wrap the cues and it is the object which is 432 // gradually scrolled out as multiple cues are appended to the region. 433 m_cueContainer = HTMLDivElement::create(m_regionDisplayTree->document()); 434 m_cueContainer->setInlineStyleProperty(CSSPropertyTop, 435 0.0, 436 CSSPrimitiveValue::CSS_PX); 437 438 m_cueContainer->setShadowPseudoId(textTrackCueContainerShadowPseudoId()); 439 m_regionDisplayTree->appendChild(m_cueContainer); 440 441 // 7.5 Every WebVTT region object is initialised with the following CSS 442 m_regionDisplayTree->setShadowPseudoId(textTrackRegionShadowPseudoId()); 443 } 444 445 void VTTRegion::startTimer() 446 { 447 WTF_LOG(Media, "VTTRegion::startTimer"); 448 449 if (m_scrollTimer.isActive()) 450 return; 451 452 double duration = isScrollingRegion() ? scrollTime : 0; 453 m_scrollTimer.startOneShot(duration, FROM_HERE); 454 } 455 456 void VTTRegion::stopTimer() 457 { 458 WTF_LOG(Media, "VTTRegion::stopTimer"); 459 460 if (m_scrollTimer.isActive()) 461 m_scrollTimer.stop(); 462 } 463 464 void VTTRegion::scrollTimerFired(Timer<VTTRegion>*) 465 { 466 WTF_LOG(Media, "VTTRegion::scrollTimerFired"); 467 468 stopTimer(); 469 displayLastVTTCueBox(); 470 } 471 472 void VTTRegion::trace(Visitor* visitor) 473 { 474 visitor->trace(m_cueContainer); 475 visitor->trace(m_regionDisplayTree); 476 visitor->trace(m_track); 477 } 478 479 } // namespace blink 480