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