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/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