Files
shaka-player/lib/net/networking_engine.js
T
Joey Parrish 836bd9a177 Make filters, requests, and responses easier
By exporting StringUtils and Uint8ArrayUtils, we can simplify the
process of wrapping, unwrapping, and parsing requests and responses
in NetworkingEngine filters.

This makes it easier to write filters that deal with text-based
data, as it is no longer the job of the application developer to
deal with subtle conversions between string and ArrayBuffer.

We will also fill in requests with defaults if needed.  This allows
developers to omit fields when initiating extra requests at the
application level.

Closes #667

Change-Id: Ie6a0f23b4d46e7e458d996759eac6cd85a36b741
2017-02-08 00:09:08 +00:00

446 lines
12 KiB
JavaScript

/**
* @license
* Copyright 2016 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.net.NetworkingEngine');
goog.require('goog.Uri');
goog.require('goog.asserts');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.PublicPromise');
/**
* NetworkingEngine wraps all networking operations. This accepts plugins that
* handle the actual request. A plugin is registered using registerScheme.
* Each scheme has at most one plugin to handle the request.
*
* @param {function(number, number)=} opt_onSegmentDownloaded Called
* when a segment is downloaded. Passed the duration, in milliseconds, that
* the request took; and the total number of bytes transferred.
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
* @export
*/
shaka.net.NetworkingEngine = function(opt_onSegmentDownloaded) {
/** @private {boolean} */
this.destroyed_ = false;
/** @private {!Array.<!Promise>} */
this.requests_ = [];
/** @private {!Array.<shakaExtern.RequestFilter>} */
this.requestFilters_ = [];
/** @private {!Array.<shakaExtern.ResponseFilter>} */
this.responseFilters_ = [];
/** @private {?function(number, number)} */
this.onSegmentDownloaded_ = opt_onSegmentDownloaded || null;
};
/**
* Request types. Allows a filter to decide which requests to read/alter.
*
* @enum {number}
* @export
*/
shaka.net.NetworkingEngine.RequestType = {
'MANIFEST': 0,
'SEGMENT': 1,
'LICENSE': 2
};
/**
* Contains the scheme plugins.
*
* @private {!Object.<string, ?shakaExtern.SchemePlugin>}
*/
shaka.net.NetworkingEngine.schemes_ = {};
/**
* Registers a scheme plugin. This plugin will handle all requests with the
* given scheme. If a plugin with the same scheme already exists, it is
* replaced.
*
* @param {string} scheme
* @param {shakaExtern.SchemePlugin} plugin
* @export
*/
shaka.net.NetworkingEngine.registerScheme = function(scheme, plugin) {
shaka.net.NetworkingEngine.schemes_[scheme] = plugin;
};
/**
* Removes a scheme plugin.
*
* @param {string} scheme
* @export
*/
shaka.net.NetworkingEngine.unregisterScheme = function(scheme) {
delete shaka.net.NetworkingEngine.schemes_[scheme];
};
/**
* Registers a new request filter. All filters are applied in the order they
* are registered.
*
* @param {shakaExtern.RequestFilter} filter
* @export
*/
shaka.net.NetworkingEngine.prototype.registerRequestFilter = function(filter) {
this.requestFilters_.push(filter);
};
/**
* Removes a request filter.
*
* @param {shakaExtern.RequestFilter} filter
* @export
*/
shaka.net.NetworkingEngine.prototype.unregisterRequestFilter =
function(filter) {
var filters = this.requestFilters_;
var i = filters.indexOf(filter);
if (i >= 0) {
filters.splice(i, 1);
}
};
/**
* Clear all request filters.
*
* @export
*/
shaka.net.NetworkingEngine.prototype.clearAllRequestFilters = function() {
this.requestFilters_ = [];
};
/**
* Registers a new response filter. All filters are applied in the order they
* are registered.
*
* @param {shakaExtern.ResponseFilter} filter
* @export
*/
shaka.net.NetworkingEngine.prototype.registerResponseFilter = function(filter) {
this.responseFilters_.push(filter);
};
/**
* Removes a response filter.
*
* @param {shakaExtern.ResponseFilter} filter
* @export
*/
shaka.net.NetworkingEngine.prototype.unregisterResponseFilter =
function(filter) {
var filters = this.responseFilters_;
var i = filters.indexOf(filter);
if (i >= 0) {
filters.splice(i, 1);
}
};
/**
* Clear all response filters.
*
* @export
*/
shaka.net.NetworkingEngine.prototype.clearAllResponseFilters = function() {
this.responseFilters_ = [];
};
/**
* Gets a copy of the default retry parameters.
*
* @return {shakaExtern.RetryParameters}
*/
shaka.net.NetworkingEngine.defaultRetryParameters = function() {
// Use a function rather than a constant member so the calling code can
// modify the values without affecting other call results.
return {
maxAttempts: 2,
baseDelay: 1000,
backoffFactor: 2,
fuzzFactor: 0.5,
timeout: 0
};
};
/**
* Makes a simple network request for the given URIs.
*
* @param {!Array.<string>} uris
* @param {shakaExtern.RetryParameters} retryParams
* @return {shakaExtern.Request}
*/
shaka.net.NetworkingEngine.makeRequest = function(
uris, retryParams) {
return {
uris: uris,
method: 'GET',
body: null,
headers: {},
allowCrossSiteCredentials: false,
retryParameters: retryParams
};
};
/**
* @override
* @export
*/
shaka.net.NetworkingEngine.prototype.destroy = function() {
var Functional = shaka.util.Functional;
this.destroyed_ = true;
this.requestFilters_ = [];
this.responseFilters_ = [];
var cleanup = [];
for (var i = 0; i < this.requests_.length; ++i) {
cleanup.push(this.requests_[i].catch(Functional.noop));
}
return Promise.all(cleanup);
};
/**
* Makes a network request and returns the resulting data.
*
* @param {shaka.net.NetworkingEngine.RequestType} type
* @param {shakaExtern.Request} request
* @return {!Promise.<shakaExtern.Response>}
* @export
*/
shaka.net.NetworkingEngine.prototype.request = function(type, request) {
// New requests made after destroy is called are rejected.
if (this.destroyed_)
return Promise.reject();
goog.asserts.assert(request.uris && request.uris.length,
'Request without URIs!');
// If a request comes from outside the library, some parameters may be left
// undefined. To make it easier for application developers, we will fill them
// in with defaults if necessary.
request.method = request.method || 'GET';
request.headers = request.headers || {};
request.retryParameters = request.retryParameters ||
shaka.net.NetworkingEngine.defaultRetryParameters();
var filterStartMs = Date.now();
// Send to the filter first, in-case they change the URI.
var p = Promise.resolve();
this.requestFilters_.forEach(function(requestFilter) {
// Request filters are resolved sequentially.
p = p.then(function() {
return Promise.resolve(requestFilter(type, request));
}.bind(this));
});
// Catch any errors thrown by request filters, and substitute
// them with a Shaka-native error.
p = p.catch(function(e) {
throw new shaka.util.Error(shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.REQUEST_FILTER_ERROR, e);
});
// Send out the request, and get a response.
// The entire code is inside a then clause; thus, if a filter
// rejects or errors, the networking engine will never send.
p = p.then(function() {
var filterTimeMs = (Date.now() - filterStartMs);
var retry = request.retryParameters || {};
var maxAttempts = retry.maxAttempts || 1;
var backoffFactor = retry.backoffFactor || 2.0;
var delay = (retry.baseDelay == null ? 1000 : retry.baseDelay);
var p = this.send_(type, request, 0, filterTimeMs);
for (var i = 1; i < maxAttempts; i++) {
var index = i % request.uris.length;
p = p.catch(
this.resend_.bind(this, type, request, delay, index, filterTimeMs));
delay *= backoffFactor;
}
return p;
}.bind(this));
// Add the request to the array.
this.requests_.push(p);
return p.then(function(response) {
if (this.requests_.indexOf(p) >= 0) {
this.requests_.splice(this.requests_.indexOf(p), 1);
}
if (this.onSegmentDownloaded_ &&
type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
this.onSegmentDownloaded_(response.timeMs, response.data.byteLength);
}
return response;
}.bind(this)).catch(function(e) {
if (this.requests_.indexOf(p) >= 0) {
this.requests_.splice(this.requests_.indexOf(p), 1);
}
return Promise.reject(e);
}.bind(this));
};
/**
* Sends the given request to the correct plugin. This does not handle retry.
*
* @param {shaka.net.NetworkingEngine.RequestType} type
* @param {shakaExtern.Request} request
* @param {number} index
* @param {number} requestFilterTime
* @return {!Promise.<shakaExtern.Response>}
* @private
*/
shaka.net.NetworkingEngine.prototype.send_ = function(
type, request, index, requestFilterTime) {
// Retries sent after destroy is called are rejected.
if (this.destroyed_)
return Promise.reject();
var uri = new goog.Uri(request.uris[index]);
var scheme = uri.getScheme();
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();
}
var plugin = shaka.net.NetworkingEngine.schemes_[scheme];
if (!plugin) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.UNSUPPORTED_SCHEME,
uri));
}
var startTimeMs = Date.now();
return plugin(request.uris[index], request, type).then(function(response) {
if (response.timeMs == undefined)
response.timeMs = Date.now() - startTimeMs;
var filterStartMs = Date.now();
var p = Promise.resolve();
this.responseFilters_.forEach(function(responseFilter) {
// Response filters are resolved sequentially.
p = p.then(function() {
return Promise.resolve(responseFilter(type, response));
}.bind(this));
});
// Catch any errors thrown by response filters, and substitute
// them with a Shaka-native error.
p = p.catch(function(e) {
throw new shaka.util.Error(shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.RESPONSE_FILTER_ERROR, e);
});
return p.then(function() {
response.timeMs += Date.now() - filterStartMs;
response.timeMs += requestFilterTime;
return response;
});
}.bind(this));
};
/**
* Resends the request after applying a delay. This does not handle retry.
*
* @param {shaka.net.NetworkingEngine.RequestType} type
* @param {shakaExtern.Request} request
* @param {number} delayMs The current base delay.
* @param {number} index
* @param {number} requestFilterTime
* @return {!Promise.<shakaExtern.Response>}
* @private
*/
shaka.net.NetworkingEngine.prototype.resend_ =
function(type, request, delayMs, index, requestFilterTime) {
var p = new shaka.util.PublicPromise();
// Fuzz the delay to avoid tons of clients hitting the server at once
// after it recovers from whatever is causing it to fail.
var retry = request.retryParameters || {};
var fuzzFactor = (retry.fuzzFactor == null ? 0.5 : retry.fuzzFactor);
var negToPosOne = (Math.random() * 2.0) - 1.0;
var negToPosFuzzFactor = negToPosOne * fuzzFactor;
var fuzzedDelay = delayMs * (1.0 + negToPosFuzzFactor);
shaka.net.NetworkingEngine.setTimeout_(p.resolve, fuzzedDelay);
return p.then(this.send_.bind(this, type, request, index, requestFilterTime));
};
/**
* This is here only for testability. We can't mock location in our tests on
* all browsers, so instead we mock this.
*
* @return {string} The value of location.protocol.
* @private
*/
shaka.net.NetworkingEngine.getLocationProtocol_ = function() {
return location.protocol;
};
/**
* This is here only for testability. Mocking global setTimeout can lead to
* unintended interactions with other tests. So instead, we mock this.
*
* @param {Function} fn The callback to invoke when the timeout expires.
* @param {number} timeoutMs The timeout in milliseconds.
* @return {number} The timeout ID.
* @private
*/
shaka.net.NetworkingEngine.setTimeout_ = function(fn, timeoutMs) {
return window.setTimeout(fn, timeoutMs);
};