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