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