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.startTime != cue2.startTime || cue1.endTime != cue2.endTime ||
  327. cue1.payload != cue2.payload) {
  328. return false;
  329. }
  330. for (const k in cue1) {
  331. if (k == 'startTime' || k == 'endTime' || k == 'payload') {
  332. // Already compared.
  333. } else if (k == 'nestedCues') {
  334. // This uses shaka.text.Cue.equal rather than just this.equal, since
  335. // otherwise recursing here will unbox the method and cause "this" to be
  336. // undefined in deeper recursion.
  337. if (!shaka.util.ArrayUtils.equal(
  338. cue1.nestedCues, cue2.nestedCues, shaka.text.Cue.equal)) {
  339. return false;
  340. }
  341. } else if (k == 'region' || k == 'cellResolution') {
  342. for (const k2 in cue1[k]) {
  343. if (cue1[k][k2] != cue2[k][k2]) {
  344. return false;
  345. }
  346. }
  347. } else if (Array.isArray(cue1[k])) {
  348. if (!shaka.util.ArrayUtils.equal(cue1[k], cue2[k])) {
  349. return false;
  350. }
  351. } else {
  352. if (cue1[k] != cue2[k]) {
  353. return false;
  354. }
  355. }
  356. }
  357. return true;
  358. }
  359. /**
  360. * Parses cue payload, searches for styling entities and, if needed,
  361. * modifies original payload and creates nested cues to better represent
  362. * styling found in payload. All changes are done in-place.
  363. * @param {!shaka.text.Cue} cue
  364. * @param {!Map<string, !shaka.text.Cue>=} styles
  365. * @export
  366. */
  367. static parseCuePayload(cue, styles = new Map()) {
  368. const StringUtils = shaka.util.StringUtils;
  369. const TXml = shaka.util.TXml;
  370. let payload = cue.payload;
  371. if (!payload.includes('<')) {
  372. cue.payload = StringUtils.htmlUnescape(payload);
  373. return;
  374. }
  375. if (styles.size === 0) {
  376. shaka.text.Cue.addDefaultTextColor(styles);
  377. }
  378. payload = shaka.text.Cue.replaceKaraokeStylePayload_(payload);
  379. payload = shaka.text.Cue.replaceVoiceStylePayload_(payload);
  380. payload = shaka.text.Cue.escapeInvalidChevrons_(payload);
  381. cue.payload = '';
  382. const xmlPayload = '<span>' + payload + '</span>';
  383. let element;
  384. try {
  385. element = TXml.parseXmlString(xmlPayload, 'span');
  386. } catch (e) {
  387. shaka.log.warning('cue parse fail: ', e);
  388. }
  389. if (element) {
  390. const childNodes = element.children;
  391. if (childNodes.length == 1) {
  392. const childNode = childNodes[0];
  393. if (!TXml.isNode(childNode)) {
  394. cue.payload = StringUtils.htmlUnescape(payload);
  395. return;
  396. }
  397. }
  398. for (const childNode of childNodes) {
  399. shaka.text.Cue.generateCueFromElement_(childNode, cue, styles);
  400. }
  401. } else {
  402. shaka.log.warning('The cue\'s markup could not be parsed: ', payload);
  403. cue.payload = StringUtils.htmlUnescape(payload);
  404. }
  405. }
  406. /**
  407. * Add default color
  408. *
  409. * @param {!Map<string, !shaka.text.Cue>} styles
  410. */
  411. static addDefaultTextColor(styles) {
  412. const textColor = shaka.text.Cue.defaultTextColor;
  413. for (const [key, value] of Object.entries(textColor)) {
  414. const cue = new shaka.text.Cue(0, 0, '');
  415. cue.color = value;
  416. styles.set('.' + key, cue);
  417. }
  418. const bgColor = shaka.text.Cue.defaultTextBackgroundColor;
  419. for (const [key, value] of Object.entries(bgColor)) {
  420. const cue = new shaka.text.Cue(0, 0, '');
  421. cue.backgroundColor = value;
  422. styles.set('.' + key, cue);
  423. }
  424. }
  425. /**
  426. * Converts karaoke style tag to be valid for xml parsing
  427. * For example,
  428. * input: Text <00:00:00.450> time <00:00:01.450> 1
  429. * output: Text <div time="00:00:00.450"> time
  430. * <div time="00:00:01.450"> 1</div></div>
  431. *
  432. * @param {string} payload
  433. * @return {string} processed payload
  434. * @private
  435. */
  436. static replaceKaraokeStylePayload_(payload) {
  437. const names = [];
  438. let nameStart = -1;
  439. for (let i = 0; i < payload.length; i++) {
  440. if (payload[i] === '<') {
  441. nameStart = i + 1;
  442. } else if (payload[i] === '>') {
  443. if (nameStart > 0) {
  444. const name = payload.substr(nameStart, i - nameStart);
  445. if (name.match(shaka.text.Cue.timeFormat_)) {
  446. names.push(name);
  447. }
  448. nameStart = -1;
  449. }
  450. }
  451. }
  452. let newPayload = payload;
  453. for (const name of names) {
  454. const replaceTag = '<' + name + '>';
  455. const startTag = '<div time="' + name + '">';
  456. const endTag = '</div>';
  457. newPayload = newPayload.replace(replaceTag, startTag);
  458. newPayload += endTag;
  459. }
  460. return newPayload;
  461. }
  462. /**
  463. * Converts voice style tag to be valid for xml parsing
  464. * For example,
  465. * input: <v Shaka>Test
  466. * output: <v.voice-Shaka>Test</v.voice-Shaka>
  467. *
  468. * @param {string} payload
  469. * @return {string} processed payload
  470. * @private
  471. */
  472. static replaceVoiceStylePayload_(payload) {
  473. const voiceTag = 'v';
  474. const names = [];
  475. let nameStart = -1;
  476. let newPayload = '';
  477. let hasVoiceEndTag = false;
  478. for (let i = 0; i < payload.length; i++) {
  479. // This condition is used to manage tags that have end tags.
  480. if (payload[i] === '/') {
  481. const end = payload.indexOf('>', i);
  482. if (end === -1) {
  483. return payload;
  484. }
  485. const tagEnd = payload.substring(i + 1, end);
  486. if (!tagEnd || tagEnd != voiceTag) {
  487. newPayload += payload[i];
  488. continue;
  489. }
  490. hasVoiceEndTag = true;
  491. let tagStart = null;
  492. if (names.length) {
  493. tagStart = names[names.length -1];
  494. }
  495. if (!tagStart) {
  496. newPayload += payload[i];
  497. } else if (tagStart === tagEnd) {
  498. newPayload += '/' + tagEnd + '>';
  499. i += tagEnd.length + 1;
  500. } else {
  501. if (!tagStart.startsWith(voiceTag)) {
  502. newPayload += payload[i];
  503. continue;
  504. }
  505. newPayload += '/' + tagStart + '>';
  506. i += tagEnd.length + 1;
  507. }
  508. } else {
  509. // Here we only want the tag name, not any other payload.
  510. if (payload[i] === '<') {
  511. nameStart = i + 1;
  512. if (payload[nameStart] != voiceTag) {
  513. nameStart = -1;
  514. }
  515. } else if (payload[i] === '>') {
  516. if (nameStart > 0) {
  517. names.push(payload.substr(nameStart, i - nameStart));
  518. nameStart = -1;
  519. }
  520. }
  521. newPayload += payload[i];
  522. }
  523. }
  524. for (const name of names) {
  525. const newName = name.replace(' ', '.voice-');
  526. newPayload = newPayload.replace(`<${name}>`, `<${newName}>`);
  527. newPayload = newPayload.replace(`</${name}>`, `</${newName}>`);
  528. if (!hasVoiceEndTag) {
  529. newPayload += `</${newName}>`;
  530. }
  531. }
  532. return newPayload;
  533. }
  534. /**
  535. * This method converts invalid > chevrons to HTML entities.
  536. * It also removes < chevrons as per spec.
  537. *
  538. * @param {!string} input
  539. * @return {string}
  540. * @private
  541. */
  542. static escapeInvalidChevrons_(input) {
  543. // Used to map HTML entities to characters.
  544. const htmlEscapes = {
  545. '< ': '',
  546. ' >': ' &gt;',
  547. };
  548. const reEscapedHtml = /(< +>|<\s|\s>)/g;
  549. const reHasEscapedHtml = RegExp(reEscapedHtml.source);
  550. // This check is an optimization, since replace always makes a copy
  551. if (input && reHasEscapedHtml.test(input)) {
  552. return input.replace(reEscapedHtml, (entity) => {
  553. return htmlEscapes[entity] || '';
  554. });
  555. }
  556. return input || '';
  557. }
  558. /**
  559. * @param {!shaka.extern.xml.Node} element
  560. * @param {!shaka.text.Cue} rootCue
  561. * @param {!Map<string, !shaka.text.Cue>} styles
  562. * @private
  563. */
  564. static generateCueFromElement_(element, rootCue, styles) {
  565. const TXml = shaka.util.TXml;
  566. const nestedCue = rootCue.clone();
  567. // We don't want propagate some properties.
  568. nestedCue.nestedCues = [];
  569. nestedCue.payload = '';
  570. nestedCue.rubyTag = '';
  571. // We don't want propagate some position settings
  572. nestedCue.line = null;
  573. nestedCue.region = new shaka.text.CueRegion();
  574. nestedCue.position = null;
  575. nestedCue.size = 0;
  576. if (TXml.isNode(element)) {
  577. const bold = shaka.text.Cue.fontWeight.BOLD;
  578. const italic = shaka.text.Cue.fontStyle.ITALIC;
  579. const underline = shaka.text.Cue.textDecoration.UNDERLINE;
  580. const tags = element.tagName.split(/(?=[ .])+/g);
  581. for (const tag of tags) {
  582. let styleTag = tag;
  583. // White blanks at start indicate that the style is a voice
  584. if (styleTag.startsWith('.voice-')) {
  585. const voice = styleTag.split('-').pop();
  586. styleTag = `v[voice="${voice}"]`;
  587. // The specification allows to have quotes and not, so we check to
  588. // see which one is being used.
  589. if (!styles.has(styleTag)) {
  590. styleTag = `v[voice=${voice}]`;
  591. }
  592. }
  593. if (styles.has(styleTag)) {
  594. shaka.text.Cue.mergeStyle_(nestedCue, styles.get(styleTag));
  595. }
  596. switch (tag) {
  597. case 'br': {
  598. const lineBreakCue = shaka.text.Cue.lineBreak(
  599. nestedCue.startTime, nestedCue.endTime);
  600. rootCue.nestedCues.push(lineBreakCue);
  601. return;
  602. }
  603. case 'b':
  604. nestedCue.fontWeight = bold;
  605. break;
  606. case 'i':
  607. nestedCue.fontStyle = italic;
  608. break;
  609. case 'u':
  610. nestedCue.textDecoration.push(underline);
  611. break;
  612. case 'font': {
  613. const color = element.attributes['color'];
  614. if (color) {
  615. nestedCue.color = color;
  616. }
  617. break;
  618. }
  619. case 'div': {
  620. const time = element.attributes['time'];
  621. if (!time) {
  622. break;
  623. }
  624. const cueTime = shaka.util.TextParser.parseTime(time);
  625. if (cueTime) {
  626. nestedCue.startTime = cueTime;
  627. }
  628. break;
  629. }
  630. case 'ruby':
  631. case 'rp':
  632. case 'rt':
  633. nestedCue.rubyTag = tag;
  634. break;
  635. default:
  636. break;
  637. }
  638. }
  639. }
  640. const isTextNode = (item) => TXml.isText(item);
  641. const childNodes = element.children;
  642. if (isTextNode(element) ||
  643. (childNodes.length == 1 && isTextNode(childNodes[0]))) {
  644. // Trailing line breaks may lost when convert cue to HTML tag
  645. // Need to insert line break cue to preserve line breaks
  646. const textArr = TXml.getTextContents(element).split('\n');
  647. let isFirst = true;
  648. for (const text of textArr) {
  649. if (!isFirst) {
  650. const lineBreakCue = shaka.text.Cue.lineBreak(
  651. nestedCue.startTime, nestedCue.endTime);
  652. rootCue.nestedCues.push(lineBreakCue);
  653. }
  654. if (text.length > 0) {
  655. const textCue = nestedCue.clone();
  656. textCue.payload = shaka.util.StringUtils.htmlUnescape(text);
  657. rootCue.nestedCues.push(textCue);
  658. }
  659. isFirst = false;
  660. }
  661. } else {
  662. rootCue.nestedCues.push(nestedCue);
  663. for (const childNode of childNodes) {
  664. shaka.text.Cue.generateCueFromElement_(childNode, nestedCue, styles);
  665. }
  666. }
  667. }
  668. /**
  669. * Merges values created in parseStyle_
  670. * @param {!shaka.text.Cue} cue
  671. * @param {shaka.text.Cue} refCue
  672. * @private
  673. */
  674. static mergeStyle_(cue, refCue) {
  675. if (!refCue) {
  676. return;
  677. }
  678. // Overwrites if new value string length > 0
  679. cue.backgroundColor = shaka.text.Cue.getOrDefault_(
  680. refCue.backgroundColor, cue.backgroundColor);
  681. cue.color = shaka.text.Cue.getOrDefault_(
  682. refCue.color, cue.color);
  683. cue.fontFamily = shaka.text.Cue.getOrDefault_(
  684. refCue.fontFamily, cue.fontFamily);
  685. cue.fontSize = shaka.text.Cue.getOrDefault_(
  686. refCue.fontSize, cue.fontSize);
  687. cue.textShadow = shaka.text.Cue.getOrDefault_(
  688. refCue.textShadow, cue.textShadow);
  689. // Overwrite with new values as unable to determine
  690. // if new value is set or not
  691. cue.fontWeight = refCue.fontWeight;
  692. cue.fontStyle = refCue.fontStyle;
  693. cue.opacity = refCue.opacity;
  694. cue.rubyTag = refCue.rubyTag;
  695. cue.textCombineUpright = refCue.textCombineUpright;
  696. cue.wrapLine = refCue.wrapLine;
  697. }
  698. /**
  699. * @param {string} value
  700. * @param {string} defaultValue
  701. * @private
  702. */
  703. static getOrDefault_(value, defaultValue) {
  704. if (value && value.length > 0) {
  705. return value;
  706. }
  707. return defaultValue;
  708. }
  709. };
  710. /**
  711. * @enum {string}
  712. * @export
  713. */
  714. shaka.text.Cue.positionAlign = {
  715. 'LEFT': 'line-left',
  716. 'RIGHT': 'line-right',
  717. 'CENTER': 'center',
  718. 'AUTO': 'auto',
  719. };
  720. /**
  721. * @enum {string}
  722. * @export
  723. */
  724. shaka.text.Cue.textAlign = {
  725. 'LEFT': 'left',
  726. 'RIGHT': 'right',
  727. 'CENTER': 'center',
  728. 'START': 'start',
  729. 'END': 'end',
  730. };
  731. /**
  732. * Vertical alignments of the cues within their extents.
  733. * 'BEFORE' means displaying at the top of the captions container box, 'CENTER'
  734. * means in the middle, 'AFTER' means at the bottom.
  735. * @enum {string}
  736. * @export
  737. */
  738. shaka.text.Cue.displayAlign = {
  739. 'BEFORE': 'before',
  740. 'CENTER': 'center',
  741. 'AFTER': 'after',
  742. };
  743. /**
  744. * @enum {string}
  745. * @export
  746. */
  747. shaka.text.Cue.direction = {
  748. 'HORIZONTAL_LEFT_TO_RIGHT': 'ltr',
  749. 'HORIZONTAL_RIGHT_TO_LEFT': 'rtl',
  750. };
  751. /**
  752. * @enum {string}
  753. * @export
  754. */
  755. shaka.text.Cue.writingMode = {
  756. 'HORIZONTAL_TOP_TO_BOTTOM': 'horizontal-tb',
  757. 'VERTICAL_LEFT_TO_RIGHT': 'vertical-lr',
  758. 'VERTICAL_RIGHT_TO_LEFT': 'vertical-rl',
  759. };
  760. /**
  761. * @enum {number}
  762. * @export
  763. */
  764. shaka.text.Cue.lineInterpretation = {
  765. 'LINE_NUMBER': 0,
  766. 'PERCENTAGE': 1,
  767. };
  768. /**
  769. * @enum {string}
  770. * @export
  771. */
  772. shaka.text.Cue.lineAlign = {
  773. 'CENTER': 'center',
  774. 'START': 'start',
  775. 'END': 'end',
  776. };
  777. /**
  778. * Default text color according to
  779. * https://w3c.github.io/webvtt/#default-text-color
  780. * @enum {string}
  781. * @export
  782. */
  783. shaka.text.Cue.defaultTextColor = {
  784. 'white': 'white',
  785. 'lime': 'lime',
  786. 'cyan': 'cyan',
  787. 'red': 'red',
  788. 'yellow': 'yellow',
  789. 'magenta': 'magenta',
  790. 'blue': 'blue',
  791. 'black': 'black',
  792. };
  793. /**
  794. * Default text background color according to
  795. * https://w3c.github.io/webvtt/#default-text-background
  796. * @enum {string}
  797. * @export
  798. */
  799. shaka.text.Cue.defaultTextBackgroundColor = {
  800. 'bg_white': 'white',
  801. 'bg_lime': 'lime',
  802. 'bg_cyan': 'cyan',
  803. 'bg_red': 'red',
  804. 'bg_yellow': 'yellow',
  805. 'bg_magenta': 'magenta',
  806. 'bg_blue': 'blue',
  807. 'bg_black': 'black',
  808. };
  809. /**
  810. * In CSS font weight can be a number, where 400 is normal and 700 is bold.
  811. * Use these values for the enum for consistency.
  812. * @enum {number}
  813. * @export
  814. */
  815. shaka.text.Cue.fontWeight = {
  816. 'NORMAL': 400,
  817. 'BOLD': 700,
  818. };
  819. /**
  820. * @enum {string}
  821. * @export
  822. */
  823. shaka.text.Cue.fontStyle = {
  824. 'NORMAL': 'normal',
  825. 'ITALIC': 'italic',
  826. 'OBLIQUE': 'oblique',
  827. };
  828. /**
  829. * @enum {string}
  830. * @export
  831. */
  832. shaka.text.Cue.textDecoration = {
  833. 'UNDERLINE': 'underline',
  834. 'LINE_THROUGH': 'lineThrough',
  835. 'OVERLINE': 'overline',
  836. };
  837. /** @private */
  838. shaka.text.Cue.timeFormat_ = /(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{2,3})/g;