update_(mediaState) {
    goog.asserts.assert(this.manifest_, 'manifest_ should not be null');
    goog.asserts.assert(this.config_, 'config_ should not be null');

    const ContentType = shaka.util.ManifestParserUtils.ContentType;

    // Do not schedule update for closed captions text mediaState, since closed
    // captions are embedded in video streams.
    if (shaka.media.StreamingEngine.isEmbeddedText_(mediaState)) {
      this.playerInterface_.mediaSourceEngine.setSelectedClosedCaptionId(
          mediaState.stream.originalId || '');
      return null;
    } else if (mediaState.type == ContentType.TEXT) {
      // Disable embedded captions if not desired (e.g. if transitioning from
      // embedded to not-embedded captions).
      this.playerInterface_.mediaSourceEngine.clearSelectedClosedCaptionId();
    }

    if (mediaState.stream.isAudioMuxedInVideo) {
      return null;
    }

    // Update updateIntervalSeconds according to our current playbackrate,
    // and always considering a minimum of 1.
    const updateIntervalSeconds = this.config_.updateIntervalSeconds /
        Math.max(1, Math.abs(this.playerInterface_.getPlaybackRate()));

    if (!this.playerInterface_.mediaSourceEngine.isStreamingAllowed() &&
        mediaState.type != ContentType.TEXT) {
      // It is not allowed to add segments yet, so we schedule an update to
      // check again later. So any prediction we make now could be terribly
      // invalid soon.
      return updateIntervalSeconds / 2;
    }

    const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);

    // Compute how far we've buffered ahead of the playhead.
    const presentationTime = this.playerInterface_.getPresentationTime();

    if (mediaState.type === ContentType.AUDIO) {
      // evict all prefetched segments that are before the presentationTime
      for (const stream of this.audioPrefetchMap_.keys()) {
        const prefetch = this.audioPrefetchMap_.get(stream);
        prefetch.evict(presentationTime, /* clearInitSegments= */ true);
        prefetch.prefetchSegmentsByTime(presentationTime);
      }
    }

    // Get the next timestamp we need.
    const timeNeeded = this.getTimeNeeded_(mediaState, presentationTime);
    shaka.log.v2(logPrefix, 'timeNeeded=' + timeNeeded);

    // Get the amount of content we have buffered, accounting for drift.  This
    // is only used to determine if we have meet the buffering goal.  This
    // should be the same method that PlayheadObserver uses.
    const bufferedAhead =
        this.playerInterface_.mediaSourceEngine.bufferedAheadOf(
            mediaState.type, presentationTime);

    shaka.log.v2(logPrefix,
        'update_:',
        'presentationTime=' + presentationTime,
        'bufferedAhead=' + bufferedAhead);

    const unscaledBufferingGoal = Math.max(
        this.config_.rebufferingGoal, this.config_.bufferingGoal);

    const scaledBufferingGoal = Math.max(1,
        unscaledBufferingGoal * this.bufferingScale_);

    // Check if we've buffered to the end of the presentation.
    const timeUntilEnd =
        this.manifest_.presentationTimeline.getDuration() - timeNeeded;
    const oneMicrosecond = 1e-6;

    const bufferEnd =
      this.playerInterface_.mediaSourceEngine.bufferEnd(mediaState.type);

    if (timeUntilEnd < oneMicrosecond && !!bufferEnd) {
      // We shouldn't rebuffer if the playhead is close to the end of the
      // presentation.
      shaka.log.debug(logPrefix, 'buffered to end of presentation');
      mediaState.endOfStream = true;

      if (mediaState.type == ContentType.VIDEO) {
        // Since the text stream of CEA closed captions doesn't have update
        // timer, we have to set the text endOfStream based on the video
        // stream's endOfStream state.
        const textState = this.mediaStates_.get(ContentType.TEXT);
        if (textState &&
            shaka.media.StreamingEngine.isEmbeddedText_(textState)) {
          textState.endOfStream = true;
        }
      }
      return null;
    }
    mediaState.endOfStream = false;

    // If we've buffered to the buffering goal then schedule an update.
    if (bufferedAhead >= scaledBufferingGoal) {
      shaka.log.v2(logPrefix, 'buffering goal met');

      // Do not try to predict the next update.  Just poll according to
      // configuration (seconds).
      return updateIntervalSeconds / 2;
    }

    // Lack of segment iterator is the best indicator stream has changed.
    const streamChanged = !mediaState.segmentIterator;
    const reference = this.getSegmentReferenceNeeded_(
        mediaState, presentationTime, bufferEnd);
    if (!reference) {
      // The segment could not be found, does not exist, or is not available.
      // In any case just try again... if the manifest is incomplete or is not
      // being updated then we'll idle forever; otherwise, we'll end up getting
      // a SegmentReference eventually.
      return updateIntervalSeconds;
    }
    // Get media state adaptation and reset this value. By guarding it during
    // actual stream change we ensure it won't be cleaned by accident on regular
    // append.
    let adaptation = false;
    if (streamChanged && mediaState.adaptation) {
      adaptation = true;
      mediaState.adaptation = false;
    }

    // Do not let any one stream get far ahead of any other.
    let minTimeNeeded = Infinity;
    const mediaStates = Array.from(this.mediaStates_.values());
    for (const otherState of mediaStates) {
      // Do not consider embedded captions in this calculation.  It could lead
      // to hangs in streaming.
      if (shaka.media.StreamingEngine.isEmbeddedText_(otherState)) {
        continue;
      }
      // If there is no next segment, ignore this stream.  This happens with
      // text when there's a Period with no text in it.
      if (otherState.segmentIterator && !otherState.segmentIterator.current()) {
        continue;
      }

      const timeNeeded = this.getTimeNeeded_(otherState, presentationTime);
      minTimeNeeded = Math.min(minTimeNeeded, timeNeeded);
    }

    const maxSegmentDuration =
        this.manifest_.presentationTimeline.getMaxSegmentDuration();
    const maxRunAhead = maxSegmentDuration *
        shaka.media.StreamingEngine.MAX_RUN_AHEAD_SEGMENTS_;
    if (timeNeeded >= minTimeNeeded + maxRunAhead) {
      // Wait and give other media types time to catch up to this one.
      // For example, let video buffering catch up to audio buffering before
      // fetching another audio segment.
      shaka.log.v2(logPrefix, 'waiting for other streams to buffer');
      return updateIntervalSeconds;
    }

    const CrossBoundaryStrategy = shaka.config.CrossBoundaryStrategy;
    if (this.config_.crossBoundaryStrategy !== CrossBoundaryStrategy.KEEP &&
        this.discardReferenceByBoundary_(mediaState, reference)) {
      // Return null as we do not want to fetch and append segments outside
      // of the current boundary.
      return null;
    }

    if (mediaState.segmentPrefetch && mediaState.segmentIterator &&
        !this.audioPrefetchMap_.has(mediaState.stream)) {
      // This will prevent duplicate segments from being downloaded when we
      // are close to the live edge.
      const fudgeTime = 0.001;
      mediaState.segmentPrefetch.evict(reference.startTime + fudgeTime);
      mediaState.segmentPrefetch.prefetchSegmentsByTime(reference.startTime)
          // We're treating this call as sync here, so ignore async errors
          // to not propagate them further.
          .catch(() => {});
    }

    const p = this.fetchAndAppend_(mediaState, presentationTime, reference,
        adaptation);
    p.catch(() => {});  // TODO(#1993): Handle asynchronous errors.
    if (mediaState.dependencyMediaState) {
      this.fetchAndAppendDependency_(
          mediaState.dependencyMediaState, presentationTime,
          scaledBufferingGoal);
    }
    return null;
  }
Updates the given MediaState.

@param {shaka.media.StreamingEngine.MediaState_} mediaState
@return {?number} The number of seconds to wait until updating again or
  null if another update does not need to be scheduled.
@private