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