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