Skip to content

Commit

Permalink
Add Progress Event Handler for XHR plugin
Browse files Browse the repository at this point in the history
This is part 1 of conditionally aborting requests when network
downgrading.
1. Add progress event handler for Http XHR Plugin, to get more frequent
updates for network bandwidth estimate and suggest streams based on
that.
2. When the plugin doesn't support progress event, call
onProgressUpdated function after every segment is downloaded.
3. Replace onSegmentDownloaded with onProgressUpdated function in
player.js and network_engine.js.
4. Since XHR Plugin supports progress event while Fetch doesn't, change
the config to prefer XHR over Fetch API.

Issue #1051.

Change-Id: Icf6775dd3520fb2e359b13d29e3b39d3792fe865
  • Loading branch information
michellezhuogg committed Sep 7, 2018
1 parent bf6fcf0 commit d6720cc
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 41 deletions.
26 changes: 23 additions & 3 deletions externs/shaka/net.js
Expand Up @@ -129,17 +129,37 @@ shaka.extern.Response;


/**
* Defines a plugin that handles a specific scheme.
*
* @typedef {!function(string,
* shaka.extern.Request,
* shaka.net.NetworkingEngine.RequestType):
* shaka.net.NetworkingEngine.RequestType,
* shaka.extern.ProgressUpdated=):
* !shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @description
* Defines a plugin that handles a specific scheme.
* The functions accepts four parameters, uri string, request, request type,
* and an optional progressUpdated function.
* @exportDoc
*/
shaka.extern.SchemePlugin;


/**
* @typedef {function(number, number)}
*
* @description
* A callback function to handle progress event through networking engine in
* player.
* The first argument is a number for duration in milliseconds, that the request
* took to complete.
* The second argument is the the total number of bytes downloaded during that
* time.
*
* @exportDoc
*/
shaka.extern.ProgressUpdated;


/**
* Defines a filter for requests. This filter takes the request and modifies
* it before it is sent to the scheme plugin.
Expand Down
6 changes: 4 additions & 2 deletions lib/net/http_fetch_plugin.js
Expand Up @@ -188,8 +188,10 @@ shaka.net.HttpFetchPlugin.Headers_ = window.Headers;


if (shaka.net.HttpFetchPlugin.isSupported()) {
// TODO: Update Fetch plugin to use progress events so we can use Fetch as
// the default.
shaka.net.NetworkingEngine.registerScheme('http', shaka.net.HttpFetchPlugin,
shaka.net.NetworkingEngine.PluginPriority.PREFERRED);
shaka.net.NetworkingEngine.PluginPriority.FALLBACK);
shaka.net.NetworkingEngine.registerScheme('https', shaka.net.HttpFetchPlugin,
shaka.net.NetworkingEngine.PluginPriority.PREFERRED);
shaka.net.NetworkingEngine.PluginPriority.FALLBACK);
}
25 changes: 22 additions & 3 deletions lib/net/http_xhr_plugin.js
Expand Up @@ -30,12 +30,19 @@ goog.require('shaka.util.Error');
* @param {string} uri
* @param {shaka.extern.Request} request
* @param {shaka.net.NetworkingEngine.RequestType} requestType
* @param {function(number, number)=} progressUpdated Called when a progress
* event happened.
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @export
*/
shaka.net.HttpXHRPlugin = function(uri, request, requestType) {
shaka.net.HttpXHRPlugin = function(uri, request, requestType, progressUpdated) {
let 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;

let promise = new Promise(function(resolve, reject) {
xhr.open(request.method, uri, true);
xhr.responseType = 'arraybuffer';
Expand Down Expand Up @@ -87,6 +94,18 @@ shaka.net.HttpXHRPlugin = function(uri, request, requestType) {
shaka.util.Error.Code.TIMEOUT,
uri, requestType));
};
xhr.onprogress = function(event) {
let 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);
lastLoaded = event.loaded;
lastTime = currentTime;
}
};

