| /* |
| * Copyright (C) 2011, 2013 Google Inc. All rights reserved. |
| * Copyright (C) 2011-2020 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "TextTrackCue.h" |
| |
| #if ENABLE(VIDEO) |
| |
| #include "CSSPropertyNames.h" |
| #include "CSSValueKeywords.h" |
| #include "DOMRect.h" |
| #include "ElementInlines.h" |
| #include "Event.h" |
| #include "EventNames.h" |
| #include "HTMLDivElement.h" |
| #include "HTMLStyleElement.h" |
| #include "Logging.h" |
| #include "NodeTraversal.h" |
| #include "Page.h" |
| #include "ScriptDisallowedScope.h" |
| #include "ShadowPseudoIds.h" |
| #include "Text.h" |
| #include "TextTrack.h" |
| #include "TextTrackCueList.h" |
| #include "VTTCue.h" |
| #include "VTTRegionList.h" |
| #include <limits.h> |
| #include <wtf/HexNumber.h> |
| #include <wtf/IsoMallocInlines.h> |
| #include <wtf/MathExtras.h> |
| #include <wtf/NeverDestroyed.h> |
| #include <wtf/OptionSet.h> |
| #include <wtf/text/StringConcatenateNumbers.h> |
| |
| namespace WebCore { |
| |
| WTF_MAKE_ISO_ALLOCATED_IMPL(TextTrackCue); |
| WTF_MAKE_ISO_ALLOCATED_IMPL(TextTrackCueBox); |
| |
| static const QualifiedName& cueAttributName() |
| { |
| static NeverDestroyed<QualifiedName> cueTag(nullAtom(), "cue"_s, nullAtom()); |
| return cueTag; |
| } |
| |
| static const QualifiedName& cueBackgroundAttributName() |
| { |
| static NeverDestroyed<QualifiedName> cueBackgroundTag(nullAtom(), "cuebackground"_s, nullAtom()); |
| return cueBackgroundTag; |
| } |
| |
| Ref<TextTrackCueBox> TextTrackCueBox::create(Document& document, TextTrackCue& cue) |
| { |
| auto box = adoptRef(*new TextTrackCueBox(document, cue)); |
| box->initialize(); |
| return box; |
| } |
| |
| TextTrackCueBox::TextTrackCueBox(Document& document, TextTrackCue& cue) |
| : HTMLElement(HTMLNames::divTag, document) |
| , m_cue(cue) |
| { |
| setHasCustomStyleResolveCallbacks(); |
| } |
| |
| void TextTrackCueBox::initialize() |
| { |
| setPseudo(ShadowPseudoIds::webkitMediaTextTrackDisplay()); |
| } |
| |
| TextTrackCue* TextTrackCueBox::getCue() const |
| { |
| return m_cue.get(); |
| } |
| |
| static inline bool isLegalNode(Node& node) |
| { |
| return node.hasTagName(HTMLNames::brTag) |
| || node.hasTagName(HTMLNames::divTag) |
| || node.hasTagName(HTMLNames::imgTag) |
| || node.hasTagName(HTMLNames::pTag) |
| || node.hasTagName(HTMLNames::rbTag) |
| || node.hasTagName(HTMLNames::rtTag) |
| || node.hasTagName(HTMLNames::rtcTag) |
| || node.hasTagName(HTMLNames::rubyTag) |
| || node.hasTagName(HTMLNames::spanTag) |
| || node.nodeType() == Node::TEXT_NODE; |
| } |
| |
| static Exception invalidNodeException(Node& node) |
| { |
| return Exception { InvalidNodeTypeError, makeString("Invalid node type: ", node.nodeName()) }; |
| } |
| |
| static ExceptionOr<void> checkForInvalidNodeTypes(Node& root) |
| { |
| if (!isLegalNode(root)) |
| return invalidNodeException(root); |
| |
| for (auto* child = root.firstChild(); child; child = child->nextSibling()) { |
| if (!isLegalNode(*child)) |
| return invalidNodeException(*child); |
| |
| if (is<ContainerNode>(*child)) { |
| auto result = checkForInvalidNodeTypes(*child); |
| if (result.hasException()) |
| return result.releaseException(); |
| } |
| } |
| |
| return { }; |
| } |
| |
| enum RequiredNodes { |
| Cue = 1 << 0, |
| CueBackground = 1 << 1, |
| }; |
| |
| static OptionSet<RequiredNodes> tagPseudoObjects(Node& node) |
| { |
| if (!is<Element>(node)) |
| return { }; |
| |
| OptionSet<RequiredNodes> nodeTypes = { }; |
| |
| auto& element = downcast<Element>(node); |
| if (element.hasAttributeWithoutSynchronization(cueAttributName())) { |
| element.setPseudo(ShadowPseudoIds::cue()); |
| nodeTypes = { RequiredNodes::Cue }; |
| } else if (element.hasAttributeWithoutSynchronization(cueBackgroundAttributName())) { |
| element.setPseudo(ShadowPseudoIds::webkitMediaTextTrackDisplayBackdrop()); |
| nodeTypes = { RequiredNodes::CueBackground }; |
| } |
| |
| for (auto* child = element.firstChild(); child; child = child->nextSibling()) |
| nodeTypes.add(tagPseudoObjects(*child)); |
| |
| return nodeTypes; |
| } |
| |
| static void removePseudoAttributes(Node& node) |
| { |
| if (!is<Element>(node)) |
| return; |
| |
| auto& element = downcast<Element>(node); |
| if (element.hasAttributeWithoutSynchronization(cueAttributName()) || element.hasAttributeWithoutSynchronization(cueBackgroundAttributName())) |
| element.removeAttribute(HTMLNames::pseudoAttr); |
| |
| for (auto* child = element.firstChild(); child; child = child->nextSibling()) |
| removePseudoAttributes(*child); |
| } |
| |
| ExceptionOr<Ref<TextTrackCue>> TextTrackCue::create(Document& document, double start, double end, DocumentFragment& cueFragment) |
| { |
| if (!cueFragment.firstChild()) |
| return Exception { InvalidNodeTypeError, "Empty cue fragment"_s }; |
| |
| for (Node* node = cueFragment.firstChild(); node; node = node->nextSibling()) { |
| auto result = checkForInvalidNodeTypes(*node); |
| if (result.hasException()) |
| return result.releaseException(); |
| } |
| |
| auto fragment = DocumentFragment::create(document); |
| for (Node* node = cueFragment.firstChild(); node; node = node->nextSibling()) { |
| auto result = fragment->ensurePreInsertionValidity(*node, nullptr); |
| if (result.hasException()) |
| return result.releaseException(); |
| } |
| cueFragment.cloneChildNodes(fragment); |
| |
| OptionSet<RequiredNodes> nodeTypes = { }; |
| for (Node* node = fragment->firstChild(); node; node = node->nextSibling()) |
| nodeTypes.add(tagPseudoObjects(*node)); |
| |
| if (!nodeTypes.contains(RequiredNodes::Cue)) |
| return Exception { InvalidStateError, makeString("Missing required attribute: ", cueAttributName().toString()) }; |
| if (!nodeTypes.contains(RequiredNodes::CueBackground)) |
| return Exception { InvalidStateError, makeString("Missing required attribute: ", cueBackgroundAttributName().toString()) }; |
| |
| auto textTrackCue = adoptRef(*new TextTrackCue(document, MediaTime::createWithDouble(start), MediaTime::createWithDouble(end), WTFMove(fragment))); |
| textTrackCue->suspendIfNeeded(); |
| return textTrackCue; |
| } |
| |
| TextTrackCue::TextTrackCue(Document& document, const MediaTime& start, const MediaTime& end, Ref<DocumentFragment>&& cueFragment) |
| : ActiveDOMObject(document) |
| , m_startTime(start) |
| , m_endTime(end) |
| , m_cueNode(WTFMove(cueFragment)) |
| { |
| } |
| |
| TextTrackCue::TextTrackCue(Document& document, const MediaTime& start, const MediaTime& end) |
| : ActiveDOMObject(document) |
| , m_startTime(start) |
| , m_endTime(end) |
| { |
| } |
| |
| ScriptExecutionContext* TextTrackCue::scriptExecutionContext() const |
| { |
| return ActiveDOMObject::scriptExecutionContext(); |
| } |
| |
| Document* TextTrackCue::document() const |
| { |
| return downcast<Document>(scriptExecutionContext()); |
| } |
| |
| void TextTrackCue::willChange() |
| { |
| if (++m_processingCueChanges > 1) |
| return; |
| |
| if (m_track) |
| m_track->cueWillChange(*this); |
| } |
| |
| void TextTrackCue::didChange() |
| { |
| ASSERT(m_processingCueChanges); |
| if (--m_processingCueChanges) |
| return; |
| |
| m_displayTreeNeedsUpdate = true; |
| |
| if (m_track) |
| m_track->cueDidChange(*this); |
| } |
| |
| TextTrack* TextTrackCue::track() const |
| { |
| return m_track; |
| } |
| |
| void TextTrackCue::setTrack(TextTrack* track) |
| { |
| m_track = track; |
| } |
| |
| void TextTrackCue::setId(const AtomString& id) |
| { |
| if (m_id == id) |
| return; |
| |
| willChange(); |
| m_id = id; |
| didChange(); |
| } |
| |
| void TextTrackCue::setStartTime(double value) |
| { |
| // TODO(93143): Add spec-compliant behavior for negative time values. |
| if (m_startTime.toDouble() == value || value < 0) |
| return; |
| |
| setStartTime(MediaTime::createWithDouble(value)); |
| } |
| |
| void TextTrackCue::setStartTime(const MediaTime& value) |
| { |
| willChange(); |
| m_startTime = value; |
| didChange(); |
| } |
| |
| void TextTrackCue::setEndTime(double value) |
| { |
| // TODO(93143): Add spec-compliant behavior for negative time values. |
| if (m_endTime.toDouble() == value || value < 0) |
| return; |
| |
| setEndTime(MediaTime::createWithDouble(value)); |
| } |
| |
| void TextTrackCue::setEndTime(const MediaTime& value) |
| { |
| willChange(); |
| m_endTime = value; |
| didChange(); |
| } |
| |
| void TextTrackCue::setPauseOnExit(bool value) |
| { |
| if (m_pauseOnExit == value) |
| return; |
| |
| m_pauseOnExit = value; |
| } |
| |
| void TextTrackCue::dispatchEvent(Event& event) |
| { |
| // When a TextTrack's mode is disabled: no cues are active, no events fired. |
| if (!track() || track()->mode() == TextTrack::Mode::Disabled) |
| return; |
| |
| EventTarget::dispatchEvent(event); |
| } |
| |
| bool TextTrackCue::isActive() const |
| { |
| return m_isActive && track() && track()->mode() != TextTrack::Mode::Disabled; |
| } |
| |
| void TextTrackCue::setIsActive(bool active) |
| { |
| m_isActive = active; |
| |
| if (m_isActive || !m_displayTree) |
| return; |
| |
| // The display tree is never exposed to author scripts so it's safe to dispatch events here. |
| ScriptDisallowedScope::EventAllowedScope allowedScope(*m_displayTree); |
| m_displayTree->remove(); |
| } |
| |
| unsigned TextTrackCue::cueIndex() const |
| { |
| ASSERT(m_track && m_track->cuesInternal()); |
| if (!m_track || !m_track->cuesInternal()) |
| return std::numeric_limits<unsigned>::max(); |
| |
| return m_track->cuesInternal()->cueIndex(*this); |
| } |
| |
| bool TextTrackCue::isOrderedBefore(const TextTrackCue* other) const |
| { |
| // ... cues must be sorted by their start time, earliest first; |
| if (startMediaTime() != other->startMediaTime()) |
| return startMediaTime() < other->startMediaTime(); |
| |
| // then, any cues with the same start time must be sorted by their end time, latest first; |
| if (endMediaTime() != other->endMediaTime()) |
| return endMediaTime() > other->endMediaTime(); |
| |
| // and finally, any cues with identical end times must be sorted in the order they were last added to |
| // their respective text track list of cues, oldest first (so e.g. for cues from a WebVTT file, that |
| // would initially be the order in which the cues were listed in the file) |
| return cueIndex() < other->cueIndex(); |
| } |
| |
| bool TextTrackCue::cueContentsMatch(const TextTrackCue& other) const |
| { |
| return m_id == other.m_id; |
| } |
| |
| bool TextTrackCue::isEqual(const TextTrackCue& other, TextTrackCue::CueMatchRules match) const |
| { |
| if (match != IgnoreDuration && endMediaTime() != other.endMediaTime()) |
| return false; |
| return cueType() == other.cueType() && hasEquivalentStartTime(other) && cueContentsMatch(other); |
| } |
| |
| bool TextTrackCue::hasEquivalentStartTime(const TextTrackCue& cue) const |
| { |
| MediaTime startTimeVariance = MediaTime::zeroTime(); |
| if (track()) |
| startTimeVariance = track()->startTimeVariance(); |
| else if (cue.track()) |
| startTimeVariance = cue.track()->startTimeVariance(); |
| |
| return abs(abs(startMediaTime()) - abs(cue.startMediaTime())) <= startTimeVariance; |
| } |
| |
| void TextTrackCue::toJSON(JSON::Object& value) const |
| { |
| ASCIILiteral type = "Generic"_s; |
| switch (cueType()) { |
| case TextTrackCue::ConvertedToWebVTT: |
| type = "ConvertedToWebVTT"_s; |
| break; |
| case TextTrackCue::WebVTT: |
| type = "WebVTT"_s; |
| break; |
| case TextTrackCue::Data: |
| type = "Data"_s; |
| break; |
| case TextTrackCue::Generic: |
| type = "Generic"_s; |
| break; |
| } |
| |
| value.setString("type"_s, type); |
| value.setDouble("startTime"_s, startTime()); |
| value.setDouble("endTime"_s, endTime()); |
| } |
| |
| String TextTrackCue::toJSONString() const |
| { |
| auto object = JSON::Object::create(); |
| toJSON(object.get()); |
| return object->toJSONString(); |
| } |
| |
| #ifndef NDEBUG |
| |
| TextStream& operator<<(TextStream& stream, const TextTrackCue& cue) |
| { |
| String text; |
| if (is<VTTCue>(cue)) |
| text = downcast<VTTCue>(cue).text(); |
| return stream << &cue << " id=" << cue.id() << " interval=" << cue.startTime() << "-->" << cue.endTime() << " cue=" << text << ')'; |
| } |
| |
| #endif |
| |
| RefPtr<DocumentFragment> TextTrackCue::getCueAsHTML() |
| { |
| if (!m_cueNode) |
| return nullptr; |
| |
| auto* document = this->document(); |
| if (!document) |
| return nullptr; |
| |
| auto clonedFragment = DocumentFragment::create(*document); |
| m_cueNode->cloneChildNodes(clonedFragment); |
| |
| for (Node* node = clonedFragment->firstChild(); node; node = node->nextSibling()) |
| removePseudoAttributes(*node); |
| |
| return clonedFragment; |
| } |
| |
| bool TextTrackCue::isRenderable() const |
| { |
| return m_cueNode && m_cueNode->firstChild(); |
| } |
| |
| RefPtr<TextTrackCueBox> TextTrackCue::getDisplayTree(const IntSize&, int) |
| { |
| if (m_displayTree && !m_displayTreeNeedsUpdate) |
| return m_displayTree; |
| |
| rebuildDisplayTree(); |
| |
| return m_displayTree; |
| } |
| |
| void TextTrackCue::removeDisplayTree() |
| { |
| if (!m_displayTree) |
| return; |
| |
| // The display tree is never exposed to author scripts so it's safe to dispatch events here. |
| ScriptDisallowedScope::EventAllowedScope allowedScope(*m_displayTree); |
| m_displayTree->remove(); |
| } |
| |
| void TextTrackCue::setFontSize(int fontSize, const IntSize&, bool important) |
| { |
| if (fontSize == m_fontSize && important == m_fontSizeIsImportant) |
| return; |
| |
| m_displayTreeNeedsUpdate = true; |
| m_fontSizeIsImportant = important; |
| m_fontSize = fontSize; |
| } |
| |
| void TextTrackCue::rebuildDisplayTree() |
| { |
| if (!m_cueNode) |
| return; |
| |
| RefPtr document = this->document(); |
| if (!document) |
| return; |
| |
| ScriptDisallowedScope::EventAllowedScope allowedScopeForReferenceTree(*m_cueNode); |
| |
| if (!m_displayTree) { |
| m_displayTree = TextTrackCueBox::create(*document, *this); |
| m_displayTree->setPseudo(ShadowPseudoIds::webkitGenericCueRoot()); |
| } |
| |
| m_displayTree->removeChildren(); |
| auto clonedFragment = DocumentFragment::create(*document); |
| m_cueNode->cloneChildNodes(clonedFragment); |
| m_displayTree->appendChild(clonedFragment); |
| |
| if (m_fontSize) { |
| if (auto page = document->page()) { |
| auto style = HTMLStyleElement::create(HTMLNames::styleTag, *document, false); |
| style->setTextContent(makeString(page->captionUserPreferencesStyleSheet(), |
| " ::", ShadowPseudoIds::cue(), "{font-size:", m_fontSize, m_fontSizeIsImportant ? "px !important}" : "px}")); |
| m_displayTree->appendChild(style); |
| } |
| } |
| |
| if (track()) { |
| if (const auto& styleSheets = track()->styleSheets()) { |
| for (const auto& cssString : *styleSheets) { |
| auto style = HTMLStyleElement::create(HTMLNames::styleTag, m_displayTree->document(), false); |
| style->setTextContent(String { cssString }); |
| m_displayTree->appendChild(WTFMove(style)); |
| } |
| } |
| } |
| |
| m_displayTreeNeedsUpdate = false; |
| } |
| |
| const char* TextTrackCue::activeDOMObjectName() const |
| { |
| return "TextTrackCue"; |
| } |
| |
| } // namespace WebCore |
| |
| #endif |