diff --git a/externs/shaka/net.js b/externs/shaka/net.js index 610a84f853..81fe73fd84 100644 --- a/externs/shaka/net.js +++ b/externs/shaka/net.js @@ -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.} + * @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. diff --git a/lib/net/http_fetch_plugin.js b/lib/net/http_fetch_plugin.js index bb26c0e331..10383bcae6 100644 --- a/lib/net/http_fetch_plugin.js +++ b/lib/net/http_fetch_plugin.js @@ -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); } diff --git a/lib/net/http_xhr_plugin.js b/lib/net/http_xhr_plugin.js index 2cb819029f..aa4fcaa1c1 100644 --- a/lib/net/http_xhr_plugin.js +++ b/lib/net/http_xhr_plugin.js @@ -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.} * @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'; @@ -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 @@ -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); diff --git a/lib/net/networking_engine.js b/lib/net/networking_engine.js index eb234e428e..da129d6fec 100644 --- a/lib/net/networking_engine.js +++ b/lib/net/networking_engine.js @@ -49,9 +49,9 @@ 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 @@ -59,7 +59,7 @@ goog.require('shaka.util.OperationManager'); * @extends {shaka.util.FakeEventTarget} * @export */ -shaka.net.NetworkingEngine = function(onSegmentDownloaded) { +shaka.net.NetworkingEngine = function(onProgressUpdated) { shaka.util.FakeEventTarget.call(this); /** @private {boolean} */ @@ -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); @@ -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 @@ -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(); @@ -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. @@ -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.} + * @return {!shaka.extern.IAbortableOperation.< + * shaka.net.NetworkingEngine.ResponseAndGotProgress>} * @private */ shaka.net.NetworkingEngine.prototype.makeRequestWithRetry_ = @@ -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.} + * @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. @@ -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) { @@ -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. @@ -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.} + * @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. diff --git a/lib/player.js b/lib/player.js index ec4a419b95..64134ea6f5 100644 --- a/lib/player.js +++ b/lib/player.js @@ -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. @@ -1046,7 +1046,7 @@ shaka.Player.prototype.createNetworkingEngine = function() { } }; - return new shaka.net.NetworkingEngine(onSegmentDownloaded); + return new shaka.net.NetworkingEngine(onProgressUpdated_); }; diff --git a/test/net/http_plugin_unit.js b/test/net/http_plugin_unit.js index bbc9a8b5dd..ae1b834aa8 100644 --- a/test/net/http_plugin_unit.js +++ b/test/net/http_plugin_unit.js @@ -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; @@ -81,7 +82,7 @@ function httpPluginTests(usingFetch) { return eventHandler; }, }); - }); + } return instance; };