mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-15 16:06:41 +03:00
5ddb92a4ee
Issue #1157 Change-Id: I33df747b0fad0e6d8f5e3d7dc8f5c93bd07800b1
947 lines
30 KiB
JavaScript
947 lines
30 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');
|
|
|
|
|
|
/**
|
|
* @implements {shaka.extern.TextParser}
|
|
*/
|
|
shaka.text.TtmlTextParser = class {
|
|
/** @override */
|
|
parseInit(data) {
|
|
goog.asserts.assert(false, 'TTML does not have init segments');
|
|
}
|
|
|
|
/** @override */
|
|
parseMedia(data, time) {
|
|
const TtmlTextParser = shaka.text.TtmlTextParser;
|
|
const XmlUtils = shaka.util.XmlUtils;
|
|
const ttpNs = TtmlTextParser.parameterNs_;
|
|
const str = shaka.util.StringUtils.fromUTF8(data);
|
|
const ret = [];
|
|
const 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;
|
|
const tts = xml.getElementsByTagName('tt');
|
|
const 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);
|
|
}
|
|
const whitespaceTrim = spaceStyle == 'default';
|
|
|
|
const rateInfo = new TtmlTextParser.RateInfo_(
|
|
frameRate, subFrameRate, frameRateMultiplier, tickRate);
|
|
|
|
const metadataElements = TtmlTextParser.getLeafNodes_(
|
|
tt.getElementsByTagName('metadata')[0]);
|
|
const styles = TtmlTextParser.getLeafNodes_(
|
|
tt.getElementsByTagName('styling')[0]);
|
|
const regionElements = TtmlTextParser.getLeafNodes_(
|
|
tt.getElementsByTagName('layout')[0]);
|
|
const cueRegions = [];
|
|
for (let i = 0; i < regionElements.length; i++) {
|
|
const cueRegion = TtmlTextParser.parseCueRegion_(
|
|
regionElements[i], styles, extent);
|
|
if (cueRegion) {
|
|
cueRegions.push(cueRegion);
|
|
}
|
|
}
|
|
|
|
const textNodes = TtmlTextParser.getLeafNodes_(
|
|
tt.getElementsByTagName('body')[0]);
|
|
|
|
for (let i = 0; i < textNodes.length; i++) {
|
|
const cue = TtmlTextParser.parseCue_(
|
|
textNodes[i], time.periodStart, rateInfo, metadataElements, styles,
|
|
regionElements, cueRegions, whitespaceTrim);
|
|
if (cue) {
|
|
ret.push(cue);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
static getLeafNodes_(element) {
|
|
let result = [];
|
|
if (!element) {
|
|
return result;
|
|
}
|
|
|
|
const 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.
|
|
const 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!');
|
|
const 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
|
|
*/
|
|
static addNewLines_(element, whitespaceTrim) {
|
|
const 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
|
|
*/
|
|
static parseCue_(
|
|
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);
|
|
const duration = shaka.text.TtmlTextParser.parseTime_(
|
|
cueElement.getAttribute('dur'), rateInfo);
|
|
const 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;
|
|
|
|
const cue = new shaka.text.Cue(start, end, payload);
|
|
|
|
// Get other properties if available.
|
|
const regionElement = shaka.text.TtmlTextParser.getElementFromCollection_(
|
|
cueElement, 'region', regionElements, /* prefix= */ '');
|
|
if (regionElement && regionElement.getAttribute('xml:id')) {
|
|
const regionId = regionElement.getAttribute('xml:id');
|
|
const regionsWithId = cueRegions.filter((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
|
|
*/
|
|
static parseCueRegion_(regionElement, styles, globalExtent) {
|
|
const TtmlTextParser = shaka.text.TtmlTextParser;
|
|
const region = new shaka.text.CueRegion();
|
|
const 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;
|
|
const 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;
|
|
}
|
|
}
|
|
|
|
const 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
|
|
*/
|
|
static addStyle_(cue, cueElement, region, imageElement, styles) {
|
|
const TtmlTextParser = shaka.text.TtmlTextParser;
|
|
const Cue = shaka.text.Cue;
|
|
|
|
const 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.
|
|
const 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;
|
|
}
|
|
|
|
const 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()];
|
|
}
|
|
|
|
const 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()];
|
|
}
|
|
|
|
const color = TtmlTextParser.getStyleAttribute_(
|
|
cueElement, region, styles, 'color');
|
|
if (color) {
|
|
cue.color = color;
|
|
}
|
|
|
|
const backgroundColor = TtmlTextParser.getStyleAttribute_(
|
|
cueElement, region, styles, 'backgroundColor');
|
|
if (backgroundColor) {
|
|
cue.backgroundColor = backgroundColor;
|
|
}
|
|
|
|
const fontFamily = TtmlTextParser.getStyleAttribute_(
|
|
cueElement, region, styles, 'fontFamily');
|
|
if (fontFamily) {
|
|
cue.fontFamily = fontFamily;
|
|
}
|
|
|
|
const fontWeight = TtmlTextParser.getStyleAttribute_(
|
|
cueElement, region, styles, 'fontWeight');
|
|
if (fontWeight && fontWeight == 'bold') {
|
|
cue.fontWeight = Cue.fontWeight.BOLD;
|
|
}
|
|
|
|
const wrapOption = TtmlTextParser.getStyleAttribute_(
|
|
cueElement, region, styles, 'wrapOption');
|
|
if (wrapOption && wrapOption == 'noWrap') {
|
|
cue.wrapLine = false;
|
|
}
|
|
|
|
const lineHeight = TtmlTextParser.getStyleAttribute_(
|
|
cueElement, region, styles, 'lineHeight');
|
|
if (lineHeight && lineHeight.match(TtmlTextParser.unitValues_)) {
|
|
cue.lineHeight = lineHeight;
|
|
}
|
|
|
|
const fontSize = TtmlTextParser.getStyleAttribute_(
|
|
cueElement, region, styles, 'fontSize');
|
|
if (fontSize && fontSize.match(TtmlTextParser.unitValues_)) {
|
|
cue.fontSize = fontSize;
|
|
}
|
|
|
|
const 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.
|
|
const textDecorationRegion = TtmlTextParser.getStyleAttributeFromRegion_(
|
|
region, styles, 'textDecoration');
|
|
if (textDecorationRegion) {
|
|
TtmlTextParser.addTextDecoration_(cue, textDecorationRegion);
|
|
}
|
|
|
|
const 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
|
|
*/
|
|
static addTextDecoration_(cue, decoration) {
|
|
const Cue = shaka.text.Cue;
|
|
const 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
|
|
*/
|
|
static getStyleAttribute_(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;
|
|
const 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
|
|
*/
|
|
static getStyleAttributeFromRegion_(region, styles, attribute) {
|
|
const XmlUtils = shaka.util.XmlUtils;
|
|
const ttsNs = shaka.text.TtmlTextParser.styleNs_;
|
|
|
|
const regionChildren = shaka.text.TtmlTextParser.getLeafNodes_(region);
|
|
for (let i = 0; i < regionChildren.length; i++) {
|
|
const attr = XmlUtils.getAttributeNS(regionChildren[i], ttsNs, attribute);
|
|
if (attr) {
|
|
return attr;
|
|
}
|
|
}
|
|
|
|
const 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
|
|
*/
|
|
static getStyleAttributeFromElement_(cueElement, styles, attribute) {
|
|
const XmlUtils = shaka.util.XmlUtils;
|
|
const ttsNs = shaka.text.TtmlTextParser.styleNs_;
|
|
|
|
const getElementFromCollection_ =
|
|
shaka.text.TtmlTextParser.getElementFromCollection_;
|
|
const 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
|
|
*/
|
|
static getElementFromCollection_(
|
|
element, attributeName, collection, prefixName) {
|
|
if (!element || collection.length < 1) {
|
|
return null;
|
|
}
|
|
let item = null;
|
|
const 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
|
|
*/
|
|
static getInheritedAttribute_(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().
|
|
const 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
|
|
*/
|
|
static parseTime_(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
|
|
*/
|
|
static parseFramesTime_(rateInfo, text) {
|
|
// 75f or 75.5f
|
|
const results = shaka.text.TtmlTextParser.timeFramesFormat_.exec(text);
|
|
const 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
|
|
*/
|
|
static parseTickTime_(rateInfo, text) {
|
|
// 50t or 50.5t
|
|
const results = shaka.text.TtmlTextParser.timeTickFormat_.exec(text);
|
|
const 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
|
|
*/
|
|
static parseColonTimeWithFrames_(rateInfo, text) {
|
|
// 01:02:43:07 ('07' is frames) or 01:02:43:07.1 (subframes)
|
|
const results = shaka.text.TtmlTextParser.timeColonFormatFrames_.exec(text);
|
|
|
|
const hours = Number(results[1]);
|
|
const minutes = Number(results[2]);
|
|
let seconds = Number(results[3]);
|
|
let frames = Number(results[4]);
|
|
const 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
|
|
*/
|
|
static parseTimeFromRegex_(regex, text) {
|
|
const 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.
|
|
const hours = Number(results[1]) || 0;
|
|
const minutes = Number(results[2]) || 0;
|
|
const seconds = Number(results[3]) || 0;
|
|
const miliseconds = Number(results[4]) || 0;
|
|
|
|
return (miliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @summary
|
|
* 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)
|
|
* @private
|
|
*/
|
|
shaka.text.TtmlTextParser.RateInfo_ = class {
|
|
/**
|
|
* @param {?string} frameRate
|
|
* @param {?string} subFrameRate
|
|
* @param {?string} frameRateMultiplier
|
|
* @param {?string} tickRate
|
|
*/
|
|
constructor(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;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @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,
|
|
};
|
|
|
|
/**
|
|
* @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';
|
|
|
|
shaka.text.TextEngine.registerParser(
|
|
'application/ttml+xml', shaka.text.TtmlTextParser);
|