createStreamInfo_(streamId, verbatimMediaPlaylistUris, codecs, type,
      languageValue, primary, name, channelsCount, closedCaptions,
      characteristics, forced, sampleRate, spatialAudio) {
    // TODO: Refactor, too many parameters

    // This stream is lazy-loaded inside the createSegmentIndex function.
    // So we start out with a stream object that does not contain the actual
    // segment index, then download when createSegmentIndex is called.
    const stream = this.makeStreamObject_(streamId, codecs, type,
        languageValue, primary, name, channelsCount, closedCaptions,
        characteristics, forced, sampleRate, spatialAudio);

    const FAKE_MUXED_URL_ = shaka.hls.HlsParser.FAKE_MUXED_URL_;
    if (verbatimMediaPlaylistUris.includes(FAKE_MUXED_URL_)) {
      stream.isAudioMuxedInVideo = true;
      // We assigned the TS mimetype because it is the only one that works
      // with this functionality. MP4 is not supported right now.
      stream.mimeType = 'video/mp2t';
      this.setFullTypeForStream_(stream);
    }

    const streamInfo = {
      stream,
      type,
      redirectUris: [],
      getUris: () => {},
      // These values are filled out or updated after lazy-loading:
      minTimestamp: 0,
      maxTimestamp: 0,
      mediaSequenceToStartTime: new Map(),
      canSkipSegments: false,
      canBlockReload: false,
      hasEndList: false,
      firstSequenceNumber: -1,
      nextMediaSequence: -1,
      nextPart: -1,
      loadedOnce: false,
    };

    const getUris = () => {
      if (this.contentSteeringManager_ &&
          verbatimMediaPlaylistUris.length > 1) {
        return this.contentSteeringManager_.getLocations(streamId);
      }
      return streamInfo.redirectUris.concat(shaka.hls.Utils.constructUris(
          [this.masterPlaylistUri_], verbatimMediaPlaylistUris,
          this.globalVariables_));
    };

    streamInfo.getUris = getUris;

    /** @param {!shaka.net.NetworkingEngine.PendingRequest} pendingRequest */
    const downloadSegmentIndex = async (pendingRequest) => {
      const ContentType = shaka.util.ManifestParserUtils.ContentType;

      try {
        // Download the actual manifest.
        const response = await pendingRequest.promise;
        if (pendingRequest.aborted) {
          return;
        }

        // Record the final URI after redirects.
        const responseUri = response.uri;
        if (responseUri != response.originalUri) {
          const uris = streamInfo.getUris();
          if (!uris.includes(responseUri)) {
            streamInfo.redirectUris.push(responseUri);
          }
        }

        // Record the redirected, final URI of this media playlist when we parse
        // it.
        /** @type {!shaka.hls.Playlist} */
        const playlist = this.manifestTextParser_.parsePlaylist(response.data);

        if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
          throw new shaka.util.Error(
              shaka.util.Error.Severity.CRITICAL,
              shaka.util.Error.Category.MANIFEST,
              shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
        }

        /** @type {!Array<!shaka.hls.Tag>} */
        const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
            'EXT-X-DEFINE');

        const mediaVariables =
            this.parseMediaVariables_(variablesTags, responseUri);

        const mimeType = undefined;

        let requestBasicInfo = false;

        // If no codec info was provided in the manifest and codec guessing is
        // disabled we try to get necessary info from the media data.
        if ((!this.codecInfoInManifest_ &&
            this.config_.hls.disableCodecGuessing) ||
            (this.needsClosedCaptionsDetection_ && type == ContentType.VIDEO &&
            !this.config_.hls.disableClosedCaptionsDetection)) {
          if (playlist.segments.length > 0) {
            this.needsClosedCaptionsDetection_ = false;
            requestBasicInfo = true;
          }
        }

        const allowOverrideMimeType = !this.codecInfoInManifest_ &&
          this.config_.hls.disableCodecGuessing;

        const wasLive = this.isLive_();
        const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
            streamId, mediaVariables, playlist, getUris, codecs,
            type, languageValue, primary, name, channelsCount, closedCaptions,
            characteristics, forced, sampleRate, spatialAudio, mimeType,
            requestBasicInfo, allowOverrideMimeType);
        if (pendingRequest.aborted) {
          return;
        }

        const realStream = realStreamInfo.stream;

        this.determineStartTime_(playlist);

        if (this.isLive_() && !wasLive) {
          // Now that we know that the presentation is live, convert the
          // timeline to live.
          this.changePresentationTimelineToLive_(playlist);
        }

        // Copy values from the real stream info to our initial one.
        streamInfo.minTimestamp = realStreamInfo.minTimestamp;
        streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
        streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
        streamInfo.canBlockReload = realStreamInfo.canBlockReload;
        streamInfo.hasEndList = realStreamInfo.hasEndList;
        streamInfo.mediaSequenceToStartTime =
            realStreamInfo.mediaSequenceToStartTime;
        streamInfo.nextMediaSequence = realStreamInfo.nextMediaSequence;
        streamInfo.nextPart = realStreamInfo.nextPart;
        streamInfo.loadedOnce = true;
        stream.segmentIndex = realStream.segmentIndex;
        stream.encrypted = realStream.encrypted;
        stream.drmInfos = realStream.drmInfos;
        stream.keyIds = realStream.keyIds;
        stream.mimeType = realStream.mimeType;
        stream.bandwidth = stream.bandwidth || realStream.bandwidth;
        stream.codecs = stream.codecs || realStream.codecs;
        stream.closedCaptions =
            stream.closedCaptions || realStream.closedCaptions;
        stream.width = stream.width || realStream.width;
        stream.height = stream.height || realStream.height;
        stream.hdr = stream.hdr || realStream.hdr;
        stream.colorGamut = stream.colorGamut || realStream.colorGamut;
        stream.frameRate = stream.frameRate || realStream.frameRate;
        if (stream.language == 'und' && realStream.language != 'und') {
          stream.language = realStream.language;
        }
        stream.language = stream.language || realStream.language;
        stream.channelsCount = stream.channelsCount || realStream.channelsCount;
        stream.audioSamplingRate =
            stream.audioSamplingRate || realStream.audioSamplingRate;
        this.setFullTypeForStream_(stream);

        // Since we lazy-loaded this content, the player may need to create new
        // sessions for the DRM info in this stream.
        if (stream.drmInfos.length) {
          this.playerInterface_.newDrmInfo(stream);
        }

        let closedCaptionsUpdated = false;
        if ((!closedCaptions && stream.closedCaptions) ||
          (closedCaptions && stream.closedCaptions &&
          closedCaptions.size != stream.closedCaptions.size)) {
          closedCaptionsUpdated = true;
        }

        if (this.manifest_ && closedCaptionsUpdated) {
          this.playerInterface_.makeTextStreamsForClosedCaptions(
              this.manifest_);
        }

        if (type == ContentType.VIDEO || type == ContentType.AUDIO) {
          for (const otherStreamInfo of this.uriToStreamInfosMap_.values()) {
            if (!otherStreamInfo.loadedOnce && otherStreamInfo.type == type &&
                !otherStreamInfo.stream.isAudioMuxedInVideo) {
              // To aid manifest filtering, assume before loading that all video
              // renditions have the same MIME type.  (And likewise for audio.)
              otherStreamInfo.stream.mimeType = realStream.mimeType;
              this.setFullTypeForStream_(otherStreamInfo.stream);
            }
          }
        }

        if (type == ContentType.TEXT) {
          const firstSegment = realStream.segmentIndex.earliestReference();
          if (firstSegment && firstSegment.initSegmentReference) {
            stream.mimeType = 'application/mp4';
            this.setFullTypeForStream_(stream);
          }
        }

        const qualityInfo =
            shaka.media.QualityObserver.createQualityInfo(stream);
        stream.segmentIndex.forEachTopLevelReference((reference) => {
          if (reference.initSegmentReference) {
            reference.initSegmentReference.mediaQuality = qualityInfo;
          }
        });

        // Add finishing touches to the stream that can only be done once we
        // have more full context on the media as a whole.
        if (this.hasEnoughInfoToFinalizeStreams_()) {
          if (!this.streamsFinalized_) {
            // Mark this manifest as having been finalized, so we don't go
            // through this whole process of finishing touches a second time.
            this.streamsFinalized_ = true;
            // Finalize all of the currently-loaded streams.
            const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
            const activeStreamInfos =
                streamInfos.filter((s) => s.stream.segmentIndex);
            this.finalizeStreams_(activeStreamInfos);
            // With the addition of this new stream, we now have enough info to
            // figure out how long the streams should be. So process all streams
            // we have downloaded up until this point.
            this.determineDuration_();
            // Finally, start the update timer, if this asset has been
            // determined to be a livestream.
            const delay = this.getUpdatePlaylistDelay_();
            if (delay > 0) {
              this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
            }
          } else {
            // We don't need to go through the full process; just finalize this
            // single stream.
            this.finalizeStreams_([streamInfo]);
          }
        }

        this.processDateRangeTags_(
            playlist.tags, stream.type, mediaVariables, getUris);

        if (this.manifest_) {
          this.manifest_.startTime = this.startTime_;
        }
      } catch (e) {
        stream.closeSegmentIndex();
        if (e.code === shaka.util.Error.Code.OPERATION_ABORTED) {
          return;
        }
        const handled = this.playerInterface_.disableStream(stream);
        if (!handled) {
          throw e;
        }
      }
    };

    /** @type {Promise} */
    let creationPromise = null;
    /** @type {!shaka.net.NetworkingEngine.PendingRequest} */
    let pendingRequest;
    const safeCreateSegmentIndex = () => {
      // An operation is already in progress.  The second and subsequent
      // callers receive the same Promise as the first caller, and only one
      // download operation will occur.
      if (creationPromise) {
        return creationPromise;
      }

      if (stream.isAudioMuxedInVideo) {
        const segmentIndex = new shaka.media.SegmentIndex([]);
        stream.segmentIndex = segmentIndex;
        return Promise.resolve();
      }

      // Create a new PendingRequest to be able to cancel this specific
      // download.
      pendingRequest = this.requestManifest_(streamInfo.getUris(),
          /* isPlaylist= */ true);

      // Create a Promise tied to the outcome of downloadSegmentIndex().  If
      // downloadSegmentIndex is rejected, creationPromise will also be
      // rejected.
      creationPromise = new Promise((resolve) => {
        resolve(downloadSegmentIndex(pendingRequest));
      });
      return creationPromise;
    };

    stream.createSegmentIndex = safeCreateSegmentIndex;

    stream.closeSegmentIndex = () => {
      // If we're mid-creation, cancel it.
      if (creationPromise && !stream.segmentIndex) {
        pendingRequest.abort();
      }
      // If we have a segment index, release it.
      if (stream.segmentIndex) {
        stream.segmentIndex.release();
        stream.segmentIndex = null;
      }
      // Clear the creation Promise so that a new operation can begin.
      creationPromise = null;
    };

    return streamInfo;
  }
@param {number} streamId
@param {!Array<string>} verbatimMediaPlaylistUris
@param {string} codecs
@param {string} type
@param {?string} languageValue
@param {boolean} primary
@param {?string} name
@param {?number} channelsCount
@param {Map<string, string>} closedCaptions
@param {?string} characteristics
@param {boolean} forced
@param {?number} sampleRate
@param {boolean} spatialAudio
@return {!shaka.hls.HlsParser.StreamInfo}
@private