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