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