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