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