Source: util/failover_uri.js

/**
 * @license
 * Copyright 2015 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

goog.provide('shaka.util.FailoverUri');

goog.require('goog.Uri');
goog.require('shaka.asserts');
goog.require('shaka.log');
goog.require('shaka.util.AjaxRequest');



/**
 * Creates a FailoverUri, which handles requests to multiple URLs in case of
 * failure.
 *
 * @param {shaka.util.FailoverUri.NetworkCallback} callback
 * @param {Array.<!goog.Uri>} urls
 * @param {number=} opt_startByte The start byte of the data, defaults to 0.
 * @param {?number=} opt_endByte The end byte of the data, null means the end;
 *     defaults to null.
 * @constructor
 */
shaka.util.FailoverUri = function(callback, urls, opt_startByte, opt_endByte) {
  shaka.asserts.assert(urls);
  shaka.asserts.assert(urls.length > 0);

  /** @const {!Array.<!goog.Uri>} */
  this.urls = urls;

  /** @const {number} */
  this.startByte = opt_startByte || 0;

  /** @const {?number} */
  this.endByte = opt_endByte != null ? opt_endByte : null;

  /** @private {?Promise} */
  this.requestPromise_ = null;

  /** @private {shaka.util.AjaxRequest} */
  this.request_ = null;

  /** @private {shaka.util.FailoverUri.NetworkCallback} */
  this.callback_ = callback;

  /** @type {goog.Uri} */
  this.currentUrl = null;
};


/**
 * A callback to the application called prior to media network events.  The
 * first parameter is the URL for the request.  The second parameter is the
 * headers for the request.  These can be modified.  The callback should return
 * a modified URL for the request, or null to use the original.
 *
 * @typedef {?function(string,!Object.<string, string>):(string?)}
 * @exportDoc
 */
shaka.util.FailoverUri.NetworkCallback;


/**
 * Resolves a relative url to the given |baseUrl|.
 *
 * @param {Array.<!goog.Uri>} baseUrl
 * @param {!goog.Uri} url
 * @return {!Array.<!goog.Uri>}
 */
shaka.util.FailoverUri.resolve = function(baseUrl, url) {
  if (!baseUrl || baseUrl.length === 0) {
    return [url];
  }

  return baseUrl.map(function(e) { return e.resolve(url); });
};


/**
 * Gets whether the URI is offline (uses the scheme idb).
 *
 * @return {boolean}
 */
shaka.util.FailoverUri.prototype.isOfflineUri = function() {
  // Offline does not use failover.
  return this.urls[0].getScheme() == 'idb';
};


/**
 * Gets the data specified by the URLs.
 *
 * @param {shaka.util.AjaxRequest.Parameters=} opt_parameters
 * @param {shaka.util.IBandwidthEstimator=} opt_estimator
 * @return {!Promise.<!ArrayBuffer|string>}
 */
shaka.util.FailoverUri.prototype.fetch =
    function(opt_parameters, opt_estimator) {
  if (this.requestPromise_) {
    // A fetch is already in progress.
    return this.requestPromise_;
  }

  var parameters = opt_parameters || new shaka.util.AjaxRequest.Parameters();
  if (this.startByte || this.endByte) {
    var rangeString =
        this.startByte + '-' + (this.endByte != null ? this.endByte : '');
    parameters.requestHeaders['Range'] = 'bytes=' + rangeString;
  }

  shaka.asserts.assert(!this.request_);
  this.requestPromise_ = this.createRequest_(0, parameters, opt_estimator);
  return this.requestPromise_;
};


/**
 * Aborts fetch() if it is pending.
 */
shaka.util.FailoverUri.prototype.abortFetch = function() {
  if (this.request_) {
    // Set the promise first to indicate to the running promise it is an abort.
    this.requestPromise_ = null;
    this.request_.abort();
    this.request_ = null;
    this.currentUrl = null;
  }
};


/**
 * Creates a request using the given url.  This will add the catch block
 * and will recursively call itself to handle failover.
 *
 * @private
 * @param {number} i
 * @param {!shaka.util.AjaxRequest.Parameters} parameters
 * @param {shaka.util.IBandwidthEstimator=} opt_estimator
 * @return {!Promise.<!ArrayBuffer|string>}
 */
shaka.util.FailoverUri.prototype.createRequest_ =
    function(i, parameters, opt_estimator) {
  shaka.asserts.assert(i < this.urls.length);

  var url = this.urls[i].toString();
  if (this.callback_) {
    url = this.callback_(url, parameters.requestHeaders) || url;
  }
  this.request_ = new shaka.util.AjaxRequest(url, parameters);
  if (opt_estimator) {
    this.request_.estimator = opt_estimator;
  }

  var p = this.request_.send().then(shaka.util.TypedBind(this,
      /** @param {!XMLHttpRequest} xhr */
      function(xhr) {
        // |requestPromise_| MUST be set to null; otherwise, we will
        // effectively cache every response.
        this.requestPromise_ = null;
        this.request_ = null;
        this.currentUrl = null;
        if (xhr.responseURL) {
          this.currentUrl = new goog.Uri(xhr.responseURL);
        } else {
          this.currentUrl = this.urls[i];
        }
        return Promise.resolve(xhr.response);
      }));

  p = p.catch(shaka.util.TypedBind(this,
      /** @param {*} error */
      function(error) {
        if (this.requestPromise_ && i + 1 < this.urls.length) {
          shaka.log.info('Trying fallback URL...');
          this.requestPromise_ = this.createRequest_(
              i + 1, parameters, opt_estimator);
          return this.requestPromise_;
        } else {
          this.request_ = null;
          this.requestPromise_ = null;
          return Promise.reject(error);
        }
      }));

  return p;
};


/**
 * Creates a deep-copy of the object.
 *
 * @return {!shaka.util.FailoverUri}
 */
shaka.util.FailoverUri.prototype.clone = function() {
  return new shaka.util.FailoverUri(
      this.callback_,
      this.urls.map(function(a) { return a.clone(); }),
      this.startByte,
      this.endByte
  );
};


/**
 * Gets the first url.
 *
 * @return {!string}
 */
shaka.util.FailoverUri.prototype.toString = function() {
  return this.urls[0].toString();
};