Source: lib/net/http_xhr_plugin.js

/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.net.HttpXHRPlugin');

goog.require('goog.asserts');
goog.require('shaka.net.HttpPluginUtils');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.AbortableOperation');
goog.require('shaka.util.Error');


/**
 * @summary A networking plugin to handle http and https URIs via XHR.
 * @export
 */
shaka.net.HttpXHRPlugin = class {
  /**
   * @param {string} uri
   * @param {shaka.extern.Request} request
   * @param {shaka.net.NetworkingEngine.RequestType} requestType
   * @param {shaka.extern.ProgressUpdated} progressUpdated Called when a
   *   progress event happened.
   * @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
   * @export
   */
  static parse(uri, request, requestType, progressUpdated) {
    const xhr = new shaka.net.HttpXHRPlugin.Xhr_();

    // Last time stamp when we got a progress event.
    let lastTime = Date.now();
    // Last number of bytes loaded, from progress event.
    let lastLoaded = 0;

    const promise = new Promise(((resolve, reject) => {
      xhr.open(request.method, uri, true);
      xhr.responseType = 'arraybuffer';
      xhr.timeout = request.retryParameters.timeout;
      xhr.withCredentials = request.allowCrossSiteCredentials;

      xhr.onabort = () => {
        reject(new shaka.util.Error(
            shaka.util.Error.Severity.RECOVERABLE,
            shaka.util.Error.Category.NETWORK,
            shaka.util.Error.Code.OPERATION_ABORTED,
            uri, requestType));
      };
      xhr.onload = (event) => {
        const target = event.target;
        goog.asserts.assert(target, 'XHR onload has no target!');
        // Since IE and Edge incorrectly return the header with a leading new
        // line character ('\n'), we trim the header here.
        const headerLines = target.getAllResponseHeaders().trim().split('\r\n');
        const headers = {};
        for (const header of headerLines) {
          /** @type {!Array.<string>} */
          const parts = header.split(': ');
          headers[parts[0].toLowerCase()] = parts.slice(1).join(': ');
        }

        try {
          const response = shaka.net.HttpPluginUtils.makeResponse(headers,
              target.response, target.status, uri, target.responseURL,
              requestType);
          resolve(response);
        } catch (error) {
          goog.asserts.assert(error instanceof shaka.util.Error,
              'Wrong error type!');
          reject(error);
        }
      };
      xhr.onerror = (event) => {
        reject(new shaka.util.Error(
            shaka.util.Error.Severity.RECOVERABLE,
            shaka.util.Error.Category.NETWORK,
            shaka.util.Error.Code.HTTP_ERROR,
            uri, event, requestType));
      };
      xhr.ontimeout = (event) => {
        reject(new shaka.util.Error(
            shaka.util.Error.Severity.RECOVERABLE,
            shaka.util.Error.Category.NETWORK,
            shaka.util.Error.Code.TIMEOUT,
            uri, requestType));
      };
      xhr.onprogress = (event) => {
        const currentTime = Date.now();
        // If the time between last time and this time we got progress event
        // is long enough, or if a whole segment is downloaded, call
        // progressUpdated().
        if (currentTime - lastTime > 100 ||
            (event.lengthComputable && event.loaded == event.total)) {
          progressUpdated(currentTime - lastTime, event.loaded - lastLoaded,
              event.total - event.loaded);
          lastLoaded = event.loaded;
          lastTime = currentTime;
        }
      };

      for (const key in request.headers) {
        // The Fetch API automatically normalizes outgoing header keys to
        // lowercase. For consistency's sake, do it here too.
        const lowercasedKey = key.toLowerCase();
        xhr.setRequestHeader(lowercasedKey, request.headers[key]);
      }
      xhr.send(request.body);
    }));

    return new shaka.util.AbortableOperation(
        promise,
        () => {
          xhr.abort();
          return Promise.resolve();
        });
  }
};


/**
 * Overridden in unit tests, but compiled out in production.
 *
 * @const {function(new: XMLHttpRequest)}
 * @private
 */
shaka.net.HttpXHRPlugin.Xhr_ = window.XMLHttpRequest;


shaka.net.NetworkingEngine.registerScheme(
    'http', shaka.net.HttpXHRPlugin.parse,
    shaka.net.NetworkingEngine.PluginPriority.FALLBACK);
shaka.net.NetworkingEngine.registerScheme(
    'https', shaka.net.HttpXHRPlugin.parse,
    shaka.net.NetworkingEngine.PluginPriority.FALLBACK);