Source: lib/dash/dash_parser.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.dash.DashParser');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.Deprecate');
  9. goog.require('shaka.abr.Ewma');
  10. goog.require('shaka.dash.ContentProtection');
  11. goog.require('shaka.dash.MpdUtils');
  12. goog.require('shaka.dash.SegmentBase');
  13. goog.require('shaka.dash.SegmentList');
  14. goog.require('shaka.dash.SegmentTemplate');
  15. goog.require('shaka.log');
  16. goog.require('shaka.media.Capabilities');
  17. goog.require('shaka.media.ManifestParser');
  18. goog.require('shaka.media.PresentationTimeline');
  19. goog.require('shaka.media.SegmentIndex');
  20. goog.require('shaka.media.SegmentUtils');
  21. goog.require('shaka.net.NetworkingEngine');
  22. goog.require('shaka.text.TextEngine');
  23. goog.require('shaka.util.ContentSteeringManager');
  24. goog.require('shaka.util.Error');
  25. goog.require('shaka.util.EventManager');
  26. goog.require('shaka.util.Functional');
  27. goog.require('shaka.util.LanguageUtils');
  28. goog.require('shaka.util.ManifestParserUtils');
  29. goog.require('shaka.util.MimeUtils');
  30. goog.require('shaka.util.Networking');
  31. goog.require('shaka.util.OperationManager');
  32. goog.require('shaka.util.PeriodCombiner');
  33. goog.require('shaka.util.PlayerConfiguration');
  34. goog.require('shaka.util.StringUtils');
  35. goog.require('shaka.util.Timer');
  36. goog.require('shaka.util.TXml');
  37. goog.require('shaka.util.XmlUtils');
  38. /**
  39. * Creates a new DASH parser.
  40. *
  41. * @implements {shaka.extern.ManifestParser}
  42. * @export
  43. */
  44. shaka.dash.DashParser = class {
  45. /** Creates a new DASH parser. */
  46. constructor() {
  47. /** @private {?shaka.extern.ManifestConfiguration} */
  48. this.config_ = null;
  49. /** @private {?shaka.extern.ManifestParser.PlayerInterface} */
  50. this.playerInterface_ = null;
  51. /** @private {!Array.<string>} */
  52. this.manifestUris_ = [];
  53. /** @private {?shaka.extern.Manifest} */
  54. this.manifest_ = null;
  55. /** @private {number} */
  56. this.globalId_ = 1;
  57. /** @private {!Array<shaka.extern.xml.Node>} */
  58. this.patchLocationNodes_ = [];
  59. /**
  60. * A context of the living manifest used for processing
  61. * Patch MPD's
  62. * @private {!shaka.dash.DashParser.PatchContext}
  63. */
  64. this.manifestPatchContext_ = {
  65. mpdId: '',
  66. type: '',
  67. profiles: [],
  68. mediaPresentationDuration: null,
  69. availabilityTimeOffset: 0,
  70. getBaseUris: null,
  71. publishTime: 0,
  72. };
  73. /**
  74. * This is a cache is used the store a snapshot of the context
  75. * object which is built up throughout node traversal to maintain
  76. * a current state. This data needs to be preserved for parsing
  77. * patches.
  78. * The key is a combination period and representation id's.
  79. * @private {!Map<string, !shaka.dash.DashParser.Context>}
  80. */
  81. this.contextCache_ = new Map();
  82. /**
  83. * A map of IDs to Stream objects.
  84. * ID: Period@id,AdaptationSet@id,@Representation@id
  85. * e.g.: '1,5,23'
  86. * @private {!Object.<string, !shaka.extern.Stream>}
  87. */
  88. this.streamMap_ = {};
  89. /**
  90. * A map of period ids to their durations
  91. * @private {!Object.<string, number>}
  92. */
  93. this.periodDurations_ = {};
  94. /** @private {shaka.util.PeriodCombiner} */
  95. this.periodCombiner_ = new shaka.util.PeriodCombiner();
  96. /**
  97. * The update period in seconds, or 0 for no updates.
  98. * @private {number}
  99. */
  100. this.updatePeriod_ = 0;
  101. /**
  102. * An ewma that tracks how long updates take.
  103. * This is to mitigate issues caused by slow parsing on embedded devices.
  104. * @private {!shaka.abr.Ewma}
  105. */
  106. this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
  107. /** @private {shaka.util.Timer} */
  108. this.updateTimer_ = new shaka.util.Timer(() => {
  109. if (this.mediaElement_ && !this.config_.continueLoadingWhenPaused) {
  110. this.eventManager_.unlisten(this.mediaElement_, 'timeupdate');
  111. if (this.mediaElement_.paused) {
  112. this.eventManager_.listenOnce(
  113. this.mediaElement_, 'timeupdate', () => this.onUpdate_());
  114. return;
  115. }
  116. }
  117. this.onUpdate_();
  118. });
  119. /** @private {!shaka.util.OperationManager} */
  120. this.operationManager_ = new shaka.util.OperationManager();
  121. /**
  122. * Largest period start time seen.
  123. * @private {?number}
  124. */
  125. this.largestPeriodStartTime_ = null;
  126. /**
  127. * Period IDs seen in previous manifest.
  128. * @private {!Array.<string>}
  129. */
  130. this.lastManifestUpdatePeriodIds_ = [];
  131. /**
  132. * The minimum of the availabilityTimeOffset values among the adaptation
  133. * sets.
  134. * @private {number}
  135. */
  136. this.minTotalAvailabilityTimeOffset_ = Infinity;
  137. /** @private {boolean} */
  138. this.lowLatencyMode_ = false;
  139. /** @private {?shaka.util.ContentSteeringManager} */
  140. this.contentSteeringManager_ = null;
  141. /** @private {number} */
  142. this.gapCount_ = 0;
  143. /** @private {boolean} */
  144. this.isLowLatency_ = false;
  145. /** @private {shaka.util.EventManager} */
  146. this.eventManager_ = new shaka.util.EventManager();
  147. /** @private {HTMLMediaElement} */
  148. this.mediaElement_ = null;
  149. /** @private {boolean} */
  150. this.isTransitionFromDynamicToStatic_ = false;
  151. }
  152. /**
  153. * @override
  154. * @exportInterface
  155. */
  156. configure(config) {
  157. goog.asserts.assert(config.dash != null,
  158. 'DashManifestConfiguration should not be null!');
  159. const needFireUpdate = this.playerInterface_ &&
  160. config.dash.updatePeriod != this.config_.dash.updatePeriod &&
  161. config.dash.updatePeriod >= 0;
  162. this.config_ = config;
  163. if (needFireUpdate && this.manifest_ &&
  164. this.manifest_.presentationTimeline.isLive()) {
  165. this.updateNow_();
  166. }
  167. if (this.contentSteeringManager_) {
  168. this.contentSteeringManager_.configure(this.config_);
  169. }
  170. if (this.periodCombiner_) {
  171. this.periodCombiner_.setAllowMultiTypeVariants(
  172. this.config_.dash.multiTypeVariantsAllowed &&
  173. shaka.media.Capabilities.isChangeTypeSupported());
  174. this.periodCombiner_.setUseStreamOnce(
  175. this.config_.dash.useStreamOnceInPeriodFlattening);
  176. }
  177. }
  178. /**
  179. * @override
  180. * @exportInterface
  181. */
  182. async start(uri, playerInterface) {
  183. goog.asserts.assert(this.config_, 'Must call configure() before start()!');
  184. this.lowLatencyMode_ = playerInterface.isLowLatencyMode();
  185. this.manifestUris_ = [uri];
  186. this.playerInterface_ = playerInterface;
  187. const updateDelay = await this.requestManifest_();
  188. if (this.playerInterface_) {
  189. this.setUpdateTimer_(updateDelay);
  190. }
  191. // Make sure that the parser has not been destroyed.
  192. if (!this.playerInterface_) {
  193. throw new shaka.util.Error(
  194. shaka.util.Error.Severity.CRITICAL,
  195. shaka.util.Error.Category.PLAYER,
  196. shaka.util.Error.Code.OPERATION_ABORTED);
  197. }
  198. goog.asserts.assert(this.manifest_, 'Manifest should be non-null!');
  199. return this.manifest_;
  200. }
  201. /**
  202. * @override
  203. * @exportInterface
  204. */
  205. stop() {
  206. // When the parser stops, release all segment indexes, which stops their
  207. // timers, as well.
  208. for (const stream of Object.values(this.streamMap_)) {
  209. if (stream.segmentIndex) {
  210. stream.segmentIndex.release();
  211. }
  212. }
  213. if (this.periodCombiner_) {
  214. this.periodCombiner_.release();
  215. }
  216. this.playerInterface_ = null;
  217. this.config_ = null;
  218. this.manifestUris_ = [];
  219. this.manifest_ = null;
  220. this.streamMap_ = {};
  221. this.contextCache_.clear();
  222. this.manifestPatchContext_ = {
  223. mpdId: '',
  224. type: '',
  225. profiles: [],
  226. mediaPresentationDuration: null,
  227. availabilityTimeOffset: 0,
  228. getBaseUris: null,
  229. publishTime: 0,
  230. };
  231. this.periodCombiner_ = null;
  232. if (this.updateTimer_ != null) {
  233. this.updateTimer_.stop();
  234. this.updateTimer_ = null;
  235. }
  236. if (this.contentSteeringManager_) {
  237. this.contentSteeringManager_.destroy();
  238. }
  239. if (this.eventManager_) {
  240. this.eventManager_.release();
  241. this.eventManager_ = null;
  242. }
  243. return this.operationManager_.destroy();
  244. }
  245. /**
  246. * @override
  247. * @exportInterface
  248. */
  249. async update() {
  250. try {
  251. await this.requestManifest_();
  252. } catch (error) {
  253. if (!this.playerInterface_ || !error) {
  254. return;
  255. }
  256. goog.asserts.assert(error instanceof shaka.util.Error, 'Bad error type');
  257. this.playerInterface_.onError(error);
  258. }
  259. }
  260. /**
  261. * @override
  262. * @exportInterface
  263. */
  264. onExpirationUpdated(sessionId, expiration) {
  265. // No-op
  266. }
  267. /**
  268. * @override
  269. * @exportInterface
  270. */
  271. onInitialVariantChosen(variant) {
  272. // For live it is necessary that the first time we update the manifest with
  273. // a shorter time than indicated to take into account that the last segment
  274. // added could be halfway, for example
  275. if (this.manifest_ && this.manifest_.presentationTimeline.isLive()) {
  276. const stream = variant.video || variant.audio;
  277. if (stream && stream.segmentIndex) {
  278. const availabilityEnd =
  279. this.manifest_.presentationTimeline.getSegmentAvailabilityEnd();
  280. const position = stream.segmentIndex.find(availabilityEnd);
  281. if (position == null) {
  282. return;
  283. }
  284. const reference = stream.segmentIndex.get(position);
  285. if (!reference) {
  286. return;
  287. }
  288. this.updatePeriod_ = reference.endTime - availabilityEnd;
  289. this.setUpdateTimer_(/* offset= */ 0);
  290. }
  291. }
  292. }
  293. /**
  294. * @override
  295. * @exportInterface
  296. */
  297. banLocation(uri) {
  298. if (this.contentSteeringManager_) {
  299. this.contentSteeringManager_.banLocation(uri);
  300. }
  301. }
  302. /**
  303. * @override
  304. * @exportInterface
  305. */
  306. setMediaElement(mediaElement) {
  307. this.mediaElement_ = mediaElement;
  308. }
  309. /**
  310. * Makes a network request for the manifest and parses the resulting data.
  311. *
  312. * @return {!Promise.<number>} Resolves with the time it took, in seconds, to
  313. * fulfill the request and parse the data.
  314. * @private
  315. */
  316. async requestManifest_() {
  317. const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  318. let type = shaka.net.NetworkingEngine.AdvancedRequestType.MPD;
  319. let rootElement = 'MPD';
  320. const patchLocationUris = this.getPatchLocationUris_();
  321. let manifestUris = this.manifestUris_;
  322. if (patchLocationUris.length) {
  323. manifestUris = patchLocationUris;
  324. rootElement = 'Patch';
  325. type = shaka.net.NetworkingEngine.AdvancedRequestType.MPD_PATCH;
  326. } else if (this.manifestUris_.length > 1 && this.contentSteeringManager_) {
  327. const locations = this.contentSteeringManager_.getLocations(
  328. 'Location', /* ignoreBaseUrls= */ true);
  329. if (locations.length) {
  330. manifestUris = locations;
  331. }
  332. }
  333. const request = shaka.net.NetworkingEngine.makeRequest(
  334. manifestUris, this.config_.retryParameters);
  335. const startTime = Date.now();
  336. const response = await this.makeNetworkRequest_(
  337. request, requestType, {type});
  338. // Detect calls to stop().
  339. if (!this.playerInterface_) {
  340. return 0;
  341. }
  342. // For redirections add the response uri to the first entry in the
  343. // Manifest Uris array.
  344. if (response.uri && response.uri != response.originalUri &&
  345. !this.manifestUris_.includes(response.uri)) {
  346. this.manifestUris_.unshift(response.uri);
  347. }
  348. // This may throw, but it will result in a failed promise.
  349. await this.parseManifest_(response.data, response.uri, rootElement);
  350. // Keep track of how long the longest manifest update took.
  351. const endTime = Date.now();
  352. const updateDuration = (endTime - startTime) / 1000.0;
  353. this.averageUpdateDuration_.sample(1, updateDuration);
  354. // Let the caller know how long this update took.
  355. return updateDuration;
  356. }
  357. /**
  358. * Parses the manifest XML. This also handles updates and will update the
  359. * stored manifest.
  360. *
  361. * @param {BufferSource} data
  362. * @param {string} finalManifestUri The final manifest URI, which may
  363. * differ from this.manifestUri_ if there has been a redirect.
  364. * @param {string} rootElement MPD or Patch, depending on context
  365. * @return {!Promise}
  366. * @private
  367. */
  368. async parseManifest_(data, finalManifestUri, rootElement) {
  369. let manifestData = data;
  370. const manifestPreprocessor = this.config_.dash.manifestPreprocessor;
  371. const defaultManifestPreprocessor =
  372. shaka.util.PlayerConfiguration.defaultManifestPreprocessor;
  373. if (manifestPreprocessor != defaultManifestPreprocessor) {
  374. shaka.Deprecate.deprecateFeature(5,
  375. 'manifest.dash.manifestPreprocessor configuration',
  376. 'Please Use manifest.dash.manifestPreprocessorTXml instead.');
  377. const mpdElement =
  378. shaka.util.XmlUtils.parseXml(manifestData, rootElement);
  379. if (!mpdElement) {
  380. throw new shaka.util.Error(
  381. shaka.util.Error.Severity.CRITICAL,
  382. shaka.util.Error.Category.MANIFEST,
  383. shaka.util.Error.Code.DASH_INVALID_XML,
  384. finalManifestUri);
  385. }
  386. manifestPreprocessor(mpdElement);
  387. manifestData = shaka.util.XmlUtils.toArrayBuffer(mpdElement);
  388. }
  389. const mpd = shaka.util.TXml.parseXml(manifestData, rootElement);
  390. if (!mpd) {
  391. throw new shaka.util.Error(
  392. shaka.util.Error.Severity.CRITICAL,
  393. shaka.util.Error.Category.MANIFEST,
  394. shaka.util.Error.Code.DASH_INVALID_XML,
  395. finalManifestUri);
  396. }
  397. const manifestPreprocessorTXml =
  398. this.config_.dash.manifestPreprocessorTXml;
  399. const defaultManifestPreprocessorTXml =
  400. shaka.util.PlayerConfiguration.defaultManifestPreprocessorTXml;
  401. if (manifestPreprocessorTXml != defaultManifestPreprocessorTXml) {
  402. manifestPreprocessorTXml(mpd);
  403. }
  404. if (rootElement === 'Patch') {
  405. return this.processPatchManifest_(mpd);
  406. }
  407. const disableXlinkProcessing = this.config_.dash.disableXlinkProcessing;
  408. if (disableXlinkProcessing) {
  409. return this.processManifest_(mpd, finalManifestUri);
  410. }
  411. // Process the mpd to account for xlink connections.
  412. const failGracefully = this.config_.dash.xlinkFailGracefully;
  413. const xlinkOperation = shaka.dash.MpdUtils.processXlinks(
  414. mpd, this.config_.retryParameters, failGracefully, finalManifestUri,
  415. this.playerInterface_.networkingEngine);
  416. this.operationManager_.manage(xlinkOperation);
  417. const finalMpd = await xlinkOperation.promise;
  418. return this.processManifest_(finalMpd, finalManifestUri);
  419. }
  420. /**
  421. * Takes a formatted MPD and converts it into a manifest.
  422. *
  423. * @param {!shaka.extern.xml.Node} mpd
  424. * @param {string} finalManifestUri The final manifest URI, which may
  425. * differ from this.manifestUri_ if there has been a redirect.
  426. * @return {!Promise}
  427. * @private
  428. */
  429. async processManifest_(mpd, finalManifestUri) {
  430. const TXml = shaka.util.TXml;
  431. goog.asserts.assert(this.config_,
  432. 'Must call configure() before processManifest_()!');
  433. if (this.contentSteeringManager_) {
  434. this.contentSteeringManager_.clearPreviousLocations();
  435. }
  436. // Get any Location elements. This will update the manifest location and
  437. // the base URI.
  438. /** @type {!Array.<string>} */
  439. let manifestBaseUris = [finalManifestUri];
  440. /** @type {!Array.<string>} */
  441. const locations = [];
  442. /** @type {!Map.<string, string>} */
  443. const locationsMapping = new Map();
  444. const locationsObjs = TXml.findChildren(mpd, 'Location');
  445. for (const locationsObj of locationsObjs) {
  446. const serviceLocation = locationsObj.attributes['serviceLocation'];
  447. const uri = TXml.getContents(locationsObj);
  448. if (!uri) {
  449. continue;
  450. }
  451. const finalUri = shaka.util.ManifestParserUtils.resolveUris(
  452. manifestBaseUris, [uri])[0];
  453. if (serviceLocation) {
  454. if (this.contentSteeringManager_) {
  455. this.contentSteeringManager_.addLocation(
  456. 'Location', serviceLocation, finalUri);
  457. } else {
  458. locationsMapping.set(serviceLocation, finalUri);
  459. }
  460. }
  461. locations.push(finalUri);
  462. }
  463. if (this.contentSteeringManager_) {
  464. const steeringlocations = this.contentSteeringManager_.getLocations(
  465. 'Location', /* ignoreBaseUrls= */ true);
  466. if (steeringlocations.length > 0) {
  467. this.manifestUris_ = steeringlocations;
  468. manifestBaseUris = steeringlocations;
  469. }
  470. } else if (locations.length) {
  471. this.manifestUris_ = locations;
  472. manifestBaseUris = locations;
  473. }
  474. this.manifestPatchContext_.mpdId = mpd.attributes['id'] || '';
  475. this.manifestPatchContext_.publishTime =
  476. TXml.parseAttr(mpd, 'publishTime', TXml.parseDate) || 0;
  477. this.patchLocationNodes_ = TXml.findChildren(mpd, 'PatchLocation');
  478. let contentSteeringPromise = Promise.resolve();
  479. const contentSteering = TXml.findChild(mpd, 'ContentSteering');
  480. if (contentSteering && this.playerInterface_) {
  481. const defaultPathwayId =
  482. contentSteering.attributes['defaultServiceLocation'];
  483. if (!this.contentSteeringManager_) {
  484. this.contentSteeringManager_ =
  485. new shaka.util.ContentSteeringManager(this.playerInterface_);
  486. this.contentSteeringManager_.configure(this.config_);
  487. this.contentSteeringManager_.setManifestType(
  488. shaka.media.ManifestParser.DASH);
  489. this.contentSteeringManager_.setBaseUris(manifestBaseUris);
  490. this.contentSteeringManager_.setDefaultPathwayId(defaultPathwayId);
  491. const uri = TXml.getContents(contentSteering);
  492. if (uri) {
  493. const queryBeforeStart =
  494. TXml.parseAttr(contentSteering, 'queryBeforeStart',
  495. TXml.parseBoolean, /* defaultValue= */ false);
  496. if (queryBeforeStart) {
  497. contentSteeringPromise =
  498. this.contentSteeringManager_.requestInfo(uri);
  499. } else {
  500. this.contentSteeringManager_.requestInfo(uri);
  501. }
  502. }
  503. } else {
  504. this.contentSteeringManager_.setBaseUris(manifestBaseUris);
  505. this.contentSteeringManager_.setDefaultPathwayId(defaultPathwayId);
  506. }
  507. for (const serviceLocation of locationsMapping.keys()) {
  508. const uri = locationsMapping.get(serviceLocation);
  509. this.contentSteeringManager_.addLocation(
  510. 'Location', serviceLocation, uri);
  511. }
  512. }
  513. const uriObjs = TXml.findChildren(mpd, 'BaseURL');
  514. let calculatedBaseUris;
  515. let someLocationValid = false;
  516. if (this.contentSteeringManager_) {
  517. for (const uriObj of uriObjs) {
  518. const serviceLocation = uriObj.attributes['serviceLocation'];
  519. const uri = TXml.getContents(uriObj);
  520. if (serviceLocation && uri) {
  521. this.contentSteeringManager_.addLocation(
  522. 'BaseURL', serviceLocation, uri);
  523. someLocationValid = true;
  524. }
  525. }
  526. }
  527. if (!someLocationValid || !this.contentSteeringManager_) {
  528. const uris = uriObjs.map(TXml.getContents);
  529. calculatedBaseUris = shaka.util.ManifestParserUtils.resolveUris(
  530. manifestBaseUris, uris);
  531. }
  532. const getBaseUris = () => {
  533. if (this.contentSteeringManager_ && someLocationValid) {
  534. return this.contentSteeringManager_.getLocations('BaseURL');
  535. }
  536. if (calculatedBaseUris) {
  537. return calculatedBaseUris;
  538. }
  539. return [];
  540. };
  541. this.manifestPatchContext_.getBaseUris = getBaseUris;
  542. let availabilityTimeOffset = 0;
  543. if (uriObjs && uriObjs.length) {
  544. availabilityTimeOffset = TXml.parseAttr(uriObjs[0],
  545. 'availabilityTimeOffset', TXml.parseFloat) || 0;
  546. }
  547. this.manifestPatchContext_.availabilityTimeOffset = availabilityTimeOffset;
  548. const ignoreMinBufferTime = this.config_.dash.ignoreMinBufferTime;
  549. let minBufferTime = 0;
  550. if (!ignoreMinBufferTime) {
  551. minBufferTime =
  552. TXml.parseAttr(mpd, 'minBufferTime', TXml.parseDuration) || 0;
  553. }
  554. this.updatePeriod_ = /** @type {number} */ (TXml.parseAttr(
  555. mpd, 'minimumUpdatePeriod', TXml.parseDuration, -1));
  556. const presentationStartTime = TXml.parseAttr(
  557. mpd, 'availabilityStartTime', TXml.parseDate);
  558. let segmentAvailabilityDuration = TXml.parseAttr(
  559. mpd, 'timeShiftBufferDepth', TXml.parseDuration);
  560. const ignoreSuggestedPresentationDelay =
  561. this.config_.dash.ignoreSuggestedPresentationDelay;
  562. let suggestedPresentationDelay = null;
  563. if (!ignoreSuggestedPresentationDelay) {
  564. suggestedPresentationDelay = TXml.parseAttr(
  565. mpd, 'suggestedPresentationDelay', TXml.parseDuration);
  566. }
  567. const ignoreMaxSegmentDuration =
  568. this.config_.dash.ignoreMaxSegmentDuration;
  569. let maxSegmentDuration = null;
  570. if (!ignoreMaxSegmentDuration) {
  571. maxSegmentDuration = TXml.parseAttr(
  572. mpd, 'maxSegmentDuration', TXml.parseDuration);
  573. }
  574. const mpdType = mpd.attributes['type'] || 'static';
  575. if (this.manifest_ && this.manifest_.presentationTimeline) {
  576. this.isTransitionFromDynamicToStatic_ =
  577. this.manifest_.presentationTimeline.isLive() && mpdType == 'static';
  578. }
  579. this.manifestPatchContext_.type = mpdType;
  580. /** @type {!shaka.media.PresentationTimeline} */
  581. let presentationTimeline;
  582. if (this.manifest_) {
  583. presentationTimeline = this.manifest_.presentationTimeline;
  584. // Before processing an update, evict from all segment indexes. Some of
  585. // them may not get updated otherwise if their corresponding Period
  586. // element has been dropped from the manifest since the last update.
  587. // Without this, playback will still work, but this is necessary to
  588. // maintain conditions that we assert on for multi-Period content.
  589. // This gives us confidence that our state is maintained correctly, and
  590. // that the complex logic of multi-Period eviction and period-flattening
  591. // is correct. See also:
  592. // https://github.com/shaka-project/shaka-player/issues/3169#issuecomment-823580634
  593. for (const stream of Object.values(this.streamMap_)) {
  594. if (stream.segmentIndex) {
  595. stream.segmentIndex.evict(
  596. presentationTimeline.getSegmentAvailabilityStart());
  597. }
  598. }
  599. } else {
  600. // DASH IOP v3.0 suggests using a default delay between minBufferTime
  601. // and timeShiftBufferDepth. This is literally the range of all
  602. // feasible choices for the value. Nothing older than
  603. // timeShiftBufferDepth is still available, and anything less than
  604. // minBufferTime will cause buffering issues.
  605. //
  606. // We have decided that our default will be the configured value, or
  607. // 1.5 * minBufferTime if not configured. This is fairly conservative.
  608. // Content providers should provide a suggestedPresentationDelay whenever
  609. // possible to optimize the live streaming experience.
  610. const defaultPresentationDelay =
  611. this.config_.defaultPresentationDelay || minBufferTime * 1.5;
  612. const presentationDelay = suggestedPresentationDelay != null ?
  613. suggestedPresentationDelay : defaultPresentationDelay;
  614. presentationTimeline = new shaka.media.PresentationTimeline(
  615. presentationStartTime, presentationDelay,
  616. this.config_.dash.autoCorrectDrift);
  617. }
  618. presentationTimeline.setStatic(mpdType == 'static');
  619. const isLive = presentationTimeline.isLive();
  620. // If it's live, we check for an override.
  621. if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
  622. segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
  623. }
  624. // If it's null, that means segments are always available. This is always
  625. // the case for VOD, and sometimes the case for live.
  626. if (segmentAvailabilityDuration == null) {
  627. segmentAvailabilityDuration = Infinity;
  628. }
  629. presentationTimeline.setSegmentAvailabilityDuration(
  630. segmentAvailabilityDuration);
  631. const profiles = mpd.attributes['profiles'] || '';
  632. this.manifestPatchContext_.profiles = profiles.split(',');
  633. /** @type {shaka.dash.DashParser.Context} */
  634. const context = {
  635. // Don't base on updatePeriod_ since emsg boxes can cause manifest
  636. // updates.
  637. dynamic: mpdType != 'static',
  638. presentationTimeline: presentationTimeline,
  639. period: null,
  640. periodInfo: null,
  641. adaptationSet: null,
  642. representation: null,
  643. bandwidth: 0,
  644. indexRangeWarningGiven: false,
  645. availabilityTimeOffset: availabilityTimeOffset,
  646. mediaPresentationDuration: null,
  647. profiles: profiles.split(','),
  648. roles: null,
  649. };
  650. this.gapCount_ = 0;
  651. const periodsAndDuration = this.parsePeriods_(context, getBaseUris, mpd);
  652. const duration = periodsAndDuration.duration;
  653. const periods = periodsAndDuration.periods;
  654. if ((mpdType == 'static' && !this.isTransitionFromDynamicToStatic_) ||
  655. !periodsAndDuration.durationDerivedFromPeriods) {
  656. // Ignore duration calculated from Period lengths if this is dynamic.
  657. presentationTimeline.setDuration(duration || Infinity);
  658. }
  659. // The segments are available earlier than the availability start time.
  660. // If the stream is low latency and the user has not configured the
  661. // lowLatencyMode, but if it has been configured to activate the
  662. // lowLatencyMode if a stream of this type is detected, we automatically
  663. // activate the lowLatencyMode.
  664. if (this.isLowLatency_ && !this.lowLatencyMode_) {
  665. const autoLowLatencyMode = this.playerInterface_.isAutoLowLatencyMode();
  666. if (autoLowLatencyMode) {
  667. this.playerInterface_.enableLowLatencyMode();
  668. this.lowLatencyMode_ = this.playerInterface_.isLowLatencyMode();
  669. }
  670. }
  671. if (this.lowLatencyMode_) {
  672. presentationTimeline.setAvailabilityTimeOffset(
  673. this.minTotalAvailabilityTimeOffset_);
  674. }
  675. // Use @maxSegmentDuration to override smaller, derived values.
  676. presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1);
  677. if (goog.DEBUG && !this.isTransitionFromDynamicToStatic_) {
  678. presentationTimeline.assertIsValid();
  679. }
  680. await contentSteeringPromise;
  681. // Set minBufferTime to 0 for low-latency DASH live stream to achieve the
  682. // best latency
  683. if (this.lowLatencyMode_) {
  684. minBufferTime = 0;
  685. const presentationDelay = suggestedPresentationDelay != null ?
  686. suggestedPresentationDelay : this.config_.defaultPresentationDelay;
  687. presentationTimeline.setDelay(presentationDelay);
  688. }
  689. // These steps are not done on manifest update.
  690. if (!this.manifest_) {
  691. await this.periodCombiner_.combinePeriods(periods, context.dynamic);
  692. this.manifest_ = {
  693. presentationTimeline: presentationTimeline,
  694. variants: this.periodCombiner_.getVariants(),
  695. textStreams: this.periodCombiner_.getTextStreams(),
  696. imageStreams: this.periodCombiner_.getImageStreams(),
  697. offlineSessionIds: [],
  698. minBufferTime: minBufferTime || 0,
  699. sequenceMode: this.config_.dash.sequenceMode,
  700. ignoreManifestTimestampsInSegmentsMode: false,
  701. type: shaka.media.ManifestParser.DASH,
  702. serviceDescription: this.parseServiceDescription_(mpd),
  703. nextUrl: this.parseMpdChaining_(mpd),
  704. periodCount: periods.length,
  705. gapCount: this.gapCount_,
  706. isLowLatency: this.isLowLatency_,
  707. };
  708. // We only need to do clock sync when we're using presentation start
  709. // time. This condition also excludes VOD streams.
  710. if (presentationTimeline.usingPresentationStartTime()) {
  711. const TXml = shaka.util.TXml;
  712. const timingElements = TXml.findChildren(mpd, 'UTCTiming');
  713. const offset = await this.parseUtcTiming_(getBaseUris, timingElements);
  714. // Detect calls to stop().
  715. if (!this.playerInterface_) {
  716. return;
  717. }
  718. presentationTimeline.setClockOffset(offset);
  719. }
  720. // This is the first point where we have a meaningful presentation start
  721. // time, and we need to tell PresentationTimeline that so that it can
  722. // maintain consistency from here on.
  723. presentationTimeline.lockStartTime();
  724. } else {
  725. this.manifest_.periodCount = periods.length;
  726. this.manifest_.gapCount = this.gapCount_;
  727. await this.postPeriodProcessing_(periods, /* isPatchUpdate= */ false);
  728. }
  729. // Add text streams to correspond to closed captions. This happens right
  730. // after period combining, while we still have a direct reference, so that
  731. // any new streams will appear in the period combiner.
  732. this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
  733. }
  734. /**
  735. * Handles common procedures after processing new periods.
  736. *
  737. * @param {!Array<shaka.extern.Period>} periods to be appended
  738. * @param {boolean} isPatchUpdate does call comes from mpd patch update
  739. * @private
  740. */
  741. async postPeriodProcessing_(periods, isPatchUpdate) {
  742. await this.periodCombiner_.combinePeriods(periods, true, isPatchUpdate);
  743. // Just update the variants and text streams, which may change as periods
  744. // are added or removed.
  745. this.manifest_.variants = this.periodCombiner_.getVariants();
  746. const textStreams = this.periodCombiner_.getTextStreams();
  747. if (textStreams.length > 0) {
  748. this.manifest_.textStreams = textStreams;
  749. }
  750. this.manifest_.imageStreams = this.periodCombiner_.getImageStreams();
  751. // Re-filter the manifest. This will check any configured restrictions on
  752. // new variants, and will pass any new init data to DrmEngine to ensure
  753. // that key rotation works correctly.
  754. this.playerInterface_.filter(this.manifest_);
  755. }
  756. /**
  757. * Takes a formatted Patch MPD and converts it into a manifest.
  758. *
  759. * @param {!shaka.extern.xml.Node} mpd
  760. * @return {!Promise}
  761. * @private
  762. */
  763. async processPatchManifest_(mpd) {
  764. const TXml = shaka.util.TXml;
  765. const mpdId = mpd.attributes['mpdId'];
  766. const originalPublishTime = TXml.parseAttr(mpd, 'originalPublishTime',
  767. TXml.parseDate);
  768. if (!mpdId || mpdId !== this.manifestPatchContext_.mpdId ||
  769. originalPublishTime !== this.manifestPatchContext_.publishTime) {
  770. // Clean patch location nodes, so it will force full MPD update.
  771. this.patchLocationNodes_ = [];
  772. throw new shaka.util.Error(
  773. shaka.util.Error.Severity.RECOVERABLE,
  774. shaka.util.Error.Category.MANIFEST,
  775. shaka.util.Error.Code.DASH_INVALID_PATCH);
  776. }
  777. /** @type {!Array<shaka.extern.Period>} */
  778. const newPeriods = [];
  779. /** @type {!Array<shaka.extern.xml.Node>} */
  780. const periodAdditions = [];
  781. /** @type {!Set<string>} */
  782. const modifiedTimelines = new Set();
  783. for (const patchNode of TXml.getChildNodes(mpd)) {
  784. let handled = true;
  785. const paths = TXml.parseXpath(patchNode.attributes['sel'] || '');
  786. const node = paths[paths.length - 1];
  787. const content = TXml.getContents(patchNode) || '';
  788. if (node.name === 'MPD') {
  789. if (node.attribute === 'mediaPresentationDuration') {
  790. const content = TXml.getContents(patchNode) || '';
  791. this.parsePatchMediaPresentationDurationChange_(content);
  792. } else if (node.attribute === 'type') {
  793. this.parsePatchMpdTypeChange_(content);
  794. } else if (node.attribute === 'publishTime') {
  795. this.manifestPatchContext_.publishTime = TXml.parseDate(content) || 0;
  796. } else if (node.attribute === null && patchNode.tagName === 'add') {
  797. periodAdditions.push(patchNode);
  798. } else {
  799. handled = false;
  800. }
  801. } else if (node.name === 'PatchLocation') {
  802. this.updatePatchLocationNodes_(patchNode);
  803. } else if (node.name === 'Period') {
  804. if (patchNode.tagName === 'add') {
  805. periodAdditions.push(patchNode);
  806. } else if (patchNode.tagName === 'remove' && node.id) {
  807. this.removePatchPeriod_(node.id);
  808. }
  809. } else if (node.name === 'SegmentTemplate') {
  810. const timelines = this.modifySegmentTemplate_(patchNode);
  811. for (const timeline of timelines) {
  812. modifiedTimelines.add(timeline);
  813. }
  814. } else if (node.name === 'SegmentTimeline' || node.name === 'S') {
  815. const timelines = this.modifyTimepoints_(patchNode);
  816. for (const timeline of timelines) {
  817. modifiedTimelines.add(timeline);
  818. }
  819. } else {
  820. handled = false;
  821. }
  822. if (!handled) {
  823. shaka.log.warning('Unhandled ' + patchNode.tagName + ' operation',
  824. patchNode.attributes['sel']);
  825. }
  826. }
  827. for (const timeline of modifiedTimelines) {
  828. this.parsePatchSegment_(timeline);
  829. }
  830. // Add new periods after extending timelines, as new periods
  831. // remove context cache of previous periods.
  832. for (const periodAddition of periodAdditions) {
  833. newPeriods.push(...this.parsePatchPeriod_(periodAddition));
  834. }
  835. if (newPeriods.length) {
  836. this.manifest_.periodCount += newPeriods.length;
  837. this.manifest_.gapCount = this.gapCount_;
  838. await this.postPeriodProcessing_(newPeriods, /* isPatchUpdate= */ true);
  839. }
  840. if (this.manifestPatchContext_.type == 'static') {
  841. const duration = this.manifestPatchContext_.mediaPresentationDuration;
  842. this.manifest_.presentationTimeline.setDuration(duration || Infinity);
  843. }
  844. }
  845. /**
  846. * Handles manifest type changes, this transition is expected to be
  847. * "dyanmic" to "static".
  848. *
  849. * @param {!string} mpdType
  850. * @private
  851. */
  852. parsePatchMpdTypeChange_(mpdType) {
  853. this.manifest_.presentationTimeline.setStatic(mpdType == 'static');
  854. this.manifestPatchContext_.type = mpdType;
  855. for (const context of this.contextCache_.values()) {
  856. context.dynamic = mpdType == 'dynamic';
  857. }
  858. if (mpdType == 'static') {
  859. // Manifest is no longer dynamic, so stop live updates.
  860. this.updatePeriod_ = -1;
  861. }
  862. }
  863. /**
  864. * @param {string} durationString
  865. * @private
  866. */
  867. parsePatchMediaPresentationDurationChange_(durationString) {
  868. const duration = shaka.util.TXml.parseDuration(durationString);
  869. if (duration == null) {
  870. return;
  871. }
  872. this.manifestPatchContext_.mediaPresentationDuration = duration;
  873. for (const context of this.contextCache_.values()) {
  874. context.mediaPresentationDuration = duration;
  875. }
  876. }
  877. /**
  878. * Ingests a full MPD period element from a patch update
  879. *
  880. * @param {!shaka.extern.xml.Node} periods
  881. * @private
  882. */
  883. parsePatchPeriod_(periods) {
  884. goog.asserts.assert(this.manifestPatchContext_.getBaseUris,
  885. 'Must provide getBaseUris on manifestPatchContext_');
  886. /** @type {shaka.dash.DashParser.Context} */
  887. const context = {
  888. dynamic: this.manifestPatchContext_.type == 'dynamic',
  889. presentationTimeline: this.manifest_.presentationTimeline,
  890. period: null,
  891. periodInfo: null,
  892. adaptationSet: null,
  893. representation: null,
  894. bandwidth: 0,
  895. indexRangeWarningGiven: false,
  896. availabilityTimeOffset: this.manifestPatchContext_.availabilityTimeOffset,
  897. profiles: this.manifestPatchContext_.profiles,
  898. mediaPresentationDuration:
  899. this.manifestPatchContext_.mediaPresentationDuration,
  900. roles: null,
  901. };
  902. const periodsAndDuration = this.parsePeriods_(context,
  903. this.manifestPatchContext_.getBaseUris, periods);
  904. return periodsAndDuration.periods;
  905. }
  906. /**
  907. * @param {string} periodId
  908. * @private
  909. */
  910. removePatchPeriod_(periodId) {
  911. const SegmentTemplate = shaka.dash.SegmentTemplate;
  912. this.manifest_.periodCount--;
  913. for (const contextId of this.contextCache_.keys()) {
  914. if (contextId.startsWith(periodId)) {
  915. const context = this.contextCache_.get(contextId);
  916. SegmentTemplate.removeTimepoints(context);
  917. this.parsePatchSegment_(contextId);
  918. this.contextCache_.delete(contextId);
  919. }
  920. }
  921. }
  922. /**
  923. * @param {!Array<shaka.util.TXml.PathNode>} paths
  924. * @return {!Array<string>}
  925. * @private
  926. */
  927. getContextIdsFromPath_(paths) {
  928. let periodId = '';
  929. let adaptationSetId = '';
  930. let representationId = '';
  931. for (const node of paths) {
  932. if (node.name === 'Period') {
  933. periodId = node.id;
  934. } else if (node.name === 'AdaptationSet') {
  935. adaptationSetId = node.id;
  936. } else if (node.name === 'Representation') {
  937. representationId = node.id;
  938. }
  939. }
  940. /** @type {!Array<string>} */
  941. const contextIds = [];
  942. if (representationId) {
  943. contextIds.push(periodId + ',' + representationId);
  944. } else {
  945. for (const context of this.contextCache_.values()) {
  946. if (context.period.id === periodId &&
  947. context.adaptationSet.id === adaptationSetId &&
  948. context.representation.id) {
  949. contextIds.push(periodId + ',' + context.representation.id);
  950. }
  951. }
  952. }
  953. return contextIds;
  954. }
  955. /**
  956. * Modifies SegmentTemplate based on MPD patch.
  957. *
  958. * @param {!shaka.extern.xml.Node} patchNode
  959. * @return {!Array<string>} context ids with updated timeline
  960. * @private
  961. */
  962. modifySegmentTemplate_(patchNode) {
  963. const TXml = shaka.util.TXml;
  964. const paths = TXml.parseXpath(patchNode.attributes['sel'] || '');
  965. const lastPath = paths[paths.length - 1];
  966. if (!lastPath.attribute) {
  967. return [];
  968. }
  969. const contextIds = this.getContextIdsFromPath_(paths);
  970. const content = TXml.getContents(patchNode) || '';
  971. for (const contextId of contextIds) {
  972. /** @type {shaka.dash.DashParser.Context} */
  973. const context = this.contextCache_.get(contextId);
  974. goog.asserts.assert(context && context.representation.segmentTemplate,
  975. 'cannot modify segment template');
  976. TXml.modifyNodeAttribute(context.representation.segmentTemplate,
  977. patchNode.tagName, lastPath.attribute, content);
  978. }
  979. return contextIds;
  980. }
  981. /**
  982. * Ingests Patch MPD segments into timeline.
  983. *
  984. * @param {!shaka.extern.xml.Node} patchNode
  985. * @return {!Array<string>} context ids with updated timeline
  986. * @private
  987. */
  988. modifyTimepoints_(patchNode) {
  989. const TXml = shaka.util.TXml;
  990. const SegmentTemplate = shaka.dash.SegmentTemplate;
  991. const paths = TXml.parseXpath(patchNode.attributes['sel'] || '');
  992. const contextIds = this.getContextIdsFromPath_(paths);
  993. for (const contextId of contextIds) {
  994. /** @type {shaka.dash.DashParser.Context} */
  995. const context = this.contextCache_.get(contextId);
  996. SegmentTemplate.modifyTimepoints(context, patchNode);
  997. }
  998. return contextIds;
  999. }
  1000. /**
  1001. * Parses modified segments.
  1002. *
  1003. * @param {string} contextId
  1004. * @private
  1005. */
  1006. parsePatchSegment_(contextId) {
  1007. /** @type {shaka.dash.DashParser.Context} */
  1008. const context = this.contextCache_.get(contextId);
  1009. const currentStream = this.streamMap_[contextId];
  1010. goog.asserts.assert(currentStream, 'stream should exist');
  1011. if (currentStream.segmentIndex) {
  1012. currentStream.segmentIndex.evict(
  1013. this.manifest_.presentationTimeline.getSegmentAvailabilityStart());
  1014. }
  1015. try {
  1016. const requestSegment = (uris, startByte, endByte, isInit) => {
  1017. return this.requestSegment_(uris, startByte, endByte, isInit);
  1018. };
  1019. // TODO we should obtain lastSegmentNumber if possible
  1020. const streamInfo = shaka.dash.SegmentTemplate.createStreamInfo(
  1021. context, requestSegment, this.streamMap_, /* isUpdate= */ true,
  1022. this.config_.dash.initialSegmentLimit, this.periodDurations_,
  1023. context.representation.aesKey, /* lastSegmentNumber= */ null,
  1024. /* isPatchUpdate= */ true);
  1025. currentStream.createSegmentIndex = async () => {
  1026. if (!currentStream.segmentIndex) {
  1027. currentStream.segmentIndex =
  1028. await streamInfo.generateSegmentIndex();
  1029. }
  1030. };
  1031. } catch (error) {
  1032. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1033. const contentType = context.representation.contentType;
  1034. const isText = contentType == ContentType.TEXT ||
  1035. contentType == ContentType.APPLICATION;
  1036. const isImage = contentType == ContentType.IMAGE;
  1037. if (!(isText || isImage) ||
  1038. error.code != shaka.util.Error.Code.DASH_NO_SEGMENT_INFO) {
  1039. // We will ignore any DASH_NO_SEGMENT_INFO errors for text/image
  1040. throw error;
  1041. }
  1042. }
  1043. }
  1044. /**
  1045. * Reads maxLatency and maxPlaybackRate properties from service
  1046. * description element.
  1047. *
  1048. * @param {!shaka.extern.xml.Node} mpd
  1049. * @return {?shaka.extern.ServiceDescription}
  1050. * @private
  1051. */
  1052. parseServiceDescription_(mpd) {
  1053. const TXml = shaka.util.TXml;
  1054. const elem = TXml.findChild(mpd, 'ServiceDescription');
  1055. if (!elem ) {
  1056. return null;
  1057. }
  1058. const latencyNode = TXml.findChild(elem, 'Latency');
  1059. const playbackRateNode = TXml.findChild(elem, 'PlaybackRate');
  1060. if (!latencyNode && !playbackRateNode) {
  1061. return null;
  1062. }
  1063. const description = {};
  1064. if (latencyNode) {
  1065. if ('target' in latencyNode.attributes) {
  1066. description.targetLatency =
  1067. parseInt(latencyNode.attributes['target'], 10) / 1000;
  1068. }
  1069. if ('max' in latencyNode.attributes) {
  1070. description.maxLatency =
  1071. parseInt(latencyNode.attributes['max'], 10) / 1000;
  1072. }
  1073. if ('min' in latencyNode.attributes) {
  1074. description.minLatency =
  1075. parseInt(latencyNode.attributes['min'], 10) / 1000;
  1076. }
  1077. }
  1078. if (playbackRateNode) {
  1079. if ('max' in playbackRateNode.attributes) {
  1080. description.maxPlaybackRate =
  1081. parseFloat(playbackRateNode.attributes['max']);
  1082. }
  1083. if ('min' in playbackRateNode.attributes) {
  1084. description.minPlaybackRate =
  1085. parseFloat(playbackRateNode.attributes['min']);
  1086. }
  1087. }
  1088. return description;
  1089. }
  1090. /**
  1091. * Reads chaining url.
  1092. *
  1093. * @param {!shaka.extern.xml.Node} mpd
  1094. * @return {?string}
  1095. * @private
  1096. */
  1097. parseMpdChaining_(mpd) {
  1098. const TXml = shaka.util.TXml;
  1099. const supplementalProperties =
  1100. TXml.findChildren(mpd, 'SupplementalProperty');
  1101. if (!supplementalProperties.length) {
  1102. return null;
  1103. }
  1104. for (const prop of supplementalProperties) {
  1105. const schemeId = prop.attributes['schemeIdUri'];
  1106. if (schemeId == 'urn:mpeg:dash:chaining:2016') {
  1107. return prop.attributes['value'];
  1108. }
  1109. }
  1110. return null;
  1111. }
  1112. /**
  1113. * Reads and parses the periods from the manifest. This first does some
  1114. * partial parsing so the start and duration is available when parsing
  1115. * children.
  1116. *
  1117. * @param {shaka.dash.DashParser.Context} context
  1118. * @param {function():!Array.<string>} getBaseUris
  1119. * @param {!shaka.extern.xml.Node} mpd
  1120. * @return {{
  1121. * periods: !Array.<shaka.extern.Period>,
  1122. * duration: ?number,
  1123. * durationDerivedFromPeriods: boolean
  1124. * }}
  1125. * @private
  1126. */
  1127. parsePeriods_(context, getBaseUris, mpd) {
  1128. const TXml = shaka.util.TXml;
  1129. let presentationDuration = context.mediaPresentationDuration;
  1130. if (!presentationDuration) {
  1131. presentationDuration = TXml.parseAttr(
  1132. mpd, 'mediaPresentationDuration', TXml.parseDuration);
  1133. this.manifestPatchContext_.mediaPresentationDuration =
  1134. presentationDuration;
  1135. }
  1136. let seekRangeStart = 0;
  1137. if (this.manifest_ && this.manifest_.presentationTimeline &&
  1138. this.isTransitionFromDynamicToStatic_) {
  1139. seekRangeStart = this.manifest_.presentationTimeline.getSeekRangeStart();
  1140. }
  1141. const periods = [];
  1142. let prevEnd = seekRangeStart;
  1143. const periodNodes = TXml.findChildren(mpd, 'Period');
  1144. for (let i = 0; i < periodNodes.length; i++) {
  1145. const elem = periodNodes[i];
  1146. const next = periodNodes[i + 1];
  1147. const start = /** @type {number} */ (
  1148. TXml.parseAttr(elem, 'start', TXml.parseDuration, prevEnd));
  1149. const periodId = elem.attributes['id'];
  1150. const givenDuration =
  1151. TXml.parseAttr(elem, 'duration', TXml.parseDuration);
  1152. let periodDuration = null;
  1153. if (next) {
  1154. // "The difference between the start time of a Period and the start time
  1155. // of the following Period is the duration of the media content
  1156. // represented by this Period."
  1157. const nextStart =
  1158. TXml.parseAttr(next, 'start', TXml.parseDuration);
  1159. if (nextStart != null) {
  1160. periodDuration = nextStart - start;
  1161. }
  1162. } else if (presentationDuration != null) {
  1163. // "The Period extends until the Period.start of the next Period, or
  1164. // until the end of the Media Presentation in the case of the last
  1165. // Period."
  1166. periodDuration = presentationDuration - start + seekRangeStart;
  1167. }
  1168. const threshold =
  1169. shaka.util.ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS;
  1170. if (periodDuration && givenDuration &&
  1171. Math.abs(periodDuration - givenDuration) > threshold) {
  1172. shaka.log.warning('There is a gap/overlap between Periods', elem);
  1173. // This means it's a gap, the distance between period starts is
  1174. // larger than the period's duration
  1175. if (periodDuration > givenDuration) {
  1176. this.gapCount_++;
  1177. }
  1178. }
  1179. // Only use the @duration in the MPD if we can't calculate it. We should
  1180. // favor the @start of the following Period. This ensures that there
  1181. // aren't gaps between Periods.
  1182. if (periodDuration == null) {
  1183. periodDuration = givenDuration;
  1184. }
  1185. /**
  1186. * This is to improve robustness when the player observes manifest with
  1187. * past periods that are inconsistent to previous ones.
  1188. *
  1189. * This may happen when a CDN or proxy server switches its upstream from
  1190. * one encoder to another redundant encoder.
  1191. *
  1192. * Skip periods that match all of the following criteria:
  1193. * - Start time is earlier than latest period start time ever seen
  1194. * - Period ID is never seen in the previous manifest
  1195. * - Not the last period in the manifest
  1196. *
  1197. * Periods that meet the aforementioned criteria are considered invalid
  1198. * and should be safe to discard.
  1199. */
  1200. if (this.largestPeriodStartTime_ !== null &&
  1201. periodId !== null && start !== null &&
  1202. start < this.largestPeriodStartTime_ &&
  1203. !this.lastManifestUpdatePeriodIds_.includes(periodId) &&
  1204. i + 1 != periodNodes.length) {
  1205. shaka.log.debug(
  1206. `Skipping Period with ID ${periodId} as its start time is smaller` +
  1207. ' than the largest period start time that has been seen, and ID ' +
  1208. 'is unseen before');
  1209. continue;
  1210. }
  1211. // Save maximum period start time if it is the last period
  1212. if (start !== null &&
  1213. (this.largestPeriodStartTime_ === null ||
  1214. start > this.largestPeriodStartTime_)) {
  1215. this.largestPeriodStartTime_ = start;
  1216. }
  1217. // Parse child nodes.
  1218. const info = {
  1219. start: start,
  1220. duration: periodDuration,
  1221. node: elem,
  1222. isLastPeriod: periodDuration == null || !next,
  1223. };
  1224. const period = this.parsePeriod_(context, getBaseUris, info);
  1225. periods.push(period);
  1226. if (context.period.id && periodDuration) {
  1227. this.periodDurations_[context.period.id] = periodDuration;
  1228. }
  1229. if (periodDuration == null) {
  1230. if (next) {
  1231. // If the duration is still null and we aren't at the end, then we
  1232. // will skip any remaining periods.
  1233. shaka.log.warning(
  1234. 'Skipping Period', i + 1, 'and any subsequent Periods:', 'Period',
  1235. i + 1, 'does not have a valid start time.', next);
  1236. }
  1237. // The duration is unknown, so the end is unknown.
  1238. prevEnd = null;
  1239. break;
  1240. }
  1241. prevEnd = start + periodDuration;
  1242. } // end of period parsing loop
  1243. // Replace previous seen periods with the current one.
  1244. this.lastManifestUpdatePeriodIds_ = periods.map((el) => el.id);
  1245. if (presentationDuration != null) {
  1246. if (prevEnd != null) {
  1247. const threshold =
  1248. shaka.util.ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS;
  1249. const diference = prevEnd - seekRangeStart - presentationDuration;
  1250. if (Math.abs(diference) > threshold) {
  1251. shaka.log.warning(
  1252. '@mediaPresentationDuration does not match the total duration ',
  1253. 'of all Periods.');
  1254. // Assume @mediaPresentationDuration is correct.
  1255. }
  1256. }
  1257. return {
  1258. periods: periods,
  1259. duration: presentationDuration + seekRangeStart,
  1260. durationDerivedFromPeriods: false,
  1261. };
  1262. } else {
  1263. return {
  1264. periods: periods,
  1265. duration: prevEnd,
  1266. durationDerivedFromPeriods: true,
  1267. };
  1268. }
  1269. }
  1270. /**
  1271. * Parses a Period XML element. Unlike the other parse methods, this is not
  1272. * given the Node; it is given a PeriodInfo structure. Also, partial parsing
  1273. * was done before this was called so start and duration are valid.
  1274. *
  1275. * @param {shaka.dash.DashParser.Context} context
  1276. * @param {function():!Array.<string>} getBaseUris
  1277. * @param {shaka.dash.DashParser.PeriodInfo} periodInfo
  1278. * @return {shaka.extern.Period}
  1279. * @private
  1280. */
  1281. parsePeriod_(context, getBaseUris, periodInfo) {
  1282. const Functional = shaka.util.Functional;
  1283. const TXml = shaka.util.TXml;
  1284. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1285. context.period = this.createFrame_(periodInfo.node, null, getBaseUris);
  1286. context.periodInfo = periodInfo;
  1287. context.period.availabilityTimeOffset = context.availabilityTimeOffset;
  1288. // If the period doesn't have an ID, give it one based on its start time.
  1289. if (!context.period.id) {
  1290. shaka.log.info(
  1291. 'No Period ID given for Period with start time ' + periodInfo.start +
  1292. ', Assigning a default');
  1293. context.period.id = '__shaka_period_' + periodInfo.start;
  1294. }
  1295. const eventStreamNodes =
  1296. TXml.findChildren(periodInfo.node, 'EventStream');
  1297. const availabilityStart =
  1298. context.presentationTimeline.getSegmentAvailabilityStart();
  1299. for (const node of eventStreamNodes) {
  1300. this.parseEventStream_(
  1301. periodInfo.start, periodInfo.duration, node, availabilityStart);
  1302. }
  1303. const adaptationSetNodes =
  1304. TXml.findChildren(periodInfo.node, 'AdaptationSet');
  1305. const adaptationSets = adaptationSetNodes
  1306. .map((node) => this.parseAdaptationSet_(context, node))
  1307. .filter(Functional.isNotNull);
  1308. // For dynamic manifests, we use rep IDs internally, and they must be
  1309. // unique.
  1310. if (context.dynamic) {
  1311. const ids = [];
  1312. for (const set of adaptationSets) {
  1313. for (const id of set.representationIds) {
  1314. ids.push(id);
  1315. }
  1316. }
  1317. const uniqueIds = new Set(ids);
  1318. if (ids.length != uniqueIds.size) {
  1319. throw new shaka.util.Error(
  1320. shaka.util.Error.Severity.CRITICAL,
  1321. shaka.util.Error.Category.MANIFEST,
  1322. shaka.util.Error.Code.DASH_DUPLICATE_REPRESENTATION_ID);
  1323. }
  1324. }
  1325. const normalAdaptationSets = adaptationSets
  1326. .filter((as) => { return !as.trickModeFor; });
  1327. const trickModeAdaptationSets = adaptationSets
  1328. .filter((as) => { return as.trickModeFor; });
  1329. // Attach trick mode tracks to normal tracks.
  1330. for (const trickModeSet of trickModeAdaptationSets) {
  1331. const targetIds = trickModeSet.trickModeFor.split(' ');
  1332. for (const normalSet of normalAdaptationSets) {
  1333. if (targetIds.includes(normalSet.id)) {
  1334. for (const stream of normalSet.streams) {
  1335. // There may be multiple trick mode streams, but we do not
  1336. // currently support that. Just choose one.
  1337. // TODO: https://github.com/shaka-project/shaka-player/issues/1528
  1338. stream.trickModeVideo = trickModeSet.streams.find((trickStream) =>
  1339. shaka.util.MimeUtils.getNormalizedCodec(stream.codecs) ==
  1340. shaka.util.MimeUtils.getNormalizedCodec(trickStream.codecs));
  1341. }
  1342. }
  1343. }
  1344. }
  1345. const audioStreams = this.getStreamsFromSets_(
  1346. this.config_.disableAudio,
  1347. normalAdaptationSets,
  1348. ContentType.AUDIO);
  1349. const videoStreams = this.getStreamsFromSets_(
  1350. this.config_.disableVideo,
  1351. normalAdaptationSets,
  1352. ContentType.VIDEO);
  1353. const textStreams = this.getStreamsFromSets_(
  1354. this.config_.disableText,
  1355. normalAdaptationSets,
  1356. ContentType.TEXT);
  1357. const imageStreams = this.getStreamsFromSets_(
  1358. this.config_.disableThumbnails,
  1359. normalAdaptationSets,
  1360. ContentType.IMAGE);
  1361. if (videoStreams.length === 0 && audioStreams.length === 0) {
  1362. throw new shaka.util.Error(
  1363. shaka.util.Error.Severity.CRITICAL,
  1364. shaka.util.Error.Category.MANIFEST,
  1365. shaka.util.Error.Code.DASH_EMPTY_PERIOD,
  1366. );
  1367. }
  1368. return {
  1369. id: context.period.id,
  1370. audioStreams,
  1371. videoStreams,
  1372. textStreams,
  1373. imageStreams,
  1374. };
  1375. }
  1376. /**
  1377. * Gets the streams from the given sets or returns an empty array if disabled
  1378. * or no streams are found.
  1379. * @param {boolean} disabled
  1380. * @param {!Array.<!shaka.dash.DashParser.AdaptationInfo>} adaptationSets
  1381. * @param {string} contentType
  1382. @private
  1383. */
  1384. getStreamsFromSets_(disabled, adaptationSets, contentType) {
  1385. if (disabled || !adaptationSets.length) {
  1386. return [];
  1387. }
  1388. return adaptationSets.reduce((all, part) => {
  1389. if (part.contentType != contentType) {
  1390. return all;
  1391. }
  1392. all.push(...part.streams);
  1393. return all;
  1394. }, []);
  1395. }
  1396. /**
  1397. * Parses an AdaptationSet XML element.
  1398. *
  1399. * @param {shaka.dash.DashParser.Context} context
  1400. * @param {!shaka.extern.xml.Node} elem The AdaptationSet element.
  1401. * @return {?shaka.dash.DashParser.AdaptationInfo}
  1402. * @private
  1403. */
  1404. parseAdaptationSet_(context, elem) {
  1405. const TXml = shaka.util.TXml;
  1406. const Functional = shaka.util.Functional;
  1407. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  1408. const ContentType = ManifestParserUtils.ContentType;
  1409. const ContentProtection = shaka.dash.ContentProtection;
  1410. context.adaptationSet = this.createFrame_(elem, context.period, null);
  1411. let main = false;
  1412. const roleElements = TXml.findChildren(elem, 'Role');
  1413. const roleValues = roleElements.map((role) => {
  1414. return role.attributes['value'];
  1415. }).filter(Functional.isNotNull);
  1416. // Default kind for text streams is 'subtitle' if unspecified in the
  1417. // manifest.
  1418. let kind = undefined;
  1419. const isText = context.adaptationSet.contentType == ContentType.TEXT;
  1420. if (isText) {
  1421. kind = ManifestParserUtils.TextStreamKind.SUBTITLE;
  1422. }
  1423. for (const roleElement of roleElements) {
  1424. const scheme = roleElement.attributes['schemeIdUri'];
  1425. if (scheme == null || scheme == 'urn:mpeg:dash:role:2011') {
  1426. // These only apply for the given scheme, but allow them to be specified
  1427. // if there is no scheme specified.
  1428. // See: DASH section 5.8.5.5
  1429. const value = roleElement.attributes['value'];
  1430. switch (value) {
  1431. case 'main':
  1432. main = true;
  1433. break;
  1434. case 'caption':
  1435. case 'subtitle':
  1436. kind = value;
  1437. break;
  1438. }
  1439. }
  1440. }
  1441. // Parallel for HLS VIDEO-RANGE as defined in DASH-IF IOP v4.3 6.2.5.1.
  1442. let videoRange;
  1443. let colorGamut;
  1444. // Ref. https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf
  1445. // If signaled, a Supplemental or Essential Property descriptor
  1446. // shall be used, with the schemeIdUri set to
  1447. // urn:mpeg:mpegB:cicp:<Parameter> as defined in
  1448. // ISO/IEC 23001-8 [49] and <Parameter> one of the
  1449. // following: ColourPrimaries, TransferCharacteristics,
  1450. // or MatrixCoefficients.
  1451. const scheme = 'urn:mpeg:mpegB:cicp';
  1452. const transferCharacteristicsScheme = `${scheme}:TransferCharacteristics`;
  1453. const colourPrimariesScheme = `${scheme}:ColourPrimaries`;
  1454. const matrixCoefficientsScheme = `${scheme}:MatrixCoefficients`;
  1455. const getVideoRangeFromTransferCharacteristicCICP = (cicp) => {
  1456. switch (cicp) {
  1457. case 1:
  1458. case 6:
  1459. case 13:
  1460. case 14:
  1461. case 15:
  1462. return 'SDR';
  1463. case 16:
  1464. return 'PQ';
  1465. case 18:
  1466. return 'HLG';
  1467. }
  1468. return undefined;
  1469. };
  1470. const getColorGamutFromColourPrimariesCICP = (cicp) => {
  1471. switch (cicp) {
  1472. case 1:
  1473. case 5:
  1474. case 6:
  1475. case 7:
  1476. return 'srgb';
  1477. case 9:
  1478. return 'rec2020';
  1479. case 11:
  1480. case 12:
  1481. return 'p3';
  1482. }
  1483. return undefined;
  1484. };
  1485. const essentialProperties =
  1486. TXml.findChildren(elem, 'EssentialProperty');
  1487. // ID of real AdaptationSet if this is a trick mode set:
  1488. let trickModeFor = null;
  1489. let isFastSwitching = false;
  1490. let unrecognizedEssentialProperty = false;
  1491. for (const prop of essentialProperties) {
  1492. const schemeId = prop.attributes['schemeIdUri'];
  1493. if (schemeId == 'http://dashif.org/guidelines/trickmode') {
  1494. trickModeFor = prop.attributes['value'];
  1495. } else if (schemeId == transferCharacteristicsScheme) {
  1496. videoRange = getVideoRangeFromTransferCharacteristicCICP(
  1497. parseInt(prop.attributes['value'], 10),
  1498. );
  1499. } else if (schemeId == colourPrimariesScheme) {
  1500. colorGamut = getColorGamutFromColourPrimariesCICP(
  1501. parseInt(prop.attributes['value'], 10),
  1502. );
  1503. } else if (schemeId == matrixCoefficientsScheme) {
  1504. continue;
  1505. } else if (schemeId == 'urn:mpeg:dash:ssr:2023' &&
  1506. this.config_.dash.enableFastSwitching) {
  1507. isFastSwitching = true;
  1508. } else {
  1509. unrecognizedEssentialProperty = true;
  1510. }
  1511. }
  1512. let lastSegmentNumber = null;
  1513. const supplementalProperties =
  1514. TXml.findChildren(elem, 'SupplementalProperty');
  1515. for (const prop of supplementalProperties) {
  1516. const schemeId = prop.attributes['schemeIdUri'];
  1517. if (schemeId == 'http://dashif.org/guidelines/last-segment-number') {
  1518. lastSegmentNumber = parseInt(prop.attributes['value'], 10) - 1;
  1519. } else if (schemeId == transferCharacteristicsScheme) {
  1520. videoRange = getVideoRangeFromTransferCharacteristicCICP(
  1521. parseInt(prop.attributes['value'], 10),
  1522. );
  1523. } else if (schemeId == colourPrimariesScheme) {
  1524. colorGamut = getColorGamutFromColourPrimariesCICP(
  1525. parseInt(prop.attributes['value'], 10),
  1526. );
  1527. }
  1528. }
  1529. const accessibilities = TXml.findChildren(elem, 'Accessibility');
  1530. const LanguageUtils = shaka.util.LanguageUtils;
  1531. const closedCaptions = new Map();
  1532. /** @type {?shaka.media.ManifestParser.AccessibilityPurpose} */
  1533. let accessibilityPurpose;
  1534. for (const prop of accessibilities) {
  1535. const schemeId = prop.attributes['schemeIdUri'];
  1536. const value = prop.attributes['value'];
  1537. if (schemeId == 'urn:scte:dash:cc:cea-608:2015' ) {
  1538. let channelId = 1;
  1539. if (value != null) {
  1540. const channelAssignments = value.split(';');
  1541. for (const captionStr of channelAssignments) {
  1542. let channel;
  1543. let language;
  1544. // Some closed caption descriptions have channel number and
  1545. // language ("CC1=eng") others may only have language ("eng,spa").
  1546. if (!captionStr.includes('=')) {
  1547. // When the channel assignemnts are not explicitly provided and
  1548. // there are only 2 values provided, it is highly likely that the
  1549. // assignments are CC1 and CC3 (most commonly used CC streams).
  1550. // Otherwise, cycle through all channels arbitrarily (CC1 - CC4)
  1551. // in order of provided langs.
  1552. channel = `CC${channelId}`;
  1553. if (channelAssignments.length == 2) {
  1554. channelId += 2;
  1555. } else {
  1556. channelId ++;
  1557. }
  1558. language = captionStr;
  1559. } else {
  1560. const channelAndLanguage = captionStr.split('=');
  1561. // The channel info can be '1' or 'CC1'.
  1562. // If the channel info only has channel number(like '1'), add 'CC'
  1563. // as prefix so that it can be a full channel id (like 'CC1').
  1564. channel = channelAndLanguage[0].startsWith('CC') ?
  1565. channelAndLanguage[0] : `CC${channelAndLanguage[0]}`;
  1566. // 3 letters (ISO 639-2). In b/187442669, we saw a blank string
  1567. // (CC2=;CC3=), so default to "und" (the code for "undetermined").
  1568. language = channelAndLanguage[1] || 'und';
  1569. }
  1570. closedCaptions.set(channel, LanguageUtils.normalize(language));
  1571. }
  1572. } else {
  1573. // If channel and language information has not been provided, assign
  1574. // 'CC1' as channel id and 'und' as language info.
  1575. closedCaptions.set('CC1', 'und');
  1576. }
  1577. } else if (schemeId == 'urn:scte:dash:cc:cea-708:2015') {
  1578. let serviceNumber = 1;
  1579. if (value != null) {
  1580. for (const captionStr of value.split(';')) {
  1581. let service;
  1582. let language;
  1583. // Similar to CEA-608, it is possible that service # assignments
  1584. // are not explicitly provided e.g. "eng;deu;swe" In this case,
  1585. // we just cycle through the services for each language one by one.
  1586. if (!captionStr.includes('=')) {
  1587. service = `svc${serviceNumber}`;
  1588. serviceNumber ++;
  1589. language = captionStr;
  1590. } else {
  1591. // Otherwise, CEA-708 caption values take the form "
  1592. // 1=lang:eng;2=lang:deu" i.e. serviceNumber=lang:threelettercode.
  1593. const serviceAndLanguage = captionStr.split('=');
  1594. service = `svc${serviceAndLanguage[0]}`;
  1595. // The language info can be different formats, lang:eng',
  1596. // or 'lang:eng,war:1,er:1'. Extract the language info.
  1597. language = serviceAndLanguage[1].split(',')[0].split(':').pop();
  1598. }
  1599. closedCaptions.set(service, LanguageUtils.normalize(language));
  1600. }
  1601. } else {
  1602. // If service and language information has not been provided, assign
  1603. // 'svc1' as service number and 'und' as language info.
  1604. closedCaptions.set('svc1', 'und');
  1605. }
  1606. } else if (schemeId == 'urn:mpeg:dash:role:2011') {
  1607. // See DASH IOP 3.9.2 Table 4.
  1608. if (value != null) {
  1609. roleValues.push(value);
  1610. if (value == 'captions') {
  1611. kind = ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
  1612. }
  1613. }
  1614. } else if (schemeId == 'urn:tva:metadata:cs:AudioPurposeCS:2007') {
  1615. // See DASH DVB Document A168 Rev.6 Table 5.
  1616. if (value == '1') {
  1617. accessibilityPurpose =
  1618. shaka.media.ManifestParser.AccessibilityPurpose.VISUALLY_IMPAIRED;
  1619. } else if (value == '2') {
  1620. accessibilityPurpose =
  1621. shaka.media.ManifestParser.AccessibilityPurpose.HARD_OF_HEARING;
  1622. }
  1623. }
  1624. }
  1625. // According to DASH spec (2014) section 5.8.4.8, "the successful processing
  1626. // of the descriptor is essential to properly use the information in the
  1627. // parent element". According to DASH IOP v3.3, section 3.3.4, "if the
  1628. // scheme or the value" for EssentialProperty is not recognized, "the DASH
  1629. // client shall ignore the parent element."
  1630. if (unrecognizedEssentialProperty) {
  1631. // Stop parsing this AdaptationSet and let the caller filter out the
  1632. // nulls.
  1633. return null;
  1634. }
  1635. const contentProtectionElems =
  1636. TXml.findChildren(elem, 'ContentProtection');
  1637. const contentProtection = ContentProtection.parseFromAdaptationSet(
  1638. contentProtectionElems,
  1639. this.config_.dash.ignoreDrmInfo,
  1640. this.config_.dash.keySystemsByURI);
  1641. const language = shaka.util.LanguageUtils.normalize(
  1642. context.adaptationSet.language || 'und');
  1643. const label = context.adaptationSet.label;
  1644. // Parse Representations into Streams.
  1645. const representations = TXml.findChildren(elem, 'Representation');
  1646. const streams = representations.map((representation) => {
  1647. const parsedRepresentation = this.parseRepresentation_(context,
  1648. contentProtection, kind, language, label, main, roleValues,
  1649. closedCaptions, representation, accessibilityPurpose,
  1650. lastSegmentNumber);
  1651. if (parsedRepresentation) {
  1652. parsedRepresentation.hdr = parsedRepresentation.hdr || videoRange;
  1653. parsedRepresentation.colorGamut =
  1654. parsedRepresentation.colorGamut || colorGamut;
  1655. parsedRepresentation.fastSwitching = isFastSwitching;
  1656. }
  1657. return parsedRepresentation;
  1658. }).filter((s) => !!s);
  1659. if (streams.length == 0) {
  1660. const isImage = context.adaptationSet.contentType == ContentType.IMAGE;
  1661. // Ignore empty AdaptationSets if ignoreEmptyAdaptationSet is true
  1662. // or they are for text/image content.
  1663. if (this.config_.dash.ignoreEmptyAdaptationSet || isText || isImage) {
  1664. return null;
  1665. }
  1666. throw new shaka.util.Error(
  1667. shaka.util.Error.Severity.CRITICAL,
  1668. shaka.util.Error.Category.MANIFEST,
  1669. shaka.util.Error.Code.DASH_EMPTY_ADAPTATION_SET);
  1670. }
  1671. // If AdaptationSet's type is unknown or is ambiguously "application",
  1672. // guess based on the information in the first stream. If the attributes
  1673. // mimeType and codecs are split across levels, they will both be inherited
  1674. // down to the stream level by this point, so the stream will have all the
  1675. // necessary information.
  1676. if (!context.adaptationSet.contentType ||
  1677. context.adaptationSet.contentType == ContentType.APPLICATION) {
  1678. const mimeType = streams[0].mimeType;
  1679. const codecs = streams[0].codecs;
  1680. context.adaptationSet.contentType =
  1681. shaka.dash.DashParser.guessContentType_(mimeType, codecs);
  1682. for (const stream of streams) {
  1683. stream.type = context.adaptationSet.contentType;
  1684. }
  1685. }
  1686. const adaptationId = context.adaptationSet.id ||
  1687. ('__fake__' + this.globalId_++);
  1688. for (const stream of streams) {
  1689. // Some DRM license providers require that we have a default
  1690. // key ID from the manifest in the wrapped license request.
  1691. // Thus, it should be put in drmInfo to be accessible to request filters.
  1692. for (const drmInfo of contentProtection.drmInfos) {
  1693. drmInfo.keyIds = drmInfo.keyIds && stream.keyIds ?
  1694. new Set([...drmInfo.keyIds, ...stream.keyIds]) :
  1695. drmInfo.keyIds || stream.keyIds;
  1696. }
  1697. if (this.config_.dash.enableAudioGroups) {
  1698. stream.groupId = adaptationId;
  1699. }
  1700. }
  1701. const repIds = representations
  1702. .map((node) => { return node.attributes['id']; })
  1703. .filter(shaka.util.Functional.isNotNull);
  1704. return {
  1705. id: adaptationId,
  1706. contentType: context.adaptationSet.contentType,
  1707. language: language,
  1708. main: main,
  1709. streams: streams,
  1710. drmInfos: contentProtection.drmInfos,
  1711. trickModeFor: trickModeFor,
  1712. representationIds: repIds,
  1713. };
  1714. }
  1715. /**
  1716. * Parses a Representation XML element.
  1717. *
  1718. * @param {shaka.dash.DashParser.Context} context
  1719. * @param {shaka.dash.ContentProtection.Context} contentProtection
  1720. * @param {(string|undefined)} kind
  1721. * @param {string} language
  1722. * @param {?string} label
  1723. * @param {boolean} isPrimary
  1724. * @param {!Array.<string>} roles
  1725. * @param {Map.<string, string>} closedCaptions
  1726. * @param {!shaka.extern.xml.Node} node
  1727. * @param {?shaka.media.ManifestParser.AccessibilityPurpose}
  1728. * accessibilityPurpose
  1729. * @param {?number} lastSegmentNumber
  1730. *
  1731. * @return {?shaka.extern.Stream} The Stream, or null when there is a
  1732. * non-critical parsing error.
  1733. * @private
  1734. */
  1735. parseRepresentation_(context, contentProtection, kind, language, label,
  1736. isPrimary, roles, closedCaptions, node, accessibilityPurpose,
  1737. lastSegmentNumber) {
  1738. const TXml = shaka.util.TXml;
  1739. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1740. context.representation =
  1741. this.createFrame_(node, context.adaptationSet, null);
  1742. const representationId = context.representation.id;
  1743. this.minTotalAvailabilityTimeOffset_ =
  1744. Math.min(this.minTotalAvailabilityTimeOffset_,
  1745. context.representation.availabilityTimeOffset);
  1746. this.isLowLatency_ = this.minTotalAvailabilityTimeOffset_ > 0;
  1747. if (!this.verifyRepresentation_(context.representation)) {
  1748. shaka.log.warning('Skipping Representation', context.representation);
  1749. return null;
  1750. }
  1751. const periodStart = context.periodInfo.start;
  1752. // NOTE: bandwidth is a mandatory attribute according to the spec, and zero
  1753. // does not make sense in the DASH spec's bandwidth formulas.
  1754. // In some content, however, the attribute is missing or zero.
  1755. // To avoid NaN at the variant level on broken content, fall back to zero.
  1756. // https://github.com/shaka-project/shaka-player/issues/938#issuecomment-317278180
  1757. context.bandwidth =
  1758. TXml.parseAttr(node, 'bandwidth', TXml.parsePositiveInt) || 0;
  1759. context.roles = roles;
  1760. /** @type {?shaka.dash.DashParser.StreamInfo} */
  1761. let streamInfo;
  1762. const contentType = context.representation.contentType;
  1763. const isText = contentType == ContentType.TEXT ||
  1764. contentType == ContentType.APPLICATION;
  1765. const isImage = contentType == ContentType.IMAGE;
  1766. try {
  1767. /** @type {shaka.extern.aesKey|undefined} */
  1768. let aesKey = undefined;
  1769. if (contentProtection.aes128Info) {
  1770. const getBaseUris = context.representation.getBaseUris;
  1771. const uris = shaka.util.ManifestParserUtils.resolveUris(
  1772. getBaseUris(), [contentProtection.aes128Info.keyUri]);
  1773. const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
  1774. const request = shaka.net.NetworkingEngine.makeRequest(
  1775. uris, this.config_.retryParameters);
  1776. aesKey = {
  1777. bitsKey: 128,
  1778. blockCipherMode: 'CBC',
  1779. iv: contentProtection.aes128Info.iv,
  1780. firstMediaSequenceNumber: 0,
  1781. };
  1782. // Don't download the key object until the segment is parsed, to
  1783. // avoid a startup delay for long manifests with lots of keys.
  1784. aesKey.fetchKey = async () => {
  1785. const keyResponse =
  1786. await this.makeNetworkRequest_(request, requestType);
  1787. // keyResponse.status is undefined when URI is
  1788. // "data:text/plain;base64,"
  1789. if (!keyResponse.data || keyResponse.data.byteLength != 16) {
  1790. throw new shaka.util.Error(
  1791. shaka.util.Error.Severity.CRITICAL,
  1792. shaka.util.Error.Category.MANIFEST,
  1793. shaka.util.Error.Code.AES_128_INVALID_KEY_LENGTH);
  1794. }
  1795. const algorithm = {
  1796. name: 'AES-CBC',
  1797. };
  1798. aesKey.cryptoKey = await window.crypto.subtle.importKey(
  1799. 'raw', keyResponse.data, algorithm, true, ['decrypt']);
  1800. aesKey.fetchKey = undefined; // No longer needed.
  1801. };
  1802. }
  1803. context.representation.aesKey = aesKey;
  1804. const requestSegment = (uris, startByte, endByte, isInit) => {
  1805. return this.requestSegment_(uris, startByte, endByte, isInit);
  1806. };
  1807. if (context.representation.segmentBase) {
  1808. streamInfo = shaka.dash.SegmentBase.createStreamInfo(
  1809. context, requestSegment, aesKey);
  1810. } else if (context.representation.segmentList) {
  1811. streamInfo = shaka.dash.SegmentList.createStreamInfo(
  1812. context, this.streamMap_, aesKey);
  1813. } else if (context.representation.segmentTemplate) {
  1814. const hasManifest = !!this.manifest_;
  1815. streamInfo = shaka.dash.SegmentTemplate.createStreamInfo(
  1816. context, requestSegment, this.streamMap_, hasManifest,
  1817. this.config_.dash.initialSegmentLimit, this.periodDurations_,
  1818. aesKey, lastSegmentNumber, /* isPatchUpdate= */ false);
  1819. } else {
  1820. goog.asserts.assert(isText,
  1821. 'Must have Segment* with non-text streams.');
  1822. const duration = context.periodInfo.duration || 0;
  1823. const getBaseUris = context.representation.getBaseUris;
  1824. streamInfo = {
  1825. generateSegmentIndex: () => {
  1826. const segmentIndex = shaka.media.SegmentIndex.forSingleSegment(
  1827. periodStart, duration, getBaseUris());
  1828. segmentIndex.forEachTopLevelReference((ref) => {
  1829. ref.mimeType = context.representation.mimeType;
  1830. ref.codecs = context.representation.codecs;
  1831. });
  1832. return Promise.resolve(segmentIndex);
  1833. },
  1834. };
  1835. }
  1836. } catch (error) {
  1837. if ((isText || isImage) &&
  1838. error.code == shaka.util.Error.Code.DASH_NO_SEGMENT_INFO) {
  1839. // We will ignore any DASH_NO_SEGMENT_INFO errors for text/image
  1840. // streams.
  1841. return null;
  1842. }
  1843. // For anything else, re-throw.
  1844. throw error;
  1845. }
  1846. const contentProtectionElems =
  1847. TXml.findChildren(node, 'ContentProtection');
  1848. const keyId = shaka.dash.ContentProtection.parseFromRepresentation(
  1849. contentProtectionElems, contentProtection,
  1850. this.config_.dash.ignoreDrmInfo,
  1851. this.config_.dash.keySystemsByURI);
  1852. const keyIds = new Set(keyId ? [keyId] : []);
  1853. // Detect the presence of E-AC3 JOC audio content, using DD+JOC signaling.
  1854. // See: ETSI TS 103 420 V1.2.1 (2018-10)
  1855. const supplementalPropertyElems =
  1856. TXml.findChildren(node, 'SupplementalProperty');
  1857. const hasJoc = supplementalPropertyElems.some((element) => {
  1858. const expectedUri = 'tag:dolby.com,2018:dash:EC3_ExtensionType:2018';
  1859. const expectedValue = 'JOC';
  1860. return element.attributes['schemeIdUri'] == expectedUri &&
  1861. element.attributes['value'] == expectedValue;
  1862. });
  1863. let spatialAudio = false;
  1864. if (hasJoc) {
  1865. spatialAudio = true;
  1866. }
  1867. let forced = false;
  1868. if (isText) {
  1869. // See: https://github.com/shaka-project/shaka-player/issues/2122 and
  1870. // https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/165
  1871. forced = roles.includes('forced_subtitle') ||
  1872. roles.includes('forced-subtitle');
  1873. }
  1874. let tilesLayout;
  1875. if (isImage) {
  1876. const essentialPropertyElems =
  1877. TXml.findChildren(node, 'EssentialProperty');
  1878. const thumbnailTileElem = essentialPropertyElems.find((element) => {
  1879. const expectedUris = [
  1880. 'http://dashif.org/thumbnail_tile',
  1881. 'http://dashif.org/guidelines/thumbnail_tile',
  1882. ];
  1883. return expectedUris.includes(element.attributes['schemeIdUri']);
  1884. });
  1885. if (thumbnailTileElem) {
  1886. tilesLayout = thumbnailTileElem.attributes['value'];
  1887. }
  1888. // Filter image adaptation sets that has no tilesLayout.
  1889. if (!tilesLayout) {
  1890. return null;
  1891. }
  1892. }
  1893. let hdr;
  1894. const profiles = context.profiles;
  1895. const codecs = context.representation.codecs;
  1896. const hevcHDR = 'http://dashif.org/guidelines/dash-if-uhd#hevc-hdr-pq10';
  1897. if (profiles.includes(hevcHDR) && (codecs.includes('hvc1.2.4.L153.B0') ||
  1898. codecs.includes('hev1.2.4.L153.B0'))) {
  1899. hdr = 'PQ';
  1900. }
  1901. const contextId = context.representation.id ?
  1902. context.period.id + ',' + context.representation.id : '';
  1903. if (this.patchLocationNodes_.length && representationId) {
  1904. this.contextCache_.set(`${context.period.id},${representationId}`,
  1905. this.cloneContext_(context));
  1906. }
  1907. /** @type {shaka.extern.Stream} */
  1908. let stream;
  1909. if (contextId && this.streamMap_[contextId]) {
  1910. stream = this.streamMap_[contextId];
  1911. } else {
  1912. stream = {
  1913. id: this.globalId_++,
  1914. originalId: context.representation.id,
  1915. groupId: null,
  1916. createSegmentIndex: () => Promise.resolve(),
  1917. closeSegmentIndex: () => {
  1918. if (stream.segmentIndex) {
  1919. stream.segmentIndex.release();
  1920. stream.segmentIndex = null;
  1921. }
  1922. },
  1923. segmentIndex: null,
  1924. mimeType: context.representation.mimeType,
  1925. codecs,
  1926. frameRate: context.representation.frameRate,
  1927. pixelAspectRatio: context.representation.pixelAspectRatio,
  1928. bandwidth: context.bandwidth,
  1929. width: context.representation.width,
  1930. height: context.representation.height,
  1931. kind,
  1932. encrypted: contentProtection.drmInfos.length > 0,
  1933. drmInfos: contentProtection.drmInfos,
  1934. keyIds,
  1935. language,
  1936. originalLanguage: context.adaptationSet.language,
  1937. label,
  1938. type: context.adaptationSet.contentType,
  1939. primary: isPrimary,
  1940. trickModeVideo: null,
  1941. emsgSchemeIdUris:
  1942. context.representation.emsgSchemeIdUris,
  1943. roles,
  1944. forced,
  1945. channelsCount: context.representation.numChannels,
  1946. audioSamplingRate: context.representation.audioSamplingRate,
  1947. spatialAudio,
  1948. closedCaptions,
  1949. hdr,
  1950. colorGamut: undefined,
  1951. videoLayout: undefined,
  1952. tilesLayout,
  1953. accessibilityPurpose,
  1954. external: false,
  1955. fastSwitching: false,
  1956. fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
  1957. context.representation.mimeType, context.representation.codecs)]),
  1958. };
  1959. }
  1960. stream.createSegmentIndex = async () => {
  1961. if (!stream.segmentIndex) {
  1962. stream.segmentIndex = await streamInfo.generateSegmentIndex();
  1963. }
  1964. };
  1965. if (contextId && context.dynamic && !this.streamMap_[contextId]) {
  1966. this.streamMap_[contextId] = stream;
  1967. }
  1968. return stream;
  1969. }
  1970. /**
  1971. * Clone context and remove xml document references.
  1972. *
  1973. * @param {!shaka.dash.DashParser.Context} context
  1974. * @return {!shaka.dash.DashParser.Context}
  1975. * @private
  1976. */
  1977. cloneContext_(context) {
  1978. const contextClone = /** @type {!shaka.dash.DashParser.Context} */({});
  1979. for (const k of Object.keys(context)) {
  1980. if (['period', 'adaptationSet', 'representation'].includes(k)) {
  1981. /** @type {shaka.dash.DashParser.InheritanceFrame} */
  1982. const frameRef = context[k];
  1983. contextClone[k] = {
  1984. segmentBase: null,
  1985. segmentList: null,
  1986. segmentTemplate: frameRef.segmentTemplate,
  1987. getBaseUris: frameRef.getBaseUris,
  1988. width: frameRef.width,
  1989. height: frameRef.height,
  1990. contentType: frameRef.contentType,
  1991. mimeType: frameRef.mimeType,
  1992. language: frameRef.language,
  1993. codecs: frameRef.codecs,
  1994. frameRate: frameRef.frameRate,
  1995. pixelAspectRatio: frameRef.pixelAspectRatio,
  1996. emsgSchemeIdUris: frameRef.emsgSchemeIdUris,
  1997. id: frameRef.id,
  1998. numChannels: frameRef.numChannels,
  1999. audioSamplingRate: frameRef.audioSamplingRate,
  2000. availabilityTimeOffset: frameRef.availabilityTimeOffset,
  2001. initialization: frameRef.initialization,
  2002. };
  2003. } else if (k == 'periodInfo') {
  2004. /** @type {shaka.dash.DashParser.PeriodInfo} */
  2005. const frameRef = context[k];
  2006. contextClone[k] = {
  2007. start: frameRef.start,
  2008. duration: frameRef.duration,
  2009. node: null,
  2010. isLastPeriod: frameRef.isLastPeriod,
  2011. };
  2012. } else {
  2013. contextClone[k] = context[k];
  2014. }
  2015. }
  2016. return contextClone;
  2017. }
  2018. /**
  2019. * Called when the update timer ticks.
  2020. *
  2021. * @return {!Promise}
  2022. * @private
  2023. */
  2024. async onUpdate_() {
  2025. goog.asserts.assert(this.updatePeriod_ >= 0,
  2026. 'There should be an update period');
  2027. shaka.log.info('Updating manifest...');
  2028. // Default the update delay to 0 seconds so that if there is an error we can
  2029. // try again right away.
  2030. let updateDelay = 0;
  2031. try {
  2032. updateDelay = await this.requestManifest_();
  2033. } catch (error) {
  2034. goog.asserts.assert(error instanceof shaka.util.Error,
  2035. 'Should only receive a Shaka error');
  2036. // Try updating again, but ensure we haven't been destroyed.
  2037. if (this.playerInterface_) {
  2038. if (this.config_.raiseFatalErrorOnManifestUpdateRequestFailure) {
  2039. this.playerInterface_.onError(error);
  2040. return;
  2041. }
  2042. // We will retry updating, so override the severity of the error.
  2043. error.severity = shaka.util.Error.Severity.RECOVERABLE;
  2044. this.playerInterface_.onError(error);
  2045. }
  2046. }
  2047. // Detect a call to stop()
  2048. if (!this.playerInterface_) {
  2049. return;
  2050. }
  2051. this.playerInterface_.onManifestUpdated();
  2052. this.setUpdateTimer_(updateDelay);
  2053. }
  2054. /**
  2055. * Update now the manifest
  2056. *
  2057. * @private
  2058. */
  2059. updateNow_() {
  2060. this.updateTimer_.tickNow();
  2061. }
  2062. /**
  2063. * Sets the update timer. Does nothing if the manifest does not specify an
  2064. * update period.
  2065. *
  2066. * @param {number} offset An offset, in seconds, to apply to the manifest's
  2067. * update period.
  2068. * @private
  2069. */
  2070. setUpdateTimer_(offset) {
  2071. // NOTE: An updatePeriod_ of -1 means the attribute was missing.
  2072. // An attribute which is present and set to 0 should still result in
  2073. // periodic updates. For more, see:
  2074. // https://github.com/Dash-Industry-Forum/Guidelines-TimingModel/issues/48
  2075. if (this.updatePeriod_ < 0) {
  2076. return;
  2077. }
  2078. let updateTime = this.updatePeriod_;
  2079. if (this.config_.dash.updatePeriod >= 0) {
  2080. updateTime = this.config_.dash.updatePeriod;
  2081. }
  2082. const finalDelay = Math.max(
  2083. updateTime - offset,
  2084. this.averageUpdateDuration_.getEstimate());
  2085. // We do not run the timer as repeating because part of update is async and
  2086. // we need schedule the update after it finished.
  2087. this.updateTimer_.tickAfter(/* seconds= */ finalDelay);
  2088. }
  2089. /**
  2090. * Creates a new inheritance frame for the given element.
  2091. *
  2092. * @param {!shaka.extern.xml.Node} elem
  2093. * @param {?shaka.dash.DashParser.InheritanceFrame} parent
  2094. * @param {?function():!Array.<string>} getBaseUris
  2095. * @return {shaka.dash.DashParser.InheritanceFrame}
  2096. * @private
  2097. */
  2098. createFrame_(elem, parent, getBaseUris) {
  2099. goog.asserts.assert(parent || getBaseUris,
  2100. 'Must provide either parent or getBaseUris');
  2101. const SCTE214 = shaka.dash.DashParser.SCTE214_;
  2102. const SegmentUtils = shaka.media.SegmentUtils;
  2103. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  2104. const TXml = shaka.util.TXml;
  2105. parent = parent || /** @type {shaka.dash.DashParser.InheritanceFrame} */ ({
  2106. contentType: '',
  2107. mimeType: '',
  2108. codecs: '',
  2109. emsgSchemeIdUris: [],
  2110. frameRate: undefined,
  2111. pixelAspectRatio: undefined,
  2112. numChannels: null,
  2113. audioSamplingRate: null,
  2114. availabilityTimeOffset: 0,
  2115. segmentSequenceCadence: 0,
  2116. });
  2117. getBaseUris = getBaseUris || parent.getBaseUris;
  2118. const parseNumber = TXml.parseNonNegativeInt;
  2119. const evalDivision = TXml.evalDivision;
  2120. const id = elem.attributes['id'];
  2121. const uriObjs = TXml.findChildren(elem, 'BaseURL');
  2122. let calculatedBaseUris;
  2123. let someLocationValid = false;
  2124. if (this.contentSteeringManager_) {
  2125. for (const uriObj of uriObjs) {
  2126. const serviceLocation = uriObj.attributes['serviceLocation'];
  2127. const uri = TXml.getContents(uriObj);
  2128. if (serviceLocation && uri) {
  2129. this.contentSteeringManager_.addLocation(
  2130. id, serviceLocation, uri);
  2131. someLocationValid = true;
  2132. }
  2133. }
  2134. }
  2135. if (!someLocationValid || !this.contentSteeringManager_) {
  2136. calculatedBaseUris = uriObjs.map(TXml.getContents);
  2137. }
  2138. const getFrameUris = () => {
  2139. if (!uriObjs.length) {
  2140. return [];
  2141. }
  2142. if (this.contentSteeringManager_ && someLocationValid) {
  2143. return this.contentSteeringManager_.getLocations(id);
  2144. }
  2145. if (calculatedBaseUris) {
  2146. return calculatedBaseUris;
  2147. }
  2148. return [];
  2149. };
  2150. let contentType = elem.attributes['contentType'] || parent.contentType;
  2151. const mimeType = elem.attributes['mimeType'] || parent.mimeType;
  2152. const allCodecs = [
  2153. elem.attributes['codecs'] || parent.codecs,
  2154. ];
  2155. const supplementalCodecs =
  2156. TXml.getAttributeNS(elem, SCTE214, 'supplementalCodecs');
  2157. if (supplementalCodecs) {
  2158. allCodecs.push(supplementalCodecs);
  2159. }
  2160. const codecs = SegmentUtils.codecsFiltering(allCodecs).join(',');
  2161. const frameRate =
  2162. TXml.parseAttr(elem, 'frameRate', evalDivision) || parent.frameRate;
  2163. const pixelAspectRatio =
  2164. elem.attributes['sar'] || parent.pixelAspectRatio;
  2165. const emsgSchemeIdUris = this.emsgSchemeIdUris_(
  2166. TXml.findChildren(elem, 'InbandEventStream'),
  2167. parent.emsgSchemeIdUris);
  2168. const audioChannelConfigs =
  2169. TXml.findChildren(elem, 'AudioChannelConfiguration');
  2170. const numChannels =
  2171. this.parseAudioChannels_(audioChannelConfigs) || parent.numChannels;
  2172. const audioSamplingRate =
  2173. TXml.parseAttr(elem, 'audioSamplingRate', parseNumber) ||
  2174. parent.audioSamplingRate;
  2175. if (!contentType) {
  2176. contentType = shaka.dash.DashParser.guessContentType_(mimeType, codecs);
  2177. }
  2178. const segmentBase = TXml.findChild(elem, 'SegmentBase');
  2179. const segmentTemplate = TXml.findChild(elem, 'SegmentTemplate');
  2180. // The availabilityTimeOffset is the sum of all @availabilityTimeOffset
  2181. // values that apply to the adaptation set, via BaseURL, SegmentBase,
  2182. // or SegmentTemplate elements.
  2183. const segmentBaseAto = segmentBase ?
  2184. (TXml.parseAttr(segmentBase, 'availabilityTimeOffset',
  2185. TXml.parseFloat) || 0) : 0;
  2186. const segmentTemplateAto = segmentTemplate ?
  2187. (TXml.parseAttr(segmentTemplate, 'availabilityTimeOffset',
  2188. TXml.parseFloat) || 0) : 0;
  2189. const baseUriAto = uriObjs && uriObjs.length ?
  2190. (TXml.parseAttr(uriObjs[0], 'availabilityTimeOffset',
  2191. TXml.parseFloat) || 0) : 0;
  2192. const availabilityTimeOffset = parent.availabilityTimeOffset + baseUriAto +
  2193. segmentBaseAto + segmentTemplateAto;
  2194. let segmentSequenceCadence = null;
  2195. const segmentSequenceProperties =
  2196. TXml.findChild(elem, 'SegmentSequenceProperties');
  2197. if (segmentSequenceProperties) {
  2198. const sap = TXml.findChild(segmentSequenceProperties, 'SAP');
  2199. if (sap) {
  2200. segmentSequenceCadence = TXml.parseAttr(sap, 'cadence',
  2201. TXml.parseInt);
  2202. }
  2203. }
  2204. // This attribute is currently non-standard, but it is supported by Kaltura.
  2205. let label = elem.attributes['label'];
  2206. // See DASH IOP 4.3 here https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf (page 35)
  2207. const labelElements = TXml.findChildren(elem, 'Label');
  2208. if (labelElements && labelElements.length) {
  2209. // NOTE: Right now only one label field is supported.
  2210. const firstLabelElement = labelElements[0];
  2211. if (TXml.getTextContents(firstLabelElement)) {
  2212. label = TXml.getTextContents(firstLabelElement);
  2213. }
  2214. }
  2215. return {
  2216. getBaseUris:
  2217. () => ManifestParserUtils.resolveUris(getBaseUris(), getFrameUris()),
  2218. segmentBase: segmentBase || parent.segmentBase,
  2219. segmentList:
  2220. TXml.findChild(elem, 'SegmentList') || parent.segmentList,
  2221. segmentTemplate: segmentTemplate || parent.segmentTemplate,
  2222. width: TXml.parseAttr(elem, 'width', parseNumber) || parent.width,
  2223. height: TXml.parseAttr(elem, 'height', parseNumber) || parent.height,
  2224. contentType: contentType,
  2225. mimeType: mimeType,
  2226. codecs: codecs,
  2227. frameRate: frameRate,
  2228. pixelAspectRatio: pixelAspectRatio,
  2229. emsgSchemeIdUris: emsgSchemeIdUris,
  2230. id: id,
  2231. language: elem.attributes['lang'],
  2232. numChannels: numChannels,
  2233. audioSamplingRate: audioSamplingRate,
  2234. availabilityTimeOffset: availabilityTimeOffset,
  2235. initialization: null,
  2236. segmentSequenceCadence:
  2237. segmentSequenceCadence || parent.segmentSequenceCadence,
  2238. label: label || null,
  2239. };
  2240. }
  2241. /**
  2242. * Returns a new array of InbandEventStream schemeIdUri containing the union
  2243. * of the ones parsed from inBandEventStreams and the ones provided in
  2244. * emsgSchemeIdUris.
  2245. *
  2246. * @param {!Array.<!shaka.extern.xml.Node>} inBandEventStreams
  2247. * Array of InbandEventStream
  2248. * elements to parse and add to the returned array.
  2249. * @param {!Array.<string>} emsgSchemeIdUris Array of parsed
  2250. * InbandEventStream schemeIdUri attributes to add to the returned array.
  2251. * @return {!Array.<string>} schemeIdUris Array of parsed
  2252. * InbandEventStream schemeIdUri attributes.
  2253. * @private
  2254. */
  2255. emsgSchemeIdUris_(inBandEventStreams, emsgSchemeIdUris) {
  2256. const schemeIdUris = emsgSchemeIdUris.slice();
  2257. for (const event of inBandEventStreams) {
  2258. const schemeIdUri = event.attributes['schemeIdUri'];
  2259. if (!schemeIdUris.includes(schemeIdUri)) {
  2260. schemeIdUris.push(schemeIdUri);
  2261. }
  2262. }
  2263. return schemeIdUris;
  2264. }
  2265. /**
  2266. * @param {!Array.<!shaka.extern.xml.Node>} audioChannelConfigs An array of
  2267. * AudioChannelConfiguration elements.
  2268. * @return {?number} The number of audio channels, or null if unknown.
  2269. * @private
  2270. */
  2271. parseAudioChannels_(audioChannelConfigs) {
  2272. for (const elem of audioChannelConfigs) {
  2273. const scheme = elem.attributes['schemeIdUri'];
  2274. if (!scheme) {
  2275. continue;
  2276. }
  2277. const value = elem.attributes['value'];
  2278. if (!value) {
  2279. continue;
  2280. }
  2281. switch (scheme) {
  2282. case 'urn:mpeg:dash:outputChannelPositionList:2012':
  2283. // A space-separated list of speaker positions, so the number of
  2284. // channels is the length of this list.
  2285. return value.trim().split(/ +/).length;
  2286. case 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011':
  2287. case 'urn:dts:dash:audio_channel_configuration:2012': {
  2288. // As far as we can tell, this is a number of channels.
  2289. const intValue = parseInt(value, 10);
  2290. if (!intValue) { // 0 or NaN
  2291. shaka.log.warning('Channel parsing failure! ' +
  2292. 'Ignoring scheme and value', scheme, value);
  2293. continue;
  2294. }
  2295. return intValue;
  2296. }
  2297. case 'tag:dolby.com,2014:dash:audio_channel_configuration:2011':
  2298. case 'urn:dolby:dash:audio_channel_configuration:2011': {
  2299. // A hex-encoded 16-bit integer, in which each bit represents a
  2300. // channel.
  2301. let hexValue = parseInt(value, 16);
  2302. if (!hexValue) { // 0 or NaN
  2303. shaka.log.warning('Channel parsing failure! ' +
  2304. 'Ignoring scheme and value', scheme, value);
  2305. continue;
  2306. }
  2307. // Count the 1-bits in hexValue.
  2308. let numBits = 0;
  2309. while (hexValue) {
  2310. if (hexValue & 1) {
  2311. ++numBits;
  2312. }
  2313. hexValue >>= 1;
  2314. }
  2315. return numBits;
  2316. }
  2317. // Defined by https://dashif.org/identifiers/audio_source_metadata/ and clause 8.2, in ISO/IEC 23001-8.
  2318. case 'urn:mpeg:mpegB:cicp:ChannelConfiguration': {
  2319. const noValue = 0;
  2320. const channelCountMapping = [
  2321. noValue, 1, 2, 3, 4, 5, 6, 8, 2, 3, /* 0--9 */
  2322. 4, 7, 8, 24, 8, 12, 10, 12, 14, 12, /* 10--19 */
  2323. 14, /* 20 */
  2324. ];
  2325. const intValue = parseInt(value, 10);
  2326. if (!intValue) { // 0 or NaN
  2327. shaka.log.warning('Channel parsing failure! ' +
  2328. 'Ignoring scheme and value', scheme, value);
  2329. continue;
  2330. }
  2331. if (intValue > noValue && intValue < channelCountMapping.length) {
  2332. return channelCountMapping[intValue];
  2333. }
  2334. continue;
  2335. }
  2336. default:
  2337. shaka.log.warning(
  2338. 'Unrecognized audio channel scheme:', scheme, value);
  2339. continue;
  2340. }
  2341. }
  2342. return null;
  2343. }
  2344. /**
  2345. * Verifies that a Representation has exactly one Segment* element. Prints
  2346. * warnings if there is a problem.
  2347. *
  2348. * @param {shaka.dash.DashParser.InheritanceFrame} frame
  2349. * @return {boolean} True if the Representation is usable; otherwise return
  2350. * false.
  2351. * @private
  2352. */
  2353. verifyRepresentation_(frame) {
  2354. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  2355. let n = 0;
  2356. n += frame.segmentBase ? 1 : 0;
  2357. n += frame.segmentList ? 1 : 0;
  2358. n += frame.segmentTemplate ? 1 : 0;
  2359. if (n == 0) {
  2360. // TODO: Extend with the list of MIME types registered to TextEngine.
  2361. if (frame.contentType == ContentType.TEXT ||
  2362. frame.contentType == ContentType.APPLICATION) {
  2363. return true;
  2364. } else {
  2365. shaka.log.warning(
  2366. 'Representation does not contain a segment information source:',
  2367. 'the Representation must contain one of SegmentBase, SegmentList,',
  2368. 'SegmentTemplate, or explicitly indicate that it is "text".',
  2369. frame);
  2370. return false;
  2371. }
  2372. }
  2373. if (n != 1) {
  2374. shaka.log.warning(
  2375. 'Representation contains multiple segment information sources:',
  2376. 'the Representation should only contain one of SegmentBase,',
  2377. 'SegmentList, or SegmentTemplate.',
  2378. frame);
  2379. if (frame.segmentBase) {
  2380. shaka.log.info('Using SegmentBase by default.');
  2381. frame.segmentList = null;
  2382. frame.segmentTemplate = null;
  2383. } else {
  2384. goog.asserts.assert(frame.segmentList, 'There should be a SegmentList');
  2385. shaka.log.info('Using SegmentList by default.');
  2386. frame.segmentTemplate = null;
  2387. }
  2388. }
  2389. return true;
  2390. }
  2391. /**
  2392. * Makes a request to the given URI and calculates the clock offset.
  2393. *
  2394. * @param {function():!Array.<string>} getBaseUris
  2395. * @param {string} uri
  2396. * @param {string} method
  2397. * @return {!Promise.<number>}
  2398. * @private
  2399. */
  2400. async requestForTiming_(getBaseUris, uri, method) {
  2401. const uris = [shaka.util.StringUtils.htmlUnescape(uri)];
  2402. const requestUris =
  2403. shaka.util.ManifestParserUtils.resolveUris(getBaseUris(), uris);
  2404. const request = shaka.net.NetworkingEngine.makeRequest(
  2405. requestUris, this.config_.retryParameters);
  2406. request.method = method;
  2407. const type = shaka.net.NetworkingEngine.RequestType.TIMING;
  2408. const operation =
  2409. this.playerInterface_.networkingEngine.request(type, request);
  2410. this.operationManager_.manage(operation);
  2411. const response = await operation.promise;
  2412. let text;
  2413. if (method == 'HEAD') {
  2414. if (!response.headers || !response.headers['date']) {
  2415. shaka.log.warning('UTC timing response is missing',
  2416. 'expected date header');
  2417. return 0;
  2418. }
  2419. text = response.headers['date'];
  2420. } else {
  2421. text = shaka.util.StringUtils.fromUTF8(response.data);
  2422. }
  2423. const date = Date.parse(text);
  2424. if (isNaN(date)) {
  2425. shaka.log.warning('Unable to parse date from UTC timing response');
  2426. return 0;
  2427. }
  2428. return (date - Date.now());
  2429. }
  2430. /**
  2431. * Parses an array of UTCTiming elements.
  2432. *
  2433. * @param {function():!Array.<string>} getBaseUris
  2434. * @param {!Array.<!shaka.extern.xml.Node>} elems
  2435. * @return {!Promise.<number>}
  2436. * @private
  2437. */
  2438. async parseUtcTiming_(getBaseUris, elems) {
  2439. const schemesAndValues = elems.map((elem) => {
  2440. return {
  2441. scheme: elem.attributes['schemeIdUri'],
  2442. value: elem.attributes['value'],
  2443. };
  2444. });
  2445. // If there's nothing specified in the manifest, but we have a default from
  2446. // the config, use that.
  2447. const clockSyncUri = this.config_.dash.clockSyncUri;
  2448. if (!schemesAndValues.length && clockSyncUri) {
  2449. schemesAndValues.push({
  2450. scheme: 'urn:mpeg:dash:utc:http-head:2014',
  2451. value: clockSyncUri,
  2452. });
  2453. }
  2454. for (const sv of schemesAndValues) {
  2455. try {
  2456. const scheme = sv.scheme;
  2457. const value = sv.value;
  2458. switch (scheme) {
  2459. // See DASH IOP Guidelines Section 4.7
  2460. // https://bit.ly/DashIop3-2
  2461. // Some old ISO23009-1 drafts used 2012.
  2462. case 'urn:mpeg:dash:utc:http-head:2014':
  2463. case 'urn:mpeg:dash:utc:http-head:2012':
  2464. // eslint-disable-next-line no-await-in-loop
  2465. return await this.requestForTiming_(getBaseUris, value, 'HEAD');
  2466. case 'urn:mpeg:dash:utc:http-xsdate:2014':
  2467. case 'urn:mpeg:dash:utc:http-iso:2014':
  2468. case 'urn:mpeg:dash:utc:http-xsdate:2012':
  2469. case 'urn:mpeg:dash:utc:http-iso:2012':
  2470. // eslint-disable-next-line no-await-in-loop
  2471. return await this.requestForTiming_(getBaseUris, value, 'GET');
  2472. case 'urn:mpeg:dash:utc:direct:2014':
  2473. case 'urn:mpeg:dash:utc:direct:2012': {
  2474. const date = Date.parse(value);
  2475. return isNaN(date) ? 0 : (date - Date.now());
  2476. }
  2477. case 'urn:mpeg:dash:utc:http-ntp:2014':
  2478. case 'urn:mpeg:dash:utc:ntp:2014':
  2479. case 'urn:mpeg:dash:utc:sntp:2014':
  2480. shaka.log.alwaysWarn('NTP UTCTiming scheme is not supported');
  2481. break;
  2482. default:
  2483. shaka.log.alwaysWarn(
  2484. 'Unrecognized scheme in UTCTiming element', scheme);
  2485. break;
  2486. }
  2487. } catch (e) {
  2488. shaka.log.warning('Error fetching time from UTCTiming elem', e.message);
  2489. }
  2490. }
  2491. shaka.log.alwaysWarn(
  2492. 'A UTCTiming element should always be given in live manifests! ' +
  2493. 'This content may not play on clients with bad clocks!');
  2494. return 0;
  2495. }
  2496. /**
  2497. * Parses an EventStream element.
  2498. *
  2499. * @param {number} periodStart
  2500. * @param {?number} periodDuration
  2501. * @param {!shaka.extern.xml.Node} elem
  2502. * @param {number} availabilityStart
  2503. * @private
  2504. */
  2505. parseEventStream_(periodStart, periodDuration, elem, availabilityStart) {
  2506. const TXml = shaka.util.TXml;
  2507. const parseNumber = shaka.util.TXml.parseNonNegativeInt;
  2508. const schemeIdUri = elem.attributes['schemeIdUri'] || '';
  2509. const value = elem.attributes['value'] || '';
  2510. const timescale = TXml.parseAttr(elem, 'timescale', parseNumber) || 1;
  2511. for (const eventNode of TXml.findChildren(elem, 'Event')) {
  2512. const presentationTime =
  2513. TXml.parseAttr(eventNode, 'presentationTime', parseNumber) || 0;
  2514. const duration =
  2515. TXml.parseAttr(eventNode, 'duration', parseNumber) || 0;
  2516. let startTime = presentationTime / timescale + periodStart;
  2517. let endTime = startTime + (duration / timescale);
  2518. if (periodDuration != null) {
  2519. // An event should not go past the Period, even if the manifest says so.
  2520. // See: Dash sec. 5.10.2.1
  2521. startTime = Math.min(startTime, periodStart + periodDuration);
  2522. endTime = Math.min(endTime, periodStart + periodDuration);
  2523. }
  2524. // Don't add unavailable regions to the timeline.
  2525. if (endTime < availabilityStart) {
  2526. continue;
  2527. }
  2528. /** @type {shaka.extern.TimelineRegionInfo} */
  2529. const region = {
  2530. schemeIdUri: schemeIdUri,
  2531. value: value,
  2532. startTime: startTime,
  2533. endTime: endTime,
  2534. id: eventNode.attributes['id'] || '',
  2535. eventElement: TXml.txmlNodeToDomElement(eventNode),
  2536. eventNode: eventNode,
  2537. };
  2538. this.playerInterface_.onTimelineRegionAdded(region);
  2539. }
  2540. }
  2541. /**
  2542. * Makes a network request on behalf of SegmentBase.createStreamInfo.
  2543. *
  2544. * @param {!Array.<string>} uris
  2545. * @param {?number} startByte
  2546. * @param {?number} endByte
  2547. * @param {boolean} isInit
  2548. * @return {!Promise.<BufferSource>}
  2549. * @private
  2550. */
  2551. async requestSegment_(uris, startByte, endByte, isInit) {
  2552. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  2553. const type = isInit ?
  2554. shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT :
  2555. shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
  2556. const request = shaka.util.Networking.createSegmentRequest(
  2557. uris,
  2558. startByte,
  2559. endByte,
  2560. this.config_.retryParameters);
  2561. const response = await this.makeNetworkRequest_(
  2562. request, requestType, {type});
  2563. return response.data;
  2564. }
  2565. /**
  2566. * Guess the content type based on MIME type and codecs.
  2567. *
  2568. * @param {string} mimeType
  2569. * @param {string} codecs
  2570. * @return {string}
  2571. * @private
  2572. */
  2573. static guessContentType_(mimeType, codecs) {
  2574. const fullMimeType = shaka.util.MimeUtils.getFullType(mimeType, codecs);
  2575. if (shaka.text.TextEngine.isTypeSupported(fullMimeType)) {
  2576. // If it's supported by TextEngine, it's definitely text.
  2577. // We don't check MediaSourceEngine, because that would report support
  2578. // for platform-supported video and audio types as well.
  2579. return shaka.util.ManifestParserUtils.ContentType.TEXT;
  2580. }
  2581. // Otherwise, just split the MIME type. This handles video and audio
  2582. // types well.
  2583. return mimeType.split('/')[0];
  2584. }
  2585. /**
  2586. * Create a networking request. This will manage the request using the
  2587. * parser's operation manager.
  2588. *
  2589. * @param {shaka.extern.Request} request
  2590. * @param {shaka.net.NetworkingEngine.RequestType} type
  2591. * @param {shaka.extern.RequestContext=} context
  2592. * @return {!Promise.<shaka.extern.Response>}
  2593. * @private
  2594. */
  2595. makeNetworkRequest_(request, type, context) {
  2596. const op = this.playerInterface_.networkingEngine.request(
  2597. type, request, context);
  2598. this.operationManager_.manage(op);
  2599. return op.promise;
  2600. }
  2601. /**
  2602. * @param {!shaka.extern.xml.Node} patchNode
  2603. * @private
  2604. */
  2605. updatePatchLocationNodes_(patchNode) {
  2606. const TXml = shaka.util.TXml;
  2607. TXml.modifyNodes(this.patchLocationNodes_, patchNode);
  2608. }
  2609. /**
  2610. * @return {!Array<string>}
  2611. * @private
  2612. */
  2613. getPatchLocationUris_() {
  2614. const TXml = shaka.util.TXml;
  2615. const mpdId = this.manifestPatchContext_.mpdId;
  2616. const publishTime = this.manifestPatchContext_.publishTime;
  2617. if (!mpdId || !publishTime || !this.patchLocationNodes_.length) {
  2618. return [];
  2619. }
  2620. const now = Date.now() / 1000;
  2621. const patchLocations = this.patchLocationNodes_.filter((patchLocation) => {
  2622. const ttl = TXml.parseNonNegativeInt(patchLocation.attributes['ttl']);
  2623. return !ttl || publishTime + ttl > now;
  2624. })
  2625. .map(TXml.getContents)
  2626. .filter(shaka.util.Functional.isNotNull);
  2627. if (!patchLocations.length) {
  2628. return [];
  2629. }
  2630. return shaka.util.ManifestParserUtils.resolveUris(
  2631. this.manifestUris_, patchLocations);
  2632. }
  2633. };
  2634. /**
  2635. * @typedef {{
  2636. * mpdId: string,
  2637. * type: string,
  2638. * mediaPresentationDuration: ?number,
  2639. * profiles: !Array.<string>,
  2640. * availabilityTimeOffset: number,
  2641. * getBaseUris: ?function():!Array.<string>,
  2642. * publishTime: number
  2643. * }}
  2644. *
  2645. * @property {string} mpdId
  2646. * ID of the original MPD file.
  2647. * @property {string} type
  2648. * Specifies the type of the dash manifest i.e. "static"
  2649. * @property {?number} mediaPresentationDuration
  2650. * Media presentation duration, or null if unknown.
  2651. * @property {!Array.<string>} profiles
  2652. * Profiles of DASH are defined to enable interoperability and the
  2653. * signaling of the use of features.
  2654. * @property {number} availabilityTimeOffset
  2655. * Specifies the total availabilityTimeOffset of the segment.
  2656. * @property {?function():!Array.<string>} getBaseUris
  2657. * An array of absolute base URIs.
  2658. * @property {number} publishTime
  2659. * Time when manifest has been published, in seconds.
  2660. */
  2661. shaka.dash.DashParser.PatchContext;
  2662. /**
  2663. * @const {string}
  2664. * @private
  2665. */
  2666. shaka.dash.DashParser.SCTE214_ = 'urn:scte:dash:scte214-extensions';
  2667. /**
  2668. * @typedef {
  2669. * function(!Array.<string>, ?number, ?number, boolean):
  2670. * !Promise.<BufferSource>
  2671. * }
  2672. */
  2673. shaka.dash.DashParser.RequestSegmentCallback;
  2674. /**
  2675. * @typedef {{
  2676. * segmentBase: ?shaka.extern.xml.Node,
  2677. * segmentList: ?shaka.extern.xml.Node,
  2678. * segmentTemplate: ?shaka.extern.xml.Node,
  2679. * getBaseUris: function():!Array.<string>,
  2680. * width: (number|undefined),
  2681. * height: (number|undefined),
  2682. * contentType: string,
  2683. * mimeType: string,
  2684. * codecs: string,
  2685. * frameRate: (number|undefined),
  2686. * pixelAspectRatio: (string|undefined),
  2687. * emsgSchemeIdUris: !Array.<string>,
  2688. * id: ?string,
  2689. * language: ?string,
  2690. * numChannels: ?number,
  2691. * audioSamplingRate: ?number,
  2692. * availabilityTimeOffset: number,
  2693. * initialization: ?string,
  2694. * aesKey: (shaka.extern.aesKey|undefined),
  2695. * segmentSequenceCadence: number,
  2696. * label: ?string
  2697. * }}
  2698. *
  2699. * @description
  2700. * A collection of elements and properties which are inherited across levels
  2701. * of a DASH manifest.
  2702. *
  2703. * @property {?shaka.extern.xml.Node} segmentBase
  2704. * The XML node for SegmentBase.
  2705. * @property {?shaka.extern.xml.Node} segmentList
  2706. * The XML node for SegmentList.
  2707. * @property {?shaka.extern.xml.Node} segmentTemplate
  2708. * The XML node for SegmentTemplate.
  2709. * @property {function():!Array.<string>} getBaseUris
  2710. * Function than returns an array of absolute base URIs for the frame.
  2711. * @property {(number|undefined)} width
  2712. * The inherited width value.
  2713. * @property {(number|undefined)} height
  2714. * The inherited height value.
  2715. * @property {string} contentType
  2716. * The inherited media type.
  2717. * @property {string} mimeType
  2718. * The inherited MIME type value.
  2719. * @property {string} codecs
  2720. * The inherited codecs value.
  2721. * @property {(number|undefined)} frameRate
  2722. * The inherited framerate value.
  2723. * @property {(string|undefined)} pixelAspectRatio
  2724. * The inherited pixel aspect ratio value.
  2725. * @property {!Array.<string>} emsgSchemeIdUris
  2726. * emsg registered schemeIdUris.
  2727. * @property {?string} id
  2728. * The ID of the element.
  2729. * @property {?string} language
  2730. * The original language of the element.
  2731. * @property {?number} numChannels
  2732. * The number of audio channels, or null if unknown.
  2733. * @property {?number} audioSamplingRate
  2734. * Specifies the maximum sampling rate of the content, or null if unknown.
  2735. * @property {number} availabilityTimeOffset
  2736. * Specifies the total availabilityTimeOffset of the segment, or 0 if unknown.
  2737. * @property {?string} initialization
  2738. * Specifies the file where the init segment is located, or null.
  2739. * @property {(shaka.extern.aesKey|undefined)} aesKey
  2740. * AES-128 Content protection key
  2741. * @property {number} segmentSequenceCadence
  2742. * Specifies the cadence of independent segments in Segment Sequence
  2743. * Representation.
  2744. * @property {?string} label
  2745. * Label or null if unknown.
  2746. */
  2747. shaka.dash.DashParser.InheritanceFrame;
  2748. /**
  2749. * @typedef {{
  2750. * dynamic: boolean,
  2751. * presentationTimeline: !shaka.media.PresentationTimeline,
  2752. * period: ?shaka.dash.DashParser.InheritanceFrame,
  2753. * periodInfo: ?shaka.dash.DashParser.PeriodInfo,
  2754. * adaptationSet: ?shaka.dash.DashParser.InheritanceFrame,
  2755. * representation: ?shaka.dash.DashParser.InheritanceFrame,
  2756. * bandwidth: number,
  2757. * indexRangeWarningGiven: boolean,
  2758. * availabilityTimeOffset: number,
  2759. * mediaPresentationDuration: ?number,
  2760. * profiles: !Array.<string>,
  2761. * roles: ?Array.<string>
  2762. * }}
  2763. *
  2764. * @description
  2765. * Contains context data for the streams. This is designed to be
  2766. * shallow-copyable, so the parser must overwrite (not modify) each key as the
  2767. * parser moves through the manifest and the parsing context changes.
  2768. *
  2769. * @property {boolean} dynamic
  2770. * True if the MPD is dynamic (not all segments available at once)
  2771. * @property {!shaka.media.PresentationTimeline} presentationTimeline
  2772. * The PresentationTimeline.
  2773. * @property {?shaka.dash.DashParser.InheritanceFrame} period
  2774. * The inheritance from the Period element.
  2775. * @property {?shaka.dash.DashParser.PeriodInfo} periodInfo
  2776. * The Period info for the current Period.
  2777. * @property {?shaka.dash.DashParser.InheritanceFrame} adaptationSet
  2778. * The inheritance from the AdaptationSet element.
  2779. * @property {?shaka.dash.DashParser.InheritanceFrame} representation
  2780. * The inheritance from the Representation element.
  2781. * @property {number} bandwidth
  2782. * The bandwidth of the Representation, or zero if missing.
  2783. * @property {boolean} indexRangeWarningGiven
  2784. * True if the warning about SegmentURL@indexRange has been printed.
  2785. * @property {number} availabilityTimeOffset
  2786. * The sum of the availabilityTimeOffset values that apply to the element.
  2787. * @property {!Array.<string>} profiles
  2788. * Profiles of DASH are defined to enable interoperability and the signaling
  2789. * of the use of features.
  2790. * @property {?number} mediaPresentationDuration
  2791. * Media presentation duration, or null if unknown.
  2792. */
  2793. shaka.dash.DashParser.Context;
  2794. /**
  2795. * @typedef {{
  2796. * start: number,
  2797. * duration: ?number,
  2798. * node: !shaka.extern.xml.Node,
  2799. * isLastPeriod: boolean
  2800. * }}
  2801. *
  2802. * @description
  2803. * Contains information about a Period element.
  2804. *
  2805. * @property {number} start
  2806. * The start time of the period.
  2807. * @property {?number} duration
  2808. * The duration of the period; or null if the duration is not given. This
  2809. * will be non-null for all periods except the last.
  2810. * @property {!shaka.extern.xml.Node} node
  2811. * The XML Node for the Period.
  2812. * @property {boolean} isLastPeriod
  2813. * Whether this Period is the last one in the manifest.
  2814. */
  2815. shaka.dash.DashParser.PeriodInfo;
  2816. /**
  2817. * @typedef {{
  2818. * id: string,
  2819. * contentType: ?string,
  2820. * language: string,
  2821. * main: boolean,
  2822. * streams: !Array.<shaka.extern.Stream>,
  2823. * drmInfos: !Array.<shaka.extern.DrmInfo>,
  2824. * trickModeFor: ?string,
  2825. * representationIds: !Array.<string>
  2826. * }}
  2827. *
  2828. * @description
  2829. * Contains information about an AdaptationSet element.
  2830. *
  2831. * @property {string} id
  2832. * The unique ID of the adaptation set.
  2833. * @property {?string} contentType
  2834. * The content type of the AdaptationSet.
  2835. * @property {string} language
  2836. * The language of the AdaptationSet.
  2837. * @property {boolean} main
  2838. * Whether the AdaptationSet has the 'main' type.
  2839. * @property {!Array.<shaka.extern.Stream>} streams
  2840. * The streams this AdaptationSet contains.
  2841. * @property {!Array.<shaka.extern.DrmInfo>} drmInfos
  2842. * The DRM info for the AdaptationSet.
  2843. * @property {?string} trickModeFor
  2844. * If non-null, this AdaptationInfo represents trick mode tracks. This
  2845. * property is the ID of the normal AdaptationSet these tracks should be
  2846. * associated with.
  2847. * @property {!Array.<string>} representationIds
  2848. * An array of the IDs of the Representations this AdaptationSet contains.
  2849. */
  2850. shaka.dash.DashParser.AdaptationInfo;
  2851. /**
  2852. * @typedef {function():!Promise.<shaka.media.SegmentIndex>}
  2853. * @description
  2854. * An async function which generates and returns a SegmentIndex.
  2855. */
  2856. shaka.dash.DashParser.GenerateSegmentIndexFunction;
  2857. /**
  2858. * @typedef {{
  2859. * generateSegmentIndex: shaka.dash.DashParser.GenerateSegmentIndexFunction
  2860. * }}
  2861. *
  2862. * @description
  2863. * Contains information about a Stream. This is passed from the createStreamInfo
  2864. * methods.
  2865. *
  2866. * @property {shaka.dash.DashParser.GenerateSegmentIndexFunction}
  2867. * generateSegmentIndex
  2868. * An async function to create the SegmentIndex for the stream.
  2869. */
  2870. shaka.dash.DashParser.StreamInfo;
  2871. shaka.media.ManifestParser.registerParserByMime(
  2872. 'application/dash+xml', () => new shaka.dash.DashParser());
  2873. shaka.media.ManifestParser.registerParserByMime(
  2874. 'video/vnd.mpeg.dash.mpd', () => new shaka.dash.DashParser());