Source: lib/dash/dash_parser.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.dash.DashParser');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.abr.Ewma');
  9. goog.require('shaka.dash.ContentProtection');
  10. goog.require('shaka.dash.MpdUtils');
  11. goog.require('shaka.dash.SegmentBase');
  12. goog.require('shaka.dash.SegmentList');
  13. goog.require('shaka.dash.SegmentTemplate');
  14. goog.require('shaka.log');
  15. goog.require('shaka.media.ManifestParser');
  16. goog.require('shaka.media.PresentationTimeline');
  17. goog.require('shaka.media.SegmentIndex');
  18. goog.require('shaka.net.NetworkingEngine');
  19. goog.require('shaka.text.TextEngine');
  20. goog.require('shaka.util.CmcdManager');
  21. goog.require('shaka.util.Error');
  22. goog.require('shaka.util.Functional');
  23. goog.require('shaka.util.LanguageUtils');
  24. goog.require('shaka.util.ManifestParserUtils');
  25. goog.require('shaka.util.MimeUtils');
  26. goog.require('shaka.util.Networking');
  27. goog.require('shaka.util.OperationManager');
  28. goog.require('shaka.util.PeriodCombiner');
  29. goog.require('shaka.util.StringUtils');
  30. goog.require('shaka.util.Timer');
  31. goog.require('shaka.util.XmlUtils');
  32. /**
  33. * Creates a new DASH parser.
  34. *
  35. * @implements {shaka.extern.ManifestParser}
  36. * @export
  37. */
  38. shaka.dash.DashParser = class {
  39. /** Creates a new DASH parser. */
  40. constructor() {
  41. /** @private {?shaka.extern.ManifestConfiguration} */
  42. this.config_ = null;
  43. /** @private {?shaka.extern.ManifestParser.PlayerInterface} */
  44. this.playerInterface_ = null;
  45. /** @private {!Array.<string>} */
  46. this.manifestUris_ = [];
  47. /** @private {?shaka.extern.Manifest} */
  48. this.manifest_ = null;
  49. /** @private {number} */
  50. this.globalId_ = 1;
  51. /**
  52. * A map of IDs to Stream objects.
  53. * ID: Period@id,AdaptationSet@id,@Representation@id
  54. * e.g.: '1,5,23'
  55. * @private {!Object.<string, !shaka.extern.Stream>}
  56. */
  57. this.streamMap_ = {};
  58. /**
  59. * A map of period ids to their durations
  60. * @private {!Object.<string, number>}
  61. */
  62. this.periodDurations_ = {};
  63. /** @private {shaka.util.PeriodCombiner} */
  64. this.periodCombiner_ = new shaka.util.PeriodCombiner();
  65. /**
  66. * The update period in seconds, or 0 for no updates.
  67. * @private {number}
  68. */
  69. this.updatePeriod_ = 0;
  70. /**
  71. * An ewma that tracks how long updates take.
  72. * This is to mitigate issues caused by slow parsing on embedded devices.
  73. * @private {!shaka.abr.Ewma}
  74. */
  75. this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
  76. /** @private {shaka.util.Timer} */
  77. this.updateTimer_ = new shaka.util.Timer(() => {
  78. this.onUpdate_();
  79. });
  80. /** @private {!shaka.util.OperationManager} */
  81. this.operationManager_ = new shaka.util.OperationManager();
  82. /**
  83. * Largest period start time seen.
  84. * @private {?number}
  85. */
  86. this.largestPeriodStartTime_ = null;
  87. /**
  88. * Period IDs seen in previous manifest.
  89. * @private {!Array.<string>}
  90. */
  91. this.lastManifestUpdatePeriodIds_ = [];
  92. /**
  93. * The minimum of the availabilityTimeOffset values among the adaptation
  94. * sets.
  95. * @private {number}
  96. */
  97. this.minTotalAvailabilityTimeOffset_ = Infinity;
  98. /** @private {boolean} */
  99. this.lowLatencyMode_ = false;
  100. }
  101. /**
  102. * @override
  103. * @exportInterface
  104. */
  105. configure(config) {
  106. goog.asserts.assert(config.dash != null,
  107. 'DashManifestConfiguration should not be null!');
  108. this.config_ = config;
  109. }
  110. /**
  111. * @override
  112. * @exportInterface
  113. */
  114. async start(uri, playerInterface) {
  115. goog.asserts.assert(this.config_, 'Must call configure() before start()!');
  116. this.lowLatencyMode_ = playerInterface.isLowLatencyMode();
  117. this.manifestUris_ = [uri];
  118. this.playerInterface_ = playerInterface;
  119. const updateDelay = await this.requestManifest_();
  120. if (this.playerInterface_) {
  121. this.setUpdateTimer_(updateDelay);
  122. }
  123. // Make sure that the parser has not been destroyed.
  124. if (!this.playerInterface_) {
  125. throw new shaka.util.Error(
  126. shaka.util.Error.Severity.CRITICAL,
  127. shaka.util.Error.Category.PLAYER,
  128. shaka.util.Error.Code.OPERATION_ABORTED);
  129. }
  130. goog.asserts.assert(this.manifest_, 'Manifest should be non-null!');
  131. return this.manifest_;
  132. }
  133. /**
  134. * @override
  135. * @exportInterface
  136. */
  137. stop() {
  138. // When the parser stops, release all segment indexes, which stops their
  139. // timers, as well.
  140. for (const stream of Object.values(this.streamMap_)) {
  141. if (stream.segmentIndex) {
  142. stream.segmentIndex.release();
  143. }
  144. }
  145. if (this.periodCombiner_) {
  146. this.periodCombiner_.release();
  147. }
  148. this.playerInterface_ = null;
  149. this.config_ = null;
  150. this.manifestUris_ = [];
  151. this.manifest_ = null;
  152. this.streamMap_ = {};
  153. this.periodCombiner_ = null;
  154. if (this.updateTimer_ != null) {
  155. this.updateTimer_.stop();
  156. this.updateTimer_ = null;
  157. }
  158. return this.operationManager_.destroy();
  159. }
  160. /**
  161. * @override
  162. * @exportInterface
  163. */
  164. async update() {
  165. try {
  166. await this.requestManifest_();
  167. } catch (error) {
  168. if (!this.playerInterface_ || !error) {
  169. return;
  170. }
  171. goog.asserts.assert(error instanceof shaka.util.Error, 'Bad error type');
  172. this.playerInterface_.onError(error);
  173. }
  174. }
  175. /**
  176. * @override
  177. * @exportInterface
  178. */
  179. onExpirationUpdated(sessionId, expiration) {
  180. // No-op
  181. }
  182. /**
  183. * @override
  184. * @exportInterface
  185. */
  186. onInitialVariantChosen(variant) {
  187. // For live it is necessary that the first time we update the manifest with
  188. // a shorter time than indicated to take into account that the last segment
  189. // added could be halfway, for example
  190. if (this.manifest_ && this.manifest_.presentationTimeline.isLive()) {
  191. const stream = variant.video || variant.audio;
  192. if (stream && stream.segmentIndex) {
  193. const availabilityEnd =
  194. this.manifest_.presentationTimeline.getSegmentAvailabilityEnd();
  195. const position = stream.segmentIndex.find(availabilityEnd);
  196. if (position == null) {
  197. return;
  198. }
  199. const reference = stream.segmentIndex.get(position);
  200. if (!reference) {
  201. return;
  202. }
  203. this.updatePeriod_ = reference.endTime - availabilityEnd;
  204. this.setUpdateTimer_(/* offset= */ 0);
  205. }
  206. }
  207. }
  208. /**
  209. * Makes a network request for the manifest and parses the resulting data.
  210. *
  211. * @return {!Promise.<number>} Resolves with the time it took, in seconds, to
  212. * fulfill the request and parse the data.
  213. * @private
  214. */
  215. async requestManifest_() {
  216. const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  217. const request = shaka.net.NetworkingEngine.makeRequest(
  218. this.manifestUris_, this.config_.retryParameters);
  219. const networkingEngine = this.playerInterface_.networkingEngine;
  220. const format = shaka.util.CmcdManager.StreamingFormat.DASH;
  221. this.playerInterface_.modifyManifestRequest(request, {format: format});
  222. const startTime = Date.now();
  223. const operation = networkingEngine.request(requestType, request);
  224. this.operationManager_.manage(operation);
  225. const response = await operation.promise;
  226. // Detect calls to stop().
  227. if (!this.playerInterface_) {
  228. return 0;
  229. }
  230. // For redirections add the response uri to the first entry in the
  231. // Manifest Uris array.
  232. if (response.uri && response.uri != response.originalUri &&
  233. !this.manifestUris_.includes(response.uri)) {
  234. this.manifestUris_.unshift(response.uri);
  235. }
  236. // This may throw, but it will result in a failed promise.
  237. await this.parseManifest_(response.data, response.uri);
  238. // Keep track of how long the longest manifest update took.
  239. const endTime = Date.now();
  240. const updateDuration = (endTime - startTime) / 1000.0;
  241. this.averageUpdateDuration_.sample(1, updateDuration);
  242. // Let the caller know how long this update took.
  243. return updateDuration;
  244. }
  245. /**
  246. * Parses the manifest XML. This also handles updates and will update the
  247. * stored manifest.
  248. *
  249. * @param {BufferSource} data
  250. * @param {string} finalManifestUri The final manifest URI, which may
  251. * differ from this.manifestUri_ if there has been a redirect.
  252. * @return {!Promise}
  253. * @private
  254. */
  255. async parseManifest_(data, finalManifestUri) {
  256. const Error = shaka.util.Error;
  257. const MpdUtils = shaka.dash.MpdUtils;
  258. const mpd = shaka.util.XmlUtils.parseXml(data, 'MPD');
  259. if (!mpd) {
  260. throw new Error(
  261. Error.Severity.CRITICAL, Error.Category.MANIFEST,
  262. Error.Code.DASH_INVALID_XML, finalManifestUri);
  263. }
  264. const disableXlinkProcessing = this.config_.dash.disableXlinkProcessing;
  265. if (disableXlinkProcessing) {
  266. return this.processManifest_(mpd, finalManifestUri);
  267. }
  268. // Process the mpd to account for xlink connections.
  269. const failGracefully = this.config_.dash.xlinkFailGracefully;
  270. const xlinkOperation = MpdUtils.processXlinks(
  271. mpd, this.config_.retryParameters, failGracefully, finalManifestUri,
  272. this.playerInterface_.networkingEngine);
  273. this.operationManager_.manage(xlinkOperation);
  274. const finalMpd = await xlinkOperation.promise;
  275. return this.processManifest_(finalMpd, finalManifestUri);
  276. }
  277. /**
  278. * Takes a formatted MPD and converts it into a manifest.
  279. *
  280. * @param {!Element} mpd
  281. * @param {string} finalManifestUri The final manifest URI, which may
  282. * differ from this.manifestUri_ if there has been a redirect.
  283. * @return {!Promise}
  284. * @private
  285. */
  286. async processManifest_(mpd, finalManifestUri) {
  287. const Functional = shaka.util.Functional;
  288. const XmlUtils = shaka.util.XmlUtils;
  289. const manifestPreprocessor = this.config_.dash.manifestPreprocessor;
  290. if (manifestPreprocessor) {
  291. manifestPreprocessor(mpd);
  292. }
  293. // Get any Location elements. This will update the manifest location and
  294. // the base URI.
  295. /** @type {!Array.<string>} */
  296. let manifestBaseUris = [finalManifestUri];
  297. /** @type {!Array.<string>} */
  298. const locations = XmlUtils.findChildren(mpd, 'Location')
  299. .map(XmlUtils.getContents)
  300. .filter(Functional.isNotNull);
  301. if (locations.length > 0) {
  302. const absoluteLocations = shaka.util.ManifestParserUtils.resolveUris(
  303. manifestBaseUris, locations);
  304. this.manifestUris_ = absoluteLocations;
  305. manifestBaseUris = absoluteLocations;
  306. }
  307. const uriObjs = XmlUtils.findChildren(mpd, 'BaseURL');
  308. const uris = uriObjs.map(XmlUtils.getContents);
  309. const baseUris = shaka.util.ManifestParserUtils.resolveUris(
  310. manifestBaseUris, uris);
  311. let availabilityTimeOffset = 0;
  312. if (uriObjs && uriObjs.length) {
  313. availabilityTimeOffset = XmlUtils.parseAttr(
  314. uriObjs[0], 'availabilityTimeOffset', XmlUtils.parseFloat) || 0;
  315. }
  316. const ignoreMinBufferTime = this.config_.dash.ignoreMinBufferTime;
  317. let minBufferTime = 0;
  318. if (!ignoreMinBufferTime) {
  319. minBufferTime =
  320. XmlUtils.parseAttr(mpd, 'minBufferTime', XmlUtils.parseDuration) || 0;
  321. }
  322. this.updatePeriod_ = /** @type {number} */ (XmlUtils.parseAttr(
  323. mpd, 'minimumUpdatePeriod', XmlUtils.parseDuration, -1));
  324. const presentationStartTime = XmlUtils.parseAttr(
  325. mpd, 'availabilityStartTime', XmlUtils.parseDate);
  326. let segmentAvailabilityDuration = XmlUtils.parseAttr(
  327. mpd, 'timeShiftBufferDepth', XmlUtils.parseDuration);
  328. const ignoreSuggestedPresentationDelay =
  329. this.config_.dash.ignoreSuggestedPresentationDelay;
  330. let suggestedPresentationDelay = null;
  331. if (!ignoreSuggestedPresentationDelay) {
  332. suggestedPresentationDelay = XmlUtils.parseAttr(
  333. mpd, 'suggestedPresentationDelay', XmlUtils.parseDuration);
  334. }
  335. const ignoreMaxSegmentDuration =
  336. this.config_.dash.ignoreMaxSegmentDuration;
  337. let maxSegmentDuration = null;
  338. if (!ignoreMaxSegmentDuration) {
  339. maxSegmentDuration = XmlUtils.parseAttr(
  340. mpd, 'maxSegmentDuration', XmlUtils.parseDuration);
  341. }
  342. const mpdType = mpd.getAttribute('type') || 'static';
  343. /** @type {!shaka.media.PresentationTimeline} */
  344. let presentationTimeline;
  345. if (this.manifest_) {
  346. presentationTimeline = this.manifest_.presentationTimeline;
  347. // Before processing an update, evict from all segment indexes. Some of
  348. // them may not get updated otherwise if their corresponding Period
  349. // element has been dropped from the manifest since the last update.
  350. // Without this, playback will still work, but this is necessary to
  351. // maintain conditions that we assert on for multi-Period content.
  352. // This gives us confidence that our state is maintained correctly, and
  353. // that the complex logic of multi-Period eviction and period-flattening
  354. // is correct. See also:
  355. // https://github.com/shaka-project/shaka-player/issues/3169#issuecomment-823580634
  356. for (const stream of Object.values(this.streamMap_)) {
  357. if (stream.segmentIndex) {
  358. stream.segmentIndex.evict(
  359. presentationTimeline.getSegmentAvailabilityStart());
  360. }
  361. }
  362. } else {
  363. // DASH IOP v3.0 suggests using a default delay between minBufferTime
  364. // and timeShiftBufferDepth. This is literally the range of all
  365. // feasible choices for the value. Nothing older than
  366. // timeShiftBufferDepth is still available, and anything less than
  367. // minBufferTime will cause buffering issues.
  368. //
  369. // We have decided that our default will be the configured value, or
  370. // 1.5 * minBufferTime if not configured. This is fairly conservative.
  371. // Content providers should provide a suggestedPresentationDelay whenever
  372. // possible to optimize the live streaming experience.
  373. const defaultPresentationDelay =
  374. this.config_.defaultPresentationDelay || minBufferTime * 1.5;
  375. const presentationDelay = suggestedPresentationDelay != null ?
  376. suggestedPresentationDelay : defaultPresentationDelay;
  377. presentationTimeline = new shaka.media.PresentationTimeline(
  378. presentationStartTime, presentationDelay,
  379. this.config_.dash.autoCorrectDrift);
  380. }
  381. presentationTimeline.setStatic(mpdType == 'static');
  382. const isLive = presentationTimeline.isLive();
  383. // If it's live, we check for an override.
  384. if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
  385. segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
  386. }
  387. // If it's null, that means segments are always available. This is always
  388. // the case for VOD, and sometimes the case for live.
  389. if (segmentAvailabilityDuration == null) {
  390. segmentAvailabilityDuration = Infinity;
  391. }
  392. presentationTimeline.setSegmentAvailabilityDuration(
  393. segmentAvailabilityDuration);
  394. const profiles = mpd.getAttribute('profiles') || '';
  395. /** @type {shaka.dash.DashParser.Context} */
  396. const context = {
  397. // Don't base on updatePeriod_ since emsg boxes can cause manifest
  398. // updates.
  399. dynamic: mpdType != 'static',
  400. presentationTimeline: presentationTimeline,
  401. period: null,
  402. periodInfo: null,
  403. adaptationSet: null,
  404. representation: null,
  405. bandwidth: 0,
  406. indexRangeWarningGiven: false,
  407. availabilityTimeOffset: availabilityTimeOffset,
  408. profiles: profiles.split(','),
  409. };
  410. const periodsAndDuration = this.parsePeriods_(context, baseUris, mpd);
  411. const duration = periodsAndDuration.duration;
  412. const periods = periodsAndDuration.periods;
  413. if (mpdType == 'static' ||
  414. !periodsAndDuration.durationDerivedFromPeriods) {
  415. // Ignore duration calculated from Period lengths if this is dynamic.
  416. presentationTimeline.setDuration(duration || Infinity);
  417. }
  418. // The segments are available earlier than the availability start time.
  419. // If the stream is low latency and the user has not configured the
  420. // lowLatencyMode, but if it has been configured to activate the
  421. // lowLatencyMode if a stream of this type is detected, we automatically
  422. // activate the lowLatencyMode.
  423. if (this.minTotalAvailabilityTimeOffset_ && !this.lowLatencyMode_) {
  424. const autoLowLatencyMode = this.playerInterface_.isAutoLowLatencyMode();
  425. if (autoLowLatencyMode) {
  426. this.playerInterface_.enableLowLatencyMode();
  427. this.lowLatencyMode_ = this.playerInterface_.isLowLatencyMode();
  428. }
  429. }
  430. if (this.lowLatencyMode_) {
  431. presentationTimeline.setAvailabilityTimeOffset(
  432. this.minTotalAvailabilityTimeOffset_);
  433. } else if (this.minTotalAvailabilityTimeOffset_) {
  434. // If the playlist contains AvailabilityTimeOffset value, the
  435. // streaming.lowLatencyMode value should be set to true to stream with low
  436. // latency mode.
  437. shaka.log.alwaysWarn('Low-latency DASH live stream detected, but ' +
  438. 'low-latency streaming mode is not enabled in Shaka Player. ' +
  439. 'Set streaming.lowLatencyMode configuration to true, and see ' +
  440. 'https://bit.ly/3clctcj for details.');
  441. }
  442. // Use @maxSegmentDuration to override smaller, derived values.
  443. presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1);
  444. if (goog.DEBUG) {
  445. presentationTimeline.assertIsValid();
  446. }
  447. await this.periodCombiner_.combinePeriods(periods, context.dynamic);
  448. // Set minBufferTime to 0 for low-latency DASH live stream to achieve the
  449. // best latency
  450. if (this.lowLatencyMode_) {
  451. minBufferTime = 0;
  452. const presentationDelay = suggestedPresentationDelay != null ?
  453. suggestedPresentationDelay : this.config_.defaultPresentationDelay;
  454. presentationTimeline.setDelay(presentationDelay);
  455. }
  456. // These steps are not done on manifest update.
  457. if (!this.manifest_) {
  458. this.manifest_ = {
  459. presentationTimeline: presentationTimeline,
  460. variants: this.periodCombiner_.getVariants(),
  461. textStreams: this.periodCombiner_.getTextStreams(),
  462. imageStreams: this.periodCombiner_.getImageStreams(),
  463. offlineSessionIds: [],
  464. minBufferTime: minBufferTime || 0,
  465. sequenceMode: this.config_.dash.sequenceMode,
  466. };
  467. // We only need to do clock sync when we're using presentation start
  468. // time. This condition also excludes VOD streams.
  469. if (presentationTimeline.usingPresentationStartTime()) {
  470. const XmlUtils = shaka.util.XmlUtils;
  471. const timingElements = XmlUtils.findChildren(mpd, 'UTCTiming');
  472. const offset = await this.parseUtcTiming_(baseUris, timingElements);
  473. // Detect calls to stop().
  474. if (!this.playerInterface_) {
  475. return;
  476. }
  477. presentationTimeline.setClockOffset(offset);
  478. }
  479. // This is the first point where we have a meaningful presentation start
  480. // time, and we need to tell PresentationTimeline that so that it can
  481. // maintain consistency from here on.
  482. presentationTimeline.lockStartTime();
  483. } else {
  484. // Just update the variants and text streams, which may change as periods
  485. // are added or removed.
  486. this.manifest_.variants = this.periodCombiner_.getVariants();
  487. const textStreams = this.periodCombiner_.getTextStreams();
  488. if (textStreams.length > 0) {
  489. this.manifest_.textStreams = textStreams;
  490. }
  491. this.manifest_.imageStreams = this.periodCombiner_.getImageStreams();
  492. // Re-filter the manifest. This will check any configured restrictions on
  493. // new variants, and will pass any new init data to DrmEngine to ensure
  494. // that key rotation works correctly.
  495. this.playerInterface_.filter(this.manifest_);
  496. }
  497. // Add text streams to correspond to closed captions. This happens right
  498. // after period combining, while we still have a direct reference, so that
  499. // any new streams will appear in the period combiner.
  500. this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
  501. }
  502. /**
  503. * Reads and parses the periods from the manifest. This first does some
  504. * partial parsing so the start and duration is available when parsing
  505. * children.
  506. *
  507. * @param {shaka.dash.DashParser.Context} context
  508. * @param {!Array.<string>} baseUris
  509. * @param {!Element} mpd
  510. * @return {{
  511. * periods: !Array.<shaka.util.PeriodCombiner.Period>,
  512. * duration: ?number,
  513. * durationDerivedFromPeriods: boolean
  514. * }}
  515. * @private
  516. */
  517. parsePeriods_(context, baseUris, mpd) {
  518. const XmlUtils = shaka.util.XmlUtils;
  519. const presentationDuration = XmlUtils.parseAttr(
  520. mpd, 'mediaPresentationDuration', XmlUtils.parseDuration);
  521. const periods = [];
  522. let prevEnd = 0;
  523. const periodNodes = XmlUtils.findChildren(mpd, 'Period');
  524. for (let i = 0; i < periodNodes.length; i++) {
  525. const elem = periodNodes[i];
  526. const next = periodNodes[i + 1];
  527. const start = /** @type {number} */ (
  528. XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd));
  529. const periodId = elem.id;
  530. const givenDuration =
  531. XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration);
  532. let periodDuration = null;
  533. if (next) {
  534. // "The difference between the start time of a Period and the start time
  535. // of the following Period is the duration of the media content
  536. // represented by this Period."
  537. const nextStart =
  538. XmlUtils.parseAttr(next, 'start', XmlUtils.parseDuration);
  539. if (nextStart != null) {
  540. periodDuration = nextStart - start;
  541. }
  542. } else if (presentationDuration != null) {
  543. // "The Period extends until the Period.start of the next Period, or
  544. // until the end of the Media Presentation in the case of the last
  545. // Period."
  546. periodDuration = presentationDuration - start;
  547. }
  548. const threshold =
  549. shaka.util.ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS;
  550. if (periodDuration && givenDuration &&
  551. Math.abs(periodDuration - givenDuration) > threshold) {
  552. shaka.log.warning('There is a gap/overlap between Periods', elem);
  553. }
  554. // Only use the @duration in the MPD if we can't calculate it. We should
  555. // favor the @start of the following Period. This ensures that there
  556. // aren't gaps between Periods.
  557. if (periodDuration == null) {
  558. periodDuration = givenDuration;
  559. }
  560. /**
  561. * This is to improve robustness when the player observes manifest with
  562. * past periods that are inconsistent to previous ones.
  563. *
  564. * This may happen when a CDN or proxy server switches its upstream from
  565. * one encoder to another redundant encoder.
  566. *
  567. * Skip periods that match all of the following criteria:
  568. * - Start time is earlier than latest period start time ever seen
  569. * - Period ID is never seen in the previous manifest
  570. * - Not the last period in the manifest
  571. *
  572. * Periods that meet the aforementioned criteria are considered invalid
  573. * and should be safe to discard.
  574. */
  575. if (this.largestPeriodStartTime_ !== null &&
  576. periodId !== null && start !== null &&
  577. start < this.largestPeriodStartTime_ &&
  578. !this.lastManifestUpdatePeriodIds_.includes(periodId) &&
  579. i + 1 != periodNodes.length) {
  580. shaka.log.debug(
  581. `Skipping Period with ID ${periodId} as its start time is smaller` +
  582. ' than the largest period start time that has been seen, and ID ' +
  583. 'is unseen before');
  584. continue;
  585. }
  586. // Save maximum period start time if it is the last period
  587. if (start !== null &&
  588. (this.largestPeriodStartTime_ === null ||
  589. start > this.largestPeriodStartTime_)) {
  590. this.largestPeriodStartTime_ = start;
  591. }
  592. // Parse child nodes.
  593. const info = {
  594. start: start,
  595. duration: periodDuration,
  596. node: elem,
  597. isLastPeriod: periodDuration == null || !next,
  598. };
  599. const period = this.parsePeriod_(context, baseUris, info);
  600. periods.push(period);
  601. if (context.period.id && periodDuration) {
  602. this.periodDurations_[context.period.id] = periodDuration;
  603. }
  604. if (periodDuration == null) {
  605. if (next) {
  606. // If the duration is still null and we aren't at the end, then we
  607. // will skip any remaining periods.
  608. shaka.log.warning(
  609. 'Skipping Period', i + 1, 'and any subsequent Periods:', 'Period',
  610. i + 1, 'does not have a valid start time.', next);
  611. }
  612. // The duration is unknown, so the end is unknown.
  613. prevEnd = null;
  614. break;
  615. }
  616. prevEnd = start + periodDuration;
  617. } // end of period parsing loop
  618. // Replace previous seen periods with the current one.
  619. this.lastManifestUpdatePeriodIds_ = periods.map((el) => el.id);
  620. if (presentationDuration != null) {
  621. if (prevEnd != presentationDuration) {
  622. shaka.log.warning(
  623. '@mediaPresentationDuration does not match the total duration of ',
  624. 'all Periods.');
  625. // Assume @mediaPresentationDuration is correct.
  626. }
  627. return {
  628. periods: periods,
  629. duration: presentationDuration,
  630. durationDerivedFromPeriods: false,
  631. };
  632. } else {
  633. return {
  634. periods: periods,
  635. duration: prevEnd,
  636. durationDerivedFromPeriods: true,
  637. };
  638. }
  639. }
  640. /**
  641. * Parses a Period XML element. Unlike the other parse methods, this is not
  642. * given the Node; it is given a PeriodInfo structure. Also, partial parsing
  643. * was done before this was called so start and duration are valid.
  644. *
  645. * @param {shaka.dash.DashParser.Context} context
  646. * @param {!Array.<string>} baseUris
  647. * @param {shaka.dash.DashParser.PeriodInfo} periodInfo
  648. * @return {shaka.util.PeriodCombiner.Period}
  649. * @private
  650. */
  651. parsePeriod_(context, baseUris, periodInfo) {
  652. const Functional = shaka.util.Functional;
  653. const XmlUtils = shaka.util.XmlUtils;
  654. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  655. context.period = this.createFrame_(periodInfo.node, null, baseUris);
  656. context.periodInfo = periodInfo;
  657. context.period.availabilityTimeOffset = context.availabilityTimeOffset;
  658. // If the period doesn't have an ID, give it one based on its start time.
  659. if (!context.period.id) {
  660. shaka.log.info(
  661. 'No Period ID given for Period with start time ' + periodInfo.start +
  662. ', Assigning a default');
  663. context.period.id = '__shaka_period_' + periodInfo.start;
  664. }
  665. const eventStreamNodes =
  666. XmlUtils.findChildren(periodInfo.node, 'EventStream');
  667. const availabilityStart =
  668. context.presentationTimeline.getSegmentAvailabilityStart();
  669. for (const node of eventStreamNodes) {
  670. this.parseEventStream_(
  671. periodInfo.start, periodInfo.duration, node, availabilityStart);
  672. }
  673. const adaptationSetNodes =
  674. XmlUtils.findChildren(periodInfo.node, 'AdaptationSet');
  675. const adaptationSets = adaptationSetNodes
  676. .map((node) => this.parseAdaptationSet_(context, node))
  677. .filter(Functional.isNotNull);
  678. // For dynamic manifests, we use rep IDs internally, and they must be
  679. // unique.
  680. if (context.dynamic) {
  681. const ids = [];
  682. for (const set of adaptationSets) {
  683. for (const id of set.representationIds) {
  684. ids.push(id);
  685. }
  686. }
  687. const uniqueIds = new Set(ids);
  688. if (ids.length != uniqueIds.size) {
  689. throw new shaka.util.Error(
  690. shaka.util.Error.Severity.CRITICAL,
  691. shaka.util.Error.Category.MANIFEST,
  692. shaka.util.Error.Code.DASH_DUPLICATE_REPRESENTATION_ID);
  693. }
  694. }
  695. const normalAdaptationSets = adaptationSets
  696. .filter((as) => { return !as.trickModeFor; });
  697. const trickModeAdaptationSets = adaptationSets
  698. .filter((as) => { return as.trickModeFor; });
  699. // Attach trick mode tracks to normal tracks.
  700. for (const trickModeSet of trickModeAdaptationSets) {
  701. const targetIds = trickModeSet.trickModeFor.split(' ');
  702. for (const normalSet of normalAdaptationSets) {
  703. if (targetIds.includes(normalSet.id)) {
  704. for (const stream of normalSet.streams) {
  705. // There may be multiple trick mode streams, but we do not
  706. // currently support that. Just choose one.
  707. // TODO: https://github.com/shaka-project/shaka-player/issues/1528
  708. stream.trickModeVideo = trickModeSet.streams.find((trickStream) =>
  709. shaka.util.MimeUtils.getNormalizedCodec(stream.codecs) ==
  710. shaka.util.MimeUtils.getNormalizedCodec(trickStream.codecs));
  711. }
  712. }
  713. }
  714. }
  715. const audioStreams = this.getStreamsFromSets_(
  716. this.config_.disableAudio,
  717. normalAdaptationSets,
  718. ContentType.AUDIO);
  719. const videoStreams = this.getStreamsFromSets_(
  720. this.config_.disableVideo,
  721. normalAdaptationSets,
  722. ContentType.VIDEO);
  723. const textStreams = this.getStreamsFromSets_(
  724. this.config_.disableText,
  725. normalAdaptationSets,
  726. ContentType.TEXT);
  727. const imageStreams = this.getStreamsFromSets_(
  728. this.config_.disableThumbnails,
  729. normalAdaptationSets,
  730. ContentType.IMAGE);
  731. if (videoStreams.length === 0 && audioStreams.length === 0) {
  732. throw new shaka.util.Error(
  733. shaka.util.Error.Severity.CRITICAL,
  734. shaka.util.Error.Category.MANIFEST,
  735. shaka.util.Error.Code.DASH_EMPTY_PERIOD,
  736. );
  737. }
  738. return {
  739. id: context.period.id,
  740. audioStreams,
  741. videoStreams,
  742. textStreams,
  743. imageStreams,
  744. };
  745. }
  746. /**
  747. * Gets the streams from the given sets or returns an empty array if disabled
  748. * or no streams are found.
  749. * @param {boolean} disabled
  750. * @param {!Array.<!shaka.dash.DashParser.AdaptationInfo>} adaptationSets
  751. * @param {string} contentType
  752. @private
  753. */
  754. getStreamsFromSets_(disabled, adaptationSets, contentType) {
  755. if (disabled || !adaptationSets.length) {
  756. return [];
  757. }
  758. return adaptationSets.reduce((all, part) => {
  759. if (part.contentType != contentType) {
  760. return all;
  761. }
  762. all.push(...part.streams);
  763. return all;
  764. }, []);
  765. }
  766. /**
  767. * Parses an AdaptationSet XML element.
  768. *
  769. * @param {shaka.dash.DashParser.Context} context
  770. * @param {!Element} elem The AdaptationSet element.
  771. * @return {?shaka.dash.DashParser.AdaptationInfo}
  772. * @private
  773. */
  774. parseAdaptationSet_(context, elem) {
  775. const XmlUtils = shaka.util.XmlUtils;
  776. const Functional = shaka.util.Functional;
  777. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  778. const ContentType = ManifestParserUtils.ContentType;
  779. const ContentProtection = shaka.dash.ContentProtection;
  780. context.adaptationSet = this.createFrame_(elem, context.period, null);
  781. let main = false;
  782. const roleElements = XmlUtils.findChildren(elem, 'Role');
  783. const roleValues = roleElements.map((role) => {
  784. return role.getAttribute('value');
  785. }).filter(Functional.isNotNull);
  786. // Default kind for text streams is 'subtitle' if unspecified in the
  787. // manifest.
  788. let kind = undefined;
  789. const isText = context.adaptationSet.contentType == ContentType.TEXT;
  790. if (isText) {
  791. kind = ManifestParserUtils.TextStreamKind.SUBTITLE;
  792. }
  793. for (const roleElement of roleElements) {
  794. const scheme = roleElement.getAttribute('schemeIdUri');
  795. if (scheme == null || scheme == 'urn:mpeg:dash:role:2011') {
  796. // These only apply for the given scheme, but allow them to be specified
  797. // if there is no scheme specified.
  798. // See: DASH section 5.8.5.5
  799. const value = roleElement.getAttribute('value');
  800. switch (value) {
  801. case 'main':
  802. main = true;
  803. break;
  804. case 'caption':
  805. case 'subtitle':
  806. kind = value;
  807. break;
  808. }
  809. }
  810. }
  811. // Parallel for HLS VIDEO-RANGE as defined in DASH-IF IOP v4.3 6.2.5.1.
  812. let videoRange;
  813. // Ref. https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf
  814. // If signaled, a Supplemental or Essential Property descriptor
  815. // shall be used, with the schemeIdUri set to
  816. // urn:mpeg:mpegB:cicp:<Parameter> as defined in
  817. // ISO/IEC 23001-8 [49] and <Parameter> one of the
  818. // following: ColourPrimaries, TransferCharacteristics,
  819. // or MatrixCoefficients.
  820. const scheme = 'urn:mpeg:mpegB:cicp';
  821. const transferCharacteristicsScheme = `${scheme}:TransferCharacteristics`;
  822. const colourPrimariesScheme = `${scheme}:ColourPrimaries`;
  823. const matrixCoefficientsScheme = `${scheme}:MatrixCoefficients`;
  824. const getVideoRangeFromTransferCharacteristicCICP = (cicp) => {
  825. switch (cicp) {
  826. case 1:
  827. case 6:
  828. case 13:
  829. case 14:
  830. case 15:
  831. return 'SDR';
  832. case 16:
  833. return 'PQ';
  834. case 18:
  835. return 'HLG';
  836. }
  837. return undefined;
  838. };
  839. const essentialProperties =
  840. XmlUtils.findChildren(elem, 'EssentialProperty');
  841. // ID of real AdaptationSet if this is a trick mode set:
  842. let trickModeFor = null;
  843. let unrecognizedEssentialProperty = false;
  844. for (const prop of essentialProperties) {
  845. const schemeId = prop.getAttribute('schemeIdUri');
  846. if (schemeId == 'http://dashif.org/guidelines/trickmode') {
  847. trickModeFor = prop.getAttribute('value');
  848. } else if (schemeId == transferCharacteristicsScheme) {
  849. videoRange = getVideoRangeFromTransferCharacteristicCICP(
  850. parseInt(prop.getAttribute('value'), 10),
  851. );
  852. } else if (schemeId == colourPrimariesScheme ||
  853. schemeId == matrixCoefficientsScheme) {
  854. continue;
  855. } else {
  856. unrecognizedEssentialProperty = true;
  857. }
  858. }
  859. const supplementalProperties =
  860. XmlUtils.findChildren(elem, 'SupplementalProperty');
  861. for (const prop of supplementalProperties) {
  862. const schemeId = prop.getAttribute('schemeIdUri');
  863. if (schemeId == transferCharacteristicsScheme) {
  864. videoRange = getVideoRangeFromTransferCharacteristicCICP(
  865. parseInt(prop.getAttribute('value'), 10),
  866. );
  867. }
  868. }
  869. const accessibilities = XmlUtils.findChildren(elem, 'Accessibility');
  870. const LanguageUtils = shaka.util.LanguageUtils;
  871. const closedCaptions = new Map();
  872. for (const prop of accessibilities) {
  873. const schemeId = prop.getAttribute('schemeIdUri');
  874. const value = prop.getAttribute('value');
  875. if (schemeId == 'urn:scte:dash:cc:cea-608:2015' ) {
  876. let channelId = 1;
  877. if (value != null) {
  878. const channelAssignments = value.split(';');
  879. for (const captionStr of channelAssignments) {
  880. let channel;
  881. let language;
  882. // Some closed caption descriptions have channel number and
  883. // language ("CC1=eng") others may only have language ("eng,spa").
  884. if (!captionStr.includes('=')) {
  885. // When the channel assignemnts are not explicitly provided and
  886. // there are only 2 values provided, it is highly likely that the
  887. // assignments are CC1 and CC3 (most commonly used CC streams).
  888. // Otherwise, cycle through all channels arbitrarily (CC1 - CC4)
  889. // in order of provided langs.
  890. channel = `CC${channelId}`;
  891. if (channelAssignments.length == 2) {
  892. channelId += 2;
  893. } else {
  894. channelId ++;
  895. }
  896. language = captionStr;
  897. } else {
  898. const channelAndLanguage = captionStr.split('=');
  899. // The channel info can be '1' or 'CC1'.
  900. // If the channel info only has channel number(like '1'), add 'CC'
  901. // as prefix so that it can be a full channel id (like 'CC1').
  902. channel = channelAndLanguage[0].startsWith('CC') ?
  903. channelAndLanguage[0] : `CC${channelAndLanguage[0]}`;
  904. // 3 letters (ISO 639-2). In b/187442669, we saw a blank string
  905. // (CC2=;CC3=), so default to "und" (the code for "undetermined").
  906. language = channelAndLanguage[1] || 'und';
  907. }
  908. closedCaptions.set(channel, LanguageUtils.normalize(language));
  909. }
  910. } else {
  911. // If channel and language information has not been provided, assign
  912. // 'CC1' as channel id and 'und' as language info.
  913. closedCaptions.set('CC1', 'und');
  914. }
  915. } else if (schemeId == 'urn:scte:dash:cc:cea-708:2015') {
  916. let serviceNumber = 1;
  917. if (value != null) {
  918. for (const captionStr of value.split(';')) {
  919. let service;
  920. let language;
  921. // Similar to CEA-608, it is possible that service # assignments
  922. // are not explicitly provided e.g. "eng;deu;swe" In this case,
  923. // we just cycle through the services for each language one by one.
  924. if (!captionStr.includes('=')) {
  925. service = `svc${serviceNumber}`;
  926. serviceNumber ++;
  927. language = captionStr;
  928. } else {
  929. // Otherwise, CEA-708 caption values take the form "
  930. // 1=lang:eng;2=lang:deu" i.e. serviceNumber=lang:threelettercode.
  931. const serviceAndLanguage = captionStr.split('=');
  932. service = `svc${serviceAndLanguage[0]}`;
  933. // The language info can be different formats, lang:eng',
  934. // or 'lang:eng,war:1,er:1'. Extract the language info.
  935. language = serviceAndLanguage[1].split(',')[0].split(':').pop();
  936. }
  937. closedCaptions.set(service, LanguageUtils.normalize(language));
  938. }
  939. } else {
  940. // If service and language information has not been provided, assign
  941. // 'svc1' as service number and 'und' as language info.
  942. closedCaptions.set('svc1', 'und');
  943. }
  944. } else if (schemeId == 'urn:mpeg:dash:role:2011') {
  945. // See DASH IOP 3.9.2 Table 4.
  946. if (value != null) {
  947. roleValues.push(value);
  948. if (value == 'captions') {
  949. kind = ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
  950. }
  951. }
  952. }
  953. }
  954. // According to DASH spec (2014) section 5.8.4.8, "the successful processing
  955. // of the descriptor is essential to properly use the information in the
  956. // parent element". According to DASH IOP v3.3, section 3.3.4, "if the
  957. // scheme or the value" for EssentialProperty is not recognized, "the DASH
  958. // client shall ignore the parent element."
  959. if (unrecognizedEssentialProperty) {
  960. // Stop parsing this AdaptationSet and let the caller filter out the
  961. // nulls.
  962. return null;
  963. }
  964. const contentProtectionElems =
  965. XmlUtils.findChildren(elem, 'ContentProtection');
  966. const contentProtection = ContentProtection.parseFromAdaptationSet(
  967. contentProtectionElems,
  968. this.config_.dash.ignoreDrmInfo,
  969. this.config_.dash.keySystemsByURI);
  970. const language =
  971. shaka.util.LanguageUtils.normalize(elem.getAttribute('lang') || 'und');
  972. // This attribute is currently non-standard, but it is supported by Kaltura.
  973. let label = elem.getAttribute('label');
  974. // See DASH IOP 4.3 here https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf (page 35)
  975. const labelElements = XmlUtils.findChildren(elem, 'Label');
  976. if (labelElements && labelElements.length) {
  977. // NOTE: Right now only one label field is supported.
  978. const firstLabelElement = labelElements[0];
  979. if (firstLabelElement.textContent) {
  980. label = firstLabelElement.textContent;
  981. }
  982. }
  983. // Parse Representations into Streams.
  984. const representations = XmlUtils.findChildren(elem, 'Representation');
  985. const streams = representations.map((representation) => {
  986. const parsedRepresentation = this.parseRepresentation_(context,
  987. contentProtection, kind, language, label, main, roleValues,
  988. closedCaptions, representation);
  989. if (parsedRepresentation) {
  990. parsedRepresentation.hdr = parsedRepresentation.hdr || videoRange;
  991. }
  992. return parsedRepresentation;
  993. }).filter((s) => !!s);
  994. if (streams.length == 0) {
  995. const isImage = context.adaptationSet.contentType == ContentType.IMAGE;
  996. // Ignore empty AdaptationSets if ignoreEmptyAdaptationSet is true
  997. // or they are for text/image content.
  998. if (this.config_.dash.ignoreEmptyAdaptationSet || isText || isImage) {
  999. return null;
  1000. }
  1001. throw new shaka.util.Error(
  1002. shaka.util.Error.Severity.CRITICAL,
  1003. shaka.util.Error.Category.MANIFEST,
  1004. shaka.util.Error.Code.DASH_EMPTY_ADAPTATION_SET);
  1005. }
  1006. // If AdaptationSet's type is unknown or is ambiguously "application",
  1007. // guess based on the information in the first stream. If the attributes
  1008. // mimeType and codecs are split across levels, they will both be inherited
  1009. // down to the stream level by this point, so the stream will have all the
  1010. // necessary information.
  1011. if (!context.adaptationSet.contentType ||
  1012. context.adaptationSet.contentType == ContentType.APPLICATION) {
  1013. const mimeType = streams[0].mimeType;
  1014. const codecs = streams[0].codecs;
  1015. context.adaptationSet.contentType =
  1016. shaka.dash.DashParser.guessContentType_(mimeType, codecs);
  1017. for (const stream of streams) {
  1018. stream.type = context.adaptationSet.contentType;
  1019. }
  1020. }
  1021. for (const stream of streams) {
  1022. // Some DRM license providers require that we have a default
  1023. // key ID from the manifest in the wrapped license request.
  1024. // Thus, it should be put in drmInfo to be accessible to request filters.
  1025. for (const drmInfo of contentProtection.drmInfos) {
  1026. drmInfo.keyIds = drmInfo.keyIds && stream.keyIds ?
  1027. new Set([...drmInfo.keyIds, ...stream.keyIds]) :
  1028. drmInfo.keyIds || stream.keyIds;
  1029. }
  1030. }
  1031. const repIds = representations
  1032. .map((node) => { return node.getAttribute('id'); })
  1033. .filter(shaka.util.Functional.isNotNull);
  1034. return {
  1035. id: context.adaptationSet.id || ('__fake__' + this.globalId_++),
  1036. contentType: context.adaptationSet.contentType,
  1037. language: language,
  1038. main: main,
  1039. streams: streams,
  1040. drmInfos: contentProtection.drmInfos,
  1041. trickModeFor: trickModeFor,
  1042. representationIds: repIds,
  1043. };
  1044. }
  1045. /**
  1046. * Parses a Representation XML element.
  1047. *
  1048. * @param {shaka.dash.DashParser.Context} context
  1049. * @param {shaka.dash.ContentProtection.Context} contentProtection
  1050. * @param {(string|undefined)} kind
  1051. * @param {string} language
  1052. * @param {string} label
  1053. * @param {boolean} isPrimary
  1054. * @param {!Array.<string>} roles
  1055. * @param {Map.<string, string>} closedCaptions
  1056. * @param {!Element} node
  1057. * @return {?shaka.extern.Stream} The Stream, or null when there is a
  1058. * non-critical parsing error.
  1059. * @private
  1060. */
  1061. parseRepresentation_(context, contentProtection, kind, language, label,
  1062. isPrimary, roles, closedCaptions, node) {
  1063. const XmlUtils = shaka.util.XmlUtils;
  1064. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1065. context.representation =
  1066. this.createFrame_(node, context.adaptationSet, null);
  1067. this.minTotalAvailabilityTimeOffset_ =
  1068. Math.min(this.minTotalAvailabilityTimeOffset_,
  1069. context.representation.availabilityTimeOffset);
  1070. if (!this.verifyRepresentation_(context.representation)) {
  1071. shaka.log.warning('Skipping Representation', context.representation);
  1072. return null;
  1073. }
  1074. const periodStart = context.periodInfo.start;
  1075. // NOTE: bandwidth is a mandatory attribute according to the spec, and zero
  1076. // does not make sense in the DASH spec's bandwidth formulas.
  1077. // In some content, however, the attribute is missing or zero.
  1078. // To avoid NaN at the variant level on broken content, fall back to zero.
  1079. // https://github.com/shaka-project/shaka-player/issues/938#issuecomment-317278180
  1080. context.bandwidth =
  1081. XmlUtils.parseAttr(node, 'bandwidth', XmlUtils.parsePositiveInt) || 0;
  1082. /** @type {?shaka.dash.DashParser.StreamInfo} */
  1083. let streamInfo;
  1084. const contentType = context.representation.contentType;
  1085. const isText = contentType == ContentType.TEXT ||
  1086. contentType == ContentType.APPLICATION;
  1087. const isImage = contentType == ContentType.IMAGE;
  1088. try {
  1089. const requestInitSegment = (uris, startByte, endByte) => {
  1090. return this.requestInitSegment_(uris, startByte, endByte);
  1091. };
  1092. if (context.representation.segmentBase) {
  1093. streamInfo = shaka.dash.SegmentBase.createStreamInfo(
  1094. context, requestInitSegment);
  1095. } else if (context.representation.segmentList) {
  1096. streamInfo = shaka.dash.SegmentList.createStreamInfo(
  1097. context, this.streamMap_);
  1098. } else if (context.representation.segmentTemplate) {
  1099. const hasManifest = !!this.manifest_;
  1100. streamInfo = shaka.dash.SegmentTemplate.createStreamInfo(
  1101. context, requestInitSegment, this.streamMap_, hasManifest,
  1102. this.config_.dash.initialSegmentLimit, this.periodDurations_);
  1103. } else {
  1104. goog.asserts.assert(isText,
  1105. 'Must have Segment* with non-text streams.');
  1106. const baseUris = context.representation.baseUris;
  1107. const duration = context.periodInfo.duration || 0;
  1108. streamInfo = {
  1109. generateSegmentIndex: () => {
  1110. return Promise.resolve(shaka.media.SegmentIndex.forSingleSegment(
  1111. periodStart, duration, baseUris));
  1112. },
  1113. };
  1114. }
  1115. } catch (error) {
  1116. if ((isText || isImage) &&
  1117. error.code == shaka.util.Error.Code.DASH_NO_SEGMENT_INFO) {
  1118. // We will ignore any DASH_NO_SEGMENT_INFO errors for text/image
  1119. // streams.
  1120. return null;
  1121. }
  1122. // For anything else, re-throw.
  1123. throw error;
  1124. }
  1125. const contentProtectionElems =
  1126. XmlUtils.findChildren(node, 'ContentProtection');
  1127. const keyId = shaka.dash.ContentProtection.parseFromRepresentation(
  1128. contentProtectionElems, contentProtection,
  1129. this.config_.dash.ignoreDrmInfo,
  1130. this.config_.dash.keySystemsByURI);
  1131. const keyIds = new Set(keyId ? [keyId] : []);
  1132. // Detect the presence of E-AC3 JOC audio content, using DD+JOC signaling.
  1133. // See: ETSI TS 103 420 V1.2.1 (2018-10)
  1134. const supplementalPropertyElems =
  1135. XmlUtils.findChildren(node, 'SupplementalProperty');
  1136. const hasJoc = supplementalPropertyElems.some((element) => {
  1137. const expectedUri = 'tag:dolby.com,2018:dash:EC3_ExtensionType:2018';
  1138. const expectedValue = 'JOC';
  1139. return element.getAttribute('schemeIdUri') == expectedUri &&
  1140. element.getAttribute('value') == expectedValue;
  1141. });
  1142. let spatialAudio = false;
  1143. if (hasJoc) {
  1144. spatialAudio = true;
  1145. }
  1146. let forced = false;
  1147. if (isText) {
  1148. // See: https://github.com/shaka-project/shaka-player/issues/2122 and
  1149. // https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/165
  1150. forced = roles.includes('forced_subtitle') ||
  1151. roles.includes('forced-subtitle');
  1152. }
  1153. let tilesLayout;
  1154. if (isImage) {
  1155. const essentialPropertyElems =
  1156. XmlUtils.findChildren(node, 'EssentialProperty');
  1157. const thumbnailTileElem = essentialPropertyElems.find((element) => {
  1158. const expectedUris = [
  1159. 'http://dashif.org/thumbnail_tile',
  1160. 'http://dashif.org/guidelines/thumbnail_tile',
  1161. ];
  1162. return expectedUris.includes(element.getAttribute('schemeIdUri'));
  1163. });
  1164. if (thumbnailTileElem) {
  1165. tilesLayout = thumbnailTileElem.getAttribute('value');
  1166. }
  1167. // Filter image adaptation sets that has no tilesLayout.
  1168. if (!tilesLayout) {
  1169. return null;
  1170. }
  1171. }
  1172. let hdr;
  1173. const profiles = context.profiles;
  1174. const codecs = context.representation.codecs;
  1175. const hevcHDR = 'http://dashif.org/guidelines/dash-if-uhd#hevc-hdr-pq10';
  1176. if (profiles.includes(hevcHDR) && (codecs.includes('hvc1.2.4.L153.B0') ||
  1177. codecs.includes('hev1.2.4.L153.B0'))) {
  1178. hdr = 'PQ';
  1179. }
  1180. const contextId = context.representation.id ?
  1181. context.period.id + ',' + context.representation.id : '';
  1182. /** @type {shaka.extern.Stream} */
  1183. let stream;
  1184. if (contextId && this.streamMap_[contextId]) {
  1185. stream = this.streamMap_[contextId];
  1186. } else {
  1187. stream = {
  1188. id: this.globalId_++,
  1189. originalId: context.representation.id,
  1190. createSegmentIndex: () => Promise.resolve(),
  1191. closeSegmentIndex: () => {
  1192. if (stream.segmentIndex) {
  1193. stream.segmentIndex.release();
  1194. stream.segmentIndex = null;
  1195. }
  1196. },
  1197. segmentIndex: null,
  1198. mimeType: context.representation.mimeType,
  1199. codecs: context.representation.codecs,
  1200. frameRate: context.representation.frameRate,
  1201. pixelAspectRatio: context.representation.pixelAspectRatio,
  1202. bandwidth: context.bandwidth,
  1203. width: context.representation.width,
  1204. height: context.representation.height,
  1205. kind,
  1206. encrypted: contentProtection.drmInfos.length > 0,
  1207. drmInfos: contentProtection.drmInfos,
  1208. keyIds,
  1209. language,
  1210. label,
  1211. type: context.adaptationSet.contentType,
  1212. primary: isPrimary,
  1213. trickModeVideo: null,
  1214. emsgSchemeIdUris:
  1215. context.representation.emsgSchemeIdUris,
  1216. roles,
  1217. forced,
  1218. channelsCount: context.representation.numChannels,
  1219. audioSamplingRate: context.representation.audioSamplingRate,
  1220. spatialAudio,
  1221. closedCaptions,
  1222. hdr,
  1223. tilesLayout,
  1224. matchedStreams: [],
  1225. external: false,
  1226. };
  1227. }
  1228. stream.createSegmentIndex = async () => {
  1229. if (!stream.segmentIndex) {
  1230. stream.segmentIndex = await streamInfo.generateSegmentIndex();
  1231. }
  1232. };
  1233. if (contextId && context.dynamic && !this.streamMap_[contextId]) {
  1234. this.streamMap_[contextId] = stream;
  1235. }
  1236. return stream;
  1237. }
  1238. /**
  1239. * Called when the update timer ticks.
  1240. *
  1241. * @return {!Promise}
  1242. * @private
  1243. */
  1244. async onUpdate_() {
  1245. goog.asserts.assert(this.updatePeriod_ >= 0,
  1246. 'There should be an update period');
  1247. shaka.log.info('Updating manifest...');
  1248. // Default the update delay to 0 seconds so that if there is an error we can
  1249. // try again right away.
  1250. let updateDelay = 0;
  1251. try {
  1252. updateDelay = await this.requestManifest_();
  1253. } catch (error) {
  1254. goog.asserts.assert(error instanceof shaka.util.Error,
  1255. 'Should only receive a Shaka error');
  1256. // Try updating again, but ensure we haven't been destroyed.
  1257. if (this.playerInterface_) {
  1258. // We will retry updating, so override the severity of the error.
  1259. error.severity = shaka.util.Error.Severity.RECOVERABLE;
  1260. this.playerInterface_.onError(error);
  1261. }
  1262. }
  1263. // Detect a call to stop()
  1264. if (!this.playerInterface_) {
  1265. return;
  1266. }
  1267. this.setUpdateTimer_(updateDelay);
  1268. }
  1269. /**
  1270. * Sets the update timer. Does nothing if the manifest does not specify an
  1271. * update period.
  1272. *
  1273. * @param {number} offset An offset, in seconds, to apply to the manifest's
  1274. * update period.
  1275. * @private
  1276. */
  1277. setUpdateTimer_(offset) {
  1278. // NOTE: An updatePeriod_ of -1 means the attribute was missing.
  1279. // An attribute which is present and set to 0 should still result in
  1280. // periodic updates. For more, see:
  1281. // https://github.com/Dash-Industry-Forum/Guidelines-TimingModel/issues/48
  1282. if (this.updatePeriod_ < 0) {
  1283. return;
  1284. }
  1285. const finalDelay = Math.max(
  1286. shaka.dash.DashParser.MIN_UPDATE_PERIOD_,
  1287. this.updatePeriod_ - offset,
  1288. this.averageUpdateDuration_.getEstimate());
  1289. // We do not run the timer as repeating because part of update is async and
  1290. // we need schedule the update after it finished.
  1291. this.updateTimer_.tickAfter(/* seconds= */ finalDelay);
  1292. }
  1293. /**
  1294. * Creates a new inheritance frame for the given element.
  1295. *
  1296. * @param {!Element} elem
  1297. * @param {?shaka.dash.DashParser.InheritanceFrame} parent
  1298. * @param {Array.<string>} baseUris
  1299. * @return {shaka.dash.DashParser.InheritanceFrame}
  1300. * @private
  1301. */
  1302. createFrame_(elem, parent, baseUris) {
  1303. goog.asserts.assert(parent || baseUris,
  1304. 'Must provide either parent or baseUris');
  1305. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  1306. const XmlUtils = shaka.util.XmlUtils;
  1307. parent = parent || /** @type {shaka.dash.DashParser.InheritanceFrame} */ ({
  1308. contentType: '',
  1309. mimeType: '',
  1310. codecs: '',
  1311. emsgSchemeIdUris: [],
  1312. frameRate: undefined,
  1313. pixelAspectRatio: undefined,
  1314. numChannels: null,
  1315. audioSamplingRate: null,
  1316. availabilityTimeOffset: 0,
  1317. });
  1318. baseUris = baseUris || parent.baseUris;
  1319. const parseNumber = XmlUtils.parseNonNegativeInt;
  1320. const evalDivision = XmlUtils.evalDivision;
  1321. const uriObjs = XmlUtils.findChildren(elem, 'BaseURL');
  1322. const uris = uriObjs.map(XmlUtils.getContents);
  1323. let contentType = elem.getAttribute('contentType') || parent.contentType;
  1324. const mimeType = elem.getAttribute('mimeType') || parent.mimeType;
  1325. const codecs = elem.getAttribute('codecs') || parent.codecs;
  1326. const frameRate =
  1327. XmlUtils.parseAttr(elem, 'frameRate', evalDivision) || parent.frameRate;
  1328. const pixelAspectRatio =
  1329. elem.getAttribute('sar') || parent.pixelAspectRatio;
  1330. const emsgSchemeIdUris = this.emsgSchemeIdUris_(
  1331. XmlUtils.findChildren(elem, 'InbandEventStream'),
  1332. parent.emsgSchemeIdUris);
  1333. const audioChannelConfigs =
  1334. XmlUtils.findChildren(elem, 'AudioChannelConfiguration');
  1335. const numChannels =
  1336. this.parseAudioChannels_(audioChannelConfigs) || parent.numChannels;
  1337. const audioSamplingRate =
  1338. XmlUtils.parseAttr(elem, 'audioSamplingRate', parseNumber) ||
  1339. parent.audioSamplingRate;
  1340. if (!contentType) {
  1341. contentType = shaka.dash.DashParser.guessContentType_(mimeType, codecs);
  1342. }
  1343. const segmentBase = XmlUtils.findChild(elem, 'SegmentBase');
  1344. const segmentTemplate = XmlUtils.findChild(elem, 'SegmentTemplate');
  1345. // The availabilityTimeOffset is the sum of all @availabilityTimeOffset
  1346. // values that apply to the adaptation set, via BaseURL, SegmentBase,
  1347. // or SegmentTemplate elements.
  1348. const segmentBaseAto = segmentBase ?
  1349. (XmlUtils.parseAttr(segmentBase, 'availabilityTimeOffset',
  1350. XmlUtils.parseFloat) || 0) : 0;
  1351. const segmentTemplateAto = segmentTemplate ?
  1352. (XmlUtils.parseAttr(segmentTemplate, 'availabilityTimeOffset',
  1353. XmlUtils.parseFloat) || 0) : 0;
  1354. const baseUriAto = uriObjs && uriObjs.length ?
  1355. (XmlUtils.parseAttr(uriObjs[0], 'availabilityTimeOffset',
  1356. XmlUtils.parseFloat) || 0) : 0;
  1357. const availabilityTimeOffset = parent.availabilityTimeOffset + baseUriAto +
  1358. segmentBaseAto + segmentTemplateAto;
  1359. return {
  1360. baseUris: ManifestParserUtils.resolveUris(baseUris, uris),
  1361. segmentBase: segmentBase || parent.segmentBase,
  1362. segmentList:
  1363. XmlUtils.findChild(elem, 'SegmentList') || parent.segmentList,
  1364. segmentTemplate: segmentTemplate || parent.segmentTemplate,
  1365. width: XmlUtils.parseAttr(elem, 'width', parseNumber) || parent.width,
  1366. height: XmlUtils.parseAttr(elem, 'height', parseNumber) || parent.height,
  1367. contentType: contentType,
  1368. mimeType: mimeType,
  1369. codecs: codecs,
  1370. frameRate: frameRate,
  1371. pixelAspectRatio: pixelAspectRatio,
  1372. emsgSchemeIdUris: emsgSchemeIdUris,
  1373. id: elem.getAttribute('id'),
  1374. numChannels: numChannels,
  1375. audioSamplingRate: audioSamplingRate,
  1376. availabilityTimeOffset: availabilityTimeOffset,
  1377. };
  1378. }
  1379. /**
  1380. * Returns a new array of InbandEventStream schemeIdUri containing the union
  1381. * of the ones parsed from inBandEventStreams and the ones provided in
  1382. * emsgSchemeIdUris.
  1383. *
  1384. * @param {!Array.<!Element>} inBandEventStreams Array of InbandEventStream
  1385. * elements to parse and add to the returned array.
  1386. * @param {!Array.<string>} emsgSchemeIdUris Array of parsed
  1387. * InbandEventStream schemeIdUri attributes to add to the returned array.
  1388. * @return {!Array.<string>} schemeIdUris Array of parsed
  1389. * InbandEventStream schemeIdUri attributes.
  1390. * @private
  1391. */
  1392. emsgSchemeIdUris_(inBandEventStreams, emsgSchemeIdUris) {
  1393. const schemeIdUris = emsgSchemeIdUris.slice();
  1394. for (const event of inBandEventStreams) {
  1395. const schemeIdUri = event.getAttribute('schemeIdUri');
  1396. if (!schemeIdUris.includes(schemeIdUri)) {
  1397. schemeIdUris.push(schemeIdUri);
  1398. }
  1399. }
  1400. return schemeIdUris;
  1401. }
  1402. /**
  1403. * @param {!Array.<!Element>} audioChannelConfigs An array of
  1404. * AudioChannelConfiguration elements.
  1405. * @return {?number} The number of audio channels, or null if unknown.
  1406. * @private
  1407. */
  1408. parseAudioChannels_(audioChannelConfigs) {
  1409. for (const elem of audioChannelConfigs) {
  1410. const scheme = elem.getAttribute('schemeIdUri');
  1411. if (!scheme) {
  1412. continue;
  1413. }
  1414. const value = elem.getAttribute('value');
  1415. if (!value) {
  1416. continue;
  1417. }
  1418. switch (scheme) {
  1419. case 'urn:mpeg:dash:outputChannelPositionList:2012':
  1420. // A space-separated list of speaker positions, so the number of
  1421. // channels is the length of this list.
  1422. return value.trim().split(/ +/).length;
  1423. case 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011':
  1424. case 'urn:dts:dash:audio_channel_configuration:2012': {
  1425. // As far as we can tell, this is a number of channels.
  1426. const intValue = parseInt(value, 10);
  1427. if (!intValue) { // 0 or NaN
  1428. shaka.log.warning('Channel parsing failure! ' +
  1429. 'Ignoring scheme and value', scheme, value);
  1430. continue;
  1431. }
  1432. return intValue;
  1433. }
  1434. case 'tag:dolby.com,2014:dash:audio_channel_configuration:2011':
  1435. case 'urn:dolby:dash:audio_channel_configuration:2011': {
  1436. // A hex-encoded 16-bit integer, in which each bit represents a
  1437. // channel.
  1438. let hexValue = parseInt(value, 16);
  1439. if (!hexValue) { // 0 or NaN
  1440. shaka.log.warning('Channel parsing failure! ' +
  1441. 'Ignoring scheme and value', scheme, value);
  1442. continue;
  1443. }
  1444. // Count the 1-bits in hexValue.
  1445. let numBits = 0;
  1446. while (hexValue) {
  1447. if (hexValue & 1) {
  1448. ++numBits;
  1449. }
  1450. hexValue >>= 1;
  1451. }
  1452. return numBits;
  1453. }
  1454. // Defined by https://dashif.org/identifiers/audio_source_metadata/ and clause 8.2, in ISO/IEC 23001-8.
  1455. case 'urn:mpeg:mpegB:cicp:ChannelConfiguration': {
  1456. const noValue = 0;
  1457. const channelCountMapping = [
  1458. noValue, 1, 2, 3, 4, 5, 6, 8, 2, 3, /* 0--9 */
  1459. 4, 7, 8, 24, 8, 12, 10, 12, 14, 12, /* 10--19 */
  1460. 14, /* 20 */
  1461. ];
  1462. const intValue = parseInt(value, 10);
  1463. if (!intValue) { // 0 or NaN
  1464. shaka.log.warning('Channel parsing failure! ' +
  1465. 'Ignoring scheme and value', scheme, value);
  1466. continue;
  1467. }
  1468. if (intValue > noValue && intValue < channelCountMapping.length) {
  1469. return channelCountMapping[intValue];
  1470. }
  1471. continue;
  1472. }
  1473. default:
  1474. shaka.log.warning(
  1475. 'Unrecognized audio channel scheme:', scheme, value);
  1476. continue;
  1477. }
  1478. }
  1479. return null;
  1480. }
  1481. /**
  1482. * Verifies that a Representation has exactly one Segment* element. Prints
  1483. * warnings if there is a problem.
  1484. *
  1485. * @param {shaka.dash.DashParser.InheritanceFrame} frame
  1486. * @return {boolean} True if the Representation is usable; otherwise return
  1487. * false.
  1488. * @private
  1489. */
  1490. verifyRepresentation_(frame) {
  1491. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1492. let n = 0;
  1493. n += frame.segmentBase ? 1 : 0;
  1494. n += frame.segmentList ? 1 : 0;
  1495. n += frame.segmentTemplate ? 1 : 0;
  1496. if (n == 0) {
  1497. // TODO: Extend with the list of MIME types registered to TextEngine.
  1498. if (frame.contentType == ContentType.TEXT ||
  1499. frame.contentType == ContentType.APPLICATION) {
  1500. return true;
  1501. } else {
  1502. shaka.log.warning(
  1503. 'Representation does not contain a segment information source:',
  1504. 'the Representation must contain one of SegmentBase, SegmentList,',
  1505. 'SegmentTemplate, or explicitly indicate that it is "text".',
  1506. frame);
  1507. return false;
  1508. }
  1509. }
  1510. if (n != 1) {
  1511. shaka.log.warning(
  1512. 'Representation contains multiple segment information sources:',
  1513. 'the Representation should only contain one of SegmentBase,',
  1514. 'SegmentList, or SegmentTemplate.',
  1515. frame);
  1516. if (frame.segmentBase) {
  1517. shaka.log.info('Using SegmentBase by default.');
  1518. frame.segmentList = null;
  1519. frame.segmentTemplate = null;
  1520. } else {
  1521. goog.asserts.assert(frame.segmentList, 'There should be a SegmentList');
  1522. shaka.log.info('Using SegmentList by default.');
  1523. frame.segmentTemplate = null;
  1524. }
  1525. }
  1526. return true;
  1527. }
  1528. /**
  1529. * Makes a request to the given URI and calculates the clock offset.
  1530. *
  1531. * @param {!Array.<string>} baseUris
  1532. * @param {string} uri
  1533. * @param {string} method
  1534. * @return {!Promise.<number>}
  1535. * @private
  1536. */
  1537. async requestForTiming_(baseUris, uri, method) {
  1538. const requestUris =
  1539. shaka.util.ManifestParserUtils.resolveUris(baseUris, [uri]);
  1540. const request = shaka.net.NetworkingEngine.makeRequest(
  1541. requestUris, this.config_.retryParameters);
  1542. request.method = method;
  1543. const type = shaka.net.NetworkingEngine.RequestType.TIMING;
  1544. const operation =
  1545. this.playerInterface_.networkingEngine.request(type, request);
  1546. this.operationManager_.manage(operation);
  1547. const response = await operation.promise;
  1548. let text;
  1549. if (method == 'HEAD') {
  1550. if (!response.headers || !response.headers['date']) {
  1551. shaka.log.warning('UTC timing response is missing',
  1552. 'expected date header');
  1553. return 0;
  1554. }
  1555. text = response.headers['date'];
  1556. } else {
  1557. text = shaka.util.StringUtils.fromUTF8(response.data);
  1558. }
  1559. const date = Date.parse(text);
  1560. if (isNaN(date)) {
  1561. shaka.log.warning('Unable to parse date from UTC timing response');
  1562. return 0;
  1563. }
  1564. return (date - Date.now());
  1565. }
  1566. /**
  1567. * Parses an array of UTCTiming elements.
  1568. *
  1569. * @param {!Array.<string>} baseUris
  1570. * @param {!Array.<!Element>} elems
  1571. * @return {!Promise.<number>}
  1572. * @private
  1573. */
  1574. async parseUtcTiming_(baseUris, elems) {
  1575. const schemesAndValues = elems.map((elem) => {
  1576. return {
  1577. scheme: elem.getAttribute('schemeIdUri'),
  1578. value: elem.getAttribute('value'),
  1579. };
  1580. });
  1581. // If there's nothing specified in the manifest, but we have a default from
  1582. // the config, use that.
  1583. const clockSyncUri = this.config_.dash.clockSyncUri;
  1584. if (!schemesAndValues.length && clockSyncUri) {
  1585. schemesAndValues.push({
  1586. scheme: 'urn:mpeg:dash:utc:http-head:2014',
  1587. value: clockSyncUri,
  1588. });
  1589. }
  1590. for (const sv of schemesAndValues) {
  1591. try {
  1592. const scheme = sv.scheme;
  1593. const value = sv.value;
  1594. switch (scheme) {
  1595. // See DASH IOP Guidelines Section 4.7
  1596. // https://bit.ly/DashIop3-2
  1597. // Some old ISO23009-1 drafts used 2012.
  1598. case 'urn:mpeg:dash:utc:http-head:2014':
  1599. case 'urn:mpeg:dash:utc:http-head:2012':
  1600. // eslint-disable-next-line no-await-in-loop
  1601. return await this.requestForTiming_(baseUris, value, 'HEAD');
  1602. case 'urn:mpeg:dash:utc:http-xsdate:2014':
  1603. case 'urn:mpeg:dash:utc:http-iso:2014':
  1604. case 'urn:mpeg:dash:utc:http-xsdate:2012':
  1605. case 'urn:mpeg:dash:utc:http-iso:2012':
  1606. // eslint-disable-next-line no-await-in-loop
  1607. return await this.requestForTiming_(baseUris, value, 'GET');
  1608. case 'urn:mpeg:dash:utc:direct:2014':
  1609. case 'urn:mpeg:dash:utc:direct:2012': {
  1610. const date = Date.parse(value);
  1611. return isNaN(date) ? 0 : (date - Date.now());
  1612. }
  1613. case 'urn:mpeg:dash:utc:http-ntp:2014':
  1614. case 'urn:mpeg:dash:utc:ntp:2014':
  1615. case 'urn:mpeg:dash:utc:sntp:2014':
  1616. shaka.log.alwaysWarn('NTP UTCTiming scheme is not supported');
  1617. break;
  1618. default:
  1619. shaka.log.alwaysWarn(
  1620. 'Unrecognized scheme in UTCTiming element', scheme);
  1621. break;
  1622. }
  1623. } catch (e) {
  1624. shaka.log.warning('Error fetching time from UTCTiming elem', e.message);
  1625. }
  1626. }
  1627. shaka.log.alwaysWarn(
  1628. 'A UTCTiming element should always be given in live manifests! ' +
  1629. 'This content may not play on clients with bad clocks!');
  1630. return 0;
  1631. }
  1632. /**
  1633. * Parses an EventStream element.
  1634. *
  1635. * @param {number} periodStart
  1636. * @param {?number} periodDuration
  1637. * @param {!Element} elem
  1638. * @param {number} availabilityStart
  1639. * @private
  1640. */
  1641. parseEventStream_(periodStart, periodDuration, elem, availabilityStart) {
  1642. const XmlUtils = shaka.util.XmlUtils;
  1643. const parseNumber = XmlUtils.parseNonNegativeInt;
  1644. const schemeIdUri = elem.getAttribute('schemeIdUri') || '';
  1645. const value = elem.getAttribute('value') || '';
  1646. const timescale = XmlUtils.parseAttr(elem, 'timescale', parseNumber) || 1;
  1647. for (const eventNode of XmlUtils.findChildren(elem, 'Event')) {
  1648. const presentationTime =
  1649. XmlUtils.parseAttr(eventNode, 'presentationTime', parseNumber) || 0;
  1650. const duration =
  1651. XmlUtils.parseAttr(eventNode, 'duration', parseNumber) || 0;
  1652. let startTime = presentationTime / timescale + periodStart;
  1653. let endTime = startTime + (duration / timescale);
  1654. if (periodDuration != null) {
  1655. // An event should not go past the Period, even if the manifest says so.
  1656. // See: Dash sec. 5.10.2.1
  1657. startTime = Math.min(startTime, periodStart + periodDuration);
  1658. endTime = Math.min(endTime, periodStart + periodDuration);
  1659. }
  1660. // Don't add unavailable regions to the timeline.
  1661. if (endTime < availabilityStart) {
  1662. continue;
  1663. }
  1664. /** @type {shaka.extern.TimelineRegionInfo} */
  1665. const region = {
  1666. schemeIdUri: schemeIdUri,
  1667. value: value,
  1668. startTime: startTime,
  1669. endTime: endTime,
  1670. id: eventNode.getAttribute('id') || '',
  1671. eventElement: eventNode,
  1672. };
  1673. this.playerInterface_.onTimelineRegionAdded(region);
  1674. }
  1675. }
  1676. /**
  1677. * Makes a network request on behalf of SegmentBase.createStreamInfo.
  1678. *
  1679. * @param {!Array.<string>} uris
  1680. * @param {?number} startByte
  1681. * @param {?number} endByte
  1682. * @return {!Promise.<BufferSource>}
  1683. * @private
  1684. */
  1685. async requestInitSegment_(uris, startByte, endByte) {
  1686. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  1687. const request = shaka.util.Networking.createSegmentRequest(
  1688. uris,
  1689. startByte,
  1690. endByte,
  1691. this.config_.retryParameters);
  1692. const networkingEngine = this.playerInterface_.networkingEngine;
  1693. const operation = networkingEngine.request(requestType, request);
  1694. this.operationManager_.manage(operation);
  1695. const response = await operation.promise;
  1696. return response.data;
  1697. }
  1698. /**
  1699. * Guess the content type based on MIME type and codecs.
  1700. *
  1701. * @param {string} mimeType
  1702. * @param {string} codecs
  1703. * @return {string}
  1704. * @private
  1705. */
  1706. static guessContentType_(mimeType, codecs) {
  1707. const fullMimeType = shaka.util.MimeUtils.getFullType(mimeType, codecs);
  1708. if (shaka.text.TextEngine.isTypeSupported(fullMimeType)) {
  1709. // If it's supported by TextEngine, it's definitely text.
  1710. // We don't check MediaSourceEngine, because that would report support
  1711. // for platform-supported video and audio types as well.
  1712. return shaka.util.ManifestParserUtils.ContentType.TEXT;
  1713. }
  1714. // Otherwise, just split the MIME type. This handles video and audio
  1715. // types well.
  1716. return mimeType.split('/')[0];
  1717. }
  1718. };
  1719. /**
  1720. * Contains the minimum amount of time, in seconds, between manifest update
  1721. * requests.
  1722. *
  1723. * @private
  1724. * @const {number}
  1725. */
  1726. shaka.dash.DashParser.MIN_UPDATE_PERIOD_ = 3;
  1727. /**
  1728. * @typedef {
  1729. * function(!Array.<string>, ?number, ?number):!Promise.<BufferSource>
  1730. * }
  1731. */
  1732. shaka.dash.DashParser.RequestInitSegmentCallback;
  1733. /**
  1734. * @typedef {{
  1735. * segmentBase: Element,
  1736. * segmentList: Element,
  1737. * segmentTemplate: Element,
  1738. * baseUris: !Array.<string>,
  1739. * width: (number|undefined),
  1740. * height: (number|undefined),
  1741. * contentType: string,
  1742. * mimeType: string,
  1743. * codecs: string,
  1744. * frameRate: (number|undefined),
  1745. * pixelAspectRatio: (string|undefined),
  1746. * emsgSchemeIdUris: !Array.<string>,
  1747. * id: ?string,
  1748. * numChannels: ?number,
  1749. * audioSamplingRate: ?number,
  1750. * availabilityTimeOffset: number
  1751. * }}
  1752. *
  1753. * @description
  1754. * A collection of elements and properties which are inherited across levels
  1755. * of a DASH manifest.
  1756. *
  1757. * @property {Element} segmentBase
  1758. * The XML node for SegmentBase.
  1759. * @property {Element} segmentList
  1760. * The XML node for SegmentList.
  1761. * @property {Element} segmentTemplate
  1762. * The XML node for SegmentTemplate.
  1763. * @property {!Array.<string>} baseUris
  1764. * An array of absolute base URIs for the frame.
  1765. * @property {(number|undefined)} width
  1766. * The inherited width value.
  1767. * @property {(number|undefined)} height
  1768. * The inherited height value.
  1769. * @property {string} contentType
  1770. * The inherited media type.
  1771. * @property {string} mimeType
  1772. * The inherited MIME type value.
  1773. * @property {string} codecs
  1774. * The inherited codecs value.
  1775. * @property {(number|undefined)} frameRate
  1776. * The inherited framerate value.
  1777. * @property {(string|undefined)} pixelAspectRatio
  1778. * The inherited pixel aspect ratio value.
  1779. * @property {!Array.<string>} emsgSchemeIdUris
  1780. * emsg registered schemeIdUris.
  1781. * @property {?string} id
  1782. * The ID of the element.
  1783. * @property {?number} numChannels
  1784. * The number of audio channels, or null if unknown.
  1785. * @property {?number} audioSamplingRate
  1786. * Specifies the maximum sampling rate of the content, or null if unknown.
  1787. * @property {number} availabilityTimeOffset
  1788. * Specifies the total availabilityTimeOffset of the segment, or 0 if unknown.
  1789. */
  1790. shaka.dash.DashParser.InheritanceFrame;
  1791. /**
  1792. * @typedef {{
  1793. * dynamic: boolean,
  1794. * presentationTimeline: !shaka.media.PresentationTimeline,
  1795. * period: ?shaka.dash.DashParser.InheritanceFrame,
  1796. * periodInfo: ?shaka.dash.DashParser.PeriodInfo,
  1797. * adaptationSet: ?shaka.dash.DashParser.InheritanceFrame,
  1798. * representation: ?shaka.dash.DashParser.InheritanceFrame,
  1799. * bandwidth: number,
  1800. * indexRangeWarningGiven: boolean,
  1801. * availabilityTimeOffset: number,
  1802. * profiles: !Array.<string>
  1803. * }}
  1804. *
  1805. * @description
  1806. * Contains context data for the streams. This is designed to be
  1807. * shallow-copyable, so the parser must overwrite (not modify) each key as the
  1808. * parser moves through the manifest and the parsing context changes.
  1809. *
  1810. * @property {boolean} dynamic
  1811. * True if the MPD is dynamic (not all segments available at once)
  1812. * @property {!shaka.media.PresentationTimeline} presentationTimeline
  1813. * The PresentationTimeline.
  1814. * @property {?shaka.dash.DashParser.InheritanceFrame} period
  1815. * The inheritance from the Period element.
  1816. * @property {?shaka.dash.DashParser.PeriodInfo} periodInfo
  1817. * The Period info for the current Period.
  1818. * @property {?shaka.dash.DashParser.InheritanceFrame} adaptationSet
  1819. * The inheritance from the AdaptationSet element.
  1820. * @property {?shaka.dash.DashParser.InheritanceFrame} representation
  1821. * The inheritance from the Representation element.
  1822. * @property {number} bandwidth
  1823. * The bandwidth of the Representation, or zero if missing.
  1824. * @property {boolean} indexRangeWarningGiven
  1825. * True if the warning about SegmentURL@indexRange has been printed.
  1826. * @property {number} availabilityTimeOffset
  1827. * The sum of the availabilityTimeOffset values that apply to the element.
  1828. * @property {!Array.<string>} profiles
  1829. * Profiles of DASH are defined to enable interoperability and the signaling
  1830. * of the use of features.
  1831. */
  1832. shaka.dash.DashParser.Context;
  1833. /**
  1834. * @typedef {{
  1835. * start: number,
  1836. * duration: ?number,
  1837. * node: !Element,
  1838. * isLastPeriod: boolean
  1839. * }}
  1840. *
  1841. * @description
  1842. * Contains information about a Period element.
  1843. *
  1844. * @property {number} start
  1845. * The start time of the period.
  1846. * @property {?number} duration
  1847. * The duration of the period; or null if the duration is not given. This
  1848. * will be non-null for all periods except the last.
  1849. * @property {!Element} node
  1850. * The XML Node for the Period.
  1851. * @property {boolean} isLastPeriod
  1852. * Whether this Period is the last one in the manifest.
  1853. */
  1854. shaka.dash.DashParser.PeriodInfo;
  1855. /**
  1856. * @typedef {{
  1857. * id: string,
  1858. * contentType: ?string,
  1859. * language: string,
  1860. * main: boolean,
  1861. * streams: !Array.<shaka.extern.Stream>,
  1862. * drmInfos: !Array.<shaka.extern.DrmInfo>,
  1863. * trickModeFor: ?string,
  1864. * representationIds: !Array.<string>
  1865. * }}
  1866. *
  1867. * @description
  1868. * Contains information about an AdaptationSet element.
  1869. *
  1870. * @property {string} id
  1871. * The unique ID of the adaptation set.
  1872. * @property {?string} contentType
  1873. * The content type of the AdaptationSet.
  1874. * @property {string} language
  1875. * The language of the AdaptationSet.
  1876. * @property {boolean} main
  1877. * Whether the AdaptationSet has the 'main' type.
  1878. * @property {!Array.<shaka.extern.Stream>} streams
  1879. * The streams this AdaptationSet contains.
  1880. * @property {!Array.<shaka.extern.DrmInfo>} drmInfos
  1881. * The DRM info for the AdaptationSet.
  1882. * @property {?string} trickModeFor
  1883. * If non-null, this AdaptationInfo represents trick mode tracks. This
  1884. * property is the ID of the normal AdaptationSet these tracks should be
  1885. * associated with.
  1886. * @property {!Array.<string>} representationIds
  1887. * An array of the IDs of the Representations this AdaptationSet contains.
  1888. */
  1889. shaka.dash.DashParser.AdaptationInfo;
  1890. /**
  1891. * @typedef {function():!Promise.<shaka.media.SegmentIndex>}
  1892. * @description
  1893. * An async function which generates and returns a SegmentIndex.
  1894. */
  1895. shaka.dash.DashParser.GenerateSegmentIndexFunction;
  1896. /**
  1897. * @typedef {{
  1898. * generateSegmentIndex: shaka.dash.DashParser.GenerateSegmentIndexFunction
  1899. * }}
  1900. *
  1901. * @description
  1902. * Contains information about a Stream. This is passed from the createStreamInfo
  1903. * methods.
  1904. *
  1905. * @property {shaka.dash.DashParser.GenerateSegmentIndexFunction}
  1906. * generateSegmentIndex
  1907. * An async function to create the SegmentIndex for the stream.
  1908. */
  1909. shaka.dash.DashParser.StreamInfo;
  1910. shaka.media.ManifestParser.registerParserByExtension(
  1911. 'mpd', () => new shaka.dash.DashParser());
  1912. shaka.media.ManifestParser.registerParserByMime(
  1913. 'application/dash+xml', () => new shaka.dash.DashParser());
  1914. shaka.media.ManifestParser.registerParserByMime(
  1915. 'video/vnd.mpeg.dash.mpd', () => new shaka.dash.DashParser());