Home | History | Annotate | Download | only in vtt
      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