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