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