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