mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
959 lines
25 KiB
JavaScript
959 lines
25 KiB
JavaScript
/*! @license
|
||
* Shaka Player
|
||
* Copyright 2016 Google LLC
|
||
* SPDX-License-Identifier: Apache-2.0
|
||
*/
|
||
|
||
goog.provide('shaka.text.Cue');
|
||
|
||
goog.require('shaka.log');
|
||
goog.require('shaka.text.CueRegion');
|
||
goog.require('shaka.util.ArrayUtils');
|
||
goog.require('shaka.util.StringUtils');
|
||
goog.require('shaka.util.TextParser');
|
||
goog.require('shaka.util.TXml');
|
||
|
||
|
||
/**
|
||
* @export
|
||
*/
|
||
shaka.text.Cue = class {
|
||
/**
|
||
* @param {number} startTime
|
||
* @param {number} endTime
|
||
* @param {string} payload
|
||
*/
|
||
constructor(startTime, endTime, payload) {
|
||
const Cue = shaka.text.Cue;
|
||
|
||
/**
|
||
* The start time of the cue in seconds, relative to the start of the
|
||
* presentation.
|
||
* @type {number}
|
||
* @export
|
||
*/
|
||
this.startTime = startTime;
|
||
|
||
/**
|
||
* The end time of the cue in seconds, relative to the start of the
|
||
* presentation.
|
||
* @type {number}
|
||
* @export
|
||
*/
|
||
this.endTime = endTime;
|
||
|
||
/**
|
||
* The text payload of the cue. If nestedCues is non-empty, this should be
|
||
* empty. Top-level block containers should have no payload of their own.
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.payload = payload;
|
||
|
||
/**
|
||
* The region to render the cue into. Only supported on top-level cues,
|
||
* because nested cues are inline elements.
|
||
* @type {shaka.text.CueRegion}
|
||
* @export
|
||
*/
|
||
this.region = new shaka.text.CueRegion();
|
||
|
||
/**
|
||
* The indent (in percent) of the cue box in the direction defined by the
|
||
* writing direction.
|
||
* @type {?number}
|
||
* @export
|
||
*/
|
||
this.position = null;
|
||
|
||
/**
|
||
* Position alignment of the cue.
|
||
* @type {shaka.text.Cue.positionAlign}
|
||
* @export
|
||
*/
|
||
this.positionAlign = Cue.positionAlign.AUTO;
|
||
|
||
/**
|
||
* Size of the cue box (in percents), where 0 means "auto".
|
||
* @type {number}
|
||
* @export
|
||
*/
|
||
this.size = 0;
|
||
|
||
/**
|
||
* Alignment of the text inside the cue box.
|
||
* @type {shaka.text.Cue.textAlign}
|
||
* @export
|
||
*/
|
||
this.textAlign = Cue.textAlign.CENTER;
|
||
|
||
/**
|
||
* Text direction of the cue.
|
||
* @type {shaka.text.Cue.direction}
|
||
* @export
|
||
*/
|
||
this.direction = Cue.direction.HORIZONTAL_LEFT_TO_RIGHT;
|
||
|
||
/**
|
||
* Text writing mode of the cue.
|
||
* @type {shaka.text.Cue.writingMode}
|
||
* @export
|
||
*/
|
||
this.writingMode = Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM;
|
||
|
||
/**
|
||
* The way to interpret line field. (Either as an integer line number or
|
||
* percentage from the display box).
|
||
* @type {shaka.text.Cue.lineInterpretation}
|
||
* @export
|
||
*/
|
||
this.lineInterpretation = Cue.lineInterpretation.LINE_NUMBER;
|
||
|
||
/**
|
||
* The offset from the display box in either number of lines or
|
||
* percentage depending on the value of lineInterpretation.
|
||
* @type {?number}
|
||
* @export
|
||
*/
|
||
this.line = null;
|
||
|
||
/**
|
||
* Separation between line areas inside the cue box in px or em
|
||
* (e.g. '100px'/'100em'). If not specified, this should be no less than
|
||
* the largest font size applied to the text in the cue.
|
||
* @type {string}.
|
||
* @export
|
||
*/
|
||
this.lineHeight = '';
|
||
|
||
/**
|
||
* Line alignment of the cue box.
|
||
* Start alignment means the cue box’s top side (for horizontal cues), left
|
||
* side (for vertical growing right), or right side (for vertical growing
|
||
* left) is aligned at the line.
|
||
* Center alignment means the cue box is centered at the line.
|
||
* End alignment The cue box’s bottom side (for horizontal cues), right side
|
||
* (for vertical growing right), or left side (for vertical growing left) is
|
||
* aligned at the line.
|
||
* @type {shaka.text.Cue.lineAlign}
|
||
* @export
|
||
*/
|
||
this.lineAlign = Cue.lineAlign.START;
|
||
|
||
/**
|
||
* Vertical alignments of the cues within their extents.
|
||
* 'BEFORE' means displaying the captions at the top of the text display
|
||
* container box, 'CENTER' means in the middle, 'AFTER' means at the bottom.
|
||
* @type {shaka.text.Cue.displayAlign}
|
||
* @export
|
||
*/
|
||
this.displayAlign = Cue.displayAlign.AFTER;
|
||
|
||
/**
|
||
* Text color as a CSS color, e.g. "#FFFFFF" or "white".
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.color = '';
|
||
|
||
/**
|
||
* Text background color as a CSS color, e.g. "#FFFFFF" or "white".
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.backgroundColor = '';
|
||
|
||
/**
|
||
* The URL of the background image, e.g. "data:[mime type];base64,[data]".
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.backgroundImage = '';
|
||
|
||
/**
|
||
* The border around this cue as a CSS border.
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.border = '';
|
||
|
||
/**
|
||
* Text font size in px or em (e.g. '100px'/'100em').
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.fontSize = '';
|
||
|
||
/**
|
||
* Text font weight. Either normal or bold.
|
||
* @type {shaka.text.Cue.fontWeight}
|
||
* @export
|
||
*/
|
||
this.fontWeight = Cue.fontWeight.NORMAL;
|
||
|
||
/**
|
||
* Text font style. Normal, italic or oblique.
|
||
* @type {shaka.text.Cue.fontStyle}
|
||
* @export
|
||
*/
|
||
this.fontStyle = Cue.fontStyle.NORMAL;
|
||
|
||
/**
|
||
* Text font family.
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.fontFamily = '';
|
||
|
||
/**
|
||
* Text letter spacing as a CSS letter-spacing value.
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.letterSpacing = '';
|
||
|
||
/**
|
||
* Text line padding as a CSS line-padding value.
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.linePadding = '';
|
||
|
||
/**
|
||
* Opacity of the cue element, from 0-1.
|
||
* @type {number}
|
||
* @export
|
||
*/
|
||
this.opacity = 1;
|
||
|
||
/**
|
||
* Text combine upright as a CSS text-combine-upright value.
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.textCombineUpright = '';
|
||
|
||
/**
|
||
* Text decoration. A combination of underline, overline
|
||
* and line through. Empty array means no decoration.
|
||
* @type {!Array<!shaka.text.Cue.textDecoration>}
|
||
* @export
|
||
*/
|
||
this.textDecoration = [];
|
||
|
||
/**
|
||
* Text shadow color as a CSS text-shadow value.
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.textShadow = '';
|
||
|
||
/**
|
||
* Text stroke color as a CSS color, e.g. "#FFFFFF" or "white".
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.textStrokeColor = '';
|
||
|
||
/**
|
||
* Text stroke width as a CSS stroke-width value.
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.textStrokeWidth = '';
|
||
|
||
/**
|
||
* Whether or not line wrapping should be applied to the cue.
|
||
* @type {boolean}
|
||
* @export
|
||
*/
|
||
this.wrapLine = true;
|
||
|
||
/**
|
||
* Id of the cue.
|
||
* @type {string}
|
||
* @export
|
||
*/
|
||
this.id = '';
|
||
|
||
/**
|
||
* Nested cues, which should be laid out horizontally in one block.
|
||
* Top-level cues are blocks, and nested cues are inline elements.
|
||
* Cues can be nested arbitrarily deeply.
|
||
* @type {!Array<!shaka.text.Cue>}
|
||
* @export
|
||
*/
|
||
this.nestedCues = [];
|
||
|
||
/**
|
||
* If true, this represents a container element that is "above" the main
|
||
* cues. For example, the <body> and <div> tags that contain the <p> tags
|
||
* in a TTML file. This controls the flow of the final cues; any nested cues
|
||
* within an "isContainer" cue will be laid out as separate lines.
|
||
* @type {boolean}
|
||
* @export
|
||
*/
|
||
this.isContainer = false;
|
||
|
||
/**
|
||
* Whether or not the cue only acts as a line break between two nested cues.
|
||
* Should only appear in nested cues.
|
||
* @type {boolean}
|
||
* @export
|
||
*/
|
||
this.lineBreak = false;
|
||
|
||
/**
|
||
* Used to indicate the type of ruby tag that should be used when rendering
|
||
* the cue. Valid values: ruby, rp, rt.
|
||
* @type {?string}
|
||
* @export
|
||
*/
|
||
this.rubyTag = null;
|
||
|
||
/**
|
||
* The number of horizontal and vertical cells into which the Root Container
|
||
* Region area is divided.
|
||
*
|
||
* @type {{ columns: number, rows: number }}
|
||
* @export
|
||
*/
|
||
this.cellResolution = {
|
||
columns: 32,
|
||
rows: 15,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @param {number} start
|
||
* @param {number} end
|
||
* @return {!shaka.text.Cue}
|
||
*/
|
||
static lineBreak(start, end) {
|
||
const cue = new shaka.text.Cue(start, end, '');
|
||
cue.lineBreak = true;
|
||
return cue;
|
||
}
|
||
|
||
/**
|
||
* Create a copy of the cue with the same properties.
|
||
* @return {!shaka.text.Cue}
|
||
* @suppress {checkTypes} since we must use [] and "in" with a struct type.
|
||
* @export
|
||
*/
|
||
clone() {
|
||
const clone = new shaka.text.Cue(0, 0, '');
|
||
|
||
/**
|
||
* Deep clone helper
|
||
* @param {*} value
|
||
* @return {*}
|
||
*/
|
||
const deepClone = (value) => {
|
||
if (value === null || typeof value !== 'object') {
|
||
return value;
|
||
}
|
||
|
||
if (Array.isArray(value)) {
|
||
return value.map(deepClone);
|
||
}
|
||
|
||
if (value instanceof shaka.text.Cue) {
|
||
const cue = /** @type {shaka.text.Cue} */(value);
|
||
return cue.clone();
|
||
}
|
||
|
||
if (value instanceof shaka.text.CueRegion) {
|
||
const region = /** @type {shaka.text.CueRegion} */(value);
|
||
return region.clone();
|
||
}
|
||
|
||
const result = {};
|
||
for (const key in value) {
|
||
result[key] = deepClone(value[key]);
|
||
}
|
||
return result;
|
||
};
|
||
|
||
for (const k in this) {
|
||
clone[k] = deepClone(this[k]);
|
||
}
|
||
|
||
return clone;
|
||
}
|
||
|
||
/**
|
||
* Check if two Cues have all the same values in all properties.
|
||
* @param {!shaka.text.Cue} cue1
|
||
* @param {!shaka.text.Cue} cue2
|
||
* @return {boolean}
|
||
* @suppress {checkTypes} since we must use [] and "in" with a struct type.
|
||
* @export
|
||
*/
|
||
static equal(cue1, cue2) {
|
||
// Compare the start time, end time and payload of the cues first for
|
||
// performance optimization. We can avoid the more expensive recursive
|
||
// checks if the top-level properties don't match.
|
||
// See: https://github.com/shaka-project/shaka-player/issues/3018
|
||
if (cue1.payload != cue2.payload) {
|
||
return false;
|
||
}
|
||
const isDiffNegligible = (a, b) => Math.abs(a - b) < 0.001;
|
||
if (!isDiffNegligible(cue1.startTime, cue2.startTime) ||
|
||
!isDiffNegligible(cue1.endTime, cue2.endTime)) {
|
||
return false;
|
||
}
|
||
for (const k in cue1) {
|
||
if (k == 'startTime' || k == 'endTime' || k == 'payload') {
|
||
// Already compared.
|
||
} else if (k == 'nestedCues') {
|
||
// This uses shaka.text.Cue.equal rather than just this.equal, since
|
||
// otherwise recursing here will unbox the method and cause "this" to be
|
||
// undefined in deeper recursion.
|
||
if (!shaka.util.ArrayUtils.equal(
|
||
cue1.nestedCues, cue2.nestedCues, shaka.text.Cue.equal)) {
|
||
return false;
|
||
}
|
||
} else if (k == 'region' || k == 'cellResolution') {
|
||
for (const k2 in cue1[k]) {
|
||
if (cue1[k][k2] != cue2[k][k2]) {
|
||
return false;
|
||
}
|
||
}
|
||
} else if (Array.isArray(cue1[k])) {
|
||
if (!shaka.util.ArrayUtils.equal(cue1[k], cue2[k])) {
|
||
return false;
|
||
}
|
||
} else {
|
||
if (cue1[k] != cue2[k]) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Parses cue payload, searches for styling entities and, if needed,
|
||
* modifies original payload and creates nested cues to better represent
|
||
* styling found in payload. All changes are done in-place.
|
||
* @param {!shaka.text.Cue} cue
|
||
* @param {!Map<string, !shaka.text.Cue>=} styles
|
||
* @export
|
||
*/
|
||
static parseCuePayload(cue, styles = new Map()) {
|
||
const StringUtils = shaka.util.StringUtils;
|
||
const TXml = shaka.util.TXml;
|
||
let payload = cue.payload;
|
||
if (!payload.includes('<')) {
|
||
cue.payload = StringUtils.htmlUnescape(payload);
|
||
return;
|
||
}
|
||
if (styles.size === 0) {
|
||
shaka.text.Cue.addDefaultTextColor(styles);
|
||
}
|
||
payload = shaka.text.Cue.replaceKaraokeStylePayload_(payload);
|
||
payload = shaka.text.Cue.replaceVoiceStylePayload_(payload);
|
||
payload = shaka.text.Cue.escapeInvalidChevrons_(payload);
|
||
|
||
cue.payload = '';
|
||
|
||
const xmlPayload = '<span>' + payload.replace(/\n/g, '<br />') + '</span>';
|
||
let element;
|
||
try {
|
||
element = TXml.parseXmlString(xmlPayload, 'span');
|
||
} catch (e) {
|
||
shaka.log.warning('cue parse fail: ', e);
|
||
}
|
||
if (element) {
|
||
const childNodes = element.children;
|
||
if (childNodes.length == 1) {
|
||
const childNode = childNodes[0];
|
||
if (!TXml.isNode(childNode)) {
|
||
cue.payload = StringUtils.htmlUnescape(payload);
|
||
return;
|
||
}
|
||
}
|
||
for (const childNode of childNodes) {
|
||
shaka.text.Cue.generateCueFromElement_(childNode, cue, styles);
|
||
}
|
||
} else {
|
||
shaka.log.warning('The cue\'s markup could not be parsed: ', payload);
|
||
cue.payload = StringUtils.htmlUnescape(payload);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Add default color
|
||
*
|
||
* @param {!Map<string, !shaka.text.Cue>} styles
|
||
*/
|
||
static addDefaultTextColor(styles) {
|
||
const textColor = shaka.text.Cue.defaultTextColor;
|
||
for (const [key, value] of Object.entries(textColor)) {
|
||
const cue = new shaka.text.Cue(0, 0, '');
|
||
cue.color = value;
|
||
styles.set('.' + key, cue);
|
||
}
|
||
|
||
const bgColor = shaka.text.Cue.defaultTextBackgroundColor;
|
||
for (const [key, value] of Object.entries(bgColor)) {
|
||
const cue = new shaka.text.Cue(0, 0, '');
|
||
cue.backgroundColor = value;
|
||
styles.set('.' + key, cue);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Converts karaoke style tag to be valid for xml parsing
|
||
* For example,
|
||
* input: Text <00:00:00.450> time <00:00:01.450> 1
|
||
* output: Text <div time="00:00:00.450"> time
|
||
* <div time="00:00:01.450"> 1</div></div>
|
||
*
|
||
* @param {string} payload
|
||
* @return {string} processed payload
|
||
* @private
|
||
*/
|
||
static replaceKaraokeStylePayload_(payload) {
|
||
const names = [];
|
||
let nameStart = -1;
|
||
for (let i = 0; i < payload.length; i++) {
|
||
if (payload[i] === '<') {
|
||
nameStart = i + 1;
|
||
} else if (payload[i] === '>') {
|
||
if (nameStart > 0) {
|
||
const name = payload.substr(nameStart, i - nameStart);
|
||
if (name.match(shaka.text.Cue.timeFormat_)) {
|
||
names.push(name);
|
||
}
|
||
nameStart = -1;
|
||
}
|
||
}
|
||
}
|
||
let newPayload = payload;
|
||
for (const name of names) {
|
||
const replaceTag = '<' + name + '>';
|
||
const startTag = '<div time="' + name + '">';
|
||
const endTag = '</div>';
|
||
newPayload = newPayload.replace(replaceTag, startTag);
|
||
newPayload += endTag;
|
||
}
|
||
return newPayload;
|
||
}
|
||
|
||
/**
|
||
* Converts voice style tag to be valid for xml parsing
|
||
* For example,
|
||
* input: <v Shaka>Test
|
||
* output: <v.voice-Shaka>Test</v.voice-Shaka>
|
||
*
|
||
* @param {string} payload
|
||
* @return {string} processed payload
|
||
* @private
|
||
*/
|
||
static replaceVoiceStylePayload_(payload) {
|
||
const voiceTag = 'v';
|
||
const names = [];
|
||
let nameStart = -1;
|
||
let newPayload = '';
|
||
let hasVoiceEndTag = false;
|
||
for (let i = 0; i < payload.length; i++) {
|
||
// This condition is used to manage tags that have end tags.
|
||
if (payload[i] === '/') {
|
||
const end = payload.indexOf('>', i);
|
||
if (end === -1) {
|
||
return payload;
|
||
}
|
||
const tagEnd = payload.substring(i + 1, end);
|
||
if (!tagEnd || tagEnd != voiceTag) {
|
||
newPayload += payload[i];
|
||
continue;
|
||
}
|
||
hasVoiceEndTag = true;
|
||
let tagStart = null;
|
||
if (names.length) {
|
||
tagStart = names[names.length -1];
|
||
}
|
||
if (!tagStart) {
|
||
newPayload += payload[i];
|
||
} else if (tagStart === tagEnd) {
|
||
newPayload += '/' + tagEnd + '>';
|
||
i += tagEnd.length + 1;
|
||
} else {
|
||
if (!tagStart.startsWith(voiceTag)) {
|
||
newPayload += payload[i];
|
||
continue;
|
||
}
|
||
newPayload += '/' + tagStart + '>';
|
||
i += tagEnd.length + 1;
|
||
}
|
||
} else {
|
||
// Here we only want the tag name, not any other payload.
|
||
if (payload[i] === '<') {
|
||
nameStart = i + 1;
|
||
if (payload[nameStart] != voiceTag) {
|
||
nameStart = -1;
|
||
}
|
||
} else if (payload[i] === '>') {
|
||
if (nameStart > 0) {
|
||
names.push(payload.substr(nameStart, i - nameStart));
|
||
nameStart = -1;
|
||
}
|
||
}
|
||
newPayload += payload[i];
|
||
}
|
||
}
|
||
for (const name of names) {
|
||
const newName = name.replace(' ', '.voice-');
|
||
newPayload = newPayload.replace(`<${name}>`, `<${newName}>`);
|
||
newPayload = newPayload.replace(`</${name}>`, `</${newName}>`);
|
||
if (!hasVoiceEndTag) {
|
||
newPayload += `</${newName}>`;
|
||
}
|
||
}
|
||
return newPayload;
|
||
}
|
||
|
||
/**
|
||
* This method converts invalid > chevrons to HTML entities.
|
||
* It also removes < chevrons as per spec.
|
||
*
|
||
* @param {!string} input
|
||
* @return {string}
|
||
* @private
|
||
*/
|
||
static escapeInvalidChevrons_(input) {
|
||
// Used to map HTML entities to characters.
|
||
const htmlEscapes = {
|
||
'< ': '',
|
||
' >': ' >',
|
||
};
|
||
|
||
const reEscapedHtml = /(< +>|<\s|\s>)/g;
|
||
const reHasEscapedHtml = RegExp(reEscapedHtml.source);
|
||
// This check is an optimization, since replace always makes a copy
|
||
if (input && reHasEscapedHtml.test(input)) {
|
||
return input.replace(reEscapedHtml, (entity) => {
|
||
return htmlEscapes[entity] || '';
|
||
});
|
||
}
|
||
return input || '';
|
||
}
|
||
|
||
/**
|
||
* @param {!shaka.extern.xml.Node} element
|
||
* @param {!shaka.text.Cue} rootCue
|
||
* @param {!Map<string, !shaka.text.Cue>} styles
|
||
* @private
|
||
*/
|
||
static generateCueFromElement_(element, rootCue, styles) {
|
||
const TXml = shaka.util.TXml;
|
||
const nestedCue = rootCue.clone();
|
||
// We don't want propagate some properties.
|
||
nestedCue.nestedCues = [];
|
||
nestedCue.payload = '';
|
||
nestedCue.rubyTag = '';
|
||
// We don't want propagate some position settings
|
||
nestedCue.line = null;
|
||
nestedCue.region = new shaka.text.CueRegion();
|
||
nestedCue.position = null;
|
||
nestedCue.size = 0;
|
||
nestedCue.textAlign = shaka.text.Cue.textAlign.CENTER;
|
||
|
||
if (TXml.isNode(element)) {
|
||
const bold = shaka.text.Cue.fontWeight.BOLD;
|
||
const italic = shaka.text.Cue.fontStyle.ITALIC;
|
||
const underline = shaka.text.Cue.textDecoration.UNDERLINE;
|
||
const tags = element.tagName.split(/(?=[ .])+/g);
|
||
for (const tag of tags) {
|
||
let styleTag = tag;
|
||
// White blanks at start indicate that the style is a voice
|
||
if (styleTag.startsWith('.voice-')) {
|
||
const voice = styleTag.split('-').pop();
|
||
styleTag = `v[voice="${voice}"]`;
|
||
// The specification allows to have quotes and not, so we check to
|
||
// see which one is being used.
|
||
if (!styles.has(styleTag)) {
|
||
styleTag = `v[voice=${voice}]`;
|
||
}
|
||
}
|
||
if (styles.has(styleTag)) {
|
||
shaka.text.Cue.mergeStyle_(nestedCue, styles.get(styleTag));
|
||
}
|
||
switch (tag) {
|
||
case 'br': {
|
||
const lineBreakCue = shaka.text.Cue.lineBreak(
|
||
nestedCue.startTime, nestedCue.endTime);
|
||
rootCue.nestedCues.push(lineBreakCue);
|
||
return;
|
||
}
|
||
case 'b':
|
||
nestedCue.fontWeight = bold;
|
||
break;
|
||
case 'i':
|
||
nestedCue.fontStyle = italic;
|
||
break;
|
||
case 'u':
|
||
nestedCue.textDecoration.push(underline);
|
||
break;
|
||
case 'font': {
|
||
const color = element.attributes['color'];
|
||
if (color) {
|
||
nestedCue.color = color;
|
||
}
|
||
break;
|
||
}
|
||
case 'div': {
|
||
const time = element.attributes['time'];
|
||
if (!time) {
|
||
break;
|
||
}
|
||
const cueTime = shaka.util.TextParser.parseTime(time);
|
||
if (cueTime) {
|
||
nestedCue.startTime = cueTime;
|
||
}
|
||
break;
|
||
}
|
||
case 'ruby':
|
||
case 'rp':
|
||
case 'rt':
|
||
nestedCue.rubyTag = tag;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
const isTextNode = (item) => TXml.isText(item);
|
||
const childNodes = element.children;
|
||
if (isTextNode(element) ||
|
||
(childNodes.length == 1 && isTextNode(childNodes[0]))) {
|
||
// Trailing line breaks may lost when convert cue to HTML tag
|
||
// Need to insert line break cue to preserve line breaks
|
||
const textArr = TXml.getTextContents(element).split('\n');
|
||
let isFirst = true;
|
||
for (const text of textArr) {
|
||
if (!isFirst) {
|
||
const lineBreakCue = shaka.text.Cue.lineBreak(
|
||
nestedCue.startTime, nestedCue.endTime);
|
||
rootCue.nestedCues.push(lineBreakCue);
|
||
}
|
||
if (text.length > 0) {
|
||
const textCue = nestedCue.clone();
|
||
textCue.payload = shaka.util.StringUtils.htmlUnescape(text);
|
||
rootCue.nestedCues.push(textCue);
|
||
}
|
||
isFirst = false;
|
||
}
|
||
} else {
|
||
rootCue.nestedCues.push(nestedCue);
|
||
for (const childNode of childNodes) {
|
||
shaka.text.Cue.generateCueFromElement_(childNode, nestedCue, styles);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Merges values created in parseStyle_
|
||
* @param {!shaka.text.Cue} cue
|
||
* @param {shaka.text.Cue} refCue
|
||
* @private
|
||
*/
|
||
static mergeStyle_(cue, refCue) {
|
||
if (!refCue) {
|
||
return;
|
||
}
|
||
|
||
// Overwrites if new value string length > 0
|
||
cue.backgroundColor = shaka.text.Cue.getOrDefault_(
|
||
refCue.backgroundColor, cue.backgroundColor);
|
||
cue.color = shaka.text.Cue.getOrDefault_(
|
||
refCue.color, cue.color);
|
||
cue.fontFamily = shaka.text.Cue.getOrDefault_(
|
||
refCue.fontFamily, cue.fontFamily);
|
||
cue.fontSize = shaka.text.Cue.getOrDefault_(
|
||
refCue.fontSize, cue.fontSize);
|
||
cue.textShadow = shaka.text.Cue.getOrDefault_(
|
||
refCue.textShadow, cue.textShadow);
|
||
|
||
// Overwrite with new values as unable to determine
|
||
// if new value is set or not
|
||
cue.fontWeight = refCue.fontWeight;
|
||
cue.fontStyle = refCue.fontStyle;
|
||
cue.opacity = refCue.opacity;
|
||
cue.rubyTag = refCue.rubyTag;
|
||
cue.textCombineUpright = refCue.textCombineUpright;
|
||
cue.wrapLine = refCue.wrapLine;
|
||
}
|
||
|
||
/**
|
||
* @param {string} value
|
||
* @param {string} defaultValue
|
||
* @return {string}
|
||
* @private
|
||
*/
|
||
static getOrDefault_(value, defaultValue) {
|
||
if (value && value.length > 0) {
|
||
return value;
|
||
}
|
||
return defaultValue;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.positionAlign = {
|
||
'LEFT': 'line-left',
|
||
'RIGHT': 'line-right',
|
||
'CENTER': 'center',
|
||
'AUTO': 'auto',
|
||
};
|
||
|
||
|
||
/**
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.textAlign = {
|
||
'LEFT': 'left',
|
||
'RIGHT': 'right',
|
||
'CENTER': 'center',
|
||
'START': 'start',
|
||
'END': 'end',
|
||
};
|
||
|
||
|
||
/**
|
||
* Vertical alignments of the cues within their extents.
|
||
* 'BEFORE' means displaying at the top of the captions container box, 'CENTER'
|
||
* means in the middle, 'AFTER' means at the bottom.
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.displayAlign = {
|
||
'BEFORE': 'before',
|
||
'CENTER': 'center',
|
||
'AFTER': 'after',
|
||
};
|
||
|
||
|
||
/**
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.direction = {
|
||
'HORIZONTAL_LEFT_TO_RIGHT': 'ltr',
|
||
'HORIZONTAL_RIGHT_TO_LEFT': 'rtl',
|
||
};
|
||
|
||
|
||
/**
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.writingMode = {
|
||
'HORIZONTAL_TOP_TO_BOTTOM': 'horizontal-tb',
|
||
'VERTICAL_LEFT_TO_RIGHT': 'vertical-lr',
|
||
'VERTICAL_RIGHT_TO_LEFT': 'vertical-rl',
|
||
};
|
||
|
||
|
||
/**
|
||
* @enum {number}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.lineInterpretation = {
|
||
'LINE_NUMBER': 0,
|
||
'PERCENTAGE': 1,
|
||
};
|
||
|
||
|
||
/**
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.lineAlign = {
|
||
'CENTER': 'center',
|
||
'START': 'start',
|
||
'END': 'end',
|
||
};
|
||
|
||
|
||
/**
|
||
* Default text color according to
|
||
* https://w3c.github.io/webvtt/#default-text-color
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.defaultTextColor = {
|
||
'white': 'white',
|
||
'lime': 'lime',
|
||
'cyan': 'cyan',
|
||
'red': 'red',
|
||
'yellow': 'yellow',
|
||
'magenta': 'magenta',
|
||
'blue': 'blue',
|
||
'black': 'black',
|
||
};
|
||
|
||
|
||
/**
|
||
* Default text background color according to
|
||
* https://w3c.github.io/webvtt/#default-text-background
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.defaultTextBackgroundColor = {
|
||
'bg_white': 'white',
|
||
'bg_lime': 'lime',
|
||
'bg_cyan': 'cyan',
|
||
'bg_red': 'red',
|
||
'bg_yellow': 'yellow',
|
||
'bg_magenta': 'magenta',
|
||
'bg_blue': 'blue',
|
||
'bg_black': 'black',
|
||
};
|
||
|
||
|
||
/**
|
||
* In CSS font weight can be a number, where 400 is normal and 700 is bold.
|
||
* Use these values for the enum for consistency.
|
||
* @enum {number}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.fontWeight = {
|
||
'NORMAL': 400,
|
||
'BOLD': 700,
|
||
};
|
||
|
||
|
||
/**
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.fontStyle = {
|
||
'NORMAL': 'normal',
|
||
'ITALIC': 'italic',
|
||
'OBLIQUE': 'oblique',
|
||
};
|
||
|
||
|
||
/**
|
||
* @enum {string}
|
||
* @export
|
||
*/
|
||
shaka.text.Cue.textDecoration = {
|
||
'UNDERLINE': 'underline',
|
||
'LINE_THROUGH': 'lineThrough',
|
||
'OVERLINE': 'overline',
|
||
};
|
||
|
||
/** @private */
|
||
shaka.text.Cue.timeFormat_ = /(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{2,3})/g;
|