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