Source: lib/dash/mpd_utils.js

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