Source: lib/text/ttml_text_parser.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.text.TtmlTextParser');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.text.Cue');
  10. goog.require('shaka.text.CueRegion');
  11. goog.require('shaka.text.TextEngine');
  12. goog.require('shaka.util.ArrayUtils');
  13. goog.require('shaka.util.Error');
  14. goog.require('shaka.util.StringUtils');
  15. goog.require('shaka.util.XmlUtils');
  16. /**
  17. * @implements {shaka.extern.TextParser}
  18. * @export
  19. */
  20. shaka.text.TtmlTextParser = class {
  21. /**
  22. * @override
  23. * @export
  24. */
  25. parseInit(data) {
  26. goog.asserts.assert(false, 'TTML does not have init segments');
  27. }
  28. /**
  29. * @override
  30. * @export
  31. */
  32. setSequenceMode(sequenceMode) {
  33. // Unused.
  34. }
  35. /**
  36. * @override
  37. * @export
  38. */
  39. parseMedia(data, time) {
  40. const TtmlTextParser = shaka.text.TtmlTextParser;
  41. const XmlUtils = shaka.util.XmlUtils;
  42. const ttpNs = TtmlTextParser.parameterNs_;
  43. const ttsNs = TtmlTextParser.styleNs_;
  44. const str = shaka.util.StringUtils.fromUTF8(data);
  45. const cues = [];
  46. const parser = new DOMParser();
  47. let xml = null;
  48. // dont try to parse empty string as
  49. // DOMParser will not throw error but return an errored xml
  50. if (str == '') {
  51. return cues;
  52. }
  53. try {
  54. xml = parser.parseFromString(str, 'text/xml');
  55. } catch (exception) {
  56. throw new shaka.util.Error(
  57. shaka.util.Error.Severity.CRITICAL,
  58. shaka.util.Error.Category.TEXT,
  59. shaka.util.Error.Code.INVALID_XML,
  60. 'Failed to parse TTML.');
  61. }
  62. if (xml) {
  63. const parserError = xml.getElementsByTagName('parsererror')[0];
  64. if (parserError) {
  65. throw new shaka.util.Error(
  66. shaka.util.Error.Severity.CRITICAL,
  67. shaka.util.Error.Category.TEXT,
  68. shaka.util.Error.Code.INVALID_XML,
  69. parserError.textContent);
  70. }
  71. const tt = xml.getElementsByTagName('tt')[0];
  72. // TTML should always have tt element.
  73. if (!tt) {
  74. throw new shaka.util.Error(
  75. shaka.util.Error.Severity.CRITICAL,
  76. shaka.util.Error.Category.TEXT,
  77. shaka.util.Error.Code.INVALID_XML,
  78. 'TTML does not contain <tt> tag.');
  79. }
  80. const body = tt.getElementsByTagName('body')[0];
  81. if (!body) {
  82. return [];
  83. }
  84. // Get the framerate, subFrameRate and frameRateMultiplier if applicable.
  85. const frameRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRate');
  86. const subFrameRate = XmlUtils.getAttributeNSList(
  87. tt, ttpNs, 'subFrameRate');
  88. const frameRateMultiplier =
  89. XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRateMultiplier');
  90. const tickRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'tickRate');
  91. const cellResolution = XmlUtils.getAttributeNSList(
  92. tt, ttpNs, 'cellResolution');
  93. const spaceStyle = tt.getAttribute('xml:space') || 'default';
  94. const extent = XmlUtils.getAttributeNSList(tt, ttsNs, 'extent');
  95. if (spaceStyle != 'default' && spaceStyle != 'preserve') {
  96. throw new shaka.util.Error(
  97. shaka.util.Error.Severity.CRITICAL,
  98. shaka.util.Error.Category.TEXT,
  99. shaka.util.Error.Code.INVALID_XML,
  100. 'Invalid xml:space value: ' + spaceStyle);
  101. }
  102. const whitespaceTrim = spaceStyle == 'default';
  103. const rateInfo = new TtmlTextParser.RateInfo_(
  104. frameRate, subFrameRate, frameRateMultiplier, tickRate);
  105. const cellResolutionInfo =
  106. TtmlTextParser.getCellResolution_(cellResolution);
  107. const metadata = tt.getElementsByTagName('metadata')[0];
  108. const metadataElements = metadata ? XmlUtils.getChildren(metadata) : [];
  109. const styles = Array.from(tt.getElementsByTagName('style'));
  110. const regionElements = Array.from(tt.getElementsByTagName('region'));
  111. const cueRegions = [];
  112. for (const region of regionElements) {
  113. const cueRegion =
  114. TtmlTextParser.parseCueRegion_(region, styles, extent);
  115. if (cueRegion) {
  116. cueRegions.push(cueRegion);
  117. }
  118. }
  119. // A <body> element should only contain <div> elements, not <p> or <span>
  120. // elements. We used to allow this, but it is non-compliant, and the
  121. // loose nature of our previous parser made it difficult to implement TTML
  122. // nesting more fully.
  123. if (XmlUtils.findChildren(body, 'p').length) {
  124. throw new shaka.util.Error(
  125. shaka.util.Error.Severity.CRITICAL,
  126. shaka.util.Error.Category.TEXT,
  127. shaka.util.Error.Code.INVALID_TEXT_CUE,
  128. '<p> can only be inside <div> in TTML');
  129. }
  130. for (const div of XmlUtils.findChildren(body, 'div')) {
  131. // A <div> element should only contain <p>, not <span>.
  132. if (XmlUtils.findChildren(div, 'span').length) {
  133. throw new shaka.util.Error(
  134. shaka.util.Error.Severity.CRITICAL,
  135. shaka.util.Error.Category.TEXT,
  136. shaka.util.Error.Code.INVALID_TEXT_CUE,
  137. '<span> can only be inside <p> in TTML');
  138. }
  139. }
  140. const cue = TtmlTextParser.parseCue_(
  141. body, time.periodStart, rateInfo, metadataElements, styles,
  142. regionElements, cueRegions, whitespaceTrim,
  143. cellResolutionInfo, /* parentCueElement= */ null,
  144. /* isContent= */ false);
  145. if (cue) {
  146. cues.push(cue);
  147. }
  148. }
  149. return cues;
  150. }
  151. /**
  152. * Parses a TTML node into a Cue.
  153. *
  154. * @param {!Node} cueNode
  155. * @param {number} offset
  156. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  157. * @param {!Array.<!Element>} metadataElements
  158. * @param {!Array.<!Element>} styles
  159. * @param {!Array.<!Element>} regionElements
  160. * @param {!Array.<!shaka.text.CueRegion>} cueRegions
  161. * @param {boolean} whitespaceTrim
  162. * @param {?{columns: number, rows: number}} cellResolution
  163. * @param {?Element} parentCueElement
  164. * @param {boolean} isContent
  165. * @return {shaka.text.Cue}
  166. * @private
  167. */
  168. static parseCue_(
  169. cueNode, offset, rateInfo, metadataElements, styles, regionElements,
  170. cueRegions, whitespaceTrim, cellResolution, parentCueElement, isContent) {
  171. /** @type {Element} */
  172. let cueElement;
  173. /** @type {Element} */
  174. let parentElement = /** @type {Element} */ (cueNode.parentNode);
  175. if (cueNode.nodeType == Node.COMMENT_NODE) {
  176. // The comments do not contain information that interests us here.
  177. return null;
  178. }
  179. if (cueNode.nodeType == Node.TEXT_NODE) {
  180. if (!isContent) {
  181. // Ignore text elements outside the content. For example, whitespace
  182. // on the same lexical level as the <p> elements, in a document with
  183. // xml:space="preserve", should not be renderer.
  184. return null;
  185. }
  186. // This should generate an "anonymous span" according to the TTML spec.
  187. // So pretend the element was a <span>. parentElement was set above, so
  188. // we should still be able to correctly traverse up for timing
  189. // information later.
  190. const span = document.createElement('span');
  191. span.textContent = cueNode.textContent;
  192. cueElement = span;
  193. } else {
  194. goog.asserts.assert(cueNode.nodeType == Node.ELEMENT_NODE,
  195. 'nodeType should be ELEMENT_NODE!');
  196. cueElement = /** @type {!Element} */(cueNode);
  197. }
  198. goog.asserts.assert(cueElement, 'cueElement should be non-null!');
  199. let imageElement = null;
  200. for (const nameSpace of shaka.text.TtmlTextParser.smpteNsList_) {
  201. imageElement = shaka.text.TtmlTextParser.getElementsFromCollection_(
  202. cueElement, 'backgroundImage', metadataElements, '#',
  203. nameSpace)[0];
  204. if (imageElement) {
  205. break;
  206. }
  207. }
  208. const parentIsContent = isContent;
  209. if (cueNode.nodeName == 'p' || imageElement) {
  210. isContent = true;
  211. }
  212. const spaceStyle = cueElement.getAttribute('xml:space') ||
  213. (whitespaceTrim ? 'default' : 'preserve');
  214. const localWhitespaceTrim = spaceStyle == 'default';
  215. // Parse any nested cues first.
  216. const isTextNode = (node) => {
  217. return node.nodeType == Node.TEXT_NODE;
  218. };
  219. const isLeafNode = Array.from(cueElement.childNodes).every(isTextNode);
  220. const nestedCues = [];
  221. if (!isLeafNode) {
  222. // Otherwise, recurse into the children. Text nodes will convert into
  223. // anonymous spans, which will then be leaf nodes.
  224. for (const childNode of cueElement.childNodes) {
  225. const nestedCue = shaka.text.TtmlTextParser.parseCue_(
  226. childNode,
  227. offset,
  228. rateInfo,
  229. metadataElements,
  230. styles,
  231. regionElements,
  232. cueRegions,
  233. localWhitespaceTrim,
  234. cellResolution,
  235. cueElement,
  236. isContent,
  237. );
  238. // This node may or may not generate a nested cue.
  239. if (nestedCue) {
  240. nestedCues.push(nestedCue);
  241. }
  242. }
  243. }
  244. const isNested = /** @type {boolean} */ (parentCueElement != null);
  245. // In this regex, "\S" means "non-whitespace character".
  246. const hasTextContent = /\S/.test(cueElement.textContent);
  247. const hasTimeAttributes =
  248. cueElement.hasAttribute('begin') ||
  249. cueElement.hasAttribute('end') ||
  250. cueElement.hasAttribute('dur');
  251. if (!hasTimeAttributes && !hasTextContent && cueElement.tagName != 'br' &&
  252. nestedCues.length == 0) {
  253. if (!isNested) {
  254. // Disregards empty <p> elements without time attributes nor content.
  255. // <p begin="..." smpte:backgroundImage="..." /> will go through,
  256. // as some information could be held by its attributes.
  257. // <p /> won't, as it would not be displayed.
  258. return null;
  259. } else if (localWhitespaceTrim) {
  260. // Disregards empty anonymous spans when (local) trim is true.
  261. return null;
  262. }
  263. }
  264. // Get local time attributes.
  265. let {start, end} = shaka.text.TtmlTextParser.parseTime_(
  266. cueElement, rateInfo);
  267. // Resolve local time relative to parent elements. Time elements can appear
  268. // all the way up to 'body', but not 'tt'.
  269. while (parentElement && parentElement.nodeType == Node.ELEMENT_NODE &&
  270. parentElement.tagName != 'tt') {
  271. ({start, end} = shaka.text.TtmlTextParser.resolveTime_(
  272. parentElement, rateInfo, start, end));
  273. parentElement = /** @type {Element} */(parentElement.parentNode);
  274. }
  275. if (start == null) {
  276. start = 0;
  277. }
  278. start += offset;
  279. // If end is null, that means the duration is effectively infinite.
  280. if (end == null) {
  281. end = Infinity;
  282. } else {
  283. end += offset;
  284. }
  285. if (!hasTimeAttributes && nestedCues.length > 0) {
  286. // If no time is defined for this cue, base the timing information on
  287. // the time of the nested cues. In the case of multiple nested cues with
  288. // different start times, it is the text displayer's responsibility to
  289. // make sure that only the appropriate nested cue is drawn at any given
  290. // time.
  291. start = Infinity;
  292. end = 0;
  293. for (const cue of nestedCues) {
  294. start = Math.min(start, cue.startTime);
  295. end = Math.max(end, cue.endTime);
  296. }
  297. }
  298. if (cueElement.tagName == 'br') {
  299. const cue = new shaka.text.Cue(start, end, '');
  300. cue.lineBreak = true;
  301. return cue;
  302. }
  303. let payload = '';
  304. if (isLeafNode) {
  305. // If the childNodes are all text, this is a leaf node. Get the payload.
  306. payload = cueElement.textContent;
  307. if (localWhitespaceTrim) {
  308. // Trim leading and trailing whitespace.
  309. payload = payload.trim();
  310. // Collapse multiple spaces into one.
  311. payload = payload.replace(/\s+/g, ' ');
  312. }
  313. }
  314. const cue = new shaka.text.Cue(start, end, payload);
  315. cue.nestedCues = nestedCues;
  316. if (!isContent) {
  317. // If this is not a <p> element or a <div> with images, and it has no
  318. // parent that was a <p> element, then it's part of the outer containers
  319. // (e.g. the <body> or a normal <div> element within it).
  320. cue.isContainer = true;
  321. }
  322. if (cellResolution) {
  323. cue.cellResolution = cellResolution;
  324. }
  325. // Get other properties if available.
  326. const regionElement = shaka.text.TtmlTextParser.getElementsFromCollection_(
  327. cueElement, 'region', regionElements, /* prefix= */ '')[0];
  328. // Do not actually apply that region unless it is non-inherited, though.
  329. // This makes it so that, if a parent element has a region, the children
  330. // don't also all independently apply the positioning of that region.
  331. if (cueElement.hasAttribute('region')) {
  332. if (regionElement && regionElement.getAttribute('xml:id')) {
  333. const regionId = regionElement.getAttribute('xml:id');
  334. cue.region = cueRegions.filter((region) => region.id == regionId)[0];
  335. }
  336. }
  337. let regionElementForStyle = regionElement;
  338. if (parentCueElement && isNested && !cueElement.getAttribute('region') &&
  339. !cueElement.getAttribute('style')) {
  340. regionElementForStyle =
  341. shaka.text.TtmlTextParser.getElementsFromCollection_(
  342. parentCueElement, 'region', regionElements, /* prefix= */ '')[0];
  343. }
  344. shaka.text.TtmlTextParser.addStyle_(
  345. cue,
  346. cueElement,
  347. regionElementForStyle,
  348. imageElement,
  349. styles,
  350. /** isNested= */ parentIsContent, // "nested in a <div>" doesn't count.
  351. /** isLeaf= */ (nestedCues.length == 0));
  352. return cue;
  353. }
  354. /**
  355. * Parses an Element into a TextTrackCue or VTTCue.
  356. *
  357. * @param {!Element} regionElement
  358. * @param {!Array.<!Element>} styles Defined in the top of tt element and
  359. * used principally for images.
  360. * @param {?string} globalExtent
  361. * @return {shaka.text.CueRegion}
  362. * @private
  363. */
  364. static parseCueRegion_(regionElement, styles, globalExtent) {
  365. const TtmlTextParser = shaka.text.TtmlTextParser;
  366. const region = new shaka.text.CueRegion();
  367. const id = regionElement.getAttribute('xml:id');
  368. if (!id) {
  369. shaka.log.warning('TtmlTextParser parser encountered a region with ' +
  370. 'no id. Region will be ignored.');
  371. return null;
  372. }
  373. region.id = id;
  374. let globalResults = null;
  375. if (globalExtent) {
  376. globalResults = TtmlTextParser.percentValues_.exec(globalExtent) ||
  377. TtmlTextParser.pixelValues_.exec(globalExtent);
  378. }
  379. const globalWidth = globalResults ? Number(globalResults[1]) : null;
  380. const globalHeight = globalResults ? Number(globalResults[2]) : null;
  381. let results = null;
  382. let percentage = null;
  383. const extent = TtmlTextParser.getStyleAttributeFromRegion_(
  384. regionElement, styles, 'extent');
  385. if (extent) {
  386. percentage = TtmlTextParser.percentValues_.exec(extent);
  387. results = percentage || TtmlTextParser.pixelValues_.exec(extent);
  388. if (results != null) {
  389. region.width = Number(results[1]);
  390. region.height = Number(results[2]);
  391. if (!percentage) {
  392. if (globalWidth != null) {
  393. region.width = region.width * 100 / globalWidth;
  394. }
  395. if (globalHeight != null) {
  396. region.height = region.height * 100 / globalHeight;
  397. }
  398. }
  399. region.widthUnits = percentage || globalWidth != null ?
  400. shaka.text.CueRegion.units.PERCENTAGE :
  401. shaka.text.CueRegion.units.PX;
  402. region.heightUnits = percentage || globalHeight != null ?
  403. shaka.text.CueRegion.units.PERCENTAGE :
  404. shaka.text.CueRegion.units.PX;
  405. }
  406. }
  407. const origin = TtmlTextParser.getStyleAttributeFromRegion_(
  408. regionElement, styles, 'origin');
  409. if (origin) {
  410. percentage = TtmlTextParser.percentValues_.exec(origin);
  411. results = percentage || TtmlTextParser.pixelValues_.exec(origin);
  412. if (results != null) {
  413. region.viewportAnchorX = Number(results[1]);
  414. region.viewportAnchorY = Number(results[2]);
  415. if (!percentage) {
  416. if (globalHeight != null) {
  417. region.viewportAnchorY = region.viewportAnchorY * 100 /
  418. globalHeight;
  419. }
  420. if (globalWidth != null) {
  421. region.viewportAnchorX = region.viewportAnchorX * 100 /
  422. globalWidth;
  423. }
  424. }
  425. region.viewportAnchorUnits = percentage || globalWidth != null ?
  426. shaka.text.CueRegion.units.PERCENTAGE :
  427. shaka.text.CueRegion.units.PX;
  428. }
  429. }
  430. return region;
  431. }
  432. /**
  433. * Adds applicable style properties to a cue.
  434. *
  435. * @param {!shaka.text.Cue} cue
  436. * @param {!Element} cueElement
  437. * @param {Element} region
  438. * @param {Element} imageElement
  439. * @param {!Array.<!Element>} styles
  440. * @param {boolean} isNested
  441. * @param {boolean} isLeaf
  442. * @private
  443. */
  444. static addStyle_(
  445. cue, cueElement, region, imageElement, styles, isNested, isLeaf) {
  446. const TtmlTextParser = shaka.text.TtmlTextParser;
  447. const Cue = shaka.text.Cue;
  448. // Styles should be inherited from regions, if a style property is not
  449. // associated with a Content element (or an anonymous span).
  450. const shouldInheritRegionStyles = isNested || isLeaf;
  451. const direction = TtmlTextParser.getStyleAttribute_(
  452. cueElement, region, styles, 'direction', shouldInheritRegionStyles);
  453. if (direction == 'rtl') {
  454. cue.direction = Cue.direction.HORIZONTAL_RIGHT_TO_LEFT;
  455. }
  456. // Direction attribute specifies one-dimentional writing direction
  457. // (left to right or right to left). Writing mode specifies that
  458. // plus whether text is vertical or horizontal.
  459. // They should not contradict each other. If they do, we give
  460. // preference to writing mode.
  461. const writingMode = TtmlTextParser.getStyleAttribute_(
  462. cueElement, region, styles, 'writingMode', shouldInheritRegionStyles);
  463. // Set cue's direction if the text is horizontal, and cue's writingMode if
  464. // it's vertical.
  465. if (writingMode == 'tb' || writingMode == 'tblr') {
  466. cue.writingMode = Cue.writingMode.VERTICAL_LEFT_TO_RIGHT;
  467. } else if (writingMode == 'tbrl') {
  468. cue.writingMode = Cue.writingMode.VERTICAL_RIGHT_TO_LEFT;
  469. } else if (writingMode == 'rltb' || writingMode == 'rl') {
  470. cue.direction = Cue.direction.HORIZONTAL_RIGHT_TO_LEFT;
  471. } else if (writingMode) {
  472. cue.direction = Cue.direction.HORIZONTAL_LEFT_TO_RIGHT;
  473. }
  474. const align = TtmlTextParser.getStyleAttribute_(
  475. cueElement, region, styles, 'textAlign', true);
  476. if (align) {
  477. cue.positionAlign = TtmlTextParser.textAlignToPositionAlign_[align];
  478. cue.lineAlign = TtmlTextParser.textAlignToLineAlign_[align];
  479. goog.asserts.assert(align.toUpperCase() in Cue.textAlign,
  480. align.toUpperCase() + ' Should be in Cue.textAlign values!');
  481. cue.textAlign = Cue.textAlign[align.toUpperCase()];
  482. } else {
  483. // Default value is START in the TTML spec: https://bit.ly/32OGmvo
  484. // But to make the subtitle render consitent with other players and the
  485. // shaka.text.Cue we use CENTER
  486. cue.textAlign = Cue.textAlign.CENTER;
  487. }
  488. const displayAlign = TtmlTextParser.getStyleAttribute_(
  489. cueElement, region, styles, 'displayAlign', true);
  490. if (displayAlign) {
  491. goog.asserts.assert(displayAlign.toUpperCase() in Cue.displayAlign,
  492. displayAlign.toUpperCase() +
  493. ' Should be in Cue.displayAlign values!');
  494. cue.displayAlign = Cue.displayAlign[displayAlign.toUpperCase()];
  495. }
  496. const color = TtmlTextParser.getStyleAttribute_(
  497. cueElement, region, styles, 'color', shouldInheritRegionStyles);
  498. if (color) {
  499. cue.color = color;
  500. }
  501. // Background color should not be set on a container. If this is a nested
  502. // cue, you can set the background. If it's a top-level that happens to
  503. // also be a leaf, you can set the background.
  504. // See https://github.com/shaka-project/shaka-player/issues/2623
  505. // This used to be handled in the displayer, but that is confusing. The Cue
  506. // structure should reflect what you want to happen in the displayer, and
  507. // the displayer shouldn't have to know about TTML.
  508. const backgroundColor = TtmlTextParser.getStyleAttribute_(
  509. cueElement, region, styles, 'backgroundColor',
  510. shouldInheritRegionStyles);
  511. if (backgroundColor) {
  512. cue.backgroundColor = backgroundColor;
  513. }
  514. const border = TtmlTextParser.getStyleAttribute_(
  515. cueElement, region, styles, 'border', shouldInheritRegionStyles);
  516. if (border) {
  517. cue.border = border;
  518. }
  519. const fontFamily = TtmlTextParser.getStyleAttribute_(
  520. cueElement, region, styles, 'fontFamily', shouldInheritRegionStyles);
  521. if (fontFamily) {
  522. cue.fontFamily = fontFamily;
  523. }
  524. const fontWeight = TtmlTextParser.getStyleAttribute_(
  525. cueElement, region, styles, 'fontWeight', shouldInheritRegionStyles);
  526. if (fontWeight && fontWeight == 'bold') {
  527. cue.fontWeight = Cue.fontWeight.BOLD;
  528. }
  529. const wrapOption = TtmlTextParser.getStyleAttribute_(
  530. cueElement, region, styles, 'wrapOption', shouldInheritRegionStyles);
  531. if (wrapOption && wrapOption == 'noWrap') {
  532. cue.wrapLine = false;
  533. } else {
  534. cue.wrapLine = true;
  535. }
  536. const lineHeight = TtmlTextParser.getStyleAttribute_(
  537. cueElement, region, styles, 'lineHeight', shouldInheritRegionStyles);
  538. if (lineHeight && lineHeight.match(TtmlTextParser.unitValues_)) {
  539. cue.lineHeight = lineHeight;
  540. }
  541. const fontSize = TtmlTextParser.getStyleAttribute_(
  542. cueElement, region, styles, 'fontSize', shouldInheritRegionStyles);
  543. if (fontSize) {
  544. const isValidFontSizeUnit =
  545. fontSize.match(TtmlTextParser.unitValues_) ||
  546. fontSize.match(TtmlTextParser.percentValue_);
  547. if (isValidFontSizeUnit) {
  548. cue.fontSize = fontSize;
  549. }
  550. }
  551. const fontStyle = TtmlTextParser.getStyleAttribute_(
  552. cueElement, region, styles, 'fontStyle', shouldInheritRegionStyles);
  553. if (fontStyle) {
  554. goog.asserts.assert(fontStyle.toUpperCase() in Cue.fontStyle,
  555. fontStyle.toUpperCase() +
  556. ' Should be in Cue.fontStyle values!');
  557. cue.fontStyle = Cue.fontStyle[fontStyle.toUpperCase()];
  558. }
  559. if (imageElement) {
  560. // According to the spec, we should use imageType (camelCase), but
  561. // historically we have checked for imagetype (lowercase).
  562. // This was the case since background image support was first introduced
  563. // in PR #1859, in April 2019, and first released in v2.5.0.
  564. // Now we check for both, although only imageType (camelCase) is to spec.
  565. const backgroundImageType =
  566. imageElement.getAttribute('imageType') ||
  567. imageElement.getAttribute('imagetype');
  568. const backgroundImageEncoding = imageElement.getAttribute('encoding');
  569. const backgroundImageData = imageElement.textContent.trim();
  570. if (backgroundImageType == 'PNG' &&
  571. backgroundImageEncoding == 'Base64' &&
  572. backgroundImageData) {
  573. cue.backgroundImage = 'data:image/png;base64,' + backgroundImageData;
  574. }
  575. }
  576. const textOutline = TtmlTextParser.getStyleAttribute_(
  577. cueElement, region, styles, 'textOutline', shouldInheritRegionStyles);
  578. if (textOutline) {
  579. // tts:textOutline isn't natively supported by browsers, but it can be
  580. // mostly replicated using the non-standard -webkit-text-stroke-width and
  581. // -webkit-text-stroke-color properties.
  582. const split = textOutline.split(' ');
  583. if (split[0].match(TtmlTextParser.unitValues_)) {
  584. // There is no defined color, so default to the text color.
  585. cue.textStrokeColor = cue.color;
  586. } else {
  587. cue.textStrokeColor = split[0];
  588. split.shift();
  589. }
  590. if (split[0] && split[0].match(TtmlTextParser.unitValues_)) {
  591. cue.textStrokeWidth = split[0];
  592. } else {
  593. // If there is no width, or the width is not a number, don't draw a
  594. // border.
  595. cue.textStrokeColor = '';
  596. }
  597. // There is an optional blur radius also, but we have no way of
  598. // replicating that, so ignore it.
  599. }
  600. const letterSpacing = TtmlTextParser.getStyleAttribute_(
  601. cueElement, region, styles, 'letterSpacing', shouldInheritRegionStyles);
  602. if (letterSpacing && letterSpacing.match(TtmlTextParser.unitValues_)) {
  603. cue.letterSpacing = letterSpacing;
  604. }
  605. const linePadding = TtmlTextParser.getStyleAttribute_(
  606. cueElement, region, styles, 'linePadding', shouldInheritRegionStyles);
  607. if (linePadding && linePadding.match(TtmlTextParser.unitValues_)) {
  608. cue.linePadding = linePadding;
  609. }
  610. const opacity = TtmlTextParser.getStyleAttribute_(
  611. cueElement, region, styles, 'opacity', shouldInheritRegionStyles);
  612. if (opacity) {
  613. cue.opacity = parseFloat(opacity);
  614. }
  615. // Text decoration is an array of values which can come both from the
  616. // element's style or be inherited from elements' parent nodes. All of those
  617. // values should be applied as long as they don't contradict each other. If
  618. // they do, elements' own style gets preference.
  619. const textDecorationRegion = TtmlTextParser.getStyleAttributeFromRegion_(
  620. region, styles, 'textDecoration');
  621. if (textDecorationRegion) {
  622. TtmlTextParser.addTextDecoration_(cue, textDecorationRegion);
  623. }
  624. const textDecorationElement = TtmlTextParser.getStyleAttributeFromElement_(
  625. cueElement, styles, 'textDecoration');
  626. if (textDecorationElement) {
  627. TtmlTextParser.addTextDecoration_(cue, textDecorationElement);
  628. }
  629. }
  630. /**
  631. * Parses text decoration values and adds/removes them to/from the cue.
  632. *
  633. * @param {!shaka.text.Cue} cue
  634. * @param {string} decoration
  635. * @private
  636. */
  637. static addTextDecoration_(cue, decoration) {
  638. const Cue = shaka.text.Cue;
  639. for (const value of decoration.split(' ')) {
  640. switch (value) {
  641. case 'underline':
  642. if (!cue.textDecoration.includes(Cue.textDecoration.UNDERLINE)) {
  643. cue.textDecoration.push(Cue.textDecoration.UNDERLINE);
  644. }
  645. break;
  646. case 'noUnderline':
  647. if (cue.textDecoration.includes(Cue.textDecoration.UNDERLINE)) {
  648. shaka.util.ArrayUtils.remove(cue.textDecoration,
  649. Cue.textDecoration.UNDERLINE);
  650. }
  651. break;
  652. case 'lineThrough':
  653. if (!cue.textDecoration.includes(Cue.textDecoration.LINE_THROUGH)) {
  654. cue.textDecoration.push(Cue.textDecoration.LINE_THROUGH);
  655. }
  656. break;
  657. case 'noLineThrough':
  658. if (cue.textDecoration.includes(Cue.textDecoration.LINE_THROUGH)) {
  659. shaka.util.ArrayUtils.remove(cue.textDecoration,
  660. Cue.textDecoration.LINE_THROUGH);
  661. }
  662. break;
  663. case 'overline':
  664. if (!cue.textDecoration.includes(Cue.textDecoration.OVERLINE)) {
  665. cue.textDecoration.push(Cue.textDecoration.OVERLINE);
  666. }
  667. break;
  668. case 'noOverline':
  669. if (cue.textDecoration.includes(Cue.textDecoration.OVERLINE)) {
  670. shaka.util.ArrayUtils.remove(cue.textDecoration,
  671. Cue.textDecoration.OVERLINE);
  672. }
  673. break;
  674. }
  675. }
  676. }
  677. /**
  678. * Finds a specified attribute on either the original cue element or its
  679. * associated region and returns the value if the attribute was found.
  680. *
  681. * @param {!Element} cueElement
  682. * @param {Element} region
  683. * @param {!Array.<!Element>} styles
  684. * @param {string} attribute
  685. * @param {boolean=} shouldInheritRegionStyles
  686. * @return {?string}
  687. * @private
  688. */
  689. static getStyleAttribute_(cueElement, region, styles, attribute,
  690. shouldInheritRegionStyles=true) {
  691. // An attribute can be specified on region level or in a styling block
  692. // associated with the region or original element.
  693. const TtmlTextParser = shaka.text.TtmlTextParser;
  694. const attr = TtmlTextParser.getStyleAttributeFromElement_(
  695. cueElement, styles, attribute);
  696. if (attr) {
  697. return attr;
  698. }
  699. if (shouldInheritRegionStyles) {
  700. return TtmlTextParser.getStyleAttributeFromRegion_(
  701. region, styles, attribute);
  702. }
  703. return null;
  704. }
  705. /**
  706. * Finds a specified attribute on the element's associated region
  707. * and returns the value if the attribute was found.
  708. *
  709. * @param {Element} region
  710. * @param {!Array.<!Element>} styles
  711. * @param {string} attribute
  712. * @return {?string}
  713. * @private
  714. */
  715. static getStyleAttributeFromRegion_(region, styles, attribute) {
  716. const XmlUtils = shaka.util.XmlUtils;
  717. const ttsNs = shaka.text.TtmlTextParser.styleNs_;
  718. if (!region) {
  719. return null;
  720. }
  721. const attr = XmlUtils.getAttributeNSList(region, ttsNs, attribute);
  722. if (attr) {
  723. return attr;
  724. }
  725. return shaka.text.TtmlTextParser.getInheritedStyleAttribute_(
  726. region, styles, attribute);
  727. }
  728. /**
  729. * Finds a specified attribute on the cue element and returns the value
  730. * if the attribute was found.
  731. *
  732. * @param {!Element} cueElement
  733. * @param {!Array.<!Element>} styles
  734. * @param {string} attribute
  735. * @return {?string}
  736. * @private
  737. */
  738. static getStyleAttributeFromElement_(cueElement, styles, attribute) {
  739. const XmlUtils = shaka.util.XmlUtils;
  740. const ttsNs = shaka.text.TtmlTextParser.styleNs_;
  741. // Styling on elements should take precedence
  742. // over the main styling attributes
  743. const elementAttribute = XmlUtils.getAttributeNSList(
  744. cueElement,
  745. ttsNs,
  746. attribute);
  747. if (elementAttribute) {
  748. return elementAttribute;
  749. }
  750. return shaka.text.TtmlTextParser.getInheritedStyleAttribute_(
  751. cueElement, styles, attribute);
  752. }
  753. /**
  754. * Finds a specified attribute on an element's styles and the styles those
  755. * styles inherit from.
  756. *
  757. * @param {!Element} element
  758. * @param {!Array.<!Element>} styles
  759. * @param {string} attribute
  760. * @return {?string}
  761. * @private
  762. */
  763. static getInheritedStyleAttribute_(element, styles, attribute) {
  764. const XmlUtils = shaka.util.XmlUtils;
  765. const ttsNs = shaka.text.TtmlTextParser.styleNs_;
  766. const ebuttsNs = shaka.text.TtmlTextParser.styleEbuttsNs_;
  767. const inheritedStyles =
  768. shaka.text.TtmlTextParser.getElementsFromCollection_(
  769. element, 'style', styles, /* prefix= */ '');
  770. let styleValue = null;
  771. // The last value in our styles stack takes the precedence over the others
  772. for (let i = 0; i < inheritedStyles.length; i++) {
  773. // Check ebu namespace first.
  774. let styleAttributeValue = XmlUtils.getAttributeNS(
  775. inheritedStyles[i],
  776. ebuttsNs,
  777. attribute);
  778. if (!styleAttributeValue) {
  779. // Fall back to tts namespace.
  780. styleAttributeValue = XmlUtils.getAttributeNSList(
  781. inheritedStyles[i],
  782. ttsNs,
  783. attribute);
  784. }
  785. if (!styleAttributeValue) {
  786. // Next, check inheritance.
  787. // Styles can inherit from other styles, so traverse up that chain.
  788. styleAttributeValue =
  789. shaka.text.TtmlTextParser.getStyleAttributeFromElement_(
  790. inheritedStyles[i], styles, attribute);
  791. }
  792. if (styleAttributeValue) {
  793. styleValue = styleAttributeValue;
  794. }
  795. }
  796. return styleValue;
  797. }
  798. /**
  799. * Selects items from |collection| whose id matches |attributeName|
  800. * from |element|.
  801. *
  802. * @param {Element} element
  803. * @param {string} attributeName
  804. * @param {!Array.<Element>} collection
  805. * @param {string} prefixName
  806. * @param {string=} nsName
  807. * @return {!Array.<!Element>}
  808. * @private
  809. */
  810. static getElementsFromCollection_(
  811. element, attributeName, collection, prefixName, nsName) {
  812. const items = [];
  813. if (!element || collection.length < 1) {
  814. return items;
  815. }
  816. const attributeValue = shaka.text.TtmlTextParser.getInheritedAttribute_(
  817. element, attributeName, nsName);
  818. if (attributeValue) {
  819. // There could be multiple items in one attribute
  820. // <span style="style1 style2">A cue</span>
  821. const itemNames = attributeValue.split(' ');
  822. for (const name of itemNames) {
  823. for (const item of collection) {
  824. if ((prefixName + item.getAttribute('xml:id')) == name) {
  825. items.push(item);
  826. break;
  827. }
  828. }
  829. }
  830. }
  831. return items;
  832. }
  833. /**
  834. * Traverses upwards from a given node until a given attribute is found.
  835. *
  836. * @param {!Element} element
  837. * @param {string} attributeName
  838. * @param {string=} nsName
  839. * @return {?string}
  840. * @private
  841. */
  842. static getInheritedAttribute_(element, attributeName, nsName) {
  843. let ret = null;
  844. const XmlUtils = shaka.util.XmlUtils;
  845. while (element) {
  846. ret = nsName ?
  847. XmlUtils.getAttributeNS(element, nsName, attributeName) :
  848. element.getAttribute(attributeName);
  849. if (ret) {
  850. break;
  851. }
  852. // Element.parentNode can lead to XMLDocument, which is not an Element and
  853. // has no getAttribute().
  854. const parentNode = element.parentNode;
  855. if (parentNode instanceof Element) {
  856. element = parentNode;
  857. } else {
  858. break;
  859. }
  860. }
  861. return ret;
  862. }
  863. /**
  864. * Factor parent/ancestor time attributes into the parsed time of a
  865. * child/descendent.
  866. *
  867. * @param {!Element} parentElement
  868. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  869. * @param {?number} start The child's start time
  870. * @param {?number} end The child's end time
  871. * @return {{start: ?number, end: ?number}}
  872. * @private
  873. */
  874. static resolveTime_(parentElement, rateInfo, start, end) {
  875. const parentTime = shaka.text.TtmlTextParser.parseTime_(
  876. parentElement, rateInfo);
  877. if (start == null) {
  878. // No start time of your own? Inherit from the parent.
  879. start = parentTime.start;
  880. } else {
  881. // Otherwise, the start time is relative to the parent's start time.
  882. if (parentTime.start != null) {
  883. start += parentTime.start;
  884. }
  885. }
  886. if (end == null) {
  887. // No end time of your own? Inherit from the parent.
  888. end = parentTime.end;
  889. } else {
  890. // Otherwise, the end time is relative to the parent's _start_ time.
  891. // This is not a typo. Both times are relative to the parent's _start_.
  892. if (parentTime.start != null) {
  893. end += parentTime.start;
  894. }
  895. }
  896. return {start, end};
  897. }
  898. /**
  899. * Parse TTML time attributes from the given element.
  900. *
  901. * @param {!Element} element
  902. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  903. * @return {{start: ?number, end: ?number}}
  904. * @private
  905. */
  906. static parseTime_(element, rateInfo) {
  907. const start = shaka.text.TtmlTextParser.parseTimeAttribute_(
  908. element.getAttribute('begin'), rateInfo);
  909. let end = shaka.text.TtmlTextParser.parseTimeAttribute_(
  910. element.getAttribute('end'), rateInfo);
  911. const duration = shaka.text.TtmlTextParser.parseTimeAttribute_(
  912. element.getAttribute('dur'), rateInfo);
  913. if (end == null && duration != null) {
  914. end = start + duration;
  915. }
  916. return {start, end};
  917. }
  918. /**
  919. * Parses a TTML time from the given attribute text.
  920. *
  921. * @param {string} text
  922. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  923. * @return {?number}
  924. * @private
  925. */
  926. static parseTimeAttribute_(text, rateInfo) {
  927. let ret = null;
  928. const TtmlTextParser = shaka.text.TtmlTextParser;
  929. if (TtmlTextParser.timeColonFormatFrames_.test(text)) {
  930. ret = TtmlTextParser.parseColonTimeWithFrames_(rateInfo, text);
  931. } else if (TtmlTextParser.timeColonFormat_.test(text)) {
  932. ret = TtmlTextParser.parseTimeFromRegex_(
  933. TtmlTextParser.timeColonFormat_, text);
  934. } else if (TtmlTextParser.timeColonFormatMilliseconds_.test(text)) {
  935. ret = TtmlTextParser.parseTimeFromRegex_(
  936. TtmlTextParser.timeColonFormatMilliseconds_, text);
  937. } else if (TtmlTextParser.timeFramesFormat_.test(text)) {
  938. ret = TtmlTextParser.parseFramesTime_(rateInfo, text);
  939. } else if (TtmlTextParser.timeTickFormat_.test(text)) {
  940. ret = TtmlTextParser.parseTickTime_(rateInfo, text);
  941. } else if (TtmlTextParser.timeHMSFormat_.test(text)) {
  942. ret = TtmlTextParser.parseTimeFromRegex_(
  943. TtmlTextParser.timeHMSFormat_, text);
  944. } else if (text) {
  945. // It's not empty or null, but it doesn't match a known format.
  946. throw new shaka.util.Error(
  947. shaka.util.Error.Severity.CRITICAL,
  948. shaka.util.Error.Category.TEXT,
  949. shaka.util.Error.Code.INVALID_TEXT_CUE,
  950. 'Could not parse cue time range in TTML');
  951. }
  952. return ret;
  953. }
  954. /**
  955. * Parses a TTML time in frame format.
  956. *
  957. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  958. * @param {string} text
  959. * @return {?number}
  960. * @private
  961. */
  962. static parseFramesTime_(rateInfo, text) {
  963. // 75f or 75.5f
  964. const results = shaka.text.TtmlTextParser.timeFramesFormat_.exec(text);
  965. const frames = Number(results[1]);
  966. return frames / rateInfo.frameRate;
  967. }
  968. /**
  969. * Parses a TTML time in tick format.
  970. *
  971. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  972. * @param {string} text
  973. * @return {?number}
  974. * @private
  975. */
  976. static parseTickTime_(rateInfo, text) {
  977. // 50t or 50.5t
  978. const results = shaka.text.TtmlTextParser.timeTickFormat_.exec(text);
  979. const ticks = Number(results[1]);
  980. return ticks / rateInfo.tickRate;
  981. }
  982. /**
  983. * Parses a TTML colon formatted time containing frames.
  984. *
  985. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  986. * @param {string} text
  987. * @return {?number}
  988. * @private
  989. */
  990. static parseColonTimeWithFrames_(rateInfo, text) {
  991. // 01:02:43:07 ('07' is frames) or 01:02:43:07.1 (subframes)
  992. const results = shaka.text.TtmlTextParser.timeColonFormatFrames_.exec(text);
  993. const hours = Number(results[1]);
  994. const minutes = Number(results[2]);
  995. let seconds = Number(results[3]);
  996. let frames = Number(results[4]);
  997. const subframes = Number(results[5]) || 0;
  998. frames += subframes / rateInfo.subFrameRate;
  999. seconds += frames / rateInfo.frameRate;
  1000. return seconds + (minutes * 60) + (hours * 3600);
  1001. }
  1002. /**
  1003. * Parses a TTML time with a given regex. Expects regex to be some
  1004. * sort of a time-matcher to match hours, minutes, seconds and milliseconds
  1005. *
  1006. * @param {!RegExp} regex
  1007. * @param {string} text
  1008. * @return {?number}
  1009. * @private
  1010. */
  1011. static parseTimeFromRegex_(regex, text) {
  1012. const results = regex.exec(text);
  1013. if (results == null || results[0] == '') {
  1014. return null;
  1015. }
  1016. // This capture is optional, but will still be in the array as undefined,
  1017. // in which case it is 0.
  1018. const hours = Number(results[1]) || 0;
  1019. const minutes = Number(results[2]) || 0;
  1020. const seconds = Number(results[3]) || 0;
  1021. const milliseconds = Number(results[4]) || 0;
  1022. return (milliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
  1023. }
  1024. /**
  1025. * If ttp:cellResolution provided returns cell resolution info
  1026. * with number of columns and rows into which the Root Container
  1027. * Region area is divided
  1028. *
  1029. * @param {?string} cellResolution
  1030. * @return {?{columns: number, rows: number}}
  1031. * @private
  1032. */
  1033. static getCellResolution_(cellResolution) {
  1034. if (!cellResolution) {
  1035. return null;
  1036. }
  1037. const matches = /^(\d+) (\d+)$/.exec(cellResolution);
  1038. if (!matches) {
  1039. return null;
  1040. }
  1041. const columns = parseInt(matches[1], 10);
  1042. const rows = parseInt(matches[2], 10);
  1043. return {columns, rows};
  1044. }
  1045. };
  1046. /**
  1047. * @summary
  1048. * Contains information about frame/subframe rate
  1049. * and frame rate multiplier for time in frame format.
  1050. *
  1051. * @example 01:02:03:04(4 frames) or 01:02:03:04.1(4 frames, 1 subframe)
  1052. * @private
  1053. */
  1054. shaka.text.TtmlTextParser.RateInfo_ = class {
  1055. /**
  1056. * @param {?string} frameRate
  1057. * @param {?string} subFrameRate
  1058. * @param {?string} frameRateMultiplier
  1059. * @param {?string} tickRate
  1060. */
  1061. constructor(frameRate, subFrameRate, frameRateMultiplier, tickRate) {
  1062. /**
  1063. * @type {number}
  1064. */
  1065. this.frameRate = Number(frameRate) || 30;
  1066. /**
  1067. * @type {number}
  1068. */
  1069. this.subFrameRate = Number(subFrameRate) || 1;
  1070. /**
  1071. * @type {number}
  1072. */
  1073. this.tickRate = Number(tickRate);
  1074. if (this.tickRate == 0) {
  1075. if (frameRate) {
  1076. this.tickRate = this.frameRate * this.subFrameRate;
  1077. } else {
  1078. this.tickRate = 1;
  1079. }
  1080. }
  1081. if (frameRateMultiplier) {
  1082. const multiplierResults = /^(\d+) (\d+)$/g.exec(frameRateMultiplier);
  1083. if (multiplierResults) {
  1084. const numerator = Number(multiplierResults[1]);
  1085. const denominator = Number(multiplierResults[2]);
  1086. const multiplierNum = numerator / denominator;
  1087. this.frameRate *= multiplierNum;
  1088. }
  1089. }
  1090. }
  1091. };
  1092. /**
  1093. * @const
  1094. * @private {!RegExp}
  1095. * @example 50.17% 10%
  1096. */
  1097. shaka.text.TtmlTextParser.percentValues_ =
  1098. /^(\d{1,2}(?:\.\d+)?|100(?:\.0+)?)% (\d{1,2}(?:\.\d+)?|100(?:\.0+)?)%$/;
  1099. /**
  1100. * @const
  1101. * @private {!RegExp}
  1102. * @example 0.6% 90%
  1103. */
  1104. shaka.text.TtmlTextParser.percentValue_ = /^(\d{1,2}(?:\.\d+)?|100)%$/;
  1105. /**
  1106. * @const
  1107. * @private {!RegExp}
  1108. * @example 100px, 8em, 0.80c
  1109. */
  1110. shaka.text.TtmlTextParser.unitValues_ = /^(\d+px|\d+em|\d*\.?\d+c)$/;
  1111. /**
  1112. * @const
  1113. * @private {!RegExp}
  1114. * @example 100px
  1115. */
  1116. shaka.text.TtmlTextParser.pixelValues_ = /^(\d+)px (\d+)px$/;
  1117. /**
  1118. * @const
  1119. * @private {!RegExp}
  1120. * @example 00:00:40:07 (7 frames) or 00:00:40:07.1 (7 frames, 1 subframe)
  1121. */
  1122. shaka.text.TtmlTextParser.timeColonFormatFrames_ =
  1123. /^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/;
  1124. /**
  1125. * @const
  1126. * @private {!RegExp}
  1127. * @example 00:00:40 or 00:40
  1128. */
  1129. shaka.text.TtmlTextParser.timeColonFormat_ = /^(?:(\d{2,}):)?(\d{2}):(\d{2})$/;
  1130. /**
  1131. * @const
  1132. * @private {!RegExp}
  1133. * @example 01:02:43.0345555 or 02:43.03
  1134. */
  1135. shaka.text.TtmlTextParser.timeColonFormatMilliseconds_ =
  1136. /^(?:(\d{2,}):)?(\d{2}):(\d{2}\.\d{2,})$/;
  1137. /**
  1138. * @const
  1139. * @private {!RegExp}
  1140. * @example 75f or 75.5f
  1141. */
  1142. shaka.text.TtmlTextParser.timeFramesFormat_ = /^(\d*(?:\.\d*)?)f$/;
  1143. /**
  1144. * @const
  1145. * @private {!RegExp}
  1146. * @example 50t or 50.5t
  1147. */
  1148. shaka.text.TtmlTextParser.timeTickFormat_ = /^(\d*(?:\.\d*)?)t$/;
  1149. /**
  1150. * @const
  1151. * @private {!RegExp}
  1152. * @example 3.45h, 3m or 4.20s
  1153. */
  1154. shaka.text.TtmlTextParser.timeHMSFormat_ =
  1155. new RegExp(['^(?:(\\d*(?:\\.\\d*)?)h)?',
  1156. '(?:(\\d*(?:\\.\\d*)?)m)?',
  1157. '(?:(\\d*(?:\\.\\d*)?)s)?',
  1158. '(?:(\\d*(?:\\.\\d*)?)ms)?$'].join(''));
  1159. /**
  1160. * @const
  1161. * @private {!Object.<string, shaka.text.Cue.lineAlign>}
  1162. */
  1163. shaka.text.TtmlTextParser.textAlignToLineAlign_ = {
  1164. 'left': shaka.text.Cue.lineAlign.START,
  1165. 'center': shaka.text.Cue.lineAlign.CENTER,
  1166. 'right': shaka.text.Cue.lineAlign.END,
  1167. 'start': shaka.text.Cue.lineAlign.START,
  1168. 'end': shaka.text.Cue.lineAlign.END,
  1169. };
  1170. /**
  1171. * @const
  1172. * @private {!Object.<string, shaka.text.Cue.positionAlign>}
  1173. */
  1174. shaka.text.TtmlTextParser.textAlignToPositionAlign_ = {
  1175. 'left': shaka.text.Cue.positionAlign.LEFT,
  1176. 'center': shaka.text.Cue.positionAlign.CENTER,
  1177. 'right': shaka.text.Cue.positionAlign.RIGHT,
  1178. };
  1179. /**
  1180. * The namespace URL for TTML parameters. Can be assigned any name in the TTML
  1181. * document, not just "ttp:", so we use this with getAttributeNS() to ensure
  1182. * that we support arbitrary namespace names.
  1183. *
  1184. * @const {!Array.<string>}
  1185. * @private
  1186. */
  1187. shaka.text.TtmlTextParser.parameterNs_ = [
  1188. 'http://www.w3.org/ns/ttml#parameter',
  1189. 'http://www.w3.org/2006/10/ttaf1#parameter',
  1190. ];
  1191. /**
  1192. * The namespace URL for TTML styles. Can be assigned any name in the TTML
  1193. * document, not just "tts:", so we use this with getAttributeNS() to ensure
  1194. * that we support arbitrary namespace names.
  1195. *
  1196. * @const {!Array.<string>}
  1197. * @private
  1198. */
  1199. shaka.text.TtmlTextParser.styleNs_ = [
  1200. 'http://www.w3.org/ns/ttml#styling',
  1201. 'http://www.w3.org/2006/10/ttaf1#styling',
  1202. ];
  1203. /**
  1204. * The namespace URL for EBU TTML styles. Can be assigned any name in the TTML
  1205. * document, not just "ebutts:", so we use this with getAttributeNS() to ensure
  1206. * that we support arbitrary namespace names.
  1207. *
  1208. * @const {string}
  1209. * @private
  1210. */
  1211. shaka.text.TtmlTextParser.styleEbuttsNs_ = 'urn:ebu:tt:style';
  1212. /**
  1213. * The supported namespace URLs for SMPTE fields.
  1214. * @const {!Array.<string>}
  1215. * @private
  1216. */
  1217. shaka.text.TtmlTextParser.smpteNsList_ = [
  1218. 'http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt',
  1219. 'http://www.smpte-ra.org/schemas/2052-1/2013/smpte-tt',
  1220. ];
  1221. shaka.text.TextEngine.registerParser(
  1222. 'application/ttml+xml', () => new shaka.text.TtmlTextParser());