#include "config.h"
#include "VTTRegion.h"
#if ENABLE(VIDEO_TRACK)
#include "DOMRect.h"
#include "DOMTokenList.h"
#include "ElementChildIterator.h"
#include "ExceptionCode.h"
#include "HTMLDivElement.h"
#include "HTMLParserIdioms.h"
#include "Logging.h"
#include "RenderElement.h"
#include "VTTCue.h"
#include "VTTScanner.h"
#include "WebVTTParser.h"
#include <wtf/MathExtras.h>
namespace WebCore {
static const float lineHeight = 5.33;
static const Seconds scrollTime { 433_ms };
VTTRegion::VTTRegion(ScriptExecutionContext& context)
: ContextDestructionObserver(&context)
, m_id(emptyString())
, m_scrollTimer(*this, &VTTRegion::scrollTimerFired)
{
}
VTTRegion::~VTTRegion()
{
}
void VTTRegion::setTrack(TextTrack* track)
{
m_track = track;
}
void VTTRegion::setId(const String& id)
{
m_id = id;
}
ExceptionOr<void> VTTRegion::setWidth(double value)
{
if (!(value >= 0 && value <= 100))
return Exception { INDEX_SIZE_ERR };
m_width = value;
return { };
}
ExceptionOr<void> VTTRegion::setHeight(int value)
{
if (value < 0)
return Exception { INDEX_SIZE_ERR };
m_heightInLines = value;
return { };
}
ExceptionOr<void> VTTRegion::setRegionAnchorX(double value)
{
if (!(value >= 0 && value <= 100))
return Exception { INDEX_SIZE_ERR };
m_regionAnchor.setX(value);
return { };
}
ExceptionOr<void> VTTRegion::setRegionAnchorY(double value)
{
if (!(value >= 0 && value <= 100))
return Exception { INDEX_SIZE_ERR };
m_regionAnchor.setY(value);
return { };
}
ExceptionOr<void> VTTRegion::setViewportAnchorX(double value)
{
if (!(value >= 0 && value <= 100))
return Exception { INDEX_SIZE_ERR };
m_viewportAnchor.setX(value);
return { };
}
ExceptionOr<void> VTTRegion::setViewportAnchorY(double value)
{
if (!(value >= 0 && value <= 100))
return Exception { INDEX_SIZE_ERR };
m_viewportAnchor.setY(value);
return { };
}
static const AtomicString& upKeyword()
{
static NeverDestroyed<const AtomicString> upKeyword("up", AtomicString::ConstructFromLiteral);
return upKeyword;
}
const AtomicString& VTTRegion::scroll() const
{
return m_scroll ? upKeyword() : emptyAtom();
}
ExceptionOr<void> VTTRegion::setScroll(const AtomicString& value)
{
if (value.isEmpty()) {
m_scroll = false;
return { };
}
if (value == upKeyword()) {
m_scroll = true;
return { };
}
return Exception { SYNTAX_ERR };
}
void VTTRegion::updateParametersFromRegion(const VTTRegion& other)
{
m_heightInLines = other.m_heightInLines;
m_width = other.m_width;
m_regionAnchor = other.m_regionAnchor;
m_viewportAnchor = other.m_viewportAnchor;
m_scroll = other.m_scroll;
}
void VTTRegion::setRegionSettings(const String& inputString)
{
m_settings = inputString;
VTTScanner input(inputString);
while (!input.isAtEnd()) {
input.skipWhile<WebVTTParser::isValidSettingDelimiter>();
if (input.isAtEnd())
break;
RegionSetting name = scanSettingName(input);
if (name == None || !input.scan('=')) {
input.skipUntil<isHTMLSpace<UChar>>();
continue;
}
parseSettingValue(name, input);
}
}
VTTRegion::RegionSetting VTTRegion::scanSettingName(VTTScanner& input)
{
if (input.scan("id"))
return Id;
if (input.scan("height"))
return Height;
if (input.scan("width"))
return Width;
if (input.scan("viewportanchor"))
return ViewportAnchor;
if (input.scan("regionanchor"))
return RegionAnchor;
if (input.scan("scroll"))
return Scroll;
return None;
}
static inline bool parsedEntireRun(const VTTScanner& input, const VTTScanner::Run& run)
{
return input.isAt(run.end());
}
void VTTRegion::parseSettingValue(RegionSetting setting, VTTScanner& input)
{
VTTScanner::Run valueRun = input.collectUntil<isHTMLSpace<UChar>>();
switch (setting) {
case Id: {
String stringValue = input.extractString(valueRun);
if (stringValue.find("-->") == notFound)
m_id = stringValue;
break;
}
case Width: {
float floatWidth;
if (WebVTTParser::parseFloatPercentageValue(input, floatWidth) && parsedEntireRun(input, valueRun))
m_width = floatWidth;
else
LOG(Media, "VTTRegion::parseSettingValue, invalid Width");
break;
}
case Height: {
int number;
if (input.scanDigits(number) && parsedEntireRun(input, valueRun))
m_heightInLines = number;
else
LOG(Media, "VTTRegion::parseSettingValue, invalid Height");
break;
}
case RegionAnchor: {
FloatPoint anchor;
if (WebVTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun))
m_regionAnchor = anchor;
else
LOG(Media, "VTTRegion::parseSettingValue, invalid RegionAnchor");
break;
}
case ViewportAnchor: {
FloatPoint anchor;
if (WebVTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun))
m_viewportAnchor = anchor;
else
LOG(Media, "VTTRegion::parseSettingValue, invalid ViewportAnchor");
break;
}
case Scroll:
if (input.scanRun(valueRun, upKeyword()))
m_scroll = true;
else
LOG(Media, "VTTRegion::parseSettingValue, invalid Scroll");
break;
case None:
break;
}
input.skipRun(valueRun);
}
const AtomicString& VTTRegion::textTrackCueContainerScrollingClass()
{
static NeverDestroyed<const AtomicString> trackRegionCueContainerScrollingClass("scrolling", AtomicString::ConstructFromLiteral);
return trackRegionCueContainerScrollingClass;
}
const AtomicString& VTTRegion::textTrackCueContainerShadowPseudoId()
{
static NeverDestroyed<const AtomicString> trackRegionCueContainerPseudoId("-webkit-media-text-track-region-container", AtomicString::ConstructFromLiteral);
return trackRegionCueContainerPseudoId;
}
const AtomicString& VTTRegion::textTrackRegionShadowPseudoId()
{
static NeverDestroyed<const AtomicString> trackRegionShadowPseudoId("-webkit-media-text-track-region", AtomicString::ConstructFromLiteral);
return trackRegionShadowPseudoId;
}
void VTTRegion::appendTextTrackCueBox(Ref<VTTCueBox>&& displayBox)
{
ASSERT(m_cueContainer);
if (m_cueContainer->contains(displayBox.ptr()))
return;
m_cueContainer->appendChild(displayBox);
displayLastTextTrackCueBox();
}
void VTTRegion::displayLastTextTrackCueBox()
{
ASSERT(m_cueContainer);
if (!m_cueContainer->renderer() || !m_cueContainer->hasChildNodes() || m_scrollTimer.isActive())
return;
if (isScrollingRegion())
m_cueContainer->classList().add(textTrackCueContainerScrollingClass());
float regionBottom = m_regionDisplayTree->getBoundingClientRect()->bottom();
for (auto& child : childrenOfType<Element>(*m_cueContainer)) {
auto rect = child.getBoundingClientRect();
float childTop = rect->top();
float childBottom = rect->bottom();
if (regionBottom >= childBottom)
continue;
float height = childBottom - childTop;
m_currentTop -= std::min(height, childBottom - regionBottom);
m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX);
startTimer();
break;
}
}
void VTTRegion::willRemoveTextTrackCueBox(VTTCueBox* box)
{
LOG(Media, "VTTRegion::willRemoveTextTrackCueBox");
ASSERT(m_cueContainer->contains(box));
double boxHeight = box->getBoundingClientRect()->bottom() - box->getBoundingClientRect()->top();
m_cueContainer->classList().remove(textTrackCueContainerScrollingClass());
m_currentTop += boxHeight;
m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX);
}
HTMLDivElement& VTTRegion::getDisplayTree()
{
if (!m_regionDisplayTree) {
m_regionDisplayTree = HTMLDivElement::create(downcast<Document>(*m_scriptExecutionContext));
prepareRegionDisplayTree();
}
return *m_regionDisplayTree;
}
void VTTRegion::prepareRegionDisplayTree()
{
ASSERT(m_regionDisplayTree);
m_regionDisplayTree->setInlineStyleProperty(CSSPropertyWidth, m_width, CSSPrimitiveValue::CSS_PERCENTAGE);
double height = lineHeight * m_heightInLines;
m_regionDisplayTree->setInlineStyleProperty(CSSPropertyHeight, height, CSSPrimitiveValue::CSS_VH);
double leftOffset = m_regionAnchor.x() * m_width / 100;
m_regionDisplayTree->setInlineStyleProperty(CSSPropertyLeft, m_viewportAnchor.x() - leftOffset, CSSPrimitiveValue::CSS_PERCENTAGE);
double topOffset = m_regionAnchor.y() * height / 100;
m_regionDisplayTree->setInlineStyleProperty(CSSPropertyTop, m_viewportAnchor.y() - topOffset, CSSPrimitiveValue::CSS_PERCENTAGE);
m_cueContainer = HTMLDivElement::create(downcast<Document>(*m_scriptExecutionContext));
m_cueContainer->setInlineStyleProperty(CSSPropertyTop, 0.0f, CSSPrimitiveValue::CSS_PX);
m_cueContainer->setPseudo(textTrackCueContainerShadowPseudoId());
m_regionDisplayTree->appendChild(*m_cueContainer);
m_regionDisplayTree->setPseudo(textTrackRegionShadowPseudoId());
}
void VTTRegion::startTimer()
{
LOG(Media, "VTTRegion::startTimer");
if (m_scrollTimer.isActive())
return;
Seconds duration = isScrollingRegion() ? scrollTime : 0_s;
m_scrollTimer.startOneShot(duration);
}
void VTTRegion::stopTimer()
{
LOG(Media, "VTTRegion::stopTimer");
if (m_scrollTimer.isActive())
m_scrollTimer.stop();
}
void VTTRegion::scrollTimerFired()
{
LOG(Media, "VTTRegion::scrollTimerFired");
stopTimer();
displayLastTextTrackCueBox();
}
}
#endif