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. * Parses common attributes for Representation, AdaptationSet, and Period.
  279. * @param {shaka.dash.DashParser.Context} context
  280. * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
  281. * @return {!Array.<!Element>}
  282. */
  283. static getNodes(context, callback) {
  284. const Functional = shaka.util.Functional;
  285. goog.asserts.assert(
  286. callback(context.representation),
  287. 'There must be at least one element of the given type',
  288. );
  289. return [
  290. callback(context.representation),
  291. callback(context.adaptationSet),
  292. callback(context.period),
  293. ].filter(Functional.isNotNull);
  294. }
  295. /**
  296. * Searches the inheritance for a Segment* with the given attribute.
  297. *
  298. * @param {shaka.dash.DashParser.Context} context
  299. * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
  300. * Gets the Element that contains the attribute to inherit.
  301. * @param {string} attribute
  302. * @return {?string}
  303. */
  304. static inheritAttribute(context, callback, attribute) {
  305. const MpdUtils = shaka.dash.MpdUtils;
  306. const nodes = MpdUtils.getNodes(context, callback);
  307. let result = null;
  308. for (const node of nodes) {
  309. result = node.getAttribute(attribute);
  310. if (result) {
  311. break;
  312. }
  313. }
  314. return result;
  315. }
  316. /**
  317. * Searches the inheritance for a Segment* with the given child.
  318. *
  319. * @param {shaka.dash.DashParser.Context} context
  320. * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
  321. * Gets the Element that contains the child to inherit.
  322. * @param {string} child
  323. * @return {Element}
  324. */
  325. static inheritChild(context, callback, child) {
  326. const MpdUtils = shaka.dash.MpdUtils;
  327. const nodes = MpdUtils.getNodes(context, callback);
  328. const XmlUtils = shaka.util.XmlUtils;
  329. let result = null;
  330. for (const node of nodes) {
  331. result = XmlUtils.findChild(node, child);
  332. if (result) {
  333. break;
  334. }
  335. }
  336. return result;
  337. }
  338. /**
  339. * Follow the xlink link contained in the given element.
  340. * It also strips the xlink properties off of the element,
  341. * even if the process fails.
  342. *
  343. * @param {!Element} element
  344. * @param {!shaka.extern.RetryParameters} retryParameters
  345. * @param {boolean} failGracefully
  346. * @param {string} baseUri
  347. * @param {!shaka.net.NetworkingEngine} networkingEngine
  348. * @param {number} linkDepth
  349. * @return {!shaka.util.AbortableOperation.<!Element>}
  350. * @private
  351. */
  352. static handleXlinkInElement_(
  353. element, retryParameters, failGracefully, baseUri, networkingEngine,
  354. linkDepth) {
  355. const MpdUtils = shaka.dash.MpdUtils;
  356. const XmlUtils = shaka.util.XmlUtils;
  357. const Error = shaka.util.Error;
  358. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  359. const NS = MpdUtils.XlinkNamespaceUri_;
  360. const xlinkHref = XmlUtils.getAttributeNS(element, NS, 'href');
  361. const xlinkActuate =
  362. XmlUtils.getAttributeNS(element, NS, 'actuate') || 'onRequest';
  363. // Remove the xlink properties, so it won't download again
  364. // when re-processed.
  365. for (const attribute of Array.from(element.attributes)) {
  366. if (attribute.namespaceURI == NS) {
  367. element.removeAttributeNS(attribute.namespaceURI, attribute.localName);
  368. }
  369. }
  370. if (linkDepth >= 5) {
  371. return shaka.util.AbortableOperation.failed(new Error(
  372. Error.Severity.CRITICAL, Error.Category.MANIFEST,
  373. Error.Code.DASH_XLINK_DEPTH_LIMIT));
  374. }
  375. if (xlinkActuate != 'onLoad') {
  376. // Only xlink:actuate="onLoad" is supported.
  377. // When no value is specified, the assumed value is "onRequest".
  378. return shaka.util.AbortableOperation.failed(new Error(
  379. Error.Severity.CRITICAL, Error.Category.MANIFEST,
  380. Error.Code.DASH_UNSUPPORTED_XLINK_ACTUATE));
  381. }
  382. // Resolve the xlink href, in case it's a relative URL.
  383. const uris = ManifestParserUtils.resolveUris([baseUri], [xlinkHref]);
  384. // Load in the linked elements.
  385. const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  386. const request =
  387. shaka.net.NetworkingEngine.makeRequest(uris, retryParameters);
  388. const requestOperation = networkingEngine.request(requestType, request);
  389. // The interface is abstract, but we know it was implemented with the
  390. // more capable internal class.
  391. goog.asserts.assert(
  392. requestOperation instanceof shaka.util.AbortableOperation,
  393. 'Unexpected implementation of IAbortableOperation!');
  394. // Satisfy the compiler with a cast.
  395. const networkOperation =
  396. /** @type {!shaka.util.AbortableOperation.<shaka.extern.Response>} */ (
  397. requestOperation);
  398. // Chain onto that operation.
  399. return networkOperation.chain(
  400. (response) => {
  401. // This only supports the case where the loaded xml has a single
  402. // top-level element. If there are multiple roots, it will be
  403. // rejected.
  404. const rootElem =
  405. shaka.util.XmlUtils.parseXml(response.data, element.tagName);
  406. if (!rootElem) {
  407. // It was not valid XML.
  408. return shaka.util.AbortableOperation.failed(new Error(
  409. Error.Severity.CRITICAL, Error.Category.MANIFEST,
  410. Error.Code.DASH_INVALID_XML, xlinkHref));
  411. }
  412. // Now that there is no other possibility of the process erroring,
  413. // the element can be changed further.
  414. // Remove the current contents of the node.
  415. while (element.childNodes.length) {
  416. element.removeChild(element.childNodes[0]);
  417. }
  418. // Move the children of the loaded xml into the current element.
  419. while (rootElem.childNodes.length) {
  420. const child = rootElem.childNodes[0];
  421. rootElem.removeChild(child);
  422. element.appendChild(child);
  423. }
  424. // Move the attributes of the loaded xml into the current element.
  425. for (const attribute of Array.from(rootElem.attributes)) {
  426. element.setAttributeNode(attribute.cloneNode(/* deep= */ false));
  427. }
  428. return shaka.dash.MpdUtils.processXlinks(
  429. element, retryParameters, failGracefully, uris[0],
  430. networkingEngine, linkDepth + 1);
  431. });
  432. }
  433. /**
  434. * Filter the contents of a node recursively, replacing xlink links
  435. * with their associated online data.
  436. *
  437. * @param {!Element} element
  438. * @param {!shaka.extern.RetryParameters} retryParameters
  439. * @param {boolean} failGracefully
  440. * @param {string} baseUri
  441. * @param {!shaka.net.NetworkingEngine} networkingEngine
  442. * @param {number=} linkDepth, default set to 0
  443. * @return {!shaka.util.AbortableOperation.<!Element>}
  444. */
  445. static processXlinks(
  446. element, retryParameters, failGracefully, baseUri, networkingEngine,
  447. linkDepth = 0) {
  448. const MpdUtils = shaka.dash.MpdUtils;
  449. const XmlUtils = shaka.util.XmlUtils;
  450. const NS = MpdUtils.XlinkNamespaceUri_;
  451. if (XmlUtils.getAttributeNS(element, NS, 'href')) {
  452. let handled = MpdUtils.handleXlinkInElement_(
  453. element, retryParameters, failGracefully, baseUri, networkingEngine,
  454. linkDepth);
  455. if (failGracefully) {
  456. // Catch any error and go on.
  457. handled = handled.chain(undefined, (error) => {
  458. // handleXlinkInElement_ strips the xlink properties off of the
  459. // element even if it fails, so calling processXlinks again will
  460. // handle whatever contents the element natively has.
  461. return MpdUtils.processXlinks(
  462. element, retryParameters, failGracefully, baseUri,
  463. networkingEngine, linkDepth);
  464. });
  465. }
  466. return handled;
  467. }
  468. const childOperations = [];
  469. for (const child of Array.from(element.childNodes)) {
  470. if (child instanceof Element) {
  471. const resolveToZeroString = 'urn:mpeg:dash:resolve-to-zero:2013';
  472. if (XmlUtils.getAttributeNS(child, NS, 'href') == resolveToZeroString) {
  473. // This is a 'resolve to zero' code; it means the element should
  474. // be removed, as specified by the mpeg-dash rules for xlink.
  475. element.removeChild(child);
  476. } else if (child.tagName != 'SegmentTimeline') {
  477. // Don't recurse into a SegmentTimeline since xlink attributes
  478. // aren't valid in there and looking at each segment can take a long
  479. // time with larger manifests.
  480. // Replace the child with its processed form.
  481. childOperations.push(shaka.dash.MpdUtils.processXlinks(
  482. /** @type {!Element} */ (child), retryParameters, failGracefully,
  483. baseUri, networkingEngine, linkDepth));
  484. }
  485. }
  486. }
  487. return shaka.util.AbortableOperation.all(childOperations).chain(() => {
  488. return element;
  489. });
  490. }
  491. };
  492. /**
  493. * @typedef {{
  494. * start: number,
  495. * unscaledStart: number,
  496. * end: number
  497. * }}
  498. *
  499. * @description
  500. * Defines a time range of a media segment. Times are in seconds.
  501. *
  502. * @property {number} start
  503. * The start time of the range.
  504. * @property {number} unscaledStart
  505. * The start time of the range in representation timescale units.
  506. * @property {number} end
  507. * The end time (exclusive) of the range.
  508. */
  509. shaka.dash.MpdUtils.TimeRange;
  510. /**
  511. * @typedef {{
  512. * timescale: number,
  513. * segmentDuration: ?number,
  514. * startNumber: number,
  515. * scaledPresentationTimeOffset: number,
  516. * unscaledPresentationTimeOffset: number,
  517. * timeline: Array.<shaka.dash.MpdUtils.TimeRange>
  518. * }}
  519. *
  520. * @description
  521. * Contains common information between SegmentList and SegmentTemplate items.
  522. *
  523. * @property {number} timescale
  524. * The time-scale of the representation.
  525. * @property {?number} segmentDuration
  526. * The duration of the segments in seconds, if given.
  527. * @property {number} startNumber
  528. * The start number of the segments; 1 or greater.
  529. * @property {number} scaledPresentationTimeOffset
  530. * The presentation time offset of the representation, in seconds.
  531. * @property {number} unscaledPresentationTimeOffset
  532. * The presentation time offset of the representation, in timescale units.
  533. * @property {Array.<shaka.dash.MpdUtils.TimeRange>} timeline
  534. * The timeline of the representation, if given. Times in seconds.
  535. */
  536. shaka.dash.MpdUtils.SegmentInfo;
  537. /**
  538. * @const {string}
  539. * @private
  540. */
  541. shaka.dash.MpdUtils.XlinkNamespaceUri_ = 'http://www.w3.org/1999/xlink';