Source: lib/text/ttml_text_parser.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.text.TtmlTextParser');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.text.Cue');
  21. goog.require('shaka.text.CueRegion');
  22. goog.require('shaka.text.TextEngine');
  23. goog.require('shaka.util.ArrayUtils');
  24. goog.require('shaka.util.Error');
  25. goog.require('shaka.util.StringUtils');
  26. goog.require('shaka.util.XmlUtils');
  27. /**
  28. * @constructor
  29. * @implements {shakaExtern.TextParser}
  30. */
  31. shaka.text.TtmlTextParser = function() {};
  32. /**
  33. * @const {string}
  34. * @private
  35. */
  36. shaka.text.TtmlTextParser.parameterNs_ = 'http://www.w3.org/ns/ttml#parameter';
  37. /**
  38. * @const {string}
  39. * @private
  40. */
  41. shaka.text.TtmlTextParser.styleNs_ = 'http://www.w3.org/ns/ttml#styling';
  42. /** @override */
  43. shaka.text.TtmlTextParser.prototype.parseInit = function(data) {
  44. goog.asserts.assert(false, 'TTML does not have init segments');
  45. };
  46. /** @override */
  47. shaka.text.TtmlTextParser.prototype.parseMedia = function(data, time) {
  48. const TtmlTextParser = shaka.text.TtmlTextParser;
  49. const XmlUtils = shaka.util.XmlUtils;
  50. const ttpNs = TtmlTextParser.parameterNs_;
  51. let str = shaka.util.StringUtils.fromUTF8(data);
  52. let ret = [];
  53. let parser = new DOMParser();
  54. let xml = null;
  55. try {
  56. xml = parser.parseFromString(str, 'text/xml');
  57. } catch (exception) {
  58. throw new shaka.util.Error(
  59. shaka.util.Error.Severity.CRITICAL,
  60. shaka.util.Error.Category.TEXT,
  61. shaka.util.Error.Code.INVALID_XML);
  62. }
  63. if (xml) {
  64. // Try to get the framerate, subFrameRate and frameRateMultiplier
  65. // if applicable
  66. let frameRate = null;
  67. let subFrameRate = null;
  68. let frameRateMultiplier = null;
  69. let tickRate = null;
  70. let spaceStyle = null;
  71. let tts = xml.getElementsByTagName('tt');
  72. let tt = tts[0];
  73. // TTML should always have tt element.
  74. if (!tt) {
  75. throw new shaka.util.Error(
  76. shaka.util.Error.Severity.CRITICAL,
  77. shaka.util.Error.Category.TEXT,
  78. shaka.util.Error.Code.INVALID_XML);
  79. } else {
  80. frameRate = XmlUtils.getAttributeNS(tt, ttpNs, 'frameRate');
  81. subFrameRate = XmlUtils.getAttributeNS(tt, ttpNs, 'subFrameRate');
  82. frameRateMultiplier =
  83. XmlUtils.getAttributeNS(tt, ttpNs, 'frameRateMultiplier');
  84. tickRate = XmlUtils.getAttributeNS(tt, ttpNs, 'tickRate');
  85. spaceStyle = tt.getAttribute('xml:space') || 'default';
  86. }
  87. if (spaceStyle != 'default' && spaceStyle != 'preserve') {
  88. throw new shaka.util.Error(
  89. shaka.util.Error.Severity.CRITICAL,
  90. shaka.util.Error.Category.TEXT,
  91. shaka.util.Error.Code.INVALID_XML);
  92. }
  93. let whitespaceTrim = spaceStyle == 'default';
  94. let rateInfo = new TtmlTextParser.RateInfo_(
  95. frameRate, subFrameRate, frameRateMultiplier, tickRate);
  96. let styles = TtmlTextParser.getLeafNodes_(
  97. tt.getElementsByTagName('styling')[0]);
  98. let regionElements = TtmlTextParser.getLeafNodes_(
  99. tt.getElementsByTagName('layout')[0]);
  100. let cueRegions = [];
  101. for (let i = 0; i < regionElements.length; i++) {
  102. let cueRegion = TtmlTextParser.parseCueRegion_(
  103. regionElements[i], styles);
  104. if (cueRegion) {
  105. cueRegions.push(cueRegion);
  106. }
  107. }
  108. let textNodes = TtmlTextParser.getLeafNodes_(
  109. tt.getElementsByTagName('body')[0]);
  110. for (let i = 0; i < textNodes.length; i++) {
  111. let cue = TtmlTextParser.parseCue_(textNodes[i],
  112. time.periodStart,
  113. rateInfo,
  114. styles,
  115. regionElements,
  116. cueRegions,
  117. whitespaceTrim);
  118. if (cue) {
  119. ret.push(cue);
  120. }
  121. }
  122. }
  123. return ret;
  124. };
  125. /**
  126. * @const
  127. * @private {!RegExp}
  128. * @example 50% 10%
  129. */
  130. shaka.text.TtmlTextParser.percentValues_ = /^(\d{1,2}|100)% (\d{1,2}|100)%$/;
  131. /**
  132. * @const
  133. * @private {!RegExp}
  134. * @example 100px
  135. */
  136. shaka.text.TtmlTextParser.unitValues_ = /^(\d+px|\d+em)$/;
  137. /**
  138. * @const
  139. * @private {!RegExp}
  140. * @example 100px
  141. */
  142. shaka.text.TtmlTextParser.pixelValues_ = /^(\d+)px (\d+)px$/;
  143. /**
  144. * @const
  145. * @private {!RegExp}
  146. * @example 00:00:40:07 (7 frames) or 00:00:40:07.1 (7 frames, 1 subframe)
  147. */
  148. shaka.text.TtmlTextParser.timeColonFormatFrames_ =
  149. /^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/;
  150. /**
  151. * @const
  152. * @private {!RegExp}
  153. * @example 00:00:40 or 00:40
  154. */
  155. shaka.text.TtmlTextParser.timeColonFormat_ = /^(?:(\d{2,}):)?(\d{2}):(\d{2})$/;
  156. /**
  157. * @const
  158. * @private {!RegExp}
  159. * @example 01:02:43.0345555 or 02:43.03
  160. */
  161. shaka.text.TtmlTextParser.timeColonFormatMilliseconds_ =
  162. /^(?:(\d{2,}):)?(\d{2}):(\d{2}\.\d{2,})$/;
  163. /**
  164. * @const
  165. * @private {!RegExp}
  166. * @example 75f or 75.5f
  167. */
  168. shaka.text.TtmlTextParser.timeFramesFormat_ = /^(\d*(?:\.\d*)?)f$/;
  169. /**
  170. * @const
  171. * @private {!RegExp}
  172. * @example 50t or 50.5t
  173. */
  174. shaka.text.TtmlTextParser.timeTickFormat_ = /^(\d*(?:\.\d*)?)t$/;
  175. /**
  176. * @const
  177. * @private {!RegExp}
  178. * @example 3.45h, 3m or 4.20s
  179. */
  180. shaka.text.TtmlTextParser.timeHMSFormat_ =
  181. new RegExp(['^(?:(\\d*(?:\\.\\d*)?)h)?',
  182. '(?:(\\d*(?:\\.\\d*)?)m)?',
  183. '(?:(\\d*(?:\\.\\d*)?)s)?',
  184. '(?:(\\d*(?:\\.\\d*)?)ms)?$'].join(''));
  185. /**
  186. * @const
  187. * @private {!Object.<string, shaka.text.Cue.lineAlign>}
  188. */
  189. shaka.text.TtmlTextParser.textAlignToLineAlign_ = {
  190. 'left': shaka.text.Cue.lineAlign.START,
  191. 'center': shaka.text.Cue.lineAlign.CENTER,
  192. 'right': shaka.text.Cue.lineAlign.END,
  193. 'start': shaka.text.Cue.lineAlign.START,
  194. 'end': shaka.text.Cue.lineAlign.END,
  195. };
  196. /**
  197. * @const
  198. * @private {!Object.<string, shaka.text.Cue.positionAlign>}
  199. */
  200. shaka.text.TtmlTextParser.textAlignToPositionAlign_ = {
  201. 'left': shaka.text.Cue.positionAlign.LEFT,
  202. 'center': shaka.text.Cue.positionAlign.CENTER,
  203. 'right': shaka.text.Cue.positionAlign.RIGHT,
  204. };
  205. /**
  206. * Gets the leaf nodes of the xml node tree. Ignores the text, br elements
  207. * and the spans positioned inside paragraphs
  208. *
  209. * @param {Element} element
  210. * @return {!Array.<!Element>}
  211. * @private
  212. */
  213. shaka.text.TtmlTextParser.getLeafNodes_ = function(element) {
  214. let result = [];
  215. if (!element) {
  216. return result;
  217. }
  218. let childNodes = element.childNodes;
  219. for (let i = 0; i < childNodes.length; i++) {
  220. // Currently we don't support styles applicable to span
  221. // elements, so they are ignored.
  222. let isSpanChildOfP = childNodes[i].nodeName == 'span' &&
  223. element.nodeName == 'p';
  224. if (childNodes[i].nodeType == Node.ELEMENT_NODE &&
  225. childNodes[i].nodeName != 'br' && !isSpanChildOfP) {
  226. // Get the leaves the child might contain.
  227. goog.asserts.assert(childNodes[i] instanceof Element,
  228. 'Node should be Element!');
  229. let leafChildren = shaka.text.TtmlTextParser.getLeafNodes_(
  230. /** @type {Element} */(childNodes[i]));
  231. goog.asserts.assert(leafChildren.length > 0,
  232. 'Only a null Element should return no leaves!');
  233. result = result.concat(leafChildren);
  234. }
  235. }
  236. // if no result at this point, the element itself must be a leaf.
  237. if (!result.length) {
  238. result.push(element);
  239. }
  240. return result;
  241. };
  242. /**
  243. * Inserts \n where <br> tags are found.
  244. *
  245. * @param {!Node} element
  246. * @param {boolean} whitespaceTrim
  247. * @private
  248. */
  249. shaka.text.TtmlTextParser.addNewLines_ = function(element, whitespaceTrim) {
  250. let childNodes = element.childNodes;
  251. for (let i = 0; i < childNodes.length; i++) {
  252. if (childNodes[i].nodeName == 'br' && i > 0) {
  253. childNodes[i - 1].textContent += '\n';
  254. } else if (childNodes[i].childNodes.length > 0) {
  255. shaka.text.TtmlTextParser.addNewLines_(childNodes[i], whitespaceTrim);
  256. } else if (whitespaceTrim) {
  257. // Trim leading and trailing whitespace.
  258. let trimmed = childNodes[i].textContent.trim();
  259. // Collapse multiple spaces into one.
  260. trimmed = trimmed.replace(/\s+/g, ' ');
  261. childNodes[i].textContent = trimmed;
  262. }
  263. }
  264. };
  265. /**
  266. * Parses an Element into a TextTrackCue or VTTCue.
  267. *
  268. * @param {!Element} cueElement
  269. * @param {number} offset
  270. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  271. * @param {!Array.<!Element>} styles
  272. * @param {!Array.<!Element>} regionElements
  273. * @param {!Array.<!shaka.text.CueRegion>} cueRegions
  274. * @param {boolean} whitespaceTrim
  275. * @return {shaka.text.Cue}
  276. * @private
  277. */
  278. shaka.text.TtmlTextParser.parseCue_ = function(
  279. cueElement, offset, rateInfo, styles, regionElements,
  280. cueRegions, whitespaceTrim) {
  281. // Disregard empty elements:
  282. // TTML allows for empty elements like <div></div>.
  283. // If cueElement has neither time attributes, nor
  284. // non-whitespace text, don't try to make a cue out of it.
  285. if (!cueElement.hasAttribute('begin') &&
  286. !cueElement.hasAttribute('end') &&
  287. /^\s*$/.test(cueElement.textContent)) {
  288. return null;
  289. }
  290. shaka.text.TtmlTextParser.addNewLines_(cueElement, whitespaceTrim);
  291. // Get time.
  292. let start = shaka.text.TtmlTextParser.parseTime_(
  293. cueElement.getAttribute('begin'), rateInfo);
  294. let end = shaka.text.TtmlTextParser.parseTime_(
  295. cueElement.getAttribute('end'), rateInfo);
  296. let duration = shaka.text.TtmlTextParser.parseTime_(
  297. cueElement.getAttribute('dur'), rateInfo);
  298. let payload = cueElement.textContent;
  299. if (end == null && duration != null) {
  300. end = start + duration;
  301. }
  302. if (start == null || end == null) {
  303. throw new shaka.util.Error(
  304. shaka.util.Error.Severity.CRITICAL,
  305. shaka.util.Error.Category.TEXT,
  306. shaka.util.Error.Code.INVALID_TEXT_CUE);
  307. }
  308. start += offset;
  309. end += offset;
  310. let cue = new shaka.text.Cue(start, end, payload);
  311. // Get other properties if available.
  312. let regionElement = shaka.text.TtmlTextParser.getElementFromCollection_(
  313. cueElement, 'region', regionElements);
  314. if (regionElement && regionElement.getAttribute('xml:id')) {
  315. let regionId = regionElement.getAttribute('xml:id');
  316. let regionsWithId = cueRegions.filter(function(region) {
  317. return region.id == regionId;
  318. });
  319. cue.region = regionsWithId[0];
  320. }
  321. shaka.text.TtmlTextParser.addStyle_(cue, cueElement, regionElement, styles);
  322. return cue;
  323. };
  324. /**
  325. * Parses an Element into a TextTrackCue or VTTCue.
  326. *
  327. * @param {!Element} regionElement
  328. * @param {!Array.<!Element>} styles
  329. * @return {shaka.text.CueRegion}
  330. * @private
  331. */
  332. shaka.text.TtmlTextParser.parseCueRegion_ = function(regionElement, styles) {
  333. const TtmlTextParser = shaka.text.TtmlTextParser;
  334. let region = new shaka.text.CueRegion();
  335. let id = regionElement.getAttribute('xml:id');
  336. if (!id) {
  337. shaka.log.warning('TtmlTextParser parser encountered a region with ' +
  338. 'no id. Region will be ignored.');
  339. return null;
  340. }
  341. region.id = id;
  342. let results = null;
  343. let percentage = null;
  344. let extent = TtmlTextParser.getStyleAttributeFromRegion_(
  345. regionElement, styles, 'extent');
  346. if (extent) {
  347. percentage = TtmlTextParser.percentValues_.exec(extent);
  348. results = percentage || TtmlTextParser.pixelValues_.exec(extent);
  349. if (results != null) {
  350. region.width = Number(results[1]);
  351. region.height = Number(results[2]);
  352. region.widthUnits = percentage ?
  353. shaka.text.CueRegion.units.PERCENTAGE :
  354. shaka.text.CueRegion.units.PX;
  355. region.heightUnits = percentage ?
  356. shaka.text.CueRegion.units.PERCENTAGE :
  357. shaka.text.CueRegion.units.PX;
  358. }
  359. }
  360. let origin = TtmlTextParser.getStyleAttributeFromRegion_(
  361. regionElement, styles, 'origin');
  362. if (origin) {
  363. percentage = TtmlTextParser.percentValues_.exec(origin);
  364. results = percentage || TtmlTextParser.pixelValues_.exec(origin);
  365. if (results != null) {
  366. region.viewportAnchorX = Number(results[1]);
  367. region.viewportAnchorY = Number(results[2]);
  368. region.viewportAnchorUnits = percentage ?
  369. shaka.text.CueRegion.units.PERCENTAGE :
  370. shaka.text.CueRegion.units.PX;
  371. }
  372. }
  373. return region;
  374. };
  375. /**
  376. * Adds applicable style properties to a cue.
  377. *
  378. * @param {!shaka.text.Cue} cue
  379. * @param {!Element} cueElement
  380. * @param {Element} region
  381. * @param {!Array.<!Element>} styles
  382. * @private
  383. */
  384. shaka.text.TtmlTextParser.addStyle_ = function(
  385. cue, cueElement, region, styles) {
  386. const TtmlTextParser = shaka.text.TtmlTextParser;
  387. const Cue = shaka.text.Cue;
  388. let direction = TtmlTextParser.getStyleAttribute_(
  389. cueElement, region, styles, 'direction');
  390. if (direction == 'rtl') {
  391. cue.writingDirection = Cue.writingDirection.HORIZONTAL_RIGHT_TO_LEFT;
  392. }
  393. // Direction attribute specifies one-dimentional writing direction
  394. // (left to right or right to left). Writing mode specifies that
  395. // plus whether text is vertical or horizontal.
  396. // They should not contradict each other. If they do, we give
  397. // preference to writing mode.
  398. let writingMode = TtmlTextParser.getStyleAttribute_(
  399. cueElement, region, styles, 'writingMode');
  400. if (writingMode == 'tb' || writingMode == 'tblr') {
  401. cue.writingDirection = Cue.writingDirection.VERTICAL_LEFT_TO_RIGHT;
  402. } else if (writingMode == 'tbrl') {
  403. cue.writingDirection = Cue.writingDirection.VERTICAL_RIGHT_TO_LEFT;
  404. } else if (writingMode == 'rltb' || writingMode == 'rl') {
  405. cue.writingDirection = Cue.writingDirection.HORIZONTAL_RIGHT_TO_LEFT;
  406. } else if (writingMode) {
  407. cue.writingDirection = Cue.writingDirection.HORIZONTAL_LEFT_TO_RIGHT;
  408. }
  409. let align = TtmlTextParser.getStyleAttribute_(
  410. cueElement, region, styles, 'textAlign');
  411. if (align) {
  412. cue.positionAlign = TtmlTextParser.textAlignToPositionAlign_[align];
  413. cue.lineAlign = TtmlTextParser.textAlignToLineAlign_[align];
  414. goog.asserts.assert(align.toUpperCase() in Cue.textAlign,
  415. align.toUpperCase() +
  416. ' Should be in Cue.textAlign values!');
  417. cue.textAlign = Cue.textAlign[align.toUpperCase()];
  418. }
  419. let displayAlign = TtmlTextParser.getStyleAttribute_(
  420. cueElement, region, styles, 'displayAlign');
  421. if (displayAlign) {
  422. goog.asserts.assert(displayAlign.toUpperCase() in Cue.displayAlign,
  423. displayAlign.toUpperCase() +
  424. ' Should be in Cue.displayAlign values!');
  425. cue.displayAlign = Cue.displayAlign[displayAlign.toUpperCase()];
  426. }
  427. let color = TtmlTextParser.getStyleAttribute_(
  428. cueElement, region, styles, 'color');
  429. if (color) {
  430. cue.color = color;
  431. }
  432. let backgroundColor = TtmlTextParser.getStyleAttribute_(
  433. cueElement, region, styles, 'backgroundColor');
  434. if (backgroundColor) {
  435. cue.backgroundColor = backgroundColor;
  436. }
  437. let fontFamily = TtmlTextParser.getStyleAttribute_(
  438. cueElement, region, styles, 'fontFamily');
  439. if (fontFamily) {
  440. cue.fontFamily = fontFamily;
  441. }
  442. let fontWeight = TtmlTextParser.getStyleAttribute_(
  443. cueElement, region, styles, 'fontWeight');
  444. if (fontWeight && fontWeight == 'bold') {
  445. cue.fontWeight = Cue.fontWeight.BOLD;
  446. }
  447. let wrapOption = TtmlTextParser.getStyleAttribute_(
  448. cueElement, region, styles, 'wrapOption');
  449. if (wrapOption && wrapOption == 'noWrap') {
  450. cue.wrapLine = false;
  451. }
  452. let lineHeight = TtmlTextParser.getStyleAttribute_(
  453. cueElement, region, styles, 'lineHeight');
  454. if (lineHeight && lineHeight.match(TtmlTextParser.unitValues_)) {
  455. cue.lineHeight = lineHeight;
  456. }
  457. let fontSize = TtmlTextParser.getStyleAttribute_(
  458. cueElement, region, styles, 'fontSize');
  459. if (fontSize && fontSize.match(TtmlTextParser.unitValues_)) {
  460. cue.fontSize = fontSize;
  461. }
  462. let fontStyle = TtmlTextParser.getStyleAttribute_(
  463. cueElement, region, styles, 'fontStyle');
  464. if (fontStyle) {
  465. goog.asserts.assert(fontStyle.toUpperCase() in Cue.fontStyle,
  466. fontStyle.toUpperCase() +
  467. ' Should be in Cue.fontStyle values!');
  468. cue.fontStyle = Cue.fontStyle[fontStyle.toUpperCase()];
  469. }
  470. // Text decoration is an array of values which can come both from the
  471. // element's style or be inherited from elements' parent nodes. All of those
  472. // values should be applied as long as they don't contradict each other. If
  473. // they do, elements' own style gets preference.
  474. let textDecorationRegion = TtmlTextParser.getStyleAttributeFromRegion_(
  475. region, styles, 'textDecoration');
  476. if (textDecorationRegion) {
  477. TtmlTextParser.addTextDecoration_(cue, textDecorationRegion);
  478. }
  479. let textDecorationElement = TtmlTextParser.getStyleAttributeFromElement_(
  480. cueElement, styles, 'textDecoration');
  481. if (textDecorationElement) {
  482. TtmlTextParser.addTextDecoration_(cue, textDecorationElement);
  483. }
  484. };
  485. /**
  486. * Parses text decoration values and adds/removes them to/from the cue.
  487. *
  488. * @param {!shaka.text.Cue} cue
  489. * @param {string} decoration
  490. * @private
  491. */
  492. shaka.text.TtmlTextParser.addTextDecoration_ = function(cue, decoration) {
  493. const Cue = shaka.text.Cue;
  494. let values = decoration.split(' ');
  495. for (let i = 0; i < values.length; i++) {
  496. switch (values[i]) {
  497. case 'underline':
  498. if (cue.textDecoration.indexOf(Cue.textDecoration.UNDERLINE) < 0) {
  499. cue.textDecoration.push(Cue.textDecoration.UNDERLINE);
  500. }
  501. break;
  502. case 'noUnderline':
  503. if (cue.textDecoration.indexOf(Cue.textDecoration.UNDERLINE) >= 0) {
  504. shaka.util.ArrayUtils.remove(cue.textDecoration,
  505. Cue.textDecoration.UNDERLINE);
  506. }
  507. break;
  508. case 'lineThrough':
  509. if (cue.textDecoration.indexOf(Cue.textDecoration.LINE_THROUGH) < 0) {
  510. cue.textDecoration.push(Cue.textDecoration.LINE_THROUGH);
  511. }
  512. break;
  513. case 'noLineThrough':
  514. if (cue.textDecoration.indexOf(Cue.textDecoration.LINE_THROUGH) >= 0) {
  515. shaka.util.ArrayUtils.remove(cue.textDecoration,
  516. Cue.textDecoration.LINE_THROUGH);
  517. }
  518. break;
  519. case 'overline':
  520. if (cue.textDecoration.indexOf(Cue.textDecoration.OVERLINE) < 0) {
  521. cue.textDecoration.push(Cue.textDecoration.OVERLINE);
  522. }
  523. break;
  524. case 'noOverline':
  525. if (cue.textDecoration.indexOf(Cue.textDecoration.OVERLINE) >= 0) {
  526. shaka.util.ArrayUtils.remove(cue.textDecoration,
  527. Cue.textDecoration.OVERLINE);
  528. }
  529. break;
  530. }
  531. }
  532. };
  533. /**
  534. * Finds a specified attribute on either the original cue element or its
  535. * associated region and returns the value if the attribute was found.
  536. *
  537. * @param {!Element} cueElement
  538. * @param {Element} region
  539. * @param {!Array.<!Element>} styles
  540. * @param {string} attribute
  541. * @return {?string}
  542. * @private
  543. */
  544. shaka.text.TtmlTextParser.getStyleAttribute_ = function(
  545. cueElement, region, styles, attribute) {
  546. // An attribute can be specified on region level or in a styling block
  547. // associated with the region or original element.
  548. const TtmlTextParser = shaka.text.TtmlTextParser;
  549. let attr = TtmlTextParser.getStyleAttributeFromElement_(
  550. cueElement, styles, attribute);
  551. if (attr) {
  552. return attr;
  553. }
  554. return TtmlTextParser.getStyleAttributeFromRegion_(
  555. region, styles, attribute);
  556. };
  557. /**
  558. * Finds a specified attribute on the element's associated region
  559. * and returns the value if the attribute was found.
  560. *
  561. * @param {Element} region
  562. * @param {!Array.<!Element>} styles
  563. * @param {string} attribute
  564. * @return {?string}
  565. * @private
  566. */
  567. shaka.text.TtmlTextParser.getStyleAttributeFromRegion_ = function(
  568. region, styles, attribute) {
  569. const XmlUtils = shaka.util.XmlUtils;
  570. const ttsNs = shaka.text.TtmlTextParser.styleNs_;
  571. let regionChildren = shaka.text.TtmlTextParser.getLeafNodes_(region);
  572. for (let i = 0; i < regionChildren.length; i++) {
  573. let attr = XmlUtils.getAttributeNS(regionChildren[i], ttsNs, attribute);
  574. if (attr) {
  575. return attr;
  576. }
  577. }
  578. let style = shaka.text.TtmlTextParser.getElementFromCollection_(
  579. region, 'style', styles);
  580. if (style) {
  581. return XmlUtils.getAttributeNS(style, ttsNs, attribute);
  582. }
  583. return null;
  584. };
  585. /**
  586. * Finds a specified attribute on the cue element and returns the value
  587. * if the attribute was found.
  588. *
  589. * @param {!Element} cueElement
  590. * @param {!Array.<!Element>} styles
  591. * @param {string} attribute
  592. * @return {?string}
  593. * @private
  594. */
  595. shaka.text.TtmlTextParser.getStyleAttributeFromElement_ = function(
  596. cueElement, styles, attribute) {
  597. const XmlUtils = shaka.util.XmlUtils;
  598. const ttsNs = shaka.text.TtmlTextParser.styleNs_;
  599. let getElementFromCollection_ =
  600. shaka.text.TtmlTextParser.getElementFromCollection_;
  601. let style = getElementFromCollection_(cueElement, 'style', styles);
  602. if (style) {
  603. return XmlUtils.getAttributeNS(style, ttsNs, attribute);
  604. }
  605. return null;
  606. };
  607. /**
  608. * Selects an item from |collection| whose id matches |attributeName|
  609. * from |element|.
  610. *
  611. * @param {Element} element
  612. * @param {string} attributeName
  613. * @param {!Array.<Element>} collection
  614. * @return {Element}
  615. * @private
  616. */
  617. shaka.text.TtmlTextParser.getElementFromCollection_ = function(
  618. element, attributeName, collection) {
  619. if (!element || collection.length < 1) {
  620. return null;
  621. }
  622. let item = null;
  623. let itemName = shaka.text.TtmlTextParser.getInheritedAttribute_(
  624. element, attributeName);
  625. if (itemName) {
  626. for (let i = 0; i < collection.length; i++) {
  627. if (collection[i].getAttribute('xml:id') == itemName) {
  628. item = collection[i];
  629. break;
  630. }
  631. }
  632. }
  633. return item;
  634. };
  635. /**
  636. * Traverses upwards from a given node until a given attribute is found.
  637. *
  638. * @param {!Element} element
  639. * @param {string} attributeName
  640. * @return {?string}
  641. * @private
  642. */
  643. shaka.text.TtmlTextParser.getInheritedAttribute_ = function(
  644. element, attributeName) {
  645. let ret = null;
  646. while (element) {
  647. ret = element.getAttribute(attributeName);
  648. if (ret) {
  649. break;
  650. }
  651. // Element.parentNode can lead to XMLDocument, which is not an Element and
  652. // has no getAttribute().
  653. let parentNode = element.parentNode;
  654. if (parentNode instanceof Element) {
  655. element = parentNode;
  656. } else {
  657. break;
  658. }
  659. }
  660. return ret;
  661. };
  662. /**
  663. * Parses a TTML time from the given word.
  664. *
  665. * @param {string} text
  666. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  667. * @return {?number}
  668. * @private
  669. */
  670. shaka.text.TtmlTextParser.parseTime_ = function(text, rateInfo) {
  671. let ret = null;
  672. const TtmlTextParser = shaka.text.TtmlTextParser;
  673. if (TtmlTextParser.timeColonFormatFrames_.test(text)) {
  674. ret = TtmlTextParser.parseColonTimeWithFrames_(rateInfo, text);
  675. } else if (TtmlTextParser.timeColonFormat_.test(text)) {
  676. ret = TtmlTextParser.parseTimeFromRegex_(
  677. TtmlTextParser.timeColonFormat_, text);
  678. } else if (TtmlTextParser.timeColonFormatMilliseconds_.test(text)) {
  679. ret = TtmlTextParser.parseTimeFromRegex_(
  680. TtmlTextParser.timeColonFormatMilliseconds_, text);
  681. } else if (TtmlTextParser.timeFramesFormat_.test(text)) {
  682. ret = TtmlTextParser.parseFramesTime_(rateInfo, text);
  683. } else if (TtmlTextParser.timeTickFormat_.test(text)) {
  684. ret = TtmlTextParser.parseTickTime_(rateInfo, text);
  685. } else if (TtmlTextParser.timeHMSFormat_.test(text)) {
  686. ret = TtmlTextParser.parseTimeFromRegex_(
  687. TtmlTextParser.timeHMSFormat_, text);
  688. }
  689. return ret;
  690. };
  691. /**
  692. * Parses a TTML time in frame format.
  693. *
  694. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  695. * @param {string} text
  696. * @return {?number}
  697. * @private
  698. */
  699. shaka.text.TtmlTextParser.parseFramesTime_ = function(rateInfo, text) {
  700. // 75f or 75.5f
  701. let results = shaka.text.TtmlTextParser.timeFramesFormat_.exec(text);
  702. let frames = Number(results[1]);
  703. return frames / rateInfo.frameRate;
  704. };
  705. /**
  706. * Parses a TTML time in tick format.
  707. *
  708. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  709. * @param {string} text
  710. * @return {?number}
  711. * @private
  712. */
  713. shaka.text.TtmlTextParser.parseTickTime_ = function(rateInfo, text) {
  714. // 50t or 50.5t
  715. let results = shaka.text.TtmlTextParser.timeTickFormat_.exec(text);
  716. let ticks = Number(results[1]);
  717. return ticks / rateInfo.tickRate;
  718. };
  719. /**
  720. * Parses a TTML colon formatted time containing frames.
  721. *
  722. * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
  723. * @param {string} text
  724. * @return {?number}
  725. * @private
  726. */
  727. shaka.text.TtmlTextParser.parseColonTimeWithFrames_ = function(
  728. rateInfo, text) {
  729. // 01:02:43:07 ('07' is frames) or 01:02:43:07.1 (subframes)
  730. let results = shaka.text.TtmlTextParser.timeColonFormatFrames_.exec(text);
  731. let hours = Number(results[1]);
  732. let minutes = Number(results[2]);
  733. let seconds = Number(results[3]);
  734. let frames = Number(results[4]);
  735. let subframes = Number(results[5]) || 0;
  736. frames += subframes / rateInfo.subFrameRate;
  737. seconds += frames / rateInfo.frameRate;
  738. return seconds + (minutes * 60) + (hours * 3600);
  739. };
  740. /**
  741. * Parses a TTML time with a given regex. Expects regex to be some
  742. * sort of a time-matcher to match hours, minutes, seconds and milliseconds
  743. *
  744. * @param {!RegExp} regex
  745. * @param {string} text
  746. * @return {?number}
  747. * @private
  748. */
  749. shaka.text.TtmlTextParser.parseTimeFromRegex_ = function(regex, text) {
  750. let results = regex.exec(text);
  751. if (results == null || results[0] == '') {
  752. return null;
  753. }
  754. // This capture is optional, but will still be in the array as undefined,
  755. // in which case it is 0.
  756. let hours = Number(results[1]) || 0;
  757. let minutes = Number(results[2]) || 0;
  758. let seconds = Number(results[3]) || 0;
  759. let miliseconds = Number(results[4]) || 0;
  760. return (miliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
  761. };
  762. /**
  763. * Contains information about frame/subframe rate
  764. * and frame rate multiplier for time in frame format.
  765. *
  766. * @example 01:02:03:04(4 frames) or 01:02:03:04.1(4 frames, 1 subframe)
  767. * @param {?string} frameRate
  768. * @param {?string} subFrameRate
  769. * @param {?string} frameRateMultiplier
  770. * @param {?string} tickRate
  771. * @constructor
  772. * @struct
  773. * @private
  774. */
  775. shaka.text.TtmlTextParser.RateInfo_ = function(
  776. frameRate, subFrameRate, frameRateMultiplier, tickRate) {
  777. /**
  778. * @type {number}
  779. */
  780. this.frameRate = Number(frameRate) || 30;
  781. /**
  782. * @type {number}
  783. */
  784. this.subFrameRate = Number(subFrameRate) || 1;
  785. /**
  786. * @type {number}
  787. */
  788. this.tickRate = Number(tickRate);
  789. if (this.tickRate == 0) {
  790. if (frameRate) {
  791. this.tickRate = this.frameRate * this.subFrameRate;
  792. } else {
  793. this.tickRate = 1;
  794. }
  795. }
  796. if (frameRateMultiplier) {
  797. let multiplierResults = /^(\d+) (\d+)$/g.exec(frameRateMultiplier);
  798. if (multiplierResults) {
  799. let numerator = multiplierResults[1];
  800. let denominator = multiplierResults[2];
  801. let multiplierNum = numerator / denominator;
  802. this.frameRate *= multiplierNum;
  803. }
  804. }
  805. };
  806. shaka.text.TextEngine.registerParser(
  807. 'application/ttml+xml',
  808. shaka.text.TtmlTextParser);