Files
shaka-player/lib/text/ttml_text_parser.js
T
Joey Parrish cbfe4f82a4 Fix warnings found by a newer compiler version
This is only some of the warnings produced.  These were not errors,
but we should fix them anyway so that we can adopt stricter settings
in future.

Change-Id: Ifd12f0e7c69f8f4b3d0d78b11794da2569a06d77
2019-05-06 22:25:12 +00:00

990 lines
29 KiB
JavaScript

/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.text.TtmlTextParser');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.text.Cue');
goog.require('shaka.text.CueRegion');
goog.require('shaka.text.TextEngine');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.XmlUtils');
/**
* @constructor
* @implements {shaka.extern.TextParser}
*/
shaka.text.TtmlTextParser = function() {};
/**
* @const {string}
* @private
*/
shaka.text.TtmlTextParser.parameterNs_ = 'http://www.w3.org/ns/ttml#parameter';
/**
* @const {string}
* @private
*/
shaka.text.TtmlTextParser.styleNs_ = 'http://www.w3.org/ns/ttml#styling';
/** @override */
shaka.text.TtmlTextParser.prototype.parseInit = function(data) {
goog.asserts.assert(false, 'TTML does not have init segments');
};
/** @override */
shaka.text.TtmlTextParser.prototype.parseMedia = function(data, time) {
const TtmlTextParser = shaka.text.TtmlTextParser;
const XmlUtils = shaka.util.XmlUtils;
const ttpNs = TtmlTextParser.parameterNs_;
let str = shaka.util.StringUtils.fromUTF8(data);
let ret = [];
let parser = new DOMParser();
let xml = null;
try {
xml = parser.parseFromString(str, 'text/xml');
} catch (exception) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_XML);
}
if (xml) {
// Try to get the framerate, subFrameRate and frameRateMultiplier
// if applicable
let frameRate = null;
let subFrameRate = null;
let frameRateMultiplier = null;
let tickRate = null;
let spaceStyle = null;
let extent = null;
let tts = xml.getElementsByTagName('tt');
let tt = tts[0];
// TTML should always have tt element.
if (!tt) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_XML);
} else {
frameRate = XmlUtils.getAttributeNS(tt, ttpNs, 'frameRate');
subFrameRate = XmlUtils.getAttributeNS(tt, ttpNs, 'subFrameRate');
frameRateMultiplier =
XmlUtils.getAttributeNS(tt, ttpNs, 'frameRateMultiplier');
tickRate = XmlUtils.getAttributeNS(tt, ttpNs, 'tickRate');
spaceStyle = tt.getAttribute('xml:space') || 'default';
extent = tt.getAttribute('tts:extent');
}
if (spaceStyle != 'default' && spaceStyle != 'preserve') {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_XML);
}
let whitespaceTrim = spaceStyle == 'default';
let rateInfo = new TtmlTextParser.RateInfo_(
frameRate, subFrameRate, frameRateMultiplier, tickRate);
const metadataElements = TtmlTextParser.getLeafNodes_(
tt.getElementsByTagName('metadata')[0]);
let styles = TtmlTextParser.getLeafNodes_(
tt.getElementsByTagName('styling')[0]);
let regionElements = TtmlTextParser.getLeafNodes_(
tt.getElementsByTagName('layout')[0]);
let cueRegions = [];
for (let i = 0; i < regionElements.length; i++) {
let cueRegion = TtmlTextParser.parseCueRegion_(
regionElements[i], styles, extent);
if (cueRegion) {
cueRegions.push(cueRegion);
}
}
let textNodes = TtmlTextParser.getLeafNodes_(
tt.getElementsByTagName('body')[0]);
for (let i = 0; i < textNodes.length; i++) {
let cue = TtmlTextParser.parseCue_(textNodes[i],
time.periodStart,
rateInfo,
metadataElements,
styles,
regionElements,
cueRegions,
whitespaceTrim);
if (cue) {
ret.push(cue);
}
}
}
return ret;
};
/**
* @const
* @private {!RegExp}
* @example 50% 10%
*/
shaka.text.TtmlTextParser.percentValues_ = /^(\d{1,2}|100)% (\d{1,2}|100)%$/;
/**
* @const
* @private {!RegExp}
* @example 100px
*/
shaka.text.TtmlTextParser.unitValues_ = /^(\d+px|\d+em)$/;
/**
* @const
* @private {!RegExp}
* @example 100px
*/
shaka.text.TtmlTextParser.pixelValues_ = /^(\d+)px (\d+)px$/;
/**
* @const
* @private {!RegExp}
* @example 00:00:40:07 (7 frames) or 00:00:40:07.1 (7 frames, 1 subframe)
*/
shaka.text.TtmlTextParser.timeColonFormatFrames_ =
/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/;
/**
* @const
* @private {!RegExp}
* @example 00:00:40 or 00:40
*/
shaka.text.TtmlTextParser.timeColonFormat_ = /^(?:(\d{2,}):)?(\d{2}):(\d{2})$/;
/**
* @const
* @private {!RegExp}
* @example 01:02:43.0345555 or 02:43.03
*/
shaka.text.TtmlTextParser.timeColonFormatMilliseconds_ =
/^(?:(\d{2,}):)?(\d{2}):(\d{2}\.\d{2,})$/;
/**
* @const
* @private {!RegExp}
* @example 75f or 75.5f
*/
shaka.text.TtmlTextParser.timeFramesFormat_ = /^(\d*(?:\.\d*)?)f$/;
/**
* @const
* @private {!RegExp}
* @example 50t or 50.5t
*/
shaka.text.TtmlTextParser.timeTickFormat_ = /^(\d*(?:\.\d*)?)t$/;
/**
* @const
* @private {!RegExp}
* @example 3.45h, 3m or 4.20s
*/
shaka.text.TtmlTextParser.timeHMSFormat_ =
new RegExp(['^(?:(\\d*(?:\\.\\d*)?)h)?',
'(?:(\\d*(?:\\.\\d*)?)m)?',
'(?:(\\d*(?:\\.\\d*)?)s)?',
'(?:(\\d*(?:\\.\\d*)?)ms)?$'].join(''));
/**
* @const
* @private {!Object.<string, shaka.text.Cue.lineAlign>}
*/
shaka.text.TtmlTextParser.textAlignToLineAlign_ = {
'left': shaka.text.Cue.lineAlign.START,
'center': shaka.text.Cue.lineAlign.CENTER,
'right': shaka.text.Cue.lineAlign.END,
'start': shaka.text.Cue.lineAlign.START,
'end': shaka.text.Cue.lineAlign.END,
};
/**
* @const
* @private {!Object.<string, shaka.text.Cue.positionAlign>}
*/
shaka.text.TtmlTextParser.textAlignToPositionAlign_ = {
'left': shaka.text.Cue.positionAlign.LEFT,
'center': shaka.text.Cue.positionAlign.CENTER,
'right': shaka.text.Cue.positionAlign.RIGHT,
};
/**
* Gets the leaf nodes of the xml node tree. Ignores the text, br elements
* and the spans positioned inside paragraphs
*
* @param {Element} element
* @return {!Array.<!Element>}
* @private
*/
shaka.text.TtmlTextParser.getLeafNodes_ = function(element) {
let result = [];
if (!element) {
return result;
}
let childNodes = element.childNodes;
for (let i = 0; i < childNodes.length; i++) {
// Currently we don't support styles applicable to span
// elements, so they are ignored.
let isSpanChildOfP = childNodes[i].nodeName == 'span' &&
element.nodeName == 'p';
if (childNodes[i].nodeType == Node.ELEMENT_NODE &&
childNodes[i].nodeName != 'br' && !isSpanChildOfP) {
// Get the leaves the child might contain.
goog.asserts.assert(childNodes[i] instanceof Element,
'Node should be Element!');
let leafChildren = shaka.text.TtmlTextParser.getLeafNodes_(
/** @type {Element} */(childNodes[i]));
goog.asserts.assert(leafChildren.length > 0,
'Only a null Element should return no leaves!');
result = result.concat(leafChildren);
}
}
// if no result at this point, the element itself must be a leaf.
if (!result.length) {
result.push(element);
}
return result;
};
/**
* Inserts \n where <br> tags are found.
*
* @param {!Node} element
* @param {boolean} whitespaceTrim
* @private
*/
shaka.text.TtmlTextParser.addNewLines_ = function(element, whitespaceTrim) {
let childNodes = element.childNodes;
for (let i = 0; i < childNodes.length; i++) {
if (childNodes[i].nodeName == 'br' && i > 0) {
childNodes[i - 1].textContent += '\n';
} else if (childNodes[i].childNodes.length > 0) {
shaka.text.TtmlTextParser.addNewLines_(childNodes[i], whitespaceTrim);
} else if (whitespaceTrim) {
// Trim leading and trailing whitespace.
let trimmed = childNodes[i].textContent.trim();
// Collapse multiple spaces into one.
trimmed = trimmed.replace(/\s+/g, ' ');
childNodes[i].textContent = trimmed;
}
}
};
/**
* Parses an Element into a TextTrackCue or VTTCue.
*
* @param {!Element} cueElement
* @param {number} offset
* @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
* @param {!Array.<!Element>} metadataElements
* @param {!Array.<!Element>} styles
* @param {!Array.<!Element>} regionElements
* @param {!Array.<!shaka.text.CueRegion>} cueRegions
* @param {boolean} whitespaceTrim
* @return {shaka.text.Cue}
* @private
*/
shaka.text.TtmlTextParser.parseCue_ = function(
cueElement, offset, rateInfo, metadataElements, styles,
regionElements, cueRegions, whitespaceTrim) {
// Disregard empty elements:
// TTML allows for empty elements like <div></div>.
// If cueElement has neither time attributes, nor
// non-whitespace text, don't try to make a cue out of it.
if (!cueElement.hasAttribute('begin') &&
!cueElement.hasAttribute('end') &&
/^\s*$/.test(cueElement.textContent)) {
return null;
}
shaka.text.TtmlTextParser.addNewLines_(cueElement, whitespaceTrim);
// Get time.
let start = shaka.text.TtmlTextParser.parseTime_(
cueElement.getAttribute('begin'), rateInfo);
let end = shaka.text.TtmlTextParser.parseTime_(
cueElement.getAttribute('end'), rateInfo);
let duration = shaka.text.TtmlTextParser.parseTime_(
cueElement.getAttribute('dur'), rateInfo);
let payload = cueElement.textContent;
if (end == null && duration != null) {
end = start + duration;
}
if (start == null || end == null) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_CUE);
}
start += offset;
end += offset;
let cue = new shaka.text.Cue(start, end, payload);
// Get other properties if available.
let regionElement = shaka.text.TtmlTextParser.getElementFromCollection_(
cueElement, 'region', regionElements, /* prefix= */ '');
if (regionElement && regionElement.getAttribute('xml:id')) {
let regionId = regionElement.getAttribute('xml:id');
let regionsWithId = cueRegions.filter(function(region) {
return region.id == regionId;
});
cue.region = regionsWithId[0];
}
const imageElement = shaka.text.TtmlTextParser.getElementFromCollection_(
cueElement, 'smpte:backgroundImage', metadataElements, '#');
shaka.text.TtmlTextParser.addStyle_(
cue,
cueElement,
regionElement,
imageElement,
styles);
return cue;
};
/**
* Parses an Element into a TextTrackCue or VTTCue.
*
* @param {!Element} regionElement
* @param {!Array.<!Element>} styles Defined in the top of tt element and
* used principally for images.
* @param {string} globalExtent
* @return {shaka.text.CueRegion}
* @private
*/
shaka.text.TtmlTextParser.parseCueRegion_ = function(regionElement, styles,
globalExtent) {
const TtmlTextParser = shaka.text.TtmlTextParser;
let region = new shaka.text.CueRegion();
let id = regionElement.getAttribute('xml:id');
if (!id) {
shaka.log.warning('TtmlTextParser parser encountered a region with ' +
'no id. Region will be ignored.');
return null;
}
region.id = id;
let globalResults = null;
if (globalExtent) {
globalResults = TtmlTextParser.percentValues_.exec(globalExtent) ||
TtmlTextParser.pixelValues_.exec(globalExtent);
}
const globalWidth = globalResults ? Number(globalResults[1]) : null;
const globalHeight = globalResults ? Number(globalResults[2]) : null;
let results = null;
let percentage = null;
let extent = TtmlTextParser.getStyleAttributeFromRegion_(
regionElement, styles, 'extent');
if (extent) {
percentage = TtmlTextParser.percentValues_.exec(extent);
results = percentage || TtmlTextParser.pixelValues_.exec(extent);
if (results != null) {
if (globalWidth != null) {
region.width = Number(results[1]) * 100 / globalWidth;
} else {
region.width = Number(results[1]);
}
if (globalHeight != null) {
region.height = Number(results[2]) * 100 / globalHeight;
} else {
region.height = Number(results[2]);
}
region.widthUnits = percentage || globalWidth != null ?
shaka.text.CueRegion.units.PERCENTAGE :
shaka.text.CueRegion.units.PX;
region.heightUnits = percentage || globalHeight != null ?
shaka.text.CueRegion.units.PERCENTAGE :
shaka.text.CueRegion.units.PX;
}
}
let origin = TtmlTextParser.getStyleAttributeFromRegion_(
regionElement, styles, 'origin');
if (origin) {
percentage = TtmlTextParser.percentValues_.exec(origin);
results = percentage || TtmlTextParser.pixelValues_.exec(origin);
if (results != null) {
if (globalHeight != null) {
region.viewportAnchorX = Number(results[1]) * 100 / globalHeight;
} else {
region.viewportAnchorX = Number(results[1]);
}
if (globalWidth != null) {
region.viewportAnchorY = Number(results[2]) * 100 / globalWidth;
} else {
region.viewportAnchorY = Number(results[2]);
}
region.viewportAnchorUnits = percentage || globalWidth != null ?
shaka.text.CueRegion.units.PERCENTAGE :
shaka.text.CueRegion.units.PX;
}
}
return region;
};
/**
* Adds applicable style properties to a cue.
*
* @param {!shaka.text.Cue} cue
* @param {!Element} cueElement
* @param {Element} region
* @param {Element} imageElement
* @param {!Array.<!Element>} styles
* @private
*/
shaka.text.TtmlTextParser.addStyle_ = function(
cue, cueElement, region, imageElement, styles) {
const TtmlTextParser = shaka.text.TtmlTextParser;
const Cue = shaka.text.Cue;
let direction = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'direction');
if (direction == 'rtl') {
cue.direction = Cue.direction.HORIZONTAL_RIGHT_TO_LEFT;
}
// Direction attribute specifies one-dimentional writing direction
// (left to right or right to left). Writing mode specifies that
// plus whether text is vertical or horizontal.
// They should not contradict each other. If they do, we give
// preference to writing mode.
let writingMode = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'writingMode');
// Set cue's direction if the text is horizontal, and cue's writingMode if
// it's vertical.
if (writingMode == 'tb' || writingMode == 'tblr') {
cue.writingMode = Cue.writingMode.VERTICAL_LEFT_TO_RIGHT;
} else if (writingMode == 'tbrl') {
cue.writingMode = Cue.writingMode.VERTICAL_RIGHT_TO_LEFT;
} else if (writingMode == 'rltb' || writingMode == 'rl') {
cue.direction = Cue.direction.HORIZONTAL_RIGHT_TO_LEFT;
} else if (writingMode) {
cue.direction = Cue.direction.HORIZONTAL_LEFT_TO_RIGHT;
}
let align = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'textAlign');
if (align) {
cue.positionAlign = TtmlTextParser.textAlignToPositionAlign_[align];
cue.lineAlign = TtmlTextParser.textAlignToLineAlign_[align];
goog.asserts.assert(align.toUpperCase() in Cue.textAlign,
align.toUpperCase() +
' Should be in Cue.textAlign values!');
cue.textAlign = Cue.textAlign[align.toUpperCase()];
}
let displayAlign = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'displayAlign');
if (displayAlign) {
goog.asserts.assert(displayAlign.toUpperCase() in Cue.displayAlign,
displayAlign.toUpperCase() +
' Should be in Cue.displayAlign values!');
cue.displayAlign = Cue.displayAlign[displayAlign.toUpperCase()];
}
let color = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'color');
if (color) {
cue.color = color;
}
let backgroundColor = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'backgroundColor');
if (backgroundColor) {
cue.backgroundColor = backgroundColor;
}
let fontFamily = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'fontFamily');
if (fontFamily) {
cue.fontFamily = fontFamily;
}
let fontWeight = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'fontWeight');
if (fontWeight && fontWeight == 'bold') {
cue.fontWeight = Cue.fontWeight.BOLD;
}
let wrapOption = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'wrapOption');
if (wrapOption && wrapOption == 'noWrap') {
cue.wrapLine = false;
}
let lineHeight = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'lineHeight');
if (lineHeight && lineHeight.match(TtmlTextParser.unitValues_)) {
cue.lineHeight = lineHeight;
}
let fontSize = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'fontSize');
if (fontSize && fontSize.match(TtmlTextParser.unitValues_)) {
cue.fontSize = fontSize;
}
let fontStyle = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'fontStyle');
if (fontStyle) {
goog.asserts.assert(fontStyle.toUpperCase() in Cue.fontStyle,
fontStyle.toUpperCase() +
' Should be in Cue.fontStyle values!');
cue.fontStyle = Cue.fontStyle[fontStyle.toUpperCase()];
}
if (imageElement) {
const backgroundImageType = imageElement.getAttribute('imagetype');
const backgroundImageEncoding = imageElement.getAttribute('encoding');
const backgroundImageData = imageElement.textContent;
if (backgroundImageType == 'PNG' &&
backgroundImageEncoding == 'Base64' &&
backgroundImageData) {
cue.backgroundImage = 'data:image/png;base64,' + backgroundImageData;
}
}
// Text decoration is an array of values which can come both from the
// element's style or be inherited from elements' parent nodes. All of those
// values should be applied as long as they don't contradict each other. If
// they do, elements' own style gets preference.
let textDecorationRegion = TtmlTextParser.getStyleAttributeFromRegion_(
region, styles, 'textDecoration');
if (textDecorationRegion) {
TtmlTextParser.addTextDecoration_(cue, textDecorationRegion);
}
let textDecorationElement = TtmlTextParser.getStyleAttributeFromElement_(
cueElement, styles, 'textDecoration');
if (textDecorationElement) {
TtmlTextParser.addTextDecoration_(cue, textDecorationElement);
}
};
/**
* Parses text decoration values and adds/removes them to/from the cue.
*
* @param {!shaka.text.Cue} cue
* @param {string} decoration
* @private
*/
shaka.text.TtmlTextParser.addTextDecoration_ = function(cue, decoration) {
const Cue = shaka.text.Cue;
let values = decoration.split(' ');
for (let i = 0; i < values.length; i++) {
switch (values[i]) {
case 'underline':
if (!cue.textDecoration.includes(Cue.textDecoration.UNDERLINE)) {
cue.textDecoration.push(Cue.textDecoration.UNDERLINE);
}
break;
case 'noUnderline':
if (cue.textDecoration.includes(Cue.textDecoration.UNDERLINE)) {
shaka.util.ArrayUtils.remove(cue.textDecoration,
Cue.textDecoration.UNDERLINE);
}
break;
case 'lineThrough':
if (!cue.textDecoration.includes(Cue.textDecoration.LINE_THROUGH)) {
cue.textDecoration.push(Cue.textDecoration.LINE_THROUGH);
}
break;
case 'noLineThrough':
if (cue.textDecoration.includes(Cue.textDecoration.LINE_THROUGH)) {
shaka.util.ArrayUtils.remove(cue.textDecoration,
Cue.textDecoration.LINE_THROUGH);
}
break;
case 'overline':
if (!cue.textDecoration.includes(Cue.textDecoration.OVERLINE)) {
cue.textDecoration.push(Cue.textDecoration.OVERLINE);
}
break;
case 'noOverline':
if (cue.textDecoration.includes(Cue.textDecoration.OVERLINE)) {
shaka.util.ArrayUtils.remove(cue.textDecoration,
Cue.textDecoration.OVERLINE);
}
break;
}
}
};
/**
* Finds a specified attribute on either the original cue element or its
* associated region and returns the value if the attribute was found.
*
* @param {!Element} cueElement
* @param {Element} region
* @param {!Array.<!Element>} styles
* @param {string} attribute
* @return {?string}
* @private
*/
shaka.text.TtmlTextParser.getStyleAttribute_ = function(
cueElement, region, styles, attribute) {
// An attribute can be specified on region level or in a styling block
// associated with the region or original element.
const TtmlTextParser = shaka.text.TtmlTextParser;
let attr = TtmlTextParser.getStyleAttributeFromElement_(
cueElement, styles, attribute);
if (attr) {
return attr;
}
return TtmlTextParser.getStyleAttributeFromRegion_(
region, styles, attribute);
};
/**
* Finds a specified attribute on the element's associated region
* and returns the value if the attribute was found.
*
* @param {Element} region
* @param {!Array.<!Element>} styles
* @param {string} attribute
* @return {?string}
* @private
*/
shaka.text.TtmlTextParser.getStyleAttributeFromRegion_ = function(
region, styles, attribute) {
const XmlUtils = shaka.util.XmlUtils;
const ttsNs = shaka.text.TtmlTextParser.styleNs_;
let regionChildren = shaka.text.TtmlTextParser.getLeafNodes_(region);
for (let i = 0; i < regionChildren.length; i++) {
let attr = XmlUtils.getAttributeNS(regionChildren[i], ttsNs, attribute);
if (attr) {
return attr;
}
}
let style = shaka.text.TtmlTextParser.getElementFromCollection_(
region, 'style', styles, /* prefix= */ '');
if (style) {
return XmlUtils.getAttributeNS(style, ttsNs, attribute);
}
return null;
};
/**
* Finds a specified attribute on the cue element and returns the value
* if the attribute was found.
*
* @param {!Element} cueElement
* @param {!Array.<!Element>} styles
* @param {string} attribute
* @return {?string}
* @private
*/
shaka.text.TtmlTextParser.getStyleAttributeFromElement_ = function(
cueElement, styles, attribute) {
const XmlUtils = shaka.util.XmlUtils;
const ttsNs = shaka.text.TtmlTextParser.styleNs_;
let getElementFromCollection_ =
shaka.text.TtmlTextParser.getElementFromCollection_;
let style = getElementFromCollection_(
cueElement, 'style', styles, /* prefix= */ '');
if (style) {
return XmlUtils.getAttributeNS(style, ttsNs, attribute);
}
return null;
};
/**
* Selects an item from |collection| whose id matches |attributeName|
* from |element|.
*
* @param {Element} element
* @param {string} attributeName
* @param {!Array.<Element>} collection
* @param {string} prefixName
* @return {Element}
* @private
*/
shaka.text.TtmlTextParser.getElementFromCollection_ = function(
element, attributeName, collection, prefixName) {
if (!element || collection.length < 1) {
return null;
}
let item = null;
let itemName = shaka.text.TtmlTextParser.getInheritedAttribute_(
element, attributeName);
if (itemName) {
for (let i = 0; i < collection.length; i++) {
if ((prefixName + collection[i].getAttribute('xml:id')) == itemName) {
item = collection[i];
break;
}
}
}
return item;
};
/**
* Traverses upwards from a given node until a given attribute is found.
*
* @param {!Element} element
* @param {string} attributeName
* @return {?string}
* @private
*/
shaka.text.TtmlTextParser.getInheritedAttribute_ = function(
element, attributeName) {
let ret = null;
while (element) {
ret = element.getAttribute(attributeName);
if (ret) {
break;
}
// Element.parentNode can lead to XMLDocument, which is not an Element and
// has no getAttribute().
let parentNode = element.parentNode;
if (parentNode instanceof Element) {
element = parentNode;
} else {
break;
}
}
return ret;
};
/**
* Parses a TTML time from the given word.
*
* @param {string} text
* @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
* @return {?number}
* @private
*/
shaka.text.TtmlTextParser.parseTime_ = function(text, rateInfo) {
let ret = null;
const TtmlTextParser = shaka.text.TtmlTextParser;
if (TtmlTextParser.timeColonFormatFrames_.test(text)) {
ret = TtmlTextParser.parseColonTimeWithFrames_(rateInfo, text);
} else if (TtmlTextParser.timeColonFormat_.test(text)) {
ret = TtmlTextParser.parseTimeFromRegex_(
TtmlTextParser.timeColonFormat_, text);
} else if (TtmlTextParser.timeColonFormatMilliseconds_.test(text)) {
ret = TtmlTextParser.parseTimeFromRegex_(
TtmlTextParser.timeColonFormatMilliseconds_, text);
} else if (TtmlTextParser.timeFramesFormat_.test(text)) {
ret = TtmlTextParser.parseFramesTime_(rateInfo, text);
} else if (TtmlTextParser.timeTickFormat_.test(text)) {
ret = TtmlTextParser.parseTickTime_(rateInfo, text);
} else if (TtmlTextParser.timeHMSFormat_.test(text)) {
ret = TtmlTextParser.parseTimeFromRegex_(
TtmlTextParser.timeHMSFormat_, text);
}
return ret;
};
/**
* Parses a TTML time in frame format.
*
* @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
* @param {string} text
* @return {?number}
* @private
*/
shaka.text.TtmlTextParser.parseFramesTime_ = function(rateInfo, text) {
// 75f or 75.5f
let results = shaka.text.TtmlTextParser.timeFramesFormat_.exec(text);
let frames = Number(results[1]);
return frames / rateInfo.frameRate;
};
/**
* Parses a TTML time in tick format.
*
* @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
* @param {string} text
* @return {?number}
* @private
*/
shaka.text.TtmlTextParser.parseTickTime_ = function(rateInfo, text) {
// 50t or 50.5t
let results = shaka.text.TtmlTextParser.timeTickFormat_.exec(text);
let ticks = Number(results[1]);
return ticks / rateInfo.tickRate;
};
/**
* Parses a TTML colon formatted time containing frames.
*
* @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
* @param {string} text
* @return {?number}
* @private
*/
shaka.text.TtmlTextParser.parseColonTimeWithFrames_ = function(
rateInfo, text) {
// 01:02:43:07 ('07' is frames) or 01:02:43:07.1 (subframes)
let results = shaka.text.TtmlTextParser.timeColonFormatFrames_.exec(text);
let hours = Number(results[1]);
let minutes = Number(results[2]);
let seconds = Number(results[3]);
let frames = Number(results[4]);
let subframes = Number(results[5]) || 0;
frames += subframes / rateInfo.subFrameRate;
seconds += frames / rateInfo.frameRate;
return seconds + (minutes * 60) + (hours * 3600);
};
/**
* Parses a TTML time with a given regex. Expects regex to be some
* sort of a time-matcher to match hours, minutes, seconds and milliseconds
*
* @param {!RegExp} regex
* @param {string} text
* @return {?number}
* @private
*/
shaka.text.TtmlTextParser.parseTimeFromRegex_ = function(regex, text) {
let results = regex.exec(text);
if (results == null || results[0] == '') {
return null;
}
// This capture is optional, but will still be in the array as undefined,
// in which case it is 0.
let hours = Number(results[1]) || 0;
let minutes = Number(results[2]) || 0;
let seconds = Number(results[3]) || 0;
let miliseconds = Number(results[4]) || 0;
return (miliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
};
/**
* Contains information about frame/subframe rate
* and frame rate multiplier for time in frame format.
*
* @example 01:02:03:04(4 frames) or 01:02:03:04.1(4 frames, 1 subframe)
* @param {?string} frameRate
* @param {?string} subFrameRate
* @param {?string} frameRateMultiplier
* @param {?string} tickRate
* @constructor
* @struct
* @private
*/
shaka.text.TtmlTextParser.RateInfo_ = function(
frameRate, subFrameRate, frameRateMultiplier, tickRate) {
/**
* @type {number}
*/
this.frameRate = Number(frameRate) || 30;
/**
* @type {number}
*/
this.subFrameRate = Number(subFrameRate) || 1;
/**
* @type {number}
*/
this.tickRate = Number(tickRate);
if (this.tickRate == 0) {
if (frameRate) {
this.tickRate = this.frameRate * this.subFrameRate;
} else {
this.tickRate = 1;
}
}
if (frameRateMultiplier) {
const multiplierResults = /^(\d+) (\d+)$/g.exec(frameRateMultiplier);
if (multiplierResults) {
const numerator = Number(multiplierResults[1]);
const denominator = Number(multiplierResults[2]);
const multiplierNum = numerator / denominator;
this.frameRate *= multiplierNum;
}
}
};
shaka.text.TextEngine.registerParser(
'application/ttml+xml',
shaka.text.TtmlTextParser);