switchInternal_(stream, clearBuffer, safeMargin, force, adaptation) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const type = /** @type {!ContentType} */(stream.type);
    const mediaState = this.mediaStates_.get(type);

    if (!mediaState && stream.type == ContentType.TEXT) {
      this.loadNewTextStream_(stream);
      return;
    }

    goog.asserts.assert(mediaState, 'switch: expected mediaState to exist');
    if (!mediaState) {
      return;
    }

    if (mediaState.restoreStreamAfterTrickPlay) {
      shaka.log.debug('switch during trick play mode', stream);

      // Already in trick play mode, so stick with trick mode tracks if
      // possible.
      if (stream.trickModeVideo) {
        // Use the trick mode stream, but revert to the new selection later.
        mediaState.restoreStreamAfterTrickPlay = stream;
        stream = stream.trickModeVideo;
        shaka.log.debug('switch found trick play stream', stream);
      } else {
        // There is no special trick mode video for this stream!
        mediaState.restoreStreamAfterTrickPlay = null;
        shaka.log.debug('switch found no special trick play stream');
      }
    }

    if (mediaState.stream == stream && !force) {
      const streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
      shaka.log.debug('switch: Stream ' + streamTag + ' already active');
      return;
    }

    if (this.audioPrefetchMap_.has(stream)) {
      mediaState.segmentPrefetch = this.audioPrefetchMap_.get(stream);
    } else if (mediaState.segmentPrefetch) {
      mediaState.segmentPrefetch.switchStream(stream);
    }

    if (stream.type == ContentType.TEXT) {
      // Mime types are allowed to change for text streams.
      // Reinitialize the text parser, but only if we are going to fetch the
      // init segment again.
      const fullMimeType = shaka.util.MimeUtils.getFullType(
          stream.mimeType, stream.codecs);
      this.playerInterface_.mediaSourceEngine.reinitText(
          fullMimeType, this.manifest_.sequenceMode, stream.external);
    }

    // Releases the segmentIndex of the old stream.
    // Do not close segment indexes we are prefetching.
    if (!this.audioPrefetchMap_.has(mediaState.stream)) {
      if (mediaState.stream.closeSegmentIndex) {
        if (mediaState.performingUpdate) {
          const oldStreamTag =
              shaka.media.StreamingEngine.logPrefix_(mediaState);
          if (!this.deferredCloseSegmentIndex_.has(oldStreamTag)) {
            // The ongoing update is still using the old stream's segment
            // reference information.
            // If we close the old stream now, the update will not complete
            // correctly.
            // The next onUpdate_() for this content type will resume the
            // closeSegmentIndex() operation for the old stream once the ongoing
            // update has finished, then immediately create a new segment index.
            this.deferredCloseSegmentIndex_.set(
                oldStreamTag, mediaState.stream.closeSegmentIndex);
          }
        } else {
          mediaState.stream.closeSegmentIndex();
        }
      }
    }

    const shouldResetMediaSource =
        mediaState.stream.isAudioMuxedInVideo != stream.isAudioMuxedInVideo;

    mediaState.stream = stream;
    mediaState.segmentIterator = null;
    mediaState.adaptation = !!adaptation;
    if (stream.dependencyStream) {
      mediaState.dependencyMediaState =
          this.createMediaState_(stream.dependencyStream);
    } else {
      mediaState.dependencyMediaState = null;
    }

    const streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
    shaka.log.debug('switch: switching to Stream ' + streamTag);

    if (shouldResetMediaSource) {
      this.resetMediaSource(/* force= */ true, /* clearBuffer= */ false);
      return;
    }

    if (clearBuffer) {
      if (mediaState.clearingBuffer) {
        // We are already going to clear the buffer, but make sure it is also
        // flushed.
        mediaState.waitingToFlushBuffer = true;
      } else if (mediaState.performingUpdate) {
        // We are performing an update, so we have to wait until it's finished.
        // onUpdate_() will call clearBuffer_() when the update has finished.
        // We need to save the safe margin because its value will be needed when
        // clearing the buffer after the update.
        mediaState.waitingToClearBuffer = true;
        mediaState.clearBufferSafeMargin = safeMargin;
        mediaState.waitingToFlushBuffer = true;
      } else {
        // Cancel the update timer, if any.
        this.cancelUpdate_(mediaState);
        // Clear right away.
        this.clearBuffer_(mediaState, /* flush= */ true, safeMargin)
            .catch((error) => {
              if (this.playerInterface_) {
                goog.asserts.assert(error instanceof shaka.util.Error,
                    'Wrong error type!');
                this.playerInterface_.onError(error);
              }
            });
      }
    } else {
      if (!mediaState.performingUpdate && !mediaState.updateTimer) {
        this.scheduleUpdate_(mediaState, 0);
      }
    }

    this.makeAbortDecision_(mediaState).catch((error) => {
      if (this.playerInterface_) {
        goog.asserts.assert(error instanceof shaka.util.Error,
            'Wrong error type!');
        this.playerInterface_.onError(error);
      }
    });
  }
Switches to the given Stream. |stream| may be from any Variant.

@param {shaka.extern.Stream} stream
@param {boolean} clearBuffer
@param {number} safeMargin
@param {boolean} force
  If true, reload the text stream even if it did not change.
@param {boolean=} adaptation
  If true, update the media state to indicate MediaSourceEngine should
  reset the timestamp offset to ensure the new track segments are correctly
  placed on the timeline.
@private