Source: lib/dash/mpd_utils.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.dash.MpdUtils');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.net.NetworkingEngine');
  21. goog.require('shaka.util.AbortableOperation');
  22. goog.require('shaka.util.Error');
  23. goog.require('shaka.util.Functional');
  24. goog.require('shaka.util.ManifestParserUtils');
  25. goog.require('shaka.util.StringUtils');
  26. goog.require('shaka.util.XmlUtils');
  27. /**
  28. * @namespace shaka.dash.MpdUtils
  29. * @summary MPD processing utility functions.
  30. */
  31. /**
  32. * @typedef {{
  33. * start: number,
  34. * unscaledStart: number,
  35. * end: number
  36. * }}
  37. *
  38. * @description
  39. * Defines a time range of a media segment. Times are in seconds.
  40. *
  41. * @property {number} start
  42. * The start time of the range.
  43. * @property {number} unscaledStart
  44. * The start time of the range in representation timescale units.
  45. * @property {number} end
  46. * The end time (exclusive) of the range.
  47. */
  48. shaka.dash.MpdUtils.TimeRange;
  49. /**
  50. * @typedef {{
  51. * timescale: number,
  52. * segmentDuration: ?number,
  53. * startNumber: number,
  54. * scaledPresentationTimeOffset: number,
  55. * unscaledPresentationTimeOffset: number,
  56. * timeline: Array.<shaka.dash.MpdUtils.TimeRange>
  57. * }}
  58. *
  59. * @description
  60. * Contains common information between SegmentList and SegmentTemplate items.
  61. *
  62. * @property {number} timescale
  63. * The time-scale of the representation.
  64. * @property {?number} segmentDuration
  65. * The duration of the segments in seconds, if given.
  66. * @property {number} startNumber
  67. * The start number of the segments; 1 or greater.
  68. * @property {number} scaledPresentationTimeOffset
  69. * The presentation time offset of the representation, in seconds.
  70. * @property {number} unscaledPresentationTimeOffset
  71. * The presentation time offset of the representation, in timescale units.
  72. * @property {Array.<shaka.dash.MpdUtils.TimeRange>} timeline
  73. * The timeline of the representation, if given. Times in seconds.
  74. */
  75. shaka.dash.MpdUtils.SegmentInfo;
  76. /**
  77. * @const {string}
  78. * @private
  79. */
  80. shaka.dash.MpdUtils.XlinkNamespaceUri_ = 'http://www.w3.org/1999/xlink';
  81. /**
  82. * Fills a SegmentTemplate URI template. This function does not validate the
  83. * resulting URI.
  84. *
  85. * @param {string} uriTemplate
  86. * @param {?string} representationId
  87. * @param {?number} number
  88. * @param {?number} bandwidth
  89. * @param {?number} time
  90. * @return {string} A URI string.
  91. * @see ISO/IEC 23009-1:2014 section 5.3.9.4.4
  92. */
  93. shaka.dash.MpdUtils.fillUriTemplate = function(
  94. uriTemplate, representationId, number, bandwidth, time) {
  95. /** @type {!Object.<string, ?number|?string>} */
  96. let valueTable = {
  97. 'RepresentationID': representationId,
  98. 'Number': number,
  99. 'Bandwidth': bandwidth,
  100. 'Time': time
  101. };
  102. const re =
  103. /\$(RepresentationID|Number|Bandwidth|Time)?(?:%0([0-9]+)([diouxX]))?\$/g;
  104. let uri = uriTemplate.replace(re, function(match, name, widthString, format) {
  105. if (match == '$$') {
  106. return '$';
  107. }
  108. let value = valueTable[name];
  109. goog.asserts.assert(value !== undefined, 'Unrecognized identifier');
  110. // Note that |value| may be 0 or ''.
  111. if (value == null) {
  112. shaka.log.warning(
  113. 'URL template does not have an available substitution for identifier',
  114. '"' + name + '":',
  115. uriTemplate);
  116. return match;
  117. }
  118. if (name == 'RepresentationID' && widthString) {
  119. shaka.log.warning(
  120. 'URL template should not contain a width specifier for identifier',
  121. '"RepresentationID":',
  122. uriTemplate);
  123. widthString = undefined;
  124. }
  125. if (name == 'Time') {
  126. goog.asserts.assert(Math.abs(value - Math.round(value)) < 0.2,
  127. 'Calculated $Time$ values must be close to integers');
  128. value = Math.round(value);
  129. }
  130. /** @type {string} */
  131. let valueString;
  132. switch (format) {
  133. case undefined: // Happens if there is no format specifier.
  134. case 'd':
  135. case 'i':
  136. case 'u':
  137. valueString = value.toString();
  138. break;
  139. case 'o':
  140. valueString = value.toString(8);
  141. break;
  142. case 'x':
  143. valueString = value.toString(16);
  144. break;
  145. case 'X':
  146. valueString = value.toString(16).toUpperCase();
  147. break;
  148. default:
  149. goog.asserts.assert(false, 'Unhandled format specifier');
  150. valueString = value.toString();
  151. break;
  152. }
  153. // Create a padding string.
  154. let width = window.parseInt(widthString, 10) || 1;
  155. let paddingSize = Math.max(0, width - valueString.length);
  156. let padding = (new Array(paddingSize + 1)).join('0');
  157. return padding + valueString;
  158. });
  159. return uri;
  160. };
  161. /**
  162. * Expands a SegmentTimeline into an array-based timeline. The results are in
  163. * seconds.
  164. *
  165. * @param {!Element} segmentTimeline
  166. * @param {number} timescale
  167. * @param {number} unscaledPresentationTimeOffset
  168. * @param {number} periodDuration The Period's duration in seconds.
  169. * Infinity indicates that the Period continues indefinitely.
  170. * @return {!Array.<shaka.dash.MpdUtils.TimeRange>}
  171. */
  172. shaka.dash.MpdUtils.createTimeline =
  173. function(segmentTimeline, timescale, unscaledPresentationTimeOffset,
  174. periodDuration) {
  175. goog.asserts.assert(
  176. timescale > 0 && timescale < Infinity,
  177. 'timescale must be a positive, finite integer');
  178. goog.asserts.assert(periodDuration > 0,
  179. 'period duration must be a positive integer');
  180. // Alias.
  181. const XmlUtils = shaka.util.XmlUtils;
  182. let timePoints = XmlUtils.findChildren(segmentTimeline, 'S');
  183. /** @type {!Array.<shaka.dash.MpdUtils.TimeRange>} */
  184. let timeline = [];
  185. let lastEndTime = 0;
  186. for (let i = 0; i < timePoints.length; ++i) {
  187. let timePoint = timePoints[i];
  188. let t = XmlUtils.parseAttr(timePoint, 't', XmlUtils.parseNonNegativeInt);
  189. let d = XmlUtils.parseAttr(timePoint, 'd', XmlUtils.parseNonNegativeInt);
  190. let r = XmlUtils.parseAttr(timePoint, 'r', XmlUtils.parseInt);
  191. // Adjust the start time to account for the presentation time offset.
  192. if (t != null) {
  193. t -= unscaledPresentationTimeOffset;
  194. }
  195. if (!d) {
  196. shaka.log.warning(
  197. '"S" element must have a duration:',
  198. 'ignoring the remaining "S" elements.',
  199. timePoint);
  200. return timeline;
  201. }
  202. let startTime = t != null ? t : lastEndTime;
  203. let repeat = r || 0;
  204. if (repeat < 0) {
  205. if (i + 1 < timePoints.length) {
  206. let nextTimePoint = timePoints[i + 1];
  207. let nextStartTime = XmlUtils.parseAttr(
  208. nextTimePoint, 't', XmlUtils.parseNonNegativeInt);
  209. if (nextStartTime == null) {
  210. shaka.log.warning(
  211. 'An "S" element cannot have a negative repeat',
  212. 'if the next "S" element does not have a valid start time:',
  213. 'ignoring the remaining "S" elements.',
  214. timePoint);
  215. return timeline;
  216. } else if (startTime >= nextStartTime) {
  217. shaka.log.warning(
  218. 'An "S" element cannot have a negative repeat',
  219. 'if its start time exceeds the next "S" element\'s start time:',
  220. 'ignoring the remaining "S" elements.',
  221. timePoint);
  222. return timeline;
  223. }
  224. repeat = Math.ceil((nextStartTime - startTime) / d) - 1;
  225. } else {
  226. if (periodDuration == Infinity) {
  227. // The DASH spec. actually allows the last "S" element to have a
  228. // negative repeat value even when the Period has an infinite
  229. // duration. No one uses this feature and no one ever should, ever.
  230. shaka.log.warning(
  231. 'The last "S" element cannot have a negative repeat',
  232. 'if the Period has an infinite duration:',
  233. 'ignoring the last "S" element.',
  234. timePoint);
  235. return timeline;
  236. } else if (startTime / timescale >= periodDuration) {
  237. shaka.log.warning(
  238. 'The last "S" element cannot have a negative repeat',
  239. 'if its start time exceeds the Period\'s duration:',
  240. 'igoring the last "S" element.',
  241. timePoint);
  242. return timeline;
  243. }
  244. repeat = Math.ceil((periodDuration * timescale - startTime) / d) - 1;
  245. }
  246. }
  247. // The end of the last segment may be before the start of the current
  248. // segment (a gap) or after the start of the current segment (an overlap).
  249. // If there is a gap/overlap then stretch/compress the end of the last
  250. // segment to the start of the current segment.
  251. //
  252. // Note: it is possible to move the start of the current segment to the
  253. // end of the last segment, but this would complicate the computation of
  254. // the $Time$ placeholder later on.
  255. if ((timeline.length > 0) && (startTime != lastEndTime)) {
  256. let delta = startTime - lastEndTime;
  257. if (Math.abs(delta / timescale) >=
  258. shaka.util.ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS) {
  259. shaka.log.warning(
  260. 'SegmentTimeline contains a large gap/overlap:',
  261. 'the content may have errors in it.',
  262. timePoint);
  263. }
  264. timeline[timeline.length - 1].end = startTime / timescale;
  265. }
  266. for (let j = 0; j <= repeat; ++j) {
  267. let endTime = startTime + d;
  268. let item = {
  269. start: startTime / timescale,
  270. end: endTime / timescale,
  271. unscaledStart: startTime
  272. };
  273. timeline.push(item);
  274. startTime = endTime;
  275. lastEndTime = endTime;
  276. }
  277. }
  278. return timeline;
  279. };
  280. /**
  281. * Parses common segment info for SegmentList and SegmentTemplate.
  282. *
  283. * @param {shaka.dash.DashParser.Context} context
  284. * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
  285. * Gets the element that contains the segment info.
  286. * @return {shaka.dash.MpdUtils.SegmentInfo}
  287. */
  288. shaka.dash.MpdUtils.parseSegmentInfo = function(context, callback) {
  289. goog.asserts.assert(
  290. callback(context.representation),
  291. 'There must be at least one element of the given type.');
  292. const MpdUtils = shaka.dash.MpdUtils;
  293. const XmlUtils = shaka.util.XmlUtils;
  294. let timescaleStr = MpdUtils.inheritAttribute(context, callback, 'timescale');
  295. let timescale = 1;
  296. if (timescaleStr) {
  297. timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1;
  298. }
  299. let durationStr = MpdUtils.inheritAttribute(context, callback, 'duration');
  300. let segmentDuration = XmlUtils.parsePositiveInt(durationStr || '');
  301. if (segmentDuration) {
  302. segmentDuration /= timescale;
  303. }
  304. let startNumberStr =
  305. MpdUtils.inheritAttribute(context, callback, 'startNumber');
  306. let unscaledPresentationTimeOffset =
  307. Number(MpdUtils.inheritAttribute(context, callback,
  308. 'presentationTimeOffset')) || 0;
  309. let startNumber = XmlUtils.parseNonNegativeInt(startNumberStr || '');
  310. if (startNumberStr == null || startNumber == null) {
  311. startNumber = 1;
  312. }
  313. let timelineNode =
  314. MpdUtils.inheritChild(context, callback, 'SegmentTimeline');
  315. /** @type {Array.<shaka.dash.MpdUtils.TimeRange>} */
  316. let timeline = null;
  317. if (timelineNode) {
  318. timeline = MpdUtils.createTimeline(
  319. timelineNode, timescale, unscaledPresentationTimeOffset,
  320. context.periodInfo.duration || Infinity);
  321. }
  322. let scaledPresentationTimeOffset =
  323. (unscaledPresentationTimeOffset / timescale) || 0;
  324. return {
  325. timescale: timescale,
  326. segmentDuration: segmentDuration,
  327. startNumber: startNumber,
  328. scaledPresentationTimeOffset: scaledPresentationTimeOffset,
  329. unscaledPresentationTimeOffset: unscaledPresentationTimeOffset,
  330. timeline: timeline
  331. };
  332. };
  333. /**
  334. * Searches the inheritance for a Segment* with the given attribute.
  335. *
  336. * @param {shaka.dash.DashParser.Context} context
  337. * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
  338. * Gets the Element that contains the attribute to inherit.
  339. * @param {string} attribute
  340. * @return {?string}
  341. */
  342. shaka.dash.MpdUtils.inheritAttribute = function(context, callback, attribute) {
  343. const Functional = shaka.util.Functional;
  344. goog.asserts.assert(
  345. callback(context.representation),
  346. 'There must be at least one element of the given type');
  347. /** @type {!Array.<!Element>} */
  348. let nodes = [
  349. callback(context.representation),
  350. callback(context.adaptationSet),
  351. callback(context.period)
  352. ].filter(Functional.isNotNull);
  353. return nodes
  354. .map(function(s) { return s.getAttribute(attribute); })
  355. .reduce(function(all, part) { return all || part; });
  356. };
  357. /**
  358. * Searches the inheritance for a Segment* with the given child.
  359. *
  360. * @param {shaka.dash.DashParser.Context} context
  361. * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
  362. * Gets the Element that contains the child to inherit.
  363. * @param {string} child
  364. * @return {Element}
  365. */
  366. shaka.dash.MpdUtils.inheritChild = function(context, callback, child) {
  367. const Functional = shaka.util.Functional;
  368. goog.asserts.assert(
  369. callback(context.representation),
  370. 'There must be at least one element of the given type');
  371. /** @type {!Array.<!Element>} */
  372. let nodes = [
  373. callback(context.representation),
  374. callback(context.adaptationSet),
  375. callback(context.period)
  376. ].filter(Functional.isNotNull);
  377. const XmlUtils = shaka.util.XmlUtils;
  378. return nodes
  379. .map(function(s) { return XmlUtils.findChild(s, child); })
  380. .reduce(function(all, part) { return all || part; });
  381. };
  382. /**
  383. * Parse some UTF8 data and return the resulting root element if
  384. * it was valid XML.
  385. * @param {ArrayBuffer} data
  386. * @param {string} expectedRootElemName
  387. * @return {Element|undefined}
  388. */
  389. shaka.dash.MpdUtils.parseXml = function(data, expectedRootElemName) {
  390. let parser = new DOMParser();
  391. let rootElem;
  392. let xml;
  393. try {
  394. let string = shaka.util.StringUtils.fromUTF8(data);
  395. xml = parser.parseFromString(string, 'text/xml');
  396. } catch (exception) {}
  397. if (xml) {
  398. // The top-level element in the loaded xml should have the
  399. // same type as the element linking.
  400. if (xml.documentElement.tagName == expectedRootElemName) {
  401. rootElem = xml.documentElement;
  402. }
  403. }
  404. if (rootElem && rootElem.getElementsByTagName('parsererror').length > 0) {
  405. return null;
  406. } // It had a parser error in it.
  407. return rootElem;
  408. };
  409. /**
  410. * Follow the xlink link contained in the given element.
  411. * It also strips the xlink properties off of the element,
  412. * even if the process fails.
  413. *
  414. * @param {!Element} element
  415. * @param {!shakaExtern.RetryParameters} retryParameters
  416. * @param {boolean} failGracefully
  417. * @param {string} baseUri
  418. * @param {!shaka.net.NetworkingEngine} networkingEngine
  419. * @param {number} linkDepth
  420. * @return {!shaka.util.AbortableOperation.<!Element>}
  421. * @private
  422. */
  423. shaka.dash.MpdUtils.handleXlinkInElement_ =
  424. function(element, retryParameters, failGracefully, baseUri,
  425. networkingEngine, linkDepth) {
  426. const MpdUtils = shaka.dash.MpdUtils;
  427. const XmlUtils = shaka.util.XmlUtils;
  428. const Error = shaka.util.Error;
  429. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  430. const NS = MpdUtils.XlinkNamespaceUri_;
  431. let xlinkHref = XmlUtils.getAttributeNS(element, NS, 'href');
  432. let xlinkActuate =
  433. XmlUtils.getAttributeNS(element, NS, 'actuate') || 'onRequest';
  434. // Remove the xlink properties, so it won't download again
  435. // when re-processed.
  436. for (let i = 0; i < element.attributes.length; i++) {
  437. let attribute = element.attributes[i];
  438. if (attribute.namespaceURI == NS) {
  439. element.removeAttributeNS(attribute.namespaceURI, attribute.localName);
  440. i -= 1;
  441. }
  442. }
  443. if (linkDepth >= 5) {
  444. return shaka.util.AbortableOperation.failed(new Error(
  445. Error.Severity.CRITICAL, Error.Category.MANIFEST,
  446. Error.Code.DASH_XLINK_DEPTH_LIMIT));
  447. }
  448. if (xlinkActuate != 'onLoad') {
  449. // Only xlink:actuate="onLoad" is supported.
  450. // When no value is specified, the assumed value is "onRequest".
  451. return shaka.util.AbortableOperation.failed(new Error(
  452. Error.Severity.CRITICAL, Error.Category.MANIFEST,
  453. Error.Code.DASH_UNSUPPORTED_XLINK_ACTUATE));
  454. }
  455. // Resolve the xlink href, in case it's a relative URL.
  456. let uris = ManifestParserUtils.resolveUris([baseUri], [xlinkHref]);
  457. // Load in the linked elements.
  458. const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  459. let request = shaka.net.NetworkingEngine.makeRequest(
  460. uris, retryParameters);
  461. let requestOperation = networkingEngine.request(requestType, request);
  462. // The interface is abstract, but we know it was implemented with the
  463. // more capable internal class.
  464. goog.asserts.assert(requestOperation instanceof shaka.util.AbortableOperation,
  465. 'Unexpected implementation of IAbortableOperation!');
  466. // Satisfy the compiler with a cast.
  467. let networkOperation =
  468. /** @type {!shaka.util.AbortableOperation.<shakaExtern.Response>} */(
  469. requestOperation);
  470. // Chain onto that operation.
  471. return networkOperation.chain((response) => {
  472. // This only supports the case where the loaded xml has a single
  473. // top-level element. If there are multiple roots, it will be rejected.
  474. let rootElem = MpdUtils.parseXml(response.data, element.tagName);
  475. if (!rootElem) {
  476. // It was not valid XML.
  477. return shaka.util.AbortableOperation.failed(new Error(
  478. Error.Severity.CRITICAL, Error.Category.MANIFEST,
  479. Error.Code.DASH_INVALID_XML, xlinkHref));
  480. }
  481. // Now that there is no other possibility of the process erroring,
  482. // the element can be changed further.
  483. // Remove the current contents of the node.
  484. while (element.childNodes.length) {
  485. element.removeChild(element.childNodes[0]);
  486. }
  487. // Move the children of the loaded xml into the current element.
  488. while (rootElem.childNodes.length) {
  489. let child = rootElem.childNodes[0];
  490. rootElem.removeChild(child);
  491. element.appendChild(child);
  492. }
  493. // Move the attributes of the loaded xml into the current element.
  494. for (let i = 0; i < rootElem.attributes.length; i++) {
  495. let attribute = rootElem.attributes[i].nodeName;
  496. let attributeValue = rootElem.getAttribute(attribute);
  497. element.setAttribute(attribute, attributeValue);
  498. }
  499. return shaka.dash.MpdUtils.processXlinks(
  500. element, retryParameters, failGracefully, uris[0], networkingEngine,
  501. linkDepth + 1);
  502. });
  503. };
  504. /**
  505. * Filter the contents of a node recursively, replacing xlink links
  506. * with their associated online data.
  507. *
  508. * @param {!Element} element
  509. * @param {!shakaExtern.RetryParameters} retryParameters
  510. * @param {boolean} failGracefully
  511. * @param {string} baseUri
  512. * @param {!shaka.net.NetworkingEngine} networkingEngine
  513. * @param {number=} opt_linkDepth
  514. * @return {!shaka.util.AbortableOperation.<!Element>}
  515. */
  516. shaka.dash.MpdUtils.processXlinks =
  517. function(element, retryParameters, failGracefully, baseUri,
  518. networkingEngine, opt_linkDepth) {
  519. const MpdUtils = shaka.dash.MpdUtils;
  520. const XmlUtils = shaka.util.XmlUtils;
  521. const NS = MpdUtils.XlinkNamespaceUri_;
  522. opt_linkDepth = opt_linkDepth || 0;
  523. if (XmlUtils.getAttributeNS(element, NS, 'href')) {
  524. let handled = MpdUtils.handleXlinkInElement_(
  525. element, retryParameters, failGracefully, baseUri,
  526. networkingEngine, opt_linkDepth);
  527. if (failGracefully) {
  528. // Catch any error and go on.
  529. handled = handled.chain(undefined, (error) => {
  530. // handleXlinkInElement_ strips the xlink properties off of the element
  531. // even if it fails, so calling processXlinks again will handle whatever
  532. // contents the element natively has.
  533. return MpdUtils.processXlinks(element, retryParameters, failGracefully,
  534. baseUri, networkingEngine,
  535. opt_linkDepth);
  536. });
  537. }
  538. return handled;
  539. }
  540. let childOperations = [];
  541. for (let i = 0; i < element.childNodes.length; i++) {
  542. let child = element.childNodes[i];
  543. if (child instanceof Element) {
  544. const resolveToZeroString = 'urn:mpeg:dash:resolve-to-zero:2013';
  545. if (XmlUtils.getAttributeNS(child, NS, 'href') == resolveToZeroString) {
  546. // This is a 'resolve to zero' code; it means the element should
  547. // be removed, as specified by the mpeg-dash rules for xlink.
  548. element.removeChild(child);
  549. i -= 1;
  550. } else if (child.tagName != 'SegmentTimeline') {
  551. // Don't recurse into a SegmentTimeline since xlink attributes aren't
  552. // valid in there and looking at each segment can take a long time with
  553. // larger manifests.
  554. // Replace the child with its processed form.
  555. childOperations.push(shaka.dash.MpdUtils.processXlinks(
  556. /** @type {!Element} */ (child), retryParameters, failGracefully,
  557. baseUri, networkingEngine, opt_linkDepth));
  558. }
  559. }
  560. }
  561. return shaka.util.AbortableOperation.all(childOperations).chain(() => {
  562. return element;
  563. });
  564. };