for (let key in request.headers) {
// The Fetch API automatically normalizes outgoing header keys to
Expand Down Expand Up @@ -116,7 +135,7 @@ shaka.net.HttpXHRPlugin.Xhr_ = window.XMLHttpRequest;


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

93 changes: 64 additions & 29 deletions lib/net/networking_engine.js
Expand Up @@ -49,17 +49,17 @@ goog.require('shaka.util.OperationManager');
* 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)=} onSegmentDownloaded Called
* when a segment is downloaded. Passed the duration, in milliseconds, that
* the request took, and the total number of bytes transferred.
* @param {function(number, number)=} onProgressUpdated Called when a progress
* event is triggered. Passed the duration, in milliseconds, that the request
* took, and the number of bytes transferred.
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
* @extends {shaka.util.FakeEventTarget}
* @export
*/
shaka.net.NetworkingEngine = function(onSegmentDownloaded) {
shaka.net.NetworkingEngine = function(onProgressUpdated) {
shaka.util.FakeEventTarget.call(this);

/** @private {boolean} */
Expand All @@ -75,7 +75,7 @@ shaka.net.NetworkingEngine = function(onSegmentDownloaded) {
this.responseFilters_ = [];

/** @private {?function(number, number)} */
this.onSegmentDownloaded_ = onSegmentDownloaded || null;
this.onProgressUpdated_ = onProgressUpdated || null;
};

goog.inherits(shaka.net.NetworkingEngine, shaka.util.FakeEventTarget);
Expand Down Expand Up @@ -131,6 +131,22 @@ shaka.net.NetworkingEngine.SchemeObject;
*/
shaka.net.NetworkingEngine.schemes_ = {};

/**
* @typedef {{
* response: shaka.extern.Response,
* gotProgress: boolean
* }}
*
* @description
* Defines a response wrapper object, including the response object and whether
* progress event is fired by the scheme plugin.
*
* @property {shaka.extern.Response} response
* @property {boolean} gotProgress
* @private
*/
shaka.net.NetworkingEngine.ResponseAndGotProgress;


/**
* Registers a scheme plugin. This plugin will handle all requests with the
Expand Down Expand Up @@ -348,7 +364,8 @@ shaka.net.NetworkingEngine.prototype.request = function(type, request) {
let requestOperation = requestFilterOperation.chain(
() => this.makeRequestWithRetry_(type, request));
let responseFilterOperation = requestOperation.chain(
(response) => this.filterResponse_(type, response));
(responseAndGotProgress) =>
this.filterResponse_(type, responseAndGotProgress));

// Keep track of time spent in filters.
let requestFilterStartTime = Date.now();
Expand All @@ -362,17 +379,19 @@ shaka.net.NetworkingEngine.prototype.request = function(type, request) {
responseFilterStartTime = Date.now();
}, () => {}); // Silence errors in this fork of the Promise chain.

let operation = responseFilterOperation.chain((response) => {
let responseFilterMs = Date.now() - responseFilterStartTime;
let operation = responseFilterOperation.chain(
(responseAndGotProgress) => {
let responseFilterMs = Date.now() - responseFilterStartTime;
let response = responseAndGotProgress.response;

response.timeMs += requestFilterMs;
response.timeMs += responseFilterMs;

if (this.onSegmentDownloaded_ && !response.fromCache &&
type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
this.onSegmentDownloaded_(response.timeMs, response.data.byteLength);
}
response.timeMs += requestFilterMs;
response.timeMs += responseFilterMs;

if (!responseAndGotProgress.gotProgress && this.onProgressUpdated_ &&
!response.fromCache &&
type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
this.onProgressUpdated_(response.timeMs, response.data.byteLength);
}
return response;
}, (e) => {
// Any error thrown from elsewhere should be recategorized as CRITICAL here.
Expand Down Expand Up @@ -425,7 +444,8 @@ shaka.net.NetworkingEngine.prototype.filterRequest_ = function(type, request) {
/**
* @param {shaka.net.NetworkingEngine.RequestType} type
* @param {shaka.extern.Request} request
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @return {!shaka.extern.IAbortableOperation.<
* shaka.net.NetworkingEngine.ResponseAndGotProgress>}
* @private
*/
shaka.net.NetworkingEngine.prototype.makeRequestWithRetry_ =
Expand All @@ -445,13 +465,16 @@ shaka.net.NetworkingEngine.prototype.makeRequestWithRetry_ =
* @param {!shaka.net.Backoff} backoff
* @param {number} index
* @param {?shaka.util.Error} lastError
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @return {!shaka.extern.IAbortableOperation.<
* shaka.net.NetworkingEngine.ResponseAndGotProgress>}
* @private
*/
shaka.net.NetworkingEngine.prototype.send_ = function(
type, request, backoff, index, lastError) {
let 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.
Expand Down Expand Up @@ -490,8 +513,14 @@ shaka.net.NetworkingEngine.prototype.send_ = function(
}

startTimeMs = Date.now();
let operation = plugin(request.uris[index], request, type);

const segment = shaka.net.NetworkingEngine.RequestType.SEGMENT;
let operation = plugin(request.uris[index], request, type,
(time, bytes) => {
if (this.onProgressUpdated_ && type == segment) {
this.onProgressUpdated_(time, bytes);
gotProgress = true;
}
});
// Backward compatibility with older scheme plugins.
// TODO: remove in v2.5
if (operation.promise == undefined) {
Expand All @@ -509,7 +538,13 @@ shaka.net.NetworkingEngine.prototype.send_ = function(
if (response.timeMs == undefined) {
response.timeMs = Date.now() - startTimeMs;
}
return response;

let responseAndGotProgress = {
response: response,
gotProgress: gotProgress,
};

return responseAndGotProgress;
}, (error) => {
if (error && error.code == shaka.util.Error.Code.OPERATION_ABORTED) {
// Don't change anything if the operation was aborted.
Expand Down Expand Up @@ -544,23 +579,23 @@ shaka.net.NetworkingEngine.prototype.send_ = function(

/**
* @param {shaka.net.NetworkingEngine.RequestType} type
* @param {shaka.extern.Response} response
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @param {shaka.net.NetworkingEngine.ResponseAndGotProgress}
* responseAndGotProgress
* @return {!shaka.extern.IAbortableOperation.<
* shaka.net.NetworkingEngine.ResponseAndGotProgress>}
* @private
*/
shaka.net.NetworkingEngine.prototype.filterResponse_ =
function(type, response) {
function(type, responseAndGotProgress) {
let filterOperation = shaka.util.AbortableOperation.completed(undefined);

this.responseFilters_.forEach((responseFilter) => {
// Response filters are run sequentially.
filterOperation =
filterOperation.chain(() => responseFilter(type, response));
filterOperation = filterOperation.chain(
responseFilter.bind(null, type, responseAndGotProgress.response));
});

// If successful, return the filtered response with whether it got progress.
return filterOperation.chain(() => {
// If successful, return the filtered response.
return response;
return responseAndGotProgress;
}, (e) => {
// Catch any errors thrown by request filters, and substitute
// them with a Shaka-native error.
Expand Down
4 changes: 2 additions & 2 deletions lib/player.js
Expand Up @@ -1037,7 +1037,7 @@ shaka.Player.prototype.createDrmEngine = async function(manifest) {
*/
shaka.Player.prototype.createNetworkingEngine = function() {
/** @type {function(number, number)} */
const onSegmentDownloaded = (deltaTimeMs, numBytes) => {
const onProgressUpdated_ = (deltaTimeMs, numBytes) => {
// In some situations, such as during offline storage, the abr manager might
// not yet exist. Therefore, we need to check if abr manager has been
// initialized before using it.
Expand All @@ -1046,7 +1046,7 @@ shaka.Player.prototype.createNetworkingEngine = function() {
}
};

return new shaka.net.NetworkingEngine(onSegmentDownloaded);
return new shaka.net.NetworkingEngine(onProgressUpdated_);
};


Expand Down
5 changes: 3 additions & 2 deletions test/net/http_plugin_unit.js
Expand Up @@ -60,7 +60,8 @@ function httpPluginTests(usingFetch) {
const MockXHR = function() {
const instance = new JasmineXHRMock();

['abort', 'load', 'error', 'timeout'].forEach(function(eventName) {
const events = ['abort', 'load', 'error', 'timeout', 'progress'];
for (const eventName of events) {
const eventHandlerName = 'on' + eventName;
let eventHandler = null;

Expand All @@ -81,7 +82,7 @@ function httpPluginTests(usingFetch) {
return eventHandler;
},
});
});
}

return instance;
};
Expand Down

0 comments on commit d6720cc

Please sign in to comment.