async makePreloadManager_(assetUri, startTime, mimeType, preloadConfig,
      allowPrefetch = true, disableVideo = false, allowMakeAbrManager = true) {
    goog.asserts.assert(this.networkingEngine_, 'Must have net engine');
    /** @type {?shaka.media.PreloadManager} */
    let preloadManager = null;

    const config = shaka.util.ObjectUtils.cloneObject(preloadConfig);
    if (disableVideo) {
      config.manifest.disableVideo = true;
    }

    const getPreloadManager = () => {
      goog.asserts.assert(preloadManager, 'Must have preload manager');
      if (preloadManager.hasBeenAttached() && preloadManager.isDestroyed()) {
        return null;
      }
      return preloadManager;
    };

    const getConfig = () => {
      if (getPreloadManager()) {
        return getPreloadManager().getConfiguration();
      } else {
        return this.config_;
      }
    };

    // Avoid having to detect the resolution again if it has already been
    // detected or set
    if (this.maxHwRes_.width == Infinity &&
        this.maxHwRes_.height == Infinity &&
        !this.config_.ignoreHardwareResolution) {
      const device = shaka.device.DeviceFactory.getDevice();
      goog.asserts.assert(device, 'device must be non-null');
      const maxResolution = await device.detectMaxHardwareResolution();
      this.maxHwRes_.width = maxResolution.width;
      this.maxHwRes_.height = maxResolution.height;
    }
    const manifestFilterer = new shaka.media.ManifestFilterer(
        config, this.maxHwRes_, null);
    const manifestPlayerInterface = {
      networkingEngine: this.networkingEngine_,
      filter: async (manifest) => {
        const tracksChanged = await manifestFilterer.filterManifest(manifest);
        if (tracksChanged) {
          // Delay the 'trackschanged' event so StreamingEngine has time to
          // absorb the changes before the user tries to query it.
          const event = shaka.Player.makeEvent_(
              shaka.util.FakeEvent.EventName.TracksChanged);
          await Promise.resolve();
          preloadManager.dispatchEvent(event);
        }
      },
      makeTextStreamsForClosedCaptions: (manifest) => {
        return this.makeTextStreamsForClosedCaptions_(manifest);
      },

      // Called when the parser finds a timeline region. This can be called
      // before we start playback or during playback (live/in-progress
      // manifest).
      onTimelineRegionAdded: (region) => {
        preloadManager.getRegionTimeline().addRegion(region);
      },

      onEvent: (event) => preloadManager.dispatchEvent(event),
      onError: (error) => preloadManager.onError(error),
      isLowLatencyMode: () => getConfig().streaming.lowLatencyMode,
      updateDuration: () => {
        if (this.streamingEngine_ && preloadManager.hasBeenAttached()) {
          this.streamingEngine_.updateDuration();
        }
      },
      newDrmInfo: (stream) => {
        // We may need to create new sessions for any new init data.
        const drmEngine = preloadManager.getDrmEngine();
        const currentDrmInfo = drmEngine ? drmEngine.getDrmInfo() : null;
        // DrmEngine.newInitData() requires mediaKeys to be available.
        if (currentDrmInfo && drmEngine.getMediaKeys()) {
          manifestFilterer.processDrmInfos(currentDrmInfo.keySystem, stream);
        }
      },
      onManifestUpdated: () => {
        const eventName = shaka.util.FakeEvent.EventName.ManifestUpdated;
        const data = (new Map()).set('isLive', this.isLive());
        preloadManager.dispatchEvent(shaka.Player.makeEvent_(eventName, data));

        preloadManager.addQueuedOperation(false, () => {
          if (this.adManager_) {
            this.adManager_.onManifestUpdated(this.isLive());
          }
        });
      },
      getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(),
      onMetadata: (type, startTime, endTime, values) => {
        let metadataType = type;
        if (type == 'com.apple.hls.interstitial') {
          metadataType = 'com.apple.quicktime.HLS';
          /** @type {shaka.extern.HLSInterstitial} */
          const interstitial = {
            startTime,
            endTime,
            values,
          };
          if (this.adManager_) {
            goog.asserts.assert(this.video_, 'Must have video');
            this.adManager_.onHLSInterstitialMetadata(
                this, this.video_, interstitial);
          }
        }
        for (const payload of values) {
          if (payload.name == 'ID') {
            continue;
          }
          preloadManager.addQueuedOperation(false, () => {
            this.addMetadataToRegionTimeline_(
                startTime, endTime, metadataType, payload);
          });
        }
      },
      disableStream: (stream) => this.disableStream(
          stream, this.config_.streaming.maxDisabledTime),
      addFont: (name, url) => this.addFont(name, url),
    };
    const regionTimeline =
        new shaka.media.RegionTimeline(() => this.seekRange());
    regionTimeline.addEventListener('regionadd', (event) => {
      /** @type {shaka.extern.TimelineRegionInfo} */
      const region = event['region'];
      this.onRegionEvent_(
          shaka.util.FakeEvent.EventName.TimelineRegionAdded, region,
          preloadManager);

      preloadManager.addQueuedOperation(false, () => {
        if (this.adManager_) {
          this.adManager_.onDashTimedMetadata(region);
          goog.asserts.assert(this.video_, 'Must have video');
          this.adManager_.onDASHInterstitialMetadata(
              this, this.video_, region);
        }
      });
    });
    let qualityObserver = null;
    if (config.streaming.observeQualityChanges) {
      qualityObserver = new shaka.media.QualityObserver(
          () => this.getBufferedInfo());

      qualityObserver.addEventListener('qualitychange', (event) => {
        /** @type {shaka.extern.MediaQualityInfo} */
        const mediaQualityInfo = event['quality'];
        /** @type {number} */
        const position = event['position'];
        this.onMediaQualityChange_(mediaQualityInfo, position);
      });

      qualityObserver.addEventListener('audiotrackchange', (event) => {
        /** @type {shaka.extern.MediaQualityInfo} */
        const mediaQualityInfo = event['quality'];
        /** @type {number} */
        const position = event['position'];
        this.onMediaQualityChange_(mediaQualityInfo, position,
            /* audioTrackChanged= */ true);
      });
    }
    let firstEvent = true;
    const drmPlayerInterface = {
      netEngine: this.networkingEngine_,
      onError: (e) => preloadManager.onError(e),
      onKeyStatus: (map) => {
        preloadManager.addQueuedOperation(true, () => {
          if (this.drmEngine_) {
            this.onKeyStatus_(map);
          }
        });
      },
      onExpirationUpdated: (id, expiration) => {
        const event = shaka.Player.makeEvent_(
            shaka.util.FakeEvent.EventName.ExpirationUpdated);
        preloadManager.dispatchEvent(event);
        const parser = preloadManager.getParser();
        if (parser && parser.onExpirationUpdated) {
          parser.onExpirationUpdated(id, expiration);
        }
      },
      onEvent: (e) => {
        preloadManager.dispatchEvent(e);
        if (e.type == shaka.util.FakeEvent.EventName.DrmSessionUpdate &&
            firstEvent) {
          firstEvent = false;
          const now = Date.now() / 1000;
          const delta = now - preloadManager.getStartTimeOfDRM();
          const stats = this.stats_ || preloadManager.getStats();
          stats.setDrmTime(delta);
          // LCEVC data by itself is not encrypted in DRM protected streams
          // and can therefore be accessed and decoded as normal. However,
          // the LCEVC decoder needs access to the VideoElement output in
          // order to apply the enhancement. In DRM contexts where the
          // browser CDM restricts access from our decoder, the enhancement
          // cannot be applied and therefore the LCEVC output canvas is
          // hidden accordingly.
          if (this.lcevcDec_) {
            this.lcevcDec_.hideCanvas();
          }
        }
      },
    };

    // Sadly, as the network engine creation code must be replaceable by tests,
    // it cannot be made and use the utilities defined in this function.
    const networkingEngine = this.createNetworkingEngine(getPreloadManager);
    this.networkingEngine_.copyFiltersInto(networkingEngine);

    /** @return {!shaka.drm.DrmEngine} */
    const createDrmEngine = () => {
      return this.createDrmEngine(drmPlayerInterface);
    };
    /** @type {!shaka.media.PreloadManager.PlayerInterface} */
    const playerInterface = {
      config,
      manifestPlayerInterface,
      regionTimeline,
      qualityObserver,
      createDrmEngine,
      manifestFilterer,
      networkingEngine,
      allowPrefetch,
      allowMakeAbrManager,
    };
    preloadManager = new shaka.media.PreloadManager(
        assetUri, mimeType, startTime, playerInterface);
    return preloadManager;
  }
@param {string} assetUri
@param {?number|Date} startTime
@param {?string} mimeType
@param {shaka.extern.PlayerConfiguration} preloadConfig
@param {boolean=} allowPrefetch
@param {boolean=} disableVideo
@param {boolean=} allowMakeAbrManager
@return {!Promise<!shaka.media.PreloadManager>}
@private