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