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