parsePeriods_(context, getBaseUris, mpd, newPeriod) {
    const TXml = shaka.util.TXml;
    let presentationDuration = context.mediaPresentationDuration;

    if (!presentationDuration) {
      presentationDuration = TXml.parseAttr(
          mpd, 'mediaPresentationDuration', TXml.parseDuration);
      this.manifestPatchContext_.mediaPresentationDuration =
          presentationDuration;
    }

    let seekRangeStart = 0;
    if (this.manifest_ && this.manifest_.presentationTimeline &&
        this.isTransitionFromDynamicToStatic_) {
      seekRangeStart = this.manifest_.presentationTimeline.getSeekRangeStart();
    }

    const periods = [];
    let prevEnd = seekRangeStart;
    const periodNodes = TXml.findChildren(mpd, 'Period');
    for (let i = 0; i < periodNodes.length; i++) {
      const elem = periodNodes[i];
      const next = periodNodes[i + 1];
      let start = /** @type {number} */ (
        TXml.parseAttr(elem, 'start', TXml.parseDuration, prevEnd));
      const periodId = elem.attributes['id'];
      const givenDuration =
          TXml.parseAttr(elem, 'duration', TXml.parseDuration);
      start = (i == 0 && start == 0 && this.isTransitionFromDynamicToStatic_) ?
          seekRangeStart : start;

      let periodDuration = null;
      if (next) {
        // "The difference between the start time of a Period and the start time
        // of the following Period is the duration of the media content
        // represented by this Period."
        const nextStart =
            TXml.parseAttr(next, 'start', TXml.parseDuration);
        if (nextStart != null) {
          periodDuration = nextStart - start + seekRangeStart;
        }
      } else if (presentationDuration != null) {
        // "The Period extends until the Period.start of the next Period, or
        // until the end of the Media Presentation in the case of the last
        // Period."
        periodDuration = presentationDuration - start + seekRangeStart;
      }

      const threshold =
          shaka.util.ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS;
      if (periodDuration && givenDuration &&
          Math.abs(periodDuration - givenDuration) > threshold) {
        shaka.log.warning('There is a gap/overlap between Periods', elem);

        // This means it's a gap, the distance between period starts is
        // larger than the period's duration
        if (periodDuration > givenDuration) {
          this.gapCount_++;
        }
      }
      // Only use the @duration in the MPD if we can't calculate it.  We should
      // favor the @start of the following Period.  This ensures that there
      // aren't gaps between Periods.
      if (periodDuration == null) {
        periodDuration = givenDuration;
      }

      /**
       * This is to improve robustness when the player observes manifest with
       * past periods that are inconsistent to previous ones.
       *
       * This may happen when a CDN or proxy server switches its upstream from
       * one encoder to another redundant encoder.
       *
       * Skip periods that match all of the following criteria:
       * - Start time is earlier than latest period start time ever seen
       * - Period ID is never seen in the previous manifest
       * - Not the last period in the manifest
       *
       * Periods that meet the aforementioned criteria are considered invalid
       * and should be safe to discard.
       */

      if (this.largestPeriodStartTime_ !== null &&
        periodId !== null && start !== null &&
        start < this.largestPeriodStartTime_ &&
        !this.lastManifestUpdatePeriodIds_.includes(periodId) &&
        i + 1 != periodNodes.length) {
        shaka.log.debug(
            `Skipping Period with ID ${periodId} as its start time is smaller` +
            ' than the largest period start time that has been seen, and ID ' +
            'is unseen before');
        continue;
      }


      // Save maximum period start time if it is the last period
      if (start !== null &&
        (this.largestPeriodStartTime_ === null ||
          start > this.largestPeriodStartTime_)) {
        this.largestPeriodStartTime_ = start;
      }

      // Parse child nodes.
      const info = {
        start: start,
        duration: periodDuration,
        node: elem,
        isLastPeriod: periodDuration == null || !next,
      };
      const period = this.parsePeriod_(context, getBaseUris, info);
      periods.push(period);

      if (context.period.id && periodDuration) {
        this.periodDurations_.set(context.period.id, periodDuration);
      }

      if (periodDuration == null) {
        if (next) {
          // If the duration is still null and we aren't at the end, then we
          // will skip any remaining periods.
          shaka.log.warning(
              'Skipping Period', i + 1, 'and any subsequent Periods:', 'Period',
              i + 1, 'does not have a valid start time.', next);
        }

        // The duration is unknown, so the end is unknown.
        prevEnd = null;
        break;
      }

      prevEnd = start + periodDuration;
    } // end of period parsing loop

    if (newPeriod) {
      // append new period from the patch manifest
      for (const el of periods) {
        const periodID = el.id;
        if (!this.lastManifestUpdatePeriodIds_.includes(periodID)) {
          this.lastManifestUpdatePeriodIds_.push(periodID);
        }
      }
    } else {
      // Replace previous seen periods with the current one.
      this.lastManifestUpdatePeriodIds_ = periods.map((el) => el.id);
    }

    if (presentationDuration != null) {
      if (prevEnd != null) {
        const threshold =
            shaka.util.ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS;
        const difference = prevEnd - seekRangeStart - presentationDuration;
        if (Math.abs(difference) > threshold) {
          shaka.log.warning(
              '@mediaPresentationDuration does not match the total duration ',
              'of all Periods.');
          // Assume @mediaPresentationDuration is correct.
        }
      }
      return {
        periods: periods,
        duration: presentationDuration + seekRangeStart,
        durationDerivedFromPeriods: false,
      };
    } else {
      return {
        periods: periods,
        duration: prevEnd,
        durationDerivedFromPeriods: true,
      };
    }
  }
Reads and parses the periods from the manifest.  This first does some
partial parsing so the start and duration is available when parsing
children.

@param {shaka.dash.DashParser.Context} context
@param {function(): !Array<string>} getBaseUris
@param {!shaka.extern.xml.Node} mpd
@param {!boolean} newPeriod
@return {{
  periods: !Array<shaka.extern.Period>,
  duration: ?number,
  durationDerivedFromPeriods: boolean
}}
@private