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