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