Files
shaka-player/lib/text/ttml_text_parser.js
T
Joey Parrish 7091275cbf Replace indexOf with includes, startsWith
This replaces almost every instance of indexOf on both String and
Array.  There are very few places where we really wanted an index.
Mostly, indexOf was used to check for inclusion.

Change-Id: I08e299768b6ffdb4bfc30b39b5d82a058c6d1b56
2018-09-14 19:10:56 +00:00

910 lines
26 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');
/**
* @constructor
* @implements {shaka.extern.TextParser}
*/
shaka.text.TtmlTextParser = function() {};
/** @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;
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 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 = tt.getAttribute('ttp:frameRate');
subFrameRate = tt.getAttribute('ttp:subFrameRate');
frameRateMultiplier = tt.getAttribute('ttp:frameRateMultiplier');
tickRate = tt.getAttribute('ttp:tickRate');
spaceStyle = tt.getAttribute('xml:space') || 'default';
}
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);
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);
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,
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>} 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, 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);
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];
}
shaka.text.TtmlTextParser.addStyle_(cue, cueElement, regionElement, styles);
return cue;
};
/**
* Parses an Element into a TextTrackCue or VTTCue.
*
* @param {!Element} regionElement
* @param {!Array.<!Element>} styles
* @return {shaka.text.CueRegion}
* @private
*/
shaka.text.TtmlTextParser.parseCueRegion_ = function(regionElement, styles) {
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 results = null;
let percentage = null;
let extent = TtmlTextParser.getStyleAttributeFromRegion_(
regionElement, styles, 'tts:extent');
if (extent) {
percentage = TtmlTextParser.percentValues_.exec(extent);
results = percentage || TtmlTextParser.pixelValues_.exec(extent);
if (results != null) {
region.width = Number(results[1]);
region.height = Number(results[2]);
region.widthUnits = percentage ?
shaka.text.CueRegion.units.PERCENTAGE :
shaka.text.CueRegion.units.PX;
region.heightUnits = percentage ?
shaka.text.CueRegion.units.PERCENTAGE :
shaka.text.CueRegion.units.PX;
}
}
let origin = TtmlTextParser.getStyleAttributeFromRegion_(
regionElement, styles, 'tts:origin');
if (origin) {
percentage = TtmlTextParser.percentValues_.exec(origin);
results = percentage || TtmlTextParser.pixelValues_.exec(origin);
if (results != null) {
region.viewportAnchorX = Number(results[1]);
region.viewportAnchorY = Number(results[2]);
region.viewportAnchorUnits = percentage ?
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 {!Array.<!Element>} styles
* @private
*/
shaka.text.TtmlTextParser.addStyle_ = function(
cue, cueElement, region, styles) {
const TtmlTextParser = shaka.text.TtmlTextParser;
const Cue = shaka.text.Cue;
let direction = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:direction');
if (direction == 'rtl') {
cue.writingDirection = Cue.writingDirection.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, 'tts:writingMode');
if (writingMode == 'tb' || writingMode == 'tblr') {
cue.writingDirection = Cue.writingDirection.VERTICAL_LEFT_TO_RIGHT;
} else if (writingMode == 'tbrl') {
cue.writingDirection = Cue.writingDirection.VERTICAL_RIGHT_TO_LEFT;
} else if (writingMode == 'rltb' || writingMode == 'rl') {
cue.writingDirection = Cue.writingDirection.HORIZONTAL_RIGHT_TO_LEFT;
} else if (writingMode) {
cue.writingDirection = Cue.writingDirection.HORIZONTAL_LEFT_TO_RIGHT;
}
let align = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts: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, 'tts: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, 'tts:color');
if (color) {
cue.color = color;
}
let backgroundColor = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:backgroundColor');
if (backgroundColor) {
cue.backgroundColor = backgroundColor;
}
let fontFamily = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:fontFamily');
if (fontFamily) {
cue.fontFamily = fontFamily;
}
let fontWeight = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:fontWeight');
if (fontWeight && fontWeight == 'bold') {
cue.fontWeight = Cue.fontWeight.BOLD;
}
let wrapOption = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:wrapOption');
if (wrapOption && wrapOption == 'noWrap') {
cue.wrapLine = false;
}
let lineHeight = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:lineHeight');
if (lineHeight && lineHeight.match(TtmlTextParser.unitValues_)) {
cue.lineHeight = lineHeight;
}
let fontSize = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:fontSize');
if (fontSize && fontSize.match(TtmlTextParser.unitValues_)) {
cue.fontSize = fontSize;
}
let fontStyle = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts: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()];
}
// 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, 'tts:textDecoration');
if (textDecorationRegion) {
TtmlTextParser.addTextDecoration_(cue, textDecorationRegion);
}
let textDecorationElement = TtmlTextParser.getStyleAttributeFromElement_(
cueElement, styles, 'tts: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) {
let regionChildren = shaka.text.TtmlTextParser.getLeafNodes_(region);
for (let i = 0; i < regionChildren.length; i++) {
let attr = regionChildren[i].getAttribute(attribute);
if (attr) {
return attr;
}
}
let style = shaka.text.TtmlTextParser.getElementFromCollection_(
region, 'style', styles);
if (style) {
return style.getAttribute(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) {
let getElementFromCollection_ =
shaka.text.TtmlTextParser.getElementFromCollection_;
let style = getElementFromCollection_(cueElement, 'style', styles);
if (style) {
return style.getAttribute(attribute);
}
return null;
};
/**
* Selects an item from |collection| whose id matches |attributeName|
* from |element|.
*
* @param {Element} element
* @param {string} attributeName
* @param {!Array.<Element>} collection
* @return {Element}
* @private
*/
shaka.text.TtmlTextParser.getElementFromCollection_ = function(
element, attributeName, collection) {
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 (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) {
let multiplierResults = /^(\d+) (\d+)$/g.exec(frameRateMultiplier);
if (multiplierResults) {
let numerator = multiplierResults[1];
let denominator = multiplierResults[2];
let multiplierNum = numerator / denominator;
this.frameRate *= multiplierNum;
}
}
};
shaka.text.TextEngine.registerParser(
'application/ttml+xml',
shaka.text.TtmlTextParser);