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