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