Source: lib/dash/dash_parser.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.DashParser');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.abr.Ewma');
  20. goog.require('shaka.dash.ContentProtection');
  21. goog.require('shaka.dash.MpdUtils');
  22. goog.require('shaka.dash.SegmentBase');
  23. goog.require('shaka.dash.SegmentList');
  24. goog.require('shaka.dash.SegmentTemplate');
  25. goog.require('shaka.log');
  26. goog.require('shaka.media.DrmEngine');
  27. goog.require('shaka.media.ManifestParser');
  28. goog.require('shaka.media.PresentationTimeline');
  29. goog.require('shaka.media.SegmentReference');
  30. goog.require('shaka.net.NetworkingEngine');
  31. goog.require('shaka.text.TextEngine');
  32. goog.require('shaka.util.Error');
  33. goog.require('shaka.util.Functional');
  34. goog.require('shaka.util.LanguageUtils');
  35. goog.require('shaka.util.ManifestParserUtils');
  36. goog.require('shaka.util.MimeUtils');
  37. goog.require('shaka.util.OperationManager');
  38. goog.require('shaka.util.StringUtils');
  39. goog.require('shaka.util.XmlUtils');
  40. /**
  41. * Creates a new DASH parser.
  42. *
  43. * @struct
  44. * @constructor
  45. * @implements {shakaExtern.ManifestParser}
  46. * @export
  47. */
  48. shaka.dash.DashParser = function() {
  49. /** @private {?shakaExtern.ManifestConfiguration} */
  50. this.config_ = null;
  51. /** @private {?shakaExtern.ManifestParser.PlayerInterface} */
  52. this.playerInterface_ = null;
  53. /** @private {!Array.<string>} */
  54. this.manifestUris_ = [];
  55. /** @private {?shakaExtern.Manifest} */
  56. this.manifest_ = null;
  57. /** @private {!Array.<string>} */
  58. this.periodIds_ = [];
  59. /** @private {number} */
  60. this.globalId_ = 1;
  61. /**
  62. * A map of IDs to SegmentIndex objects.
  63. * ID: Period@id,AdaptationSet@id,@Representation@id
  64. * e.g.: '1,5,23'
  65. * @private {!Object.<string, !shaka.media.SegmentIndex>}
  66. */
  67. this.segmentIndexMap_ = {};
  68. /**
  69. * The update period in seconds, or 0 for no updates.
  70. * @private {number}
  71. */
  72. this.updatePeriod_ = 0;
  73. /**
  74. * An ewma that tracks how long updates take.
  75. * This is to mitigate issues caused by slow parsing on embedded devices.
  76. * @private {!shaka.abr.Ewma}
  77. */
  78. this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
  79. /** @private {?number} */
  80. this.updateTimer_ = null;
  81. /** @private {!shaka.util.OperationManager} */
  82. this.operationManager_ = new shaka.util.OperationManager();
  83. };
  84. /**
  85. * Contains the minimum amount of time, in seconds, between manifest update
  86. * requests.
  87. *
  88. * @private
  89. * @const {number}
  90. */
  91. shaka.dash.DashParser.MIN_UPDATE_PERIOD_ = 3;
  92. /**
  93. * @typedef {
  94. * function(!Array.<string>, ?number, ?number):!Promise.<!ArrayBuffer>
  95. * }
  96. */
  97. shaka.dash.DashParser.RequestInitSegmentCallback;
  98. /**
  99. * @typedef {{
  100. * segmentBase: Element,
  101. * segmentList: Element,
  102. * segmentTemplate: Element,
  103. * baseUris: !Array.<string>,
  104. * width: (number|undefined),
  105. * height: (number|undefined),
  106. * contentType: string,
  107. * mimeType: string,
  108. * codecs: string,
  109. * frameRate: (number|undefined),
  110. * containsEmsgBoxes: boolean,
  111. * id: string,
  112. * numChannels: ?number
  113. * }}
  114. *
  115. * @description
  116. * A collection of elements and properties which are inherited across levels
  117. * of a DASH manifest.
  118. *
  119. * @property {Element} segmentBase
  120. * The XML node for SegmentBase.
  121. * @property {Element} segmentList
  122. * The XML node for SegmentList.
  123. * @property {Element} segmentTemplate
  124. * The XML node for SegmentTemplate.
  125. * @property {!Array.<string>} baseUris
  126. * An array of absolute base URIs for the frame.
  127. * @property {(number|undefined)} width
  128. * The inherited width value.
  129. * @property {(number|undefined)} height
  130. * The inherited height value.
  131. * @property {string} contentType
  132. * The inherited media type.
  133. * @property {string} mimeType
  134. * The inherited MIME type value.
  135. * @property {string} codecs
  136. * The inherited codecs value.
  137. * @property {(number|undefined)} frameRate
  138. * The inherited framerate value.
  139. * @property {boolean} containsEmsgBoxes
  140. * Whether there are 'emsg' boxes.
  141. * @property {string} id
  142. * The ID of the element.
  143. * @property {?number} numChannels
  144. * The number of audio channels, or null if unknown.
  145. */
  146. shaka.dash.DashParser.InheritanceFrame;
  147. /**
  148. * @typedef {{
  149. * dynamic: boolean,
  150. * presentationTimeline: !shaka.media.PresentationTimeline,
  151. * period: ?shaka.dash.DashParser.InheritanceFrame,
  152. * periodInfo: ?shaka.dash.DashParser.PeriodInfo,
  153. * adaptationSet: ?shaka.dash.DashParser.InheritanceFrame,
  154. * representation: ?shaka.dash.DashParser.InheritanceFrame,
  155. * bandwidth: number,
  156. * indexRangeWarningGiven: boolean
  157. * }}
  158. *
  159. * @description
  160. * Contains context data for the streams.
  161. *
  162. * @property {boolean} dynamic
  163. * True if the MPD is dynamic (not all segments available at once)
  164. * @property {!shaka.media.PresentationTimeline} presentationTimeline
  165. * The PresentationTimeline.
  166. * @property {?shaka.dash.DashParser.InheritanceFrame} period
  167. * The inheritance from the Period element.
  168. * @property {?shaka.dash.DashParser.PeriodInfo} periodInfo
  169. * The Period info for the current Period.
  170. * @property {?shaka.dash.DashParser.InheritanceFrame} adaptationSet
  171. * The inheritance from the AdaptationSet element.
  172. * @property {?shaka.dash.DashParser.InheritanceFrame} representation
  173. * The inheritance from the Representation element.
  174. * @property {number} bandwidth
  175. * The bandwidth of the Representation, or zero if missing.
  176. * @property {boolean} indexRangeWarningGiven
  177. * True if the warning about SegmentURL@indexRange has been printed.
  178. */
  179. shaka.dash.DashParser.Context;
  180. /**
  181. * @typedef {{
  182. * start: number,
  183. * duration: ?number,
  184. * node: !Element,
  185. * index: number,
  186. * isLastPeriod: boolean
  187. * }}
  188. *
  189. * @description
  190. * Contains information about a Period element.
  191. *
  192. * @property {number} start
  193. * The start time of the period.
  194. * @property {?number} duration
  195. * The duration of the period; or null if the duration is not given. This
  196. * will be non-null for all periods except the last.
  197. * @property {!Element} node
  198. * The XML Node for the Period.
  199. * @property {number} index
  200. * The 0-base index of this Period within the manifest.
  201. * @property {boolean} isLastPeriod
  202. * Whether this Period is the last one in the manifest.
  203. */
  204. shaka.dash.DashParser.PeriodInfo;
  205. /**
  206. * @typedef {{
  207. * id: string,
  208. * contentType: ?string,
  209. * language: string,
  210. * main: boolean,
  211. * streams: !Array.<shakaExtern.Stream>,
  212. * drmInfos: !Array.<shakaExtern.DrmInfo>,
  213. * trickModeFor: ?string,
  214. * representationIds: !Array.<string>
  215. * }}
  216. *
  217. * @description
  218. * Contains information about an AdaptationSet element.
  219. *
  220. * @property {string} id
  221. * The unique ID of the adaptation set.
  222. * @property {?string} contentType
  223. * The content type of the AdaptationSet.
  224. * @property {string} language
  225. * The language of the AdaptationSet.
  226. * @property {boolean} main
  227. * Whether the AdaptationSet has the 'main' type.
  228. * @property {!Array.<shakaExtern.Stream>} streams
  229. * The streams this AdaptationSet contains.
  230. * @property {!Array.<shakaExtern.DrmInfo>} drmInfos
  231. * The DRM info for the AdaptationSet.
  232. * @property {?string} trickModeFor
  233. * If non-null, this AdaptationInfo represents trick mode tracks. This
  234. * property is the ID of the normal AdaptationSet these tracks should be
  235. * associated with.
  236. * @property {!Array.<string>} representationIds
  237. * An array of the IDs of the Representations this AdaptationSet contains.
  238. */
  239. shaka.dash.DashParser.AdaptationInfo;
  240. /**
  241. * @typedef {{
  242. * createSegmentIndex: shakaExtern.CreateSegmentIndexFunction,
  243. * findSegmentPosition: shakaExtern.FindSegmentPositionFunction,
  244. * getSegmentReference: shakaExtern.GetSegmentReferenceFunction
  245. * }}
  246. *
  247. * @description
  248. * Contains functions used to create and find segment references. Used as
  249. * a return value, to temporarily store them before StreamInfo is created.
  250. *
  251. * @property {shakaExtern.CreateSegmentIndexFunction} createSegmentIndex
  252. * The createSegmentIndex function.
  253. * @property {shakaExtern.FindSegmentPositionFunction} findSegmentPosition
  254. * The findSegmentPosition function.
  255. * @property {shakaExtern.GetSegmentReferenceFunction} getSegmentReference
  256. * The getSegmentReference function.
  257. */
  258. shaka.dash.DashParser.SegmentIndexFunctions;
  259. /**
  260. * @typedef {{
  261. * createSegmentIndex: shakaExtern.CreateSegmentIndexFunction,
  262. * findSegmentPosition: shakaExtern.FindSegmentPositionFunction,
  263. * getSegmentReference: shakaExtern.GetSegmentReferenceFunction,
  264. * initSegmentReference: shaka.media.InitSegmentReference,
  265. * scaledPresentationTimeOffset: number
  266. * }}
  267. *
  268. * @description
  269. * Contains information about a Stream. This is passed from the createStream
  270. * methods.
  271. *
  272. * @property {shakaExtern.CreateSegmentIndexFunction} createSegmentIndex
  273. * The createSegmentIndex function for the stream.
  274. * @property {shakaExtern.FindSegmentPositionFunction} findSegmentPosition
  275. * The findSegmentPosition function for the stream.
  276. * @property {shakaExtern.GetSegmentReferenceFunction} getSegmentReference
  277. * The getSegmentReference function for the stream.
  278. * @property {shaka.media.InitSegmentReference} initSegmentReference
  279. * The init segment for the stream.
  280. * @property {number} scaledPresentationTimeOffset
  281. * The presentation time offset for the stream, in seconds.
  282. */
  283. shaka.dash.DashParser.StreamInfo;
  284. /**
  285. * @override
  286. * @exportInterface
  287. */
  288. shaka.dash.DashParser.prototype.configure = function(config) {
  289. goog.asserts.assert(config.dash != null,
  290. 'DashManifestConfiguration should not be null!');
  291. this.config_ = config;
  292. };
  293. /**
  294. * @override
  295. * @exportInterface
  296. */
  297. shaka.dash.DashParser.prototype.start = function(uri, playerInterface) {
  298. goog.asserts.assert(this.config_, 'Must call configure() before start()!');
  299. this.manifestUris_ = [uri];
  300. this.playerInterface_ = playerInterface;
  301. return this.requestManifest_().then(function(updateDuration) {
  302. if (this.playerInterface_) {
  303. this.setUpdateTimer_(updateDuration);
  304. }
  305. return this.manifest_;
  306. }.bind(this));
  307. };
  308. /**
  309. * @override
  310. * @exportInterface
  311. */
  312. shaka.dash.DashParser.prototype.stop = function() {
  313. this.playerInterface_ = null;
  314. this.config_ = null;
  315. this.manifestUris_ = [];
  316. this.manifest_ = null;
  317. this.periodIds_ = [];
  318. this.segmentIndexMap_ = {};
  319. if (this.updateTimer_ != null) {
  320. window.clearTimeout(this.updateTimer_);
  321. this.updateTimer_ = null;
  322. }
  323. return this.operationManager_.destroy();
  324. };
  325. /**
  326. * @override
  327. * @exportInterface
  328. */
  329. shaka.dash.DashParser.prototype.update = function() {
  330. this.requestManifest_().catch(function(error) {
  331. if (!this.playerInterface_) return;
  332. this.playerInterface_.onError(error);
  333. }.bind(this));
  334. };
  335. /**
  336. * @override
  337. * @exportInterface
  338. */
  339. shaka.dash.DashParser.prototype.onExpirationUpdated = function(
  340. sessionId, expiration) {
  341. // No-op
  342. };
  343. /**
  344. * Makes a network request for the manifest and parses the resulting data.
  345. *
  346. * @return {!Promise.<number>} Resolves with the time it took, in seconds, to
  347. * fulfill the request and parse the data.
  348. * @private
  349. */
  350. shaka.dash.DashParser.prototype.requestManifest_ = function() {
  351. const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  352. let request = shaka.net.NetworkingEngine.makeRequest(
  353. this.manifestUris_, this.config_.retryParameters);
  354. let networkingEngine = this.playerInterface_.networkingEngine;
  355. const startTime = Date.now();
  356. let operation = networkingEngine.request(requestType, request);
  357. this.operationManager_.manage(operation);
  358. return operation.promise.then((response) => {
  359. // Detect calls to stop().
  360. if (!this.playerInterface_) {
  361. return;
  362. }
  363. // This may throw, but it will result in a failed promise.
  364. return this.parseManifest_(response.data, response.uri);
  365. }).then(() => {
  366. // Keep track of how long the longest manifest update took.
  367. const endTime = Date.now();
  368. const updateDuration = (endTime - startTime) / 1000.0;
  369. this.averageUpdateDuration_.sample(1, updateDuration);
  370. // Let the caller know how long this update took.
  371. return updateDuration;
  372. });
  373. };
  374. /**
  375. * Parses the manifest XML. This also handles updates and will update the
  376. * stored manifest.
  377. *
  378. * @param {ArrayBuffer} data
  379. * @param {string} finalManifestUri The final manifest URI, which may
  380. * differ from this.manifestUri_ if there has been a redirect.
  381. * @return {!Promise}
  382. * @throws shaka.util.Error When there is a parsing error.
  383. * @private
  384. */
  385. shaka.dash.DashParser.prototype.parseManifest_ =
  386. function(data, finalManifestUri) {
  387. const Error = shaka.util.Error;
  388. const MpdUtils = shaka.dash.MpdUtils;
  389. let mpd = MpdUtils.parseXml(data, 'MPD');
  390. if (!mpd) {
  391. throw new Error(
  392. Error.Severity.CRITICAL, Error.Category.MANIFEST,
  393. Error.Code.DASH_INVALID_XML, finalManifestUri);
  394. }
  395. // Process the mpd to account for xlink connections.
  396. let failGracefully = this.config_.dash.xlinkFailGracefully;
  397. let xlinkOperation = MpdUtils.processXlinks(
  398. mpd, this.config_.retryParameters, failGracefully, finalManifestUri,
  399. this.playerInterface_.networkingEngine);
  400. this.operationManager_.manage(xlinkOperation);
  401. return xlinkOperation.promise.then((finalMpd) => {
  402. return this.processManifest_(finalMpd, finalManifestUri);
  403. });
  404. };
  405. /**
  406. * Takes a formatted MPD and converts it into a manifest.
  407. *
  408. * @param {!Element} mpd
  409. * @param {string} finalManifestUri The final manifest URI, which may
  410. * differ from this.manifestUri_ if there has been a redirect.
  411. * @return {!Promise}
  412. * @throws shaka.util.Error When there is a parsing error.
  413. * @private
  414. */
  415. shaka.dash.DashParser.prototype.processManifest_ =
  416. function(mpd, finalManifestUri) {
  417. const Functional = shaka.util.Functional;
  418. const XmlUtils = shaka.util.XmlUtils;
  419. // Get any Location elements. This will update the manifest location and
  420. // the base URI.
  421. /** @type {!Array.<string>} */
  422. let manifestBaseUris = [finalManifestUri];
  423. /** @type {!Array.<string>} */
  424. let locations = XmlUtils.findChildren(mpd, 'Location')
  425. .map(XmlUtils.getContents)
  426. .filter(Functional.isNotNull);
  427. if (locations.length > 0) {
  428. const absoluteLocations = shaka.util.ManifestParserUtils.resolveUris(
  429. manifestBaseUris, locations);
  430. this.manifestUris_ = absoluteLocations;
  431. manifestBaseUris = absoluteLocations;
  432. }
  433. let uris = XmlUtils.findChildren(mpd, 'BaseURL').map(XmlUtils.getContents);
  434. let baseUris = shaka.util.ManifestParserUtils.resolveUris(
  435. manifestBaseUris, uris);
  436. let minBufferTime =
  437. XmlUtils.parseAttr(mpd, 'minBufferTime', XmlUtils.parseDuration);
  438. this.updatePeriod_ = /** @type {number} */ (XmlUtils.parseAttr(
  439. mpd, 'minimumUpdatePeriod', XmlUtils.parseDuration, -1));
  440. let presentationStartTime = XmlUtils.parseAttr(
  441. mpd, 'availabilityStartTime', XmlUtils.parseDate);
  442. let segmentAvailabilityDuration = XmlUtils.parseAttr(
  443. mpd, 'timeShiftBufferDepth', XmlUtils.parseDuration);
  444. let suggestedPresentationDelay = XmlUtils.parseAttr(
  445. mpd, 'suggestedPresentationDelay', XmlUtils.parseDuration);
  446. let maxSegmentDuration = XmlUtils.parseAttr(
  447. mpd, 'maxSegmentDuration', XmlUtils.parseDuration);
  448. let mpdType = mpd.getAttribute('type') || 'static';
  449. /** @type {!shaka.media.PresentationTimeline} */
  450. let presentationTimeline;
  451. if (this.manifest_) {
  452. presentationTimeline = this.manifest_.presentationTimeline;
  453. } else {
  454. // DASH IOP v3.0 suggests using a default delay between minBufferTime and
  455. // timeShiftBufferDepth. This is literally the range of all feasible
  456. // choices for the value. Nothing older than timeShiftBufferDepth is still
  457. // available, and anything less than minBufferTime will cause buffering
  458. // issues.
  459. //
  460. // We have decided that our default will be 1.5 * minBufferTime,
  461. // or 10s (configurable) whichever is larger. This is fairly conservative.
  462. // Content providers should provide a suggestedPresentationDelay
  463. // whenever possible to optimize the live streaming experience.
  464. let defaultPresentationDelay = Math.max(
  465. this.config_.dash.defaultPresentationDelay,
  466. minBufferTime * 1.5);
  467. let presentationDelay = suggestedPresentationDelay != null ?
  468. suggestedPresentationDelay : defaultPresentationDelay;
  469. presentationTimeline = new shaka.media.PresentationTimeline(
  470. presentationStartTime, presentationDelay);
  471. }
  472. /** @type {shaka.dash.DashParser.Context} */
  473. let context = {
  474. // Don't base on updatePeriod_ since emsg boxes can cause manifest updates.
  475. dynamic: mpdType != 'static',
  476. presentationTimeline: presentationTimeline,
  477. period: null,
  478. periodInfo: null,
  479. adaptationSet: null,
  480. representation: null,
  481. bandwidth: 0,
  482. indexRangeWarningGiven: false
  483. };
  484. let periodsAndDuration = this.parsePeriods_(context, baseUris, mpd);
  485. let duration = periodsAndDuration.duration;
  486. let periods = periodsAndDuration.periods;
  487. presentationTimeline.setStatic(mpdType == 'static');
  488. if (mpdType == 'static' || !periodsAndDuration.durationDerivedFromPeriods) {
  489. // Ignore duration calculated from Period lengths if this is dynamic.
  490. presentationTimeline.setDuration(duration || Infinity);
  491. }
  492. let isLive = presentationTimeline.isLive();
  493. // If it's live, we check for an override.
  494. if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
  495. segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
  496. }
  497. // If it's null, that means segments are always available. This is always the
  498. // case for VOD, and sometimes the case for live.
  499. if (segmentAvailabilityDuration == null) {
  500. segmentAvailabilityDuration = Infinity;
  501. }
  502. presentationTimeline.setSegmentAvailabilityDuration(
  503. segmentAvailabilityDuration);
  504. // Use @maxSegmentDuration to override smaller, derived values.
  505. presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1);
  506. if (goog.DEBUG) presentationTimeline.assertIsValid();
  507. if (this.manifest_) {
  508. // This is a manifest update, so we're done.
  509. return Promise.resolve();
  510. }
  511. // This is the first manifest parse, so we cannot return until we calculate
  512. // the clock offset.
  513. let timingElements = XmlUtils.findChildren(mpd, 'UTCTiming');
  514. return this.parseUtcTiming_(
  515. baseUris, timingElements, isLive).then(function(offset) {
  516. // Detect calls to stop().
  517. if (!this.playerInterface_) {
  518. return;
  519. }
  520. presentationTimeline.setClockOffset(offset);
  521. this.manifest_ = {
  522. presentationTimeline: presentationTimeline,
  523. periods: periods,
  524. offlineSessionIds: [],
  525. minBufferTime: minBufferTime || 0
  526. };
  527. }.bind(this));
  528. };
  529. /**
  530. * Reads and parses the periods from the manifest. This first does some
  531. * partial parsing so the start and duration is available when parsing children.
  532. *
  533. * @param {shaka.dash.DashParser.Context} context
  534. * @param {!Array.<string>} baseUris
  535. * @param {!Element} mpd
  536. * @return {{
  537. * periods: !Array.<shakaExtern.Period>,
  538. * duration: ?number,
  539. * durationDerivedFromPeriods: boolean
  540. * }}
  541. * @private
  542. */
  543. shaka.dash.DashParser.prototype.parsePeriods_ = function(
  544. context, baseUris, mpd) {
  545. const XmlUtils = shaka.util.XmlUtils;
  546. let presentationDuration = XmlUtils.parseAttr(
  547. mpd, 'mediaPresentationDuration', XmlUtils.parseDuration);
  548. let periods = [];
  549. let prevEnd = 0;
  550. let periodNodes = XmlUtils.findChildren(mpd, 'Period');
  551. for (let i = 0; i < periodNodes.length; i++) {
  552. let elem = periodNodes[i];
  553. let start = /** @type {number} */ (
  554. XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd));
  555. let givenDuration =
  556. XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration);
  557. let periodDuration = null;
  558. if (i != periodNodes.length - 1) {
  559. // "The difference between the start time of a Period and the start time
  560. // of the following Period is the duration of the media content
  561. // represented by this Period."
  562. let nextPeriod = periodNodes[i + 1];
  563. let nextStart =
  564. XmlUtils.parseAttr(nextPeriod, 'start', XmlUtils.parseDuration);
  565. if (nextStart != null) {
  566. periodDuration = nextStart - start;
  567. }
  568. } else if (presentationDuration != null) {
  569. // "The Period extends until the Period.start of the next Period, or
  570. // until the end of the Media Presentation in the case of the last
  571. // Period."
  572. periodDuration = presentationDuration - start;
  573. }
  574. let threshold =
  575. shaka.util.ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS;
  576. if (periodDuration && givenDuration &&
  577. Math.abs(periodDuration - givenDuration) > threshold) {
  578. shaka.log.warning('There is a gap/overlap between Periods', elem);
  579. }
  580. // Only use the @duration in the MPD if we can't calculate it. We should
  581. // favor the @start of the following Period. This ensures that there aren't
  582. // gaps between Periods.
  583. if (periodDuration == null) {
  584. periodDuration = givenDuration;
  585. }
  586. // Parse child nodes.
  587. let info = {
  588. start: start,
  589. duration: periodDuration,
  590. node: elem,
  591. index: i,
  592. isLastPeriod: periodDuration == null || i == periodNodes.length - 1
  593. };
  594. let period = this.parsePeriod_(context, baseUris, info);
  595. periods.push(period);
  596. // If the period ID is new, add it to the list. This must be done for both
  597. // the initial manifest parse and for updates.
  598. // See https://github.com/google/shaka-player/issues/963
  599. let periodId = context.period.id;
  600. if (this.periodIds_.indexOf(periodId) == -1) {
  601. this.periodIds_.push(periodId);
  602. // If this is an update, call filterNewPeriod and add it to the manifest.
  603. // If this is the first parse of the manifest (this.manifest_ == null),
  604. // filterAllPeriods will be called later.
  605. if (this.manifest_) {
  606. this.playerInterface_.filterNewPeriod(period);
  607. this.manifest_.periods.push(period);
  608. }
  609. }
  610. if (periodDuration == null) {
  611. if (i != periodNodes.length - 1) {
  612. // If the duration is still null and we aren't at the end, then we will
  613. // skip any remaining periods.
  614. shaka.log.warning(
  615. 'Skipping Period', i + 1, 'and any subsequent Periods:', 'Period',
  616. i + 1, 'does not have a valid start time.', periods[i + 1]);
  617. }
  618. // The duration is unknown, so the end is unknown.
  619. prevEnd = null;
  620. break;
  621. }
  622. prevEnd = start + periodDuration;
  623. } // end of period parsing loop
  624. // Call filterAllPeriods if this is the initial parse.
  625. if (this.manifest_ == null) {
  626. this.playerInterface_.filterAllPeriods(periods);
  627. }
  628. if (presentationDuration != null) {
  629. if (prevEnd != presentationDuration) {
  630. shaka.log.warning(
  631. '@mediaPresentationDuration does not match the total duration of all',
  632. 'Periods.');
  633. // Assume @mediaPresentationDuration is correct.
  634. }
  635. return {
  636. periods: periods,
  637. duration: presentationDuration,
  638. durationDerivedFromPeriods: false
  639. };
  640. } else {
  641. return {
  642. periods: periods,
  643. duration: prevEnd,
  644. durationDerivedFromPeriods: true
  645. };
  646. }
  647. };
  648. /**
  649. * Parses a Period XML element. Unlike the other parse methods, this is not
  650. * given the Node; it is given a PeriodInfo structure. Also, partial parsing
  651. * was done before this was called so start and duration are valid.
  652. *
  653. * @param {shaka.dash.DashParser.Context} context
  654. * @param {!Array.<string>} baseUris
  655. * @param {shaka.dash.DashParser.PeriodInfo} periodInfo
  656. * @return {shakaExtern.Period}
  657. * @throws shaka.util.Error When there is a parsing error.
  658. * @private
  659. */
  660. shaka.dash.DashParser.prototype.parsePeriod_ = function(
  661. context, baseUris, periodInfo) {
  662. const Functional = shaka.util.Functional;
  663. const XmlUtils = shaka.util.XmlUtils;
  664. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  665. context.period = this.createFrame_(periodInfo.node, null, baseUris);
  666. context.periodInfo = periodInfo;
  667. // If the period doesn't have an ID, give it one based on its start time.
  668. if (!context.period.id) {
  669. shaka.log.info(
  670. 'No Period ID given for Period with start time ' + periodInfo.start +
  671. ', Assigning a default');
  672. context.period.id = '__shaka_period_' + periodInfo.start;
  673. }
  674. let eventStreamNodes = XmlUtils.findChildren(periodInfo.node, 'EventStream');
  675. eventStreamNodes.forEach(
  676. this.parseEventStream_.bind(this, periodInfo.start, periodInfo.duration));
  677. let adaptationSetNodes =
  678. XmlUtils.findChildren(periodInfo.node, 'AdaptationSet');
  679. let adaptationSets = adaptationSetNodes
  680. .map(this.parseAdaptationSet_.bind(this, context))
  681. .filter(Functional.isNotNull);
  682. let representationIds = adaptationSets
  683. .map(function(as) { return as.representationIds; })
  684. .reduce(Functional.collapseArrays, []);
  685. let uniqueRepIds = representationIds.filter(Functional.isNotDuplicate);
  686. if (context.dynamic && representationIds.length != uniqueRepIds.length) {
  687. throw new shaka.util.Error(
  688. shaka.util.Error.Severity.CRITICAL,
  689. shaka.util.Error.Category.MANIFEST,
  690. shaka.util.Error.Code.DASH_DUPLICATE_REPRESENTATION_ID);
  691. }
  692. let normalAdaptationSets = adaptationSets
  693. .filter(function(as) { return !as.trickModeFor; });
  694. let trickModeAdaptationSets = adaptationSets
  695. .filter(function(as) { return as.trickModeFor; });
  696. // Attach trick mode tracks to normal tracks.
  697. trickModeAdaptationSets.forEach(function(trickModeSet) {
  698. // There may be multiple trick mode streams, but we do not currently
  699. // support that. Just choose one.
  700. let trickModeVideo = trickModeSet.streams[0];
  701. let targetId = trickModeSet.trickModeFor;
  702. normalAdaptationSets.forEach(function(normalSet) {
  703. if (normalSet.id == targetId) {
  704. normalSet.streams.forEach(function(stream) {
  705. stream.trickModeVideo = trickModeVideo;
  706. });
  707. }
  708. });
  709. });
  710. let videoSets = this.getSetsOfType_(normalAdaptationSets, ContentType.VIDEO);
  711. let audioSets = this.getSetsOfType_(normalAdaptationSets, ContentType.AUDIO);
  712. if (!videoSets.length && !audioSets.length) {
  713. throw new shaka.util.Error(
  714. shaka.util.Error.Severity.CRITICAL,
  715. shaka.util.Error.Category.MANIFEST,
  716. shaka.util.Error.Code.DASH_EMPTY_PERIOD);
  717. }
  718. // In case of audio-only or video-only content, we create an array of one item
  719. // containing a null. This way, the double-loop works for all kinds of
  720. // content.
  721. if (!audioSets.length) {
  722. audioSets = [null];
  723. }
  724. if (!videoSets.length) {
  725. videoSets = [null];
  726. }
  727. // TODO: Limit number of combinations. Come up with a heuristic
  728. // to decide which audio tracks to combine with which video tracks.
  729. let variants = [];
  730. for (let i = 0; i < audioSets.length; i++) {
  731. for (let j = 0; j < videoSets.length; j++) {
  732. let audioSet = audioSets[i];
  733. let videoSet = videoSets[j];
  734. this.createVariants_(audioSet, videoSet, variants);
  735. }
  736. }
  737. let textSets = this.getSetsOfType_(normalAdaptationSets, ContentType.TEXT);
  738. let textStreams = [];
  739. for (let i = 0; i < textSets.length; i++) {
  740. textStreams.push.apply(textStreams, textSets[i].streams);
  741. }
  742. return {
  743. startTime: periodInfo.start,
  744. textStreams: textStreams,
  745. variants: variants
  746. };
  747. };
  748. /**
  749. * @param {!Array.<!shaka.dash.DashParser.AdaptationInfo>} adaptationSets
  750. * @param {string} type
  751. * @return {!Array.<!shaka.dash.DashParser.AdaptationInfo>}
  752. * @private
  753. */
  754. shaka.dash.DashParser.prototype.getSetsOfType_ = function(
  755. adaptationSets, type) {
  756. return adaptationSets.filter(function(as) {
  757. return as.contentType == type;
  758. });
  759. };
  760. /**
  761. * Combines Streams into Variants
  762. *
  763. * @param {?shaka.dash.DashParser.AdaptationInfo} audio
  764. * @param {?shaka.dash.DashParser.AdaptationInfo} video
  765. * @param {!Array.<shakaExtern.Variant>} variants New variants are pushed onto
  766. * this array.
  767. * @private
  768. */
  769. shaka.dash.DashParser.prototype.createVariants_ =
  770. function(audio, video, variants) {
  771. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  772. // Since both audio and video are of the same type, this assertion will catch
  773. // certain mistakes at runtime that the compiler would miss.
  774. goog.asserts.assert(!audio || audio.contentType == ContentType.AUDIO,
  775. 'Audio parameter mismatch!');
  776. goog.asserts.assert(!video || video.contentType == ContentType.VIDEO,
  777. 'Video parameter mismatch!');
  778. /** @type {number} */
  779. let bandwidth;
  780. /** @type {shakaExtern.Variant} */
  781. let variant;
  782. if (!audio && !video) {
  783. return;
  784. }
  785. if (audio && video) {
  786. // Audio+video variants
  787. const DrmEngine = shaka.media.DrmEngine;
  788. if (DrmEngine.areDrmCompatible(audio.drmInfos, video.drmInfos)) {
  789. let drmInfos = DrmEngine.getCommonDrmInfos(audio.drmInfos,
  790. video.drmInfos);
  791. for (let i = 0; i < audio.streams.length; i++) {
  792. for (let j = 0; j < video.streams.length; j++) {
  793. bandwidth =
  794. (video.streams[j].bandwidth || 0) +
  795. (audio.streams[i].bandwidth || 0);
  796. variant = {
  797. id: this.globalId_++,
  798. language: audio.language,
  799. primary: audio.main || video.main,
  800. audio: audio.streams[i],
  801. video: video.streams[j],
  802. bandwidth: bandwidth,
  803. drmInfos: drmInfos,
  804. allowedByApplication: true,
  805. allowedByKeySystem: true
  806. };
  807. variants.push(variant);
  808. }
  809. }
  810. }
  811. } else {
  812. // Audio or video only variants
  813. let set = audio || video;
  814. for (let i = 0; i < set.streams.length; i++) {
  815. bandwidth = set.streams[i].bandwidth || 0;
  816. variant = {
  817. id: this.globalId_++,
  818. language: set.language || 'und',
  819. primary: set.main,
  820. audio: audio ? set.streams[i] : null,
  821. video: video ? set.streams[i] : null,
  822. bandwidth: bandwidth,
  823. drmInfos: set.drmInfos,
  824. allowedByApplication: true,
  825. allowedByKeySystem: true
  826. };
  827. variants.push(variant);
  828. }
  829. }
  830. };
  831. /**
  832. * Parses an AdaptationSet XML element.
  833. *
  834. * @param {shaka.dash.DashParser.Context} context
  835. * @param {!Element} elem The AdaptationSet element.
  836. * @return {?shaka.dash.DashParser.AdaptationInfo}
  837. * @throws shaka.util.Error When there is a parsing error.
  838. * @private
  839. */
  840. shaka.dash.DashParser.prototype.parseAdaptationSet_ = function(context, elem) {
  841. const XmlUtils = shaka.util.XmlUtils;
  842. const Functional = shaka.util.Functional;
  843. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  844. const ContentType = ManifestParserUtils.ContentType;
  845. context.adaptationSet = this.createFrame_(elem, context.period, null);
  846. let main = false;
  847. let roleElements = XmlUtils.findChildren(elem, 'Role');
  848. let roleValues = roleElements.map(function(role) {
  849. return role.getAttribute('value');
  850. }).filter(Functional.isNotNull);
  851. // Default kind for text streams is 'subtitle' if unspecified in the manifest.
  852. let kind = undefined;
  853. const isText =
  854. context.adaptationSet.contentType == ManifestParserUtils.ContentType.TEXT;
  855. if (isText) {
  856. kind = ManifestParserUtils.TextStreamKind.SUBTITLE;
  857. }
  858. for (let i = 0; i < roleElements.length; i++) {
  859. let scheme = roleElements[i].getAttribute('schemeIdUri');
  860. if (scheme == null || scheme == 'urn:mpeg:dash:role:2011') {
  861. // These only apply for the given scheme, but allow them to be specified
  862. // if there is no scheme specified.
  863. // See: DASH section 5.8.5.5
  864. let value = roleElements[i].getAttribute('value');
  865. switch (value) {
  866. case 'main':
  867. main = true;
  868. break;
  869. case 'caption':
  870. case 'subtitle':
  871. kind = value;
  872. break;
  873. }
  874. }
  875. }
  876. let essentialProperties = XmlUtils.findChildren(elem, 'EssentialProperty');
  877. // ID of real AdaptationSet if this is a trick mode set:
  878. let trickModeFor = null;
  879. let unrecognizedEssentialProperty = false;
  880. essentialProperties.forEach(function(prop) {
  881. let schemeId = prop.getAttribute('schemeIdUri');
  882. if (schemeId == 'http://dashif.org/guidelines/trickmode') {
  883. trickModeFor = prop.getAttribute('value');
  884. } else {
  885. unrecognizedEssentialProperty = true;
  886. }
  887. });
  888. // According to DASH spec (2014) section 5.8.4.8, "the successful processing
  889. // of the descriptor is essential to properly use the information in the
  890. // parent element". According to DASH IOP v3.3, section 3.3.4, "if the scheme
  891. // or the value" for EssentialProperty is not recognized, "the DASH client
  892. // shall ignore the parent element."
  893. if (unrecognizedEssentialProperty) {
  894. // Stop parsing this AdaptationSet and let the caller filter out the nulls.
  895. return null;
  896. }
  897. let contentProtectionElems = XmlUtils.findChildren(elem, 'ContentProtection');
  898. let contentProtection = shaka.dash.ContentProtection.parseFromAdaptationSet(
  899. contentProtectionElems, this.config_.dash.customScheme,
  900. this.config_.dash.ignoreDrmInfo);
  901. let language =
  902. shaka.util.LanguageUtils.normalize(elem.getAttribute('lang') || 'und');
  903. // This attribute is currently non-standard, but it is supported by Kaltura.
  904. let label = elem.getAttribute('label');
  905. // Parse Representations into Streams.
  906. let representations = XmlUtils.findChildren(elem, 'Representation');
  907. let streams = representations
  908. .map(this.parseRepresentation_.bind(this, context, contentProtection,
  909. kind, language, label, main, roleValues))
  910. .filter(function(s) { return !!s; });
  911. if (streams.length == 0) {
  912. // Ignore empty AdaptationSets if they are for text content.
  913. if (isText) {
  914. return null;
  915. }
  916. throw new shaka.util.Error(
  917. shaka.util.Error.Severity.CRITICAL,
  918. shaka.util.Error.Category.MANIFEST,
  919. shaka.util.Error.Code.DASH_EMPTY_ADAPTATION_SET);
  920. }
  921. // If AdaptationSet's type is unknown or is ambiguously "application",
  922. // guess based on the information in the first stream. If the attributes
  923. // mimeType and codecs are split across levels, they will both be inherited
  924. // down to the stream level by this point, so the stream will have all the
  925. // necessary information.
  926. if (!context.adaptationSet.contentType ||
  927. context.adaptationSet.contentType == ContentType.APPLICATION) {
  928. let mimeType = streams[0].mimeType;
  929. let codecs = streams[0].codecs;
  930. context.adaptationSet.contentType =
  931. shaka.dash.DashParser.guessContentType_(mimeType, codecs);
  932. streams.forEach(function(stream) {
  933. stream.type = context.adaptationSet.contentType;
  934. });
  935. }
  936. streams.forEach(function(stream) {
  937. // Some DRM license providers require that we have a default
  938. // key ID from the manifest in the wrapped license request.
  939. // Thus, it should be put in drmInfo to be accessible to request filters.
  940. contentProtection.drmInfos.forEach(function(drmInfo) {
  941. if (stream.keyId) {
  942. drmInfo.keyIds.push(stream.keyId);
  943. }
  944. });
  945. });
  946. let repIds = representations
  947. .map(function(node) { return node.getAttribute('id'); })
  948. .filter(shaka.util.Functional.isNotNull);
  949. return {
  950. id: context.adaptationSet.id || ('__fake__' + this.globalId_++),
  951. contentType: context.adaptationSet.contentType,
  952. language: language,
  953. main: main,
  954. streams: streams,
  955. drmInfos: contentProtection.drmInfos,
  956. trickModeFor: trickModeFor,
  957. representationIds: repIds
  958. };
  959. };
  960. /**
  961. * Parses a Representation XML element.
  962. *
  963. * @param {shaka.dash.DashParser.Context} context
  964. * @param {shaka.dash.ContentProtection.Context} contentProtection
  965. * @param {(string|undefined)} kind
  966. * @param {string} language
  967. * @param {string} label
  968. * @param {boolean} isPrimary
  969. * @param {!Array.<string>} roles
  970. * @param {!Element} node
  971. * @return {?shakaExtern.Stream} The Stream, or null when there is a
  972. * non-critical parsing error.
  973. * @throws shaka.util.Error When there is a parsing error.
  974. * @private
  975. */
  976. shaka.dash.DashParser.prototype.parseRepresentation_ = function(
  977. context, contentProtection, kind, language, label, isPrimary, roles, node) {
  978. const XmlUtils = shaka.util.XmlUtils;
  979. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  980. context.representation = this.createFrame_(node, context.adaptationSet, null);
  981. if (!this.verifyRepresentation_(context.representation)) {
  982. shaka.log.warning('Skipping Representation', context.representation);
  983. return null;
  984. }
  985. // NOTE: bandwidth is a mandatory attribute according to the spec, and zero
  986. // does not make sense in the DASH spec's bandwidth formulas.
  987. // In some content, however, the attribute is missing or zero.
  988. // To avoid NaN at the variant level on broken content, fall back to zero.
  989. // https://github.com/google/shaka-player/issues/938#issuecomment-317278180
  990. context.bandwidth =
  991. XmlUtils.parseAttr(node, 'bandwidth', XmlUtils.parsePositiveInt) || 0;
  992. /** @type {?shaka.dash.DashParser.StreamInfo} */
  993. let streamInfo;
  994. const contentType = context.representation.contentType;
  995. const isText = contentType == ContentType.TEXT ||
  996. contentType == ContentType.APPLICATION;
  997. try {
  998. const requestInitSegment = this.requestInitSegment_.bind(this);
  999. if (context.representation.segmentBase) {
  1000. streamInfo = shaka.dash.SegmentBase.createStream(
  1001. context, requestInitSegment);
  1002. } else if (context.representation.segmentList) {
  1003. streamInfo = shaka.dash.SegmentList.createStream(
  1004. context, this.segmentIndexMap_);
  1005. } else if (context.representation.segmentTemplate) {
  1006. streamInfo = shaka.dash.SegmentTemplate.createStream(
  1007. context, requestInitSegment, this.segmentIndexMap_, !!this.manifest_);
  1008. } else {
  1009. goog.asserts.assert(isText,
  1010. 'Must have Segment* with non-text streams.');
  1011. let baseUris = context.representation.baseUris;
  1012. let duration = context.periodInfo.duration || 0;
  1013. streamInfo = {
  1014. createSegmentIndex: Promise.resolve.bind(Promise),
  1015. findSegmentPosition:
  1016. /** @return {?number} */ function(/** number */ time) {
  1017. if (time >= 0 && time < duration) {
  1018. return 1;
  1019. } else {
  1020. return null;
  1021. }
  1022. },
  1023. getSegmentReference:
  1024. /** @return {shaka.media.SegmentReference} */
  1025. function(/** number */ ref) {
  1026. if (ref != 1) {
  1027. return null;
  1028. }
  1029. return new shaka.media.SegmentReference(
  1030. 1, 0, duration, function() { return baseUris; }, 0, null);
  1031. },
  1032. initSegmentReference: null,
  1033. scaledPresentationTimeOffset: 0
  1034. };
  1035. }
  1036. } catch (error) {
  1037. if (isText && error.code == shaka.util.Error.Code.DASH_NO_SEGMENT_INFO) {
  1038. // We will ignore any DASH_NO_SEGMENT_INFO errors for text streams.
  1039. return null;
  1040. }
  1041. // For anything else, re-throw.
  1042. throw error;
  1043. }
  1044. let contentProtectionElems = XmlUtils.findChildren(node, 'ContentProtection');
  1045. let keyId = shaka.dash.ContentProtection.parseFromRepresentation(
  1046. contentProtectionElems, this.config_.dash.customScheme,
  1047. contentProtection, this.config_.dash.ignoreDrmInfo);
  1048. return {
  1049. id: this.globalId_++,
  1050. createSegmentIndex: streamInfo.createSegmentIndex,
  1051. findSegmentPosition: streamInfo.findSegmentPosition,
  1052. getSegmentReference: streamInfo.getSegmentReference,
  1053. initSegmentReference: streamInfo.initSegmentReference,
  1054. presentationTimeOffset: streamInfo.scaledPresentationTimeOffset,
  1055. mimeType: context.representation.mimeType,
  1056. codecs: context.representation.codecs,
  1057. frameRate: context.representation.frameRate,
  1058. bandwidth: context.bandwidth,
  1059. width: context.representation.width,
  1060. height: context.representation.height,
  1061. kind: kind,
  1062. encrypted: contentProtection.drmInfos.length > 0,
  1063. keyId: keyId,
  1064. language: language,
  1065. label: label,
  1066. type: context.adaptationSet.contentType,
  1067. primary: isPrimary,
  1068. trickModeVideo: null,
  1069. containsEmsgBoxes: context.representation.containsEmsgBoxes,
  1070. roles: roles,
  1071. channelsCount: context.representation.numChannels
  1072. };
  1073. };
  1074. /**
  1075. * Called when the update timer ticks.
  1076. *
  1077. * @private
  1078. */
  1079. shaka.dash.DashParser.prototype.onUpdate_ = function() {
  1080. goog.asserts.assert(this.updateTimer_, 'Should only be called by timer');
  1081. goog.asserts.assert(this.updatePeriod_ >= 0,
  1082. 'There should be an update period');
  1083. shaka.log.info('Updating manifest...');
  1084. this.updateTimer_ = null;
  1085. this.requestManifest_().then(function(updateDuration) {
  1086. // Detect a call to stop()
  1087. if (!this.playerInterface_) {
  1088. return;
  1089. }
  1090. // Ensure the next update occurs within |updatePeriod_| seconds by taking
  1091. // into account the time it took to update the manifest.
  1092. this.setUpdateTimer_(updateDuration);
  1093. }.bind(this)).catch(function(error) {
  1094. goog.asserts.assert(error instanceof shaka.util.Error,
  1095. 'Should only receive a Shaka error');
  1096. // Try updating again, but ensure we haven't been destroyed.
  1097. if (this.playerInterface_) {
  1098. // We will retry updating, so override the severity of the error.
  1099. error.severity = shaka.util.Error.Severity.RECOVERABLE;
  1100. this.playerInterface_.onError(error);
  1101. this.setUpdateTimer_(0);
  1102. }
  1103. }.bind(this));
  1104. };
  1105. /**
  1106. * Sets the update timer. Does nothing if the manifest does not specify an
  1107. * update period.
  1108. *
  1109. * @param {number} offset An offset, in seconds, to apply to the manifest's
  1110. * update period.
  1111. * @private
  1112. */
  1113. shaka.dash.DashParser.prototype.setUpdateTimer_ = function(offset) {
  1114. // NOTE: An updatePeriod_ of -1 means the attribute was missing.
  1115. // An attribute which is present and set to 0 should still result in periodic
  1116. // updates. For more, see: https://github.com/google/shaka-player/issues/331
  1117. if (this.updatePeriod_ < 0) {
  1118. return;
  1119. }
  1120. goog.asserts.assert(this.updateTimer_ == null,
  1121. 'Timer should not be already set');
  1122. let period = Math.max(
  1123. shaka.dash.DashParser.MIN_UPDATE_PERIOD_,
  1124. this.updatePeriod_ - offset,
  1125. this.averageUpdateDuration_.getEstimate());
  1126. shaka.log.debug('actual update period', period);
  1127. let callback = this.onUpdate_.bind(this);
  1128. this.updateTimer_ = window.setTimeout(callback, 1000 * period);
  1129. };
  1130. /**
  1131. * Creates a new inheritance frame for the given element.
  1132. *
  1133. * @param {!Element} elem
  1134. * @param {?shaka.dash.DashParser.InheritanceFrame} parent
  1135. * @param {Array.<string>} baseUris
  1136. * @return {shaka.dash.DashParser.InheritanceFrame}
  1137. * @private
  1138. */
  1139. shaka.dash.DashParser.prototype.createFrame_ = function(
  1140. elem, parent, baseUris) {
  1141. goog.asserts.assert(parent || baseUris,
  1142. 'Must provide either parent or baseUris');
  1143. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  1144. const XmlUtils = shaka.util.XmlUtils;
  1145. parent = parent || /** @type {shaka.dash.DashParser.InheritanceFrame} */ ({
  1146. contentType: '',
  1147. mimeType: '',
  1148. codecs: '',
  1149. containsEmsgBoxes: false,
  1150. frameRate: undefined,
  1151. numChannels: null
  1152. });
  1153. baseUris = baseUris || parent.baseUris;
  1154. let parseNumber = XmlUtils.parseNonNegativeInt;
  1155. let evalDivision = XmlUtils.evalDivision;
  1156. let uris = XmlUtils.findChildren(elem, 'BaseURL').map(XmlUtils.getContents);
  1157. let contentType = elem.getAttribute('contentType') || parent.contentType;
  1158. let mimeType = elem.getAttribute('mimeType') || parent.mimeType;
  1159. let codecs = elem.getAttribute('codecs') || parent.codecs;
  1160. let frameRate =
  1161. XmlUtils.parseAttr(elem, 'frameRate', evalDivision) || parent.frameRate;
  1162. let containsEmsgBoxes =
  1163. !!XmlUtils.findChildren(elem, 'InbandEventStream').length;
  1164. let audioChannelConfigs =
  1165. XmlUtils.findChildren(elem, 'AudioChannelConfiguration');
  1166. let numChannels =
  1167. this.parseAudioChannels_(audioChannelConfigs) || parent.numChannels;
  1168. if (!contentType) {
  1169. contentType = shaka.dash.DashParser.guessContentType_(mimeType, codecs);
  1170. }
  1171. return {
  1172. baseUris: ManifestParserUtils.resolveUris(baseUris, uris),
  1173. segmentBase: XmlUtils.findChild(elem, 'SegmentBase') || parent.segmentBase,
  1174. segmentList: XmlUtils.findChild(elem, 'SegmentList') || parent.segmentList,
  1175. segmentTemplate:
  1176. XmlUtils.findChild(elem, 'SegmentTemplate') || parent.segmentTemplate,
  1177. width: XmlUtils.parseAttr(elem, 'width', parseNumber) || parent.width,
  1178. height: XmlUtils.parseAttr(elem, 'height', parseNumber) || parent.height,
  1179. contentType: contentType,
  1180. mimeType: mimeType,
  1181. codecs: codecs,
  1182. frameRate: frameRate,
  1183. containsEmsgBoxes: containsEmsgBoxes || parent.containsEmsgBoxes,
  1184. id: elem.getAttribute('id'),
  1185. numChannels: numChannels
  1186. };
  1187. };
  1188. /**
  1189. * @param {!Array.<!Element>} audioChannelConfigs An array of
  1190. * AudioChannelConfiguration elements.
  1191. * @return {?number} The number of audio channels, or null if unknown.
  1192. * @private
  1193. */
  1194. shaka.dash.DashParser.prototype.parseAudioChannels_ =
  1195. function(audioChannelConfigs) {
  1196. for (let i = 0; i < audioChannelConfigs.length; ++i) {
  1197. let elem = audioChannelConfigs[i];
  1198. let scheme = elem.getAttribute('schemeIdUri');
  1199. if (!scheme) continue;
  1200. let value = elem.getAttribute('value');
  1201. if (!value) continue;
  1202. switch (scheme) {
  1203. case 'urn:mpeg:dash:outputChannelPositionList:2012':
  1204. // A space-separated list of speaker positions, so the number of
  1205. // channels is the length of this list.
  1206. return value.trim().split(/ +/).length;
  1207. case 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011':
  1208. case 'urn:dts:dash:audio_channel_configuration:2012': {
  1209. // As far as we can tell, this is a number of channels.
  1210. let intValue = parseInt(value, 10);
  1211. if (!intValue) { // 0 or NaN
  1212. shaka.log.warning('Channel parsing failure! ' +
  1213. 'Ignoring scheme and value', scheme, value);
  1214. continue;
  1215. }
  1216. return intValue;
  1217. }
  1218. case 'tag:dolby.com,2014:dash:audio_channel_configuration:2011':
  1219. case 'urn:dolby:dash:audio_channel_configuration:2011': {
  1220. // A hex-encoded 16-bit integer, in which each bit represents a channel.
  1221. let hexValue = parseInt(value, 16);
  1222. if (!hexValue) { // 0 or NaN
  1223. shaka.log.warning('Channel parsing failure! ' +
  1224. 'Ignoring scheme and value', scheme, value);
  1225. continue;
  1226. }
  1227. // Count the 1-bits in hexValue.
  1228. let numBits = 0;
  1229. while (hexValue) {
  1230. if (hexValue & 1) ++numBits;
  1231. hexValue >>= 1;
  1232. }
  1233. return numBits;
  1234. }
  1235. default:
  1236. shaka.log.warning('Unrecognized audio channel scheme:', scheme, value);
  1237. continue;
  1238. }
  1239. }
  1240. return null;
  1241. };
  1242. /**
  1243. * Verifies that a Representation has exactly one Segment* element. Prints
  1244. * warnings if there is a problem.
  1245. *
  1246. * @param {shaka.dash.DashParser.InheritanceFrame} frame
  1247. * @return {boolean} True if the Representation is usable; otherwise return
  1248. * false.
  1249. * @private
  1250. */
  1251. shaka.dash.DashParser.prototype.verifyRepresentation_ = function(frame) {
  1252. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1253. let n = 0;
  1254. n += frame.segmentBase ? 1 : 0;
  1255. n += frame.segmentList ? 1 : 0;
  1256. n += frame.segmentTemplate ? 1 : 0;
  1257. if (n == 0) {
  1258. // TODO: Extend with the list of MIME types registered to TextEngine.
  1259. if (frame.contentType == ContentType.TEXT ||
  1260. frame.contentType == ContentType.APPLICATION) {
  1261. return true;
  1262. } else {
  1263. shaka.log.warning(
  1264. 'Representation does not contain a segment information source:',
  1265. 'the Representation must contain one of SegmentBase, SegmentList,',
  1266. 'SegmentTemplate, or explicitly indicate that it is "text".',
  1267. frame);
  1268. return false;
  1269. }
  1270. }
  1271. if (n != 1) {
  1272. shaka.log.warning(
  1273. 'Representation contains multiple segment information sources:',
  1274. 'the Representation should only contain one of SegmentBase,',
  1275. 'SegmentList, or SegmentTemplate.',
  1276. frame);
  1277. if (frame.segmentBase) {
  1278. shaka.log.info('Using SegmentBase by default.');
  1279. frame.segmentList = null;
  1280. frame.segmentTemplate = null;
  1281. } else {
  1282. goog.asserts.assert(frame.segmentList, 'There should be a SegmentList');
  1283. shaka.log.info('Using SegmentList by default.');
  1284. frame.segmentTemplate = null;
  1285. }
  1286. }
  1287. return true;
  1288. };
  1289. /**
  1290. * Makes a request to the given URI and calculates the clock offset.
  1291. *
  1292. * @param {!Array.<string>} baseUris
  1293. * @param {string} uri
  1294. * @param {string} method
  1295. * @return {!Promise.<number>}
  1296. * @private
  1297. */
  1298. shaka.dash.DashParser.prototype.requestForTiming_ =
  1299. function(baseUris, uri, method) {
  1300. let requestUris = shaka.util.ManifestParserUtils.resolveUris(baseUris, [uri]);
  1301. let request = shaka.net.NetworkingEngine.makeRequest(
  1302. requestUris, this.config_.retryParameters);
  1303. request.method = method;
  1304. const type = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  1305. let operation = this.playerInterface_.networkingEngine.request(type, request);
  1306. this.operationManager_.manage(operation);
  1307. return operation.promise.then((response) => {
  1308. let text;
  1309. if (method == 'HEAD') {
  1310. if (!response.headers || !response.headers['date']) {
  1311. shaka.log.warning('UTC timing response is missing',
  1312. 'expected date header');
  1313. return 0;
  1314. }
  1315. text = response.headers['date'];
  1316. } else {
  1317. text = shaka.util.StringUtils.fromUTF8(response.data);
  1318. }
  1319. let date = Date.parse(text);
  1320. if (isNaN(date)) {
  1321. shaka.log.warning('Unable to parse date from UTC timing response');
  1322. return 0;
  1323. }
  1324. return (date - Date.now());
  1325. });
  1326. };
  1327. /**
  1328. * Parses an array of UTCTiming elements.
  1329. *
  1330. * @param {!Array.<string>} baseUris
  1331. * @param {!Array.<!Element>} elems
  1332. * @param {boolean} isLive
  1333. * @return {!Promise.<number>}
  1334. * @private
  1335. */
  1336. shaka.dash.DashParser.prototype.parseUtcTiming_ =
  1337. function(baseUris, elems, isLive) {
  1338. let schemesAndValues = elems.map(function(elem) {
  1339. return {
  1340. scheme: elem.getAttribute('schemeIdUri'),
  1341. value: elem.getAttribute('value')
  1342. };
  1343. });
  1344. // If there's nothing specified in the manifest, but we have a default from
  1345. // the config, use that.
  1346. let clockSyncUri = this.config_.dash.clockSyncUri;
  1347. if (isLive && !schemesAndValues.length && clockSyncUri) {
  1348. schemesAndValues.push({
  1349. scheme: 'urn:mpeg:dash:utc:http-head:2014',
  1350. value: clockSyncUri
  1351. });
  1352. }
  1353. const Functional = shaka.util.Functional;
  1354. return Functional.createFallbackPromiseChain(schemesAndValues, function(sv) {
  1355. let scheme = sv.scheme;
  1356. let value = sv.value;
  1357. switch (scheme) {
  1358. // See DASH IOP Guidelines Section 4.7
  1359. // http://goo.gl/CQFNJT
  1360. // Some old ISO23009-1 drafts used 2012.
  1361. case 'urn:mpeg:dash:utc:http-head:2014':
  1362. case 'urn:mpeg:dash:utc:http-head:2012':
  1363. return this.requestForTiming_(baseUris, value, 'HEAD');
  1364. case 'urn:mpeg:dash:utc:http-xsdate:2014':
  1365. case 'urn:mpeg:dash:utc:http-iso:2014':
  1366. case 'urn:mpeg:dash:utc:http-xsdate:2012':
  1367. case 'urn:mpeg:dash:utc:http-iso:2012':
  1368. return this.requestForTiming_(baseUris, value, 'GET');
  1369. case 'urn:mpeg:dash:utc:direct:2014':
  1370. case 'urn:mpeg:dash:utc:direct:2012': {
  1371. let date = Date.parse(value);
  1372. return isNaN(date) ? 0 : (date - Date.now());
  1373. }
  1374. case 'urn:mpeg:dash:utc:http-ntp:2014':
  1375. case 'urn:mpeg:dash:utc:ntp:2014':
  1376. case 'urn:mpeg:dash:utc:sntp:2014':
  1377. shaka.log.warning('NTP UTCTiming scheme is not supported');
  1378. return Promise.reject();
  1379. default:
  1380. shaka.log.warning(
  1381. 'Unrecognized scheme in UTCTiming element', scheme);
  1382. return Promise.reject();
  1383. }
  1384. }.bind(this)).catch(function() {
  1385. if (isLive) {
  1386. shaka.log.warning(
  1387. 'A UTCTiming element should always be given in live manifests! ' +
  1388. 'This content may not play on clients with bad clocks!');
  1389. }
  1390. return 0;
  1391. });
  1392. };
  1393. /**
  1394. * Parses an EventStream element.
  1395. *
  1396. * @param {number} periodStart
  1397. * @param {?number} periodDuration
  1398. * @param {!Element} elem
  1399. * @private
  1400. */
  1401. shaka.dash.DashParser.prototype.parseEventStream_ = function(
  1402. periodStart, periodDuration, elem) {
  1403. const XmlUtils = shaka.util.XmlUtils;
  1404. let parseNumber = XmlUtils.parseNonNegativeInt;
  1405. let schemeIdUri = elem.getAttribute('schemeIdUri') || '';
  1406. let value = elem.getAttribute('value') || '';
  1407. let timescale = XmlUtils.parseAttr(elem, 'timescale', parseNumber) || 1;
  1408. XmlUtils.findChildren(elem, 'Event').forEach(function(eventNode) {
  1409. let presentationTime =
  1410. XmlUtils.parseAttr(eventNode, 'presentationTime', parseNumber) || 0;
  1411. let duration = XmlUtils.parseAttr(eventNode, 'duration', parseNumber) || 0;
  1412. let startTime = presentationTime / timescale + periodStart;
  1413. let endTime = startTime + (duration / timescale);
  1414. if (periodDuration != null) {
  1415. // An event should not go past the Period, even if the manifest says so.
  1416. // See: Dash sec. 5.10.2.1
  1417. startTime = Math.min(startTime, periodStart + periodDuration);
  1418. endTime = Math.min(endTime, periodStart + periodDuration);
  1419. }
  1420. /** @type {shakaExtern.TimelineRegionInfo} */
  1421. let region = {
  1422. schemeIdUri: schemeIdUri,
  1423. value: value,
  1424. startTime: startTime,
  1425. endTime: endTime,
  1426. id: eventNode.getAttribute('id') || '',
  1427. eventElement: eventNode
  1428. };
  1429. this.playerInterface_.onTimelineRegionAdded(region);
  1430. }.bind(this));
  1431. };
  1432. /**
  1433. * Makes a network request on behalf of SegmentBase.createStream.
  1434. *
  1435. * @param {!Array.<string>} uris
  1436. * @param {?number} startByte
  1437. * @param {?number} endByte
  1438. * @return {!Promise.<!ArrayBuffer>}
  1439. * @private
  1440. */
  1441. shaka.dash.DashParser.prototype.requestInitSegment_ = function(
  1442. uris, startByte, endByte) {
  1443. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  1444. let request = shaka.net.NetworkingEngine.makeRequest(
  1445. uris, this.config_.retryParameters);
  1446. if (startByte != null) {
  1447. let end = (endByte != null ? endByte : '');
  1448. request.headers['Range'] = 'bytes=' + startByte + '-' + end;
  1449. }
  1450. let networkingEngine = this.playerInterface_.networkingEngine;
  1451. let operation = networkingEngine.request(requestType, request);
  1452. this.operationManager_.manage(operation);
  1453. return operation.promise.then((response) => response.data);
  1454. };
  1455. /**
  1456. * Guess the content type based on MIME type and codecs.
  1457. *
  1458. * @param {string} mimeType
  1459. * @param {string} codecs
  1460. * @return {string}
  1461. * @private
  1462. */
  1463. shaka.dash.DashParser.guessContentType_ = function(mimeType, codecs) {
  1464. let fullMimeType = shaka.util.MimeUtils.getFullType(mimeType, codecs);
  1465. if (shaka.text.TextEngine.isTypeSupported(fullMimeType)) {
  1466. // If it's supported by TextEngine, it's definitely text.
  1467. // We don't check MediaSourceEngine, because that would report support
  1468. // for platform-supported video and audio types as well.
  1469. return shaka.util.ManifestParserUtils.ContentType.TEXT;
  1470. }
  1471. // Otherwise, just split the MIME type. This handles video and audio
  1472. // types well.
  1473. return mimeType.split('/')[0];
  1474. };
  1475. shaka.media.ManifestParser.registerParserByExtension(
  1476. 'mpd', shaka.dash.DashParser);
  1477. shaka.media.ManifestParser.registerParserByMime(
  1478. 'application/dash+xml', shaka.dash.DashParser);