send_(type, request, context, backoff, index, lastError,
      numBytesRemainingObj) {
    goog.asserts.assert(this.config_, 'Config must not be null!');
    if (this.config_.forceHTTP) {
      request.uris[index] = request.uris[index].replace('https://', 'http://');
    }
    if (this.config_.forceHTTPS) {
      request.uris[index] = request.uris[index].replace('http://', 'https://');
    }

    if (index > 0 && this.onRetry_) {
      const newUri = request.uris[index];
      const oldUri = request.uris[index - 1];
      this.onRetry_(type, context, newUri, oldUri);
    }

    const uri = new goog.Uri(request.uris[index]);
    let scheme = uri.getScheme();
    // Whether it got a progress event.
    let gotProgress = false;
    if (!scheme) {
      // If there is no scheme, infer one from the location.
      scheme = shaka.net.NetworkingEngine.getLocationProtocol_();
      goog.asserts.assert(
          scheme[scheme.length - 1] == ':',
          'location.protocol expected to end with a colon!');
      // Drop the colon.
      scheme = scheme.slice(0, -1);

      // Override the original URI to make the scheme explicit.
      uri.setScheme(scheme);
      request.uris[index] = uri.toString();
    }

    // Schemes are meant to be case-insensitive.
    // See https://github.com/shaka-project/shaka-player/issues/2173
    // and https://tools.ietf.org/html/rfc3986#section-3.1
    scheme = scheme.toLowerCase();

    const object = shaka.net.NetworkingEngine.schemes_.get(scheme);
    const plugin = object ? object.plugin : null;
    if (!plugin) {
      return shaka.util.AbortableOperation.failed(
          new shaka.util.Error(
              shaka.util.Error.Severity.CRITICAL,
              shaka.util.Error.Category.NETWORK,
              shaka.util.Error.Code.UNSUPPORTED_SCHEME,
              uri));
    }
    const progressSupport = object.progressSupport;

    // Add headers from CommonAccessToken
    const commonAccessToken =
        this.hostCommonAccessTokenMap_.get(uri.getDomain());
    if (commonAccessToken) {
      request.headers[shaka.net.NetworkingEngine.CommonAccessTokenHeaderName_] =
          commonAccessToken;
    }

    // Every attempt must have an associated backoff.attempt() call so that the
    // accounting is correct.
    const backoffOperation =
        shaka.util.AbortableOperation.notAbortable(backoff.attempt());

    /** @type {?shaka.util.Timer} */
    let connectionTimer = null;

    /** @type {?shaka.util.Timer} */
    let stallTimer = null;

    let aborted = false;

    let headersReceivedCalled = false;

    let startTimeMs;
    const sendOperation = backoffOperation.chain(() => {
      if (this.destroyed_) {
        return shaka.util.AbortableOperation.aborted();
      }

      startTimeMs = Date.now();
      const segment = shaka.net.NetworkingEngine.RequestType.SEGMENT;
      let packetNumber = 0;

      const progressUpdated = (time, bytes, numBytesRemaining) => {
        if (connectionTimer) {
          connectionTimer.stop();
        }
        if (stallTimer) {
          stallTimer.tickAfter(stallTimeoutMs / 1000);
        }
        if (this.onProgressUpdated_ && type == segment) {
          packetNumber++;
          request.packetNumber = packetNumber;
          const allowSwitch = this.allowSwitch_(context);
          this.onProgressUpdated_(time, bytes, allowSwitch, request, context);
          gotProgress = true;
          numBytesRemainingObj.setBytes(numBytesRemaining);
        }
      };
      const headersReceived = (headers) => {
        headersReceivedCalled = true;
        request.timeToFirstByte = Date.now() -
        /** @type {number} */ (request.requestStartTime);
        if (this.onHeadersReceived_) {
          this.onHeadersReceived_(headers, request, type);
        }
      };
      request.requestStartTime = Date.now();

      const requestPlugin = plugin(
          request.uris[index], request, type, progressUpdated, headersReceived,
          {
            minBytesForProgressEvents: this.config_.minBytesForProgressEvents,
          });

      if (!progressSupport) {
        return requestPlugin;
      }

      const connectionTimeoutMs = request.retryParameters.connectionTimeout;
      if (connectionTimeoutMs) {
        connectionTimer = new shaka.util.Timer(() => {
          aborted = true;
          requestPlugin.abort();
        });

        connectionTimer.tickAfter(connectionTimeoutMs / 1000);
      }

      const stallTimeoutMs = request.retryParameters.stallTimeout;
      if (stallTimeoutMs) {
        stallTimer = new shaka.util.Timer(() => {
          aborted = true;
          requestPlugin.abort();
        });
      }

      return requestPlugin;
    }).chain((response) => {
      if (connectionTimer) {
        connectionTimer.stop();
      }
      if (stallTimer) {
        stallTimer.stop();
      }
      if (response.timeMs == undefined) {
        response.timeMs = Date.now() - startTimeMs;
      }
      // Process headers to get the new CommonAccessToken
      const commonAccessTokenHeader = response.headers[
          shaka.net.NetworkingEngine.CommonAccessTokenHeaderName_];
      if (commonAccessTokenHeader) {
        const responseHost = new goog.Uri(response.uri);
        this.hostCommonAccessTokenMap_.set(
            responseHost.getDomain(), commonAccessTokenHeader);
      }
      const responseAndGotProgress = {
        response: response,
        gotProgress: gotProgress,
      };
      if (!headersReceivedCalled) {
        // The plugin did not call headersReceived, perhaps because it is not
        // able to track that information. So, fire the event manually.
        if (this.onHeadersReceived_) {
          this.onHeadersReceived_(response.headers, request, type);
        }
      }
      if (this.onDownloadCompleted_) {
        this.onDownloadCompleted_(request, response);
      }
      return responseAndGotProgress;
    }, (error) => {
      if (connectionTimer) {
        connectionTimer.stop();
      }
      if (stallTimer) {
        stallTimer.stop();
      }
      if (this.onDownloadFailed_) {
        let shakaError = null;
        let httpResponseCode = 0;
        if (error instanceof shaka.util.Error) {
          shakaError = error;
          if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS) {
            httpResponseCode = /** @type {number} */ (error.data[1]);
          }
        }
        this.onDownloadFailed_(request, shakaError, httpResponseCode, aborted);
      }
      if (this.destroyed_) {
        return shaka.util.AbortableOperation.aborted();
      }

      if (aborted) {
        // It is necessary to change the error code to the correct one because
        // otherwise the retry logic would not work.
        error = new shaka.util.Error(
            shaka.util.Error.Severity.RECOVERABLE,
            shaka.util.Error.Category.NETWORK,
            shaka.util.Error.Code.TIMEOUT,
            request.uris[index], type);
      }

      if (error instanceof shaka.util.Error) {
        if (error.code == shaka.util.Error.Code.OPERATION_ABORTED) {
          // Don't change anything if the operation was aborted.
          throw error;
        } else if (error.code == shaka.util.Error.Code.ATTEMPTS_EXHAUSTED) {
          goog.asserts.assert(lastError, 'Should have last error');
          throw lastError;
        }

        if (error.severity == shaka.util.Error.Severity.RECOVERABLE) {
          const data = (new Map()).set('error', error);
          const event = new shaka.util.FakeEvent('retry', data);
          // A user can call preventDefault() on a cancelable event.
          event.cancelable = true;
          this.dispatchEvent(event);

          if (event.defaultPrevented) {
            // If the caller uses preventDefault() on the 'retry' event, don't
            // retry any more.
            throw error;
          }

          // Move to the next URI.
          index = (index + 1) % request.uris.length;
          return this.send_(
              type, request, context, backoff, index, error,
              numBytesRemainingObj);
        }
      }

      // The error was not recoverable, so do not try again.
      throw error;
    });

    return sendOperation;
  }
Sends the given request to the correct plugin and retry using Backoff.

@param {shaka.net.NetworkingEngine.RequestType} type
@param {shaka.extern.Request} request
@param {(shaka.extern.RequestContext|undefined)} context
@param {!shaka.net.Backoff} backoff
@param {number} index
@param {?shaka.util.Error} lastError
@param {shaka.net.NetworkingEngine.NumBytesRemainingClass
       } numBytesRemainingObj
@return {!shaka.extern.IAbortableOperation.<
              shaka.net.NetworkingEngine.ResponseAndGotProgress>}
@private