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