Source: lib/text/cue.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.text.Cue');
  7. goog.require('shaka.log');
  8. goog.require('shaka.text.CueRegion');
  9. goog.require('shaka.util.ArrayUtils');
  10. goog.require('shaka.util.StringUtils');
  11. goog.require('shaka.util.TextParser');
  12. goog.require('shaka.util.TXml');
  13. /**
  14. * @export
  15. */
  16. shaka.text.Cue = class {
  17. /**
  18. * @param {number} startTime
  19. * @param {number} endTime
  20. * @param {string} payload
  21. */
  22. constructor(startTime, endTime, payload) {
  23. const Cue = shaka.text.Cue;
  24. /**
  25. * The start time of the cue in seconds, relative to the start of the
  26. * presentation.
  27. * @type {number}
  28. * @export
  29. */
  30. this.startTime = startTime;
  31. /**
  32. * The end time of the cue in seconds, relative to the start of the
  33. * presentation.
  34. * @type {number}
  35. * @export
  36. */
  37. this.endTime = endTime;
  38. /**
  39. * The text payload of the cue. If nestedCues is non-empty, this should be
  40. * empty. Top-level block containers should have no payload of their own.
  41. * @type {string}
  42. * @export
  43. */
  44. this.payload = payload;
  45. /**
  46. * The region to render the cue into. Only supported on top-level cues,
  47. * because nested cues are inline elements.
  48. * @type {shaka.text.CueRegion}
  49. * @export
  50. */
  51. this.region = new shaka.text.CueRegion();
  52. /**
  53. * The indent (in percent) of the cue box in the direction defined by the
  54. * writing direction.
  55. * @type {?number}
  56. * @export
  57. */
  58. this.position = null;
  59. /**
  60. * Position alignment of the cue.
  61. * @type {shaka.text.Cue.positionAlign}
  62. * @export
  63. */
  64. this.positionAlign = Cue.positionAlign.AUTO;
  65. /**
  66. * Size of the cue box (in percents), where 0 means "auto".
  67. * @type {number}
  68. * @export
  69. */
  70. this.size = 0;
  71. /**
  72. * Alignment of the text inside the cue box.
  73. * @type {shaka.text.Cue.textAlign}
  74. * @export
  75. */
  76. this.textAlign = Cue.textAlign.CENTER;
  77. /**
  78. * Text direction of the cue.
  79. * @type {shaka.text.Cue.direction}
  80. * @export
  81. */
  82. this.direction = Cue.direction.HORIZONTAL_LEFT_TO_RIGHT;
  83. /**
  84. * Text writing mode of the cue.
  85. * @type {shaka.text.Cue.writingMode}
  86. * @export
  87. */
  88. this.writingMode = Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM;
  89. /**
  90. * The way to interpret line field. (Either as an integer line number or
  91. * percentage from the display box).
  92. * @type {shaka.text.Cue.lineInterpretation}
  93. * @export
  94. */
  95. this.lineInterpretation = Cue.lineInterpretation.LINE_NUMBER;
  96. /**
  97. * The offset from the display box in either number of lines or
  98. * percentage depending on the value of lineInterpretation.
  99. * @type {?number}
  100. * @export
  101. */
  102. this.line = null;
  103. /**
  104. * Separation between line areas inside the cue box in px or em
  105. * (e.g. '100px'/'100em'). If not specified, this should be no less than
  106. * the largest font size applied to the text in the cue.
  107. * @type {string}.
  108. * @export
  109. */
  110. this.lineHeight = '';
  111. /**
  112. * Line alignment of the cue box.
  113. * Start alignment means the cue box’s top side (for horizontal cues), left
  114. * side (for vertical growing right), or right side (for vertical growing
  115. * left) is aligned at the line.
  116. * Center alignment means the cue box is centered at the line.
  117. * End alignment The cue box’s bottom side (for horizontal cues), right side
  118. * (for vertical growing right), or left side (for vertical growing left) is
  119. * aligned at the line.
  120. * @type {shaka.text.Cue.lineAlign}
  121. * @export
  122. */
  123. this.lineAlign = Cue.lineAlign.START;
  124. /**
  125. * Vertical alignments of the cues within their extents.
  126. * 'BEFORE' means displaying the captions at the top of the text display
  127. * container box, 'CENTER' means in the middle, 'AFTER' means at the bottom.
  128. * @type {shaka.text.Cue.displayAlign}
  129. * @export
  130. */
  131. this.displayAlign = Cue.displayAlign.AFTER;
  132. /**
  133. * Text color as a CSS color, e.g. "#FFFFFF" or "white".
  134. * @type {string}
  135. * @export
  136. */
  137. this.color = '';
  138. /**
  139. * Text background color as a CSS color, e.g. "#FFFFFF" or "white".
  140. * @type {string}
  141. * @export
  142. */
  143. this.backgroundColor = '';
  144. /**
  145. * The URL of the background image, e.g. "data:[mime type];base64,[data]".
  146. * @type {string}
  147. * @export
  148. */
  149. this.backgroundImage = '';
  150. /**
  151. * The border around this cue as a CSS border.
  152. * @type {string}
  153. * @export
  154. */
  155. this.border = '';
  156. /**
  157. * Text font size in px or em (e.g. '100px'/'100em').
  158. * @type {string}
  159. * @export
  160. */
  161. this.fontSize = '';
  162. /**
  163. * Text font weight. Either normal or bold.
  164. * @type {shaka.text.Cue.fontWeight}
  165. * @export
  166. */
  167. this.fontWeight = Cue.fontWeight.NORMAL;
  168. /**
  169. * Text font style. Normal, italic or oblique.
  170. * @type {shaka.text.Cue.fontStyle}
  171. * @export
  172. */
  173. this.fontStyle = Cue.fontStyle.NORMAL;
  174. /**
  175. * Text font family.
  176. * @type {string}
  177. * @export
  178. */
  179. this.fontFamily = '';
  180. /**
  181. * Text letter spacing as a CSS letter-spacing value.
  182. * @type {string}
  183. * @export
  184. */
  185. this.letterSpacing = '';
  186. /**
  187. * Text line padding as a CSS line-padding value.
  188. * @type {string}
  189. * @export
  190. */
  191. this.linePadding = '';
  192. /**
  193. * Opacity of the cue element, from 0-1.
  194. * @type {number}
  195. * @export
  196. */
  197. this.opacity = 1;
  198. /**
  199. * Text combine upright as a CSS text-combine-upright value.
  200. * @type {string}
  201. * @export
  202. */
  203. this.textCombineUpright = '';
  204. /**
  205. * Text decoration. A combination of underline, overline
  206. * and line through. Empty array means no decoration.
  207. * @type {!Array.<!shaka.text.Cue.textDecoration>}
  208. * @export
  209. */
  210. this.textDecoration = [];
  211. /**
  212. * Text shadow color as a CSS text-shadow value.
  213. * @type {string}
  214. * @export
  215. */
  216. this.textShadow = '';
  217. /**
  218. * Text stroke color as a CSS color, e.g. "#FFFFFF" or "white".
  219. * @type {string}
  220. * @export
  221. */
  222. this.textStrokeColor = '';
  223. /**
  224. * Text stroke width as a CSS stroke-width value.
  225. * @type {string}
  226. * @export
  227. */
  228. this.textStrokeWidth = '';
  229. /**
  230. * Whether or not line wrapping should be applied to the cue.
  231. * @type {boolean}
  232. * @export
  233. */
  234. this.wrapLine = true;
  235. /**
  236. * Id of the cue.
  237. * @type {string}
  238. * @export
  239. */
  240. this.id = '';
  241. /**
  242. * Nested cues, which should be laid out horizontally in one block.
  243. * Top-level cues are blocks, and nested cues are inline elements.
  244. * Cues can be nested arbitrarily deeply.
  245. * @type {!Array.<!shaka.text.Cue>}
  246. * @export
  247. */
  248. this.nestedCues = [];
  249. /**
  250. * If true, this represents a container element that is "above" the main
  251. * cues. For example, the <body> and <div> tags that contain the <p> tags
  252. * in a TTML file. This controls the flow of the final cues; any nested cues
  253. * within an "isContainer" cue will be laid out as separate lines.
  254. * @type {boolean}
  255. * @export
  256. */
  257. this.isContainer = false;
  258. /**
  259. * Whether or not the cue only acts as a line break between two nested cues.
  260. * Should only appear in nested cues.
  261. * @type {boolean}
  262. * @export
  263. */
  264. this.lineBreak = false;
  265. /**
  266. * Used to indicate the type of ruby tag that should be used when rendering
  267. * the cue. Valid values: ruby, rp, rt.
  268. * @type {?string}
  269. * @export
  270. */
  271. this.rubyTag = null;
  272. /**
  273. * The number of horizontal and vertical cells into which the Root Container
  274. * Region area is divided.
  275. *
  276. * @type {{ columns: number, rows: number }}
  277. * @export
  278. */
  279. this.cellResolution = {
  280. columns: 32,
  281. rows: 15,
  282. };
  283. }
  284. /**
  285. * @param {number} start
  286. * @param {number} end
  287. * @return {!shaka.text.Cue}
  288. */
  289. static lineBreak(start, end) {
  290. const cue = new shaka.text.Cue(start, end, '');
  291. cue.lineBreak = true;
  292. return cue;
  293. }
  294. /**
  295. * Create a copy of the cue with the same properties.
  296. * @return {!shaka.text.Cue}
  297. * @suppress {checkTypes} since we must use [] and "in" with a struct type.
  298. * @export
  299. */
  300. clone() {
  301. const clone = new shaka.text.Cue(0, 0, '');
  302. for (const k in this) {
  303. clone[k] = this[k];
  304. // Make copies of array fields, but only one level deep. That way, if we
  305. // change, for instance, textDecoration on the clone, we don't affect the
  306. // original.
  307. if (clone[k] && clone[k].constructor == Array) {
  308. clone[k] = /** @type {!Array} */(clone[k]).slice();
  309. }
  310. }
  311. return clone;
  312. }
  313. /**
  314. * Check if two Cues have all the same values in all properties.
  315. * @param {!shaka.text.Cue} cue1
  316. * @param {!shaka.text.Cue} cue2
  317. * @return {boolean}
  318. * @suppress {checkTypes} since we must use [] and "in" with a struct type.
  319. * @export
  320. */
  321. static equal(cue1, cue2) {
  322. // Compare the start time, end time and payload of the cues first for
  323. // performance optimization. We can avoid the more expensive recursive
  324. // checks if the top-level properties don't match.
  325. // See: https://github.com/shaka-project/shaka-player/issues/3018
  326. if (cue1.payload != cue2.payload) {
  327. return false;
  328. }
  329. const isDiffNegligible = (a, b) => Math.abs(a - b) < 0.001;
  330. if (!isDiffNegligible(cue1.startTime, cue2.startTime) ||
  331. !isDiffNegligible(cue1.endTime, cue2.endTime)) {
  332. return false;
  333. }
  334. for (const k in cue1) {
  335. if (k == 'startTime' || k == 'endTime' || k == 'payload') {
  336. // Already compared.
  337. } else if (k == 'nestedCues') {
  338. // This uses shaka.text.Cue.equal rather than just this.equal, since
  339. // otherwise recursing here will unbox the method and cause "this" to be
  340. // undefined in deeper recursion.
  341. if (!shaka.util.ArrayUtils.equal(
  342. cue1.nestedCues, cue2.nestedCues, shaka.text.Cue.equal)) {
  343. return false;
  344. }
  345. } else if (k == 'region' || k == 'cellResolution') {
  346. for (const k2 in cue1[k]) {
  347. if (cue1[k][k2] != cue2[k][k2]) {
  348. return false;
  349. }
  350. }
  351. } else if (Array.isArray(cue1[k])) {
  352. if (!shaka.util.ArrayUtils.equal(cue1[k], cue2[k])) {
  353. return false;
  354. }
  355. } else {
  356. if (cue1[k] != cue2[k]) {
  357. return false;
  358. }
  359. }
  360. }
  361. return true;
  362. }
  363. /**
  364. * Parses cue payload, searches for styling entities and, if needed,
  365. * modifies original payload and creates nested cues to better represent
  366. * styling found in payload. All changes are done in-place.
  367. * @param {!shaka.text.Cue} cue
  368. * @param {!Map<string, !shaka.text.Cue>=} styles
  369. * @export
  370. */
  371. static parseCuePayload(cue, styles = new Map()) {
  372. const StringUtils = shaka.util.StringUtils;
  373. const TXml = shaka.util.TXml;
  374. let payload = cue.payload;
  375. if (!payload.includes('<')) {
  376. cue.payload = StringUtils.htmlUnescape(payload);
  377. return;
  378. }
  379. if (styles.size === 0) {
  380. shaka.text.Cue.addDefaultTextColor(styles);
  381. }
  382. payload = shaka.text.Cue.replaceKaraokeStylePayload_(payload);
  383. payload = shaka.text.Cue.replaceVoiceStylePayload_(payload);
  384. payload = shaka.text.Cue.escapeInvalidChevrons_(payload);
  385. cue.payload = '';
  386. const xmlPayload = '<span>' + payload + '</span>';
  387. let element;
  388. try {
  389. element = TXml.parseXmlString(xmlPayload, 'span');
  390. } catch (e) {
  391. shaka.log.warning('cue parse fail: ', e);
  392. }
  393. if (element) {
  394. const childNodes = element.children;
  395. if (childNodes.length == 1) {
  396. const childNode = childNodes[0];
  397. if (!TXml.isNode(childNode)) {
  398. cue.payload = StringUtils.htmlUnescape(payload);
  399. return;
  400. }
  401. }
  402. for (const childNode of childNodes) {
  403. shaka.text.Cue.generateCueFromElement_(childNode, cue, styles);
  404. }
  405. } else {
  406. shaka.log.warning('The cue\'s markup could not be parsed: ', payload);
  407. cue.payload = StringUtils.htmlUnescape(payload);
  408. }
  409. }
  410. /**
  411. * Add default color
  412. *
  413. * @param {!Map<string, !shaka.text.Cue>} styles
  414. */
  415. static addDefaultTextColor(styles) {
  416. const textColor = shaka.text.Cue.defaultTextColor;
  417. for (const [key, value] of Object.entries(textColor)) {
  418. const cue = new shaka.text.Cue(0, 0, '');
  419. cue.color = value;
  420. styles.set('.' + key, cue);
  421. }
  422. const bgColor = shaka.text.Cue.defaultTextBackgroundColor;
  423. for (const [key, value] of Object.entries(bgColor)) {
  424. const cue = new shaka.text.Cue(0, 0, '');
  425. cue.backgroundColor = value;
  426. styles.set('.' + key, cue);
  427. }
  428. }
  429. /**
  430. * Converts karaoke style tag to be valid for xml parsing
  431. * For example,
  432. * input: Text <00:00:00.450> time <00:00:01.450> 1
  433. * output: Text <div time="00:00:00.450"> time
  434. * <div time="00:00:01.450"> 1</div></div>
  435. *
  436. * @param {string} payload
  437. * @return {string} processed payload
  438. * @private
  439. */
  440. static replaceKaraokeStylePayload_(payload) {
  441. const names = [];
  442. let nameStart = -1;
  443. for (let i = 0; i < payload.length; i++) {
  444. if (payload[i] === '<') {
  445. nameStart = i + 1;
  446. } else if (payload[i] === '>') {
  447. if (nameStart > 0) {
  448. const name = payload.substr(nameStart, i - nameStart);
  449. if (name.match(shaka.text.Cue.timeFormat_)) {
  450. names.push(name);
  451. }
  452. nameStart = -1;
  453. }
  454. }
  455. }
  456. let newPayload = payload;
  457. for (const name of names) {
  458. const replaceTag = '<' + name + '>';
  459. const startTag = '<div time="' + name + '">';
  460. const endTag = '</div>';
  461. newPayload = newPayload.replace(replaceTag, startTag);
  462. newPayload += endTag;
  463. }
  464. return newPayload;
  465. }
  466. /**
  467. * Converts voice style tag to be valid for xml parsing
  468. * For example,
  469. * input: <v Shaka>Test
  470. * output: <v.voice-Shaka>Test</v.voice-Shaka>
  471. *
  472. * @param {string} payload
  473. * @return {string} processed payload
  474. * @private
  475. */
  476. static replaceVoiceStylePayload_(payload) {
  477. const voiceTag = 'v';
  478. const names = [];
  479. let nameStart = -1;
  480. let newPayload = '';
  481. let hasVoiceEndTag = false;
  482. for (let i = 0; i < payload.length; i++) {
  483. // This condition is used to manage tags that have end tags.
  484. if (payload[i] === '/') {
  485. const end = payload.indexOf('>', i);
  486. if (end === -1) {
  487. return payload;
  488. }
  489. const tagEnd = payload.substring(i + 1, end);
  490. if (!tagEnd || tagEnd != voiceTag) {
  491. newPayload += payload[i];
  492. continue;
  493. }
  494. hasVoiceEndTag = true;
  495. let tagStart = null;
  496. if (names.length) {
  497. tagStart = names[names.length -1];
  498. }
  499. if (!tagStart) {
  500. newPayload += payload[i];
  501. } else if (tagStart === tagEnd) {
  502. newPayload += '/' + tagEnd + '>';
  503. i += tagEnd.length + 1;
  504. } else {
  505. if (!tagStart.startsWith(voiceTag)) {
  506. newPayload += payload[i];
  507. continue;
  508. }
  509. newPayload += '/' + tagStart + '>';
  510. i += tagEnd.length + 1;
  511. }
  512. } else {
  513. // Here we only want the tag name, not any other payload.
  514. if (payload[i] === '<') {
  515. nameStart = i + 1;
  516. if (payload[nameStart] != voiceTag) {
  517. nameStart = -1;
  518. }
  519. } else if (payload[i] === '>') {
  520. if (nameStart > 0) {
  521. names.push(payload.substr(nameStart, i - nameStart));
  522. nameStart = -1;
  523. }
  524. }
  525. newPayload += payload[i];
  526. }
  527. }
  528. for (const name of names) {
  529. const newName = name.replace(' ', '.voice-');
  530. newPayload = newPayload.replace(`<${name}>`, `<${newName}>`);
  531. newPayload = newPayload.replace(`</${name}>`, `</${newName}>`);
  532. if (!hasVoiceEndTag) {
  533. newPayload += `</${newName}>`;
  534. }
  535. }
  536. return newPayload;
  537. }
  538. /**
  539. * This method converts invalid > chevrons to HTML entities.
  540. * It also removes < chevrons as per spec.
  541. *
  542. * @param {!string} input
  543. * @return {string}
  544. * @private
  545. */
  546. static escapeInvalidChevrons_(input) {
  547. // Used to map HTML entities to characters.
  548. const htmlEscapes = {
  549. '< ': '',
  550. ' >': ' &gt;',
  551. };
  552. const reEscapedHtml = /(< +>|<\s|\s>)/g;
  553. const reHasEscapedHtml = RegExp(reEscapedHtml.source);
  554. // This check is an optimization, since replace always makes a copy
  555. if (input && reHasEscapedHtml.test(input)) {
  556. return input.replace(reEscapedHtml, (entity) => {
  557. return htmlEscapes[entity] || '';
  558. });
  559. }
  560. return input || '';
  561. }
  562. /**
  563. * @param {!shaka.extern.xml.Node} element
  564. * @param {!shaka.text.Cue} rootCue
  565. * @param {!Map<string, !shaka.text.Cue>} styles
  566. * @private
  567. */
  568. static generateCueFromElement_(element, rootCue, styles) {
  569. const TXml = shaka.util.TXml;
  570. const nestedCue = rootCue.clone();
  571. // We don't want propagate some properties.
  572. nestedCue.nestedCues = [];
  573. nestedCue.payload = '';
  574. nestedCue.rubyTag = '';
  575. // We don't want propagate some position settings
  576. nestedCue.line = null;
  577. nestedCue.region = new shaka.text.CueRegion();
  578. nestedCue.position = null;
  579. nestedCue.size = 0;
  580. nestedCue.textAlign = shaka.text.Cue.textAlign.CENTER;
  581. if (TXml.isNode(element)) {
  582. const bold = shaka.text.Cue.fontWeight.BOLD;
  583. const italic = shaka.text.Cue.fontStyle.ITALIC;
  584. const underline = shaka.text.Cue.textDecoration.UNDERLINE;
  585. const tags = element.tagName.split(/(?=[ .])+/g);
  586. for (const tag of tags) {
  587. let styleTag = tag;
  588. // White blanks at start indicate that the style is a voice
  589. if (styleTag.startsWith('.voice-')) {
  590. const voice = styleTag.split('-').pop();
  591. styleTag = `v[voice="${voice}"]`;
  592. // The specification allows to have quotes and not, so we check to
  593. // see which one is being used.
  594. if (!styles.has(styleTag)) {
  595. styleTag = `v[voice=${voice}]`;
  596. }
  597. }
  598. if (styles.has(styleTag)) {
  599. shaka.text.Cue.mergeStyle_(nestedCue, styles.get(styleTag));
  600. }
  601. switch (tag) {
  602. case 'br': {
  603. const lineBreakCue = shaka.text.Cue.lineBreak(
  604. nestedCue.startTime, nestedCue.endTime);
  605. rootCue.nestedCues.push(lineBreakCue);
  606. return;
  607. }
  608. case 'b':
  609. nestedCue.fontWeight = bold;
  610. break;
  611. case 'i':
  612. nestedCue.fontStyle = italic;
  613. break;
  614. case 'u':
  615. nestedCue.textDecoration.push(underline);
  616. break;
  617. case 'font': {
  618. const color = element.attributes['color'];
  619. if (color) {
  620. nestedCue.color = color;
  621. }
  622. break;
  623. }
  624. case 'div': {
  625. const time = element.attributes['time'];
  626. if (!time) {
  627. break;
  628. }
  629. const cueTime = shaka.util.TextParser.parseTime(time);
  630. if (cueTime) {
  631. nestedCue.startTime = cueTime;
  632. }
  633. break;
  634. }
  635. case 'ruby':
  636. case 'rp':
  637. case 'rt':
  638. nestedCue.rubyTag = tag;
  639. break;
  640. default:
  641. break;
  642. }
  643. }
  644. }
  645. const isTextNode = (item) => TXml.isText(item);
  646. const childNodes = element.children;
  647. if (isTextNode(element) ||
  648. (childNodes.length == 1 && isTextNode(childNodes[0]))) {
  649. // Trailing line breaks may lost when convert cue to HTML tag
  650. // Need to insert line break cue to preserve line breaks
  651. const textArr = TXml.getTextContents(element).split('\n');
  652. let isFirst = true;
  653. for (const text of textArr) {
  654. if (!isFirst) {
  655. const lineBreakCue = shaka.text.Cue.lineBreak(
  656. nestedCue.startTime, nestedCue.endTime);
  657. rootCue.nestedCues.push(lineBreakCue);
  658. }
  659. if (text.length > 0) {
  660. const textCue = nestedCue.clone();
  661. textCue.payload = shaka.util.StringUtils.htmlUnescape(text);
  662. rootCue.nestedCues.push(textCue);
  663. }
  664. isFirst = false;
  665. }
  666. } else {
  667. rootCue.nestedCues.push(nestedCue);
  668. for (const childNode of childNodes) {
  669. shaka.text.Cue.generateCueFromElement_(childNode, nestedCue, styles);
  670. }
  671. }
  672. }
  673. /**
  674. * Merges values created in parseStyle_
  675. * @param {!shaka.text.Cue} cue
  676. * @param {shaka.text.Cue} refCue
  677. * @private
  678. */
  679. static mergeStyle_(cue, refCue) {
  680. if (!refCue) {
  681. return;
  682. }
  683. // Overwrites if new value string length > 0
  684. cue.backgroundColor = shaka.text.Cue.getOrDefault_(
  685. refCue.backgroundColor, cue.backgroundColor);
  686. cue.color = shaka.text.Cue.getOrDefault_(
  687. refCue.color, cue.color);
  688. cue.fontFamily = shaka.text.Cue.getOrDefault_(
  689. refCue.fontFamily, cue.fontFamily);
  690. cue.fontSize = shaka.text.Cue.getOrDefault_(
  691. refCue.fontSize, cue.fontSize);
  692. cue.textShadow = shaka.text.Cue.getOrDefault_(
  693. refCue.textShadow, cue.textShadow);
  694. // Overwrite with new values as unable to determine
  695. // if new value is set or not
  696. cue.fontWeight = refCue.fontWeight;
  697. cue.fontStyle = refCue.fontStyle;
  698. cue.opacity = refCue.opacity;
  699. cue.rubyTag = refCue.rubyTag;
  700. cue.textCombineUpright = refCue.textCombineUpright;
  701. cue.wrapLine = refCue.wrapLine;
  702. }
  703. /**
  704. * @param {string} value
  705. * @param {string} defaultValue
  706. * @private
  707. */
  708. static getOrDefault_(value, defaultValue) {
  709. if (value && value.length > 0) {
  710. return value;
  711. }
  712. return defaultValue;
  713. }
  714. };
  715. /**
  716. * @enum {string}
  717. * @export
  718. */
  719. shaka.text.Cue.positionAlign = {
  720. 'LEFT': 'line-left',
  721. 'RIGHT': 'line-right',
  722. 'CENTER': 'center',
  723. 'AUTO': 'auto',
  724. };
  725. /**
  726. * @enum {string}
  727. * @export
  728. */
  729. shaka.text.Cue.textAlign = {
  730. 'LEFT': 'left',
  731. 'RIGHT': 'right',
  732. 'CENTER': 'center',
  733. 'START': 'start',
  734. 'END': 'end',
  735. };
  736. /**
  737. * Vertical alignments of the cues within their extents.
  738. * 'BEFORE' means displaying at the top of the captions container box, 'CENTER'
  739. * means in the middle, 'AFTER' means at the bottom.
  740. * @enum {string}
  741. * @export
  742. */
  743. shaka.text.Cue.displayAlign = {
  744. 'BEFORE': 'before',
  745. 'CENTER': 'center',
  746. 'AFTER': 'after',
  747. };
  748. /**
  749. * @enum {string}
  750. * @export
  751. */
  752. shaka.text.Cue.direction = {
  753. 'HORIZONTAL_LEFT_TO_RIGHT': 'ltr',
  754. 'HORIZONTAL_RIGHT_TO_LEFT': 'rtl',
  755. };
  756. /**
  757. * @enum {string}
  758. * @export
  759. */
  760. shaka.text.Cue.writingMode = {
  761. 'HORIZONTAL_TOP_TO_BOTTOM': 'horizontal-tb',
  762. 'VERTICAL_LEFT_TO_RIGHT': 'vertical-lr',
  763. 'VERTICAL_RIGHT_TO_LEFT': 'vertical-rl',
  764. };
  765. /**
  766. * @enum {number}
  767. * @export
  768. */
  769. shaka.text.Cue.lineInterpretation = {
  770. 'LINE_NUMBER': 0,
  771. 'PERCENTAGE': 1,
  772. };
  773. /**
  774. * @enum {string}
  775. * @export
  776. */
  777. shaka.text.Cue.lineAlign = {
  778. 'CENTER': 'center',
  779. 'START': 'start',
  780. 'END': 'end',
  781. };
  782. /**
  783. * Default text color according to
  784. * https://w3c.github.io/webvtt/#default-text-color
  785. * @enum {string}
  786. * @export
  787. */
  788. shaka.text.Cue.defaultTextColor = {
  789. 'white': 'white',
  790. 'lime': 'lime',
  791. 'cyan': 'cyan',
  792. 'red': 'red',
  793. 'yellow': 'yellow',
  794. 'magenta': 'magenta',
  795. 'blue': 'blue',
  796. 'black': 'black',
  797. };
  798. /**
  799. * Default text background color according to
  800. * https://w3c.github.io/webvtt/#default-text-background
  801. * @enum {string}
  802. * @export
  803. */
  804. shaka.text.Cue.defaultTextBackgroundColor = {
  805. 'bg_white': 'white',
  806. 'bg_lime': 'lime',
  807. 'bg_cyan': 'cyan',
  808. 'bg_red': 'red',
  809. 'bg_yellow': 'yellow',
  810. 'bg_magenta': 'magenta',
  811. 'bg_blue': 'blue',
  812. 'bg_black': 'black',
  813. };
  814. /**
  815. * In CSS font weight can be a number, where 400 is normal and 700 is bold.
  816. * Use these values for the enum for consistency.
  817. * @enum {number}
  818. * @export
  819. */
  820. shaka.text.Cue.fontWeight = {
  821. 'NORMAL': 400,
  822. 'BOLD': 700,
  823. };
  824. /**
  825. * @enum {string}
  826. * @export
  827. */
  828. shaka.text.Cue.fontStyle = {
  829. 'NORMAL': 'normal',
  830. 'ITALIC': 'italic',
  831. 'OBLIQUE': 'oblique',
  832. };
  833. /**
  834. * @enum {string}
  835. * @export
  836. */
  837. shaka.text.Cue.textDecoration = {
  838. 'UNDERLINE': 'underline',
  839. 'LINE_THROUGH': 'lineThrough',
  840. 'OVERLINE': 'overline',
  841. };
  842. /** @private */
  843. shaka.text.Cue.timeFormat_ = /(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{2,3})/g;