From 2f55d2a3bdf487dc2a719dcf189b2c035d3f7d88 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Mon, 22 Jan 2018 19:03:23 -0800 Subject: [PATCH] Use AbortableOperation in networking This uses AbortableOperation in all networking, from the scheme plugins all the way to the request interface. This also updates all default scheme plugins, docs, and sample code. Backward compatibility is provided for scheme plugins and the request API in NetworkingEngine. This compatibility will be removed in v2.5. Two cancelation-related tests have been disabled in player_integration until the new abort interface has been adopted in the manifest parsers. Issue #829 Change-Id: I91c8e6efe97798d111e8ddca5655cddc1f6bcbf3 --- demo/asset_section.js | 5 +- docs/tutorials/license-server-auth.md | 4 +- docs/tutorials/manifest-parser.md | 2 +- docs/tutorials/upgrade-v2-0.md | 11 +- externs/shaka/net.js | 2 +- lib/dash/dash_parser.js | 10 +- lib/dash/mpd_utils.js | 2 +- lib/hls/hls_parser.js | 35 +- lib/media/drm_engine.js | 2 +- lib/media/manifest_parser.js | 2 +- lib/media/streaming_engine.js | 4 +- lib/net/data_uri_plugin.js | 11 +- lib/net/http_plugin.js | 14 +- lib/net/networking_engine.js | 332 +++++++++++------ lib/offline/download_manager.js | 2 +- lib/offline/offline_scheme.js | 25 +- lib/util/error.js | 12 + shaka-player.uncompiled.js | 1 - test/dash/dash_parser_live_unit.js | 27 +- test/dash/dash_parser_manifest_unit.js | 13 +- test/hls/hls_live_unit.js | 2 +- test/media/drm_engine_unit.js | 34 +- test/media/streaming_engine_unit.js | 6 +- test/net/data_uri_plugin_unit.js | 4 +- test/net/http_plugin_unit.js | 28 +- test/net/networking_engine_unit.js | 442 +++++++++++++---------- test/offline/offline_scheme_unit.js | 8 +- test/player_integration.js | 6 +- test/test/util/fake_networking_engine.js | 42 +-- test/test/util/streaming_engine_util.js | 2 +- test/test/util/test_scheme.js | 25 +- 31 files changed, 659 insertions(+), 456 deletions(-) diff --git a/demo/asset_section.js b/demo/asset_section.js index f6d57c282..ae1473099 100644 --- a/demo/asset_section.js +++ b/demo/asset_section.js @@ -141,9 +141,8 @@ shakaDemo.requestCertificate_ = function(uri) { var requestType = shaka.net.NetworkingEngine.RequestType.APP; var request = /** @type {shakaExtern.Request} */ ({ uris: [uri] }); - return netEngine.request(requestType, request).then(function(response) { - return response.data; - }); + return netEngine.request(requestType, request).promise + .then((response) => response.data); }; diff --git a/docs/tutorials/license-server-auth.md b/docs/tutorials/license-server-auth.md index a5de6ea37..cc604b43d 100644 --- a/docs/tutorials/license-server-auth.md +++ b/docs/tutorials/license-server-auth.md @@ -219,8 +219,8 @@ Now change the request filter: method: 'POST', }; var requestType = shaka.net.NetworkingEngine.RequestType.APP; - return player.getNetworkingEngine().request(requestType, authRequest).then( - function(response) { + return player.getNetworkingEngine().request(requestType, authRequest) + .promise.then(function(response) { // This endpoint responds with the value we should use in the header. authToken = shaka.util.StringUtils.fromUTF8(response.data); console.log('Received auth token', authToken); diff --git a/docs/tutorials/manifest-parser.md b/docs/tutorials/manifest-parser.md index bf8f2bc42..e5e4a9961 100644 --- a/docs/tutorials/manifest-parser.md +++ b/docs/tutorials/manifest-parser.md @@ -27,7 +27,7 @@ MyManifestParser.prototype.start = function(uri, playerInterface) { method: 'GET', retryParameters: this.config_.retryParameters }; - return playerInterface.networkingEngine.request(type, request) + return playerInterface.networkingEngine.request(type, request).promise .then(function(response) { return this.loadManifest_(response.data); }); diff --git a/docs/tutorials/upgrade-v2-0.md b/docs/tutorials/upgrade-v2-0.md index 53815f90c..60c41d490 100644 --- a/docs/tutorials/upgrade-v2-0.md +++ b/docs/tutorials/upgrade-v2-0.md @@ -366,11 +366,12 @@ MyManifestParser.prototype.start = var type = shaka.net.NetworkingEngine.RequestType.MANIFEST; var request = shaka.net.NetworkingEngine.makeRequest( [uri], this.config_.retryParameters); - return this.networkingEngine_.request(type, request).then(function(response) { - this.manifest_ = this.parseInternal_(response.data); - this.updateInterval_ = setInterval(this.updateManifest_.bind(this), 5000); - return this.manifest_; - }); + return this.networkingEngine_.request(type, request).promise + .then(function(response) { + this.manifest_ = this.parseInternal_(response.data); + this.updateInterval_ = setInterval(this.updateManifest_.bind(this), 5000); + return this.manifest_; + }); }; /** @return {!Promise} */ diff --git a/externs/shaka/net.js b/externs/shaka/net.js index a9bb6025e..ba470979f 100644 --- a/externs/shaka/net.js +++ b/externs/shaka/net.js @@ -126,7 +126,7 @@ shakaExtern.Response; * @typedef {!function(string, * shakaExtern.Request, * shaka.net.NetworkingEngine.RequestType): - * !Promise.} + * !shakaExtern.IAbortableOperation.} * @exportDoc */ shakaExtern.SchemePlugin; diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 4da9992f6..822e66b75 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -379,10 +379,8 @@ shaka.dash.DashParser.prototype.requestManifest_ = function() { var request = shaka.net.NetworkingEngine.makeRequest( this.manifestUris_, this.config_.retryParameters); var networkingEngine = this.playerInterface_.networkingEngine; - var isCanceled = (function() { - return !this.playerInterface_; - }).bind(this); - return networkingEngine.request(requestType, request, isCanceled) + // TODO(#829): abort these requests on destroy + return networkingEngine.request(requestType, request).promise .then(function(response) { // Detect calls to stop(). if (!this.playerInterface_) @@ -1410,7 +1408,7 @@ shaka.dash.DashParser.prototype.requestForTiming_ = requestUris, this.config_.retryParameters); request.method = method; var type = shaka.net.NetworkingEngine.RequestType.MANIFEST; - return this.playerInterface_.networkingEngine.request(type, request) + return this.playerInterface_.networkingEngine.request(type, request).promise .then(function(response) { var text; if (method == 'HEAD') { @@ -1563,7 +1561,7 @@ shaka.dash.DashParser.prototype.requestInitSegment_ = function( } return this.playerInterface_.networkingEngine.request(requestType, request) - .then(function(response) { return response.data; }); + .promise.then(function(response) { return response.data; }); }; diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index bd25efe27..ad6382133 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -483,7 +483,7 @@ shaka.dash.MpdUtils.handleXlinkInElement_ = var requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST; var request = shaka.net.NetworkingEngine.makeRequest( uris, retryParameters); - var requestPromise = networkingEngine.request(requestType, request); + var requestPromise = networkingEngine.request(requestType, request).promise; return requestPromise.then(function(response) { // This only supports the case where the loaded xml has a single // top-level element. If there are multiple roots, it will be rejected. diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index b684d2801..4750a3d9b 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1295,19 +1295,20 @@ shaka.hls.HlsParser.prototype.fetchPartialSegment_ = function(segmentRef) { // Try a partial request first. request.headers = partialSegmentHeaders; - return networkingEngine.request(requestType, request).catch(function(error) { - // The partial request may fail for a number of reasons. - // Some servers do not support Range requests, and others do not support the - // OPTIONS request which must be made before any cross-origin Range request. - // Since this fallback is expensive, warn the app developer. - shaka.log.alwaysWarn('Unable to fetch a partial HLS segment! ' + - 'Falling back to a full segment request, ' + - 'which is expensive! Your server should support ' + - 'Range requests and CORS preflights.', - request.uris[0]); - request.headers = fullSegmentHeaders; - return networkingEngine.request(requestType, request); - }); + return networkingEngine.request(requestType, request).promise + .catch(function(error) { + // The partial request may fail for a number of reasons. + // Some servers do not support Range requests, and others do not support + // the OPTIONS request which must be made before any cross-origin Range + // request. Since this fallback is expensive, warn the app developer. + shaka.log.alwaysWarn('Unable to fetch a partial HLS segment! ' + + 'Falling back to a full segment request, ' + + 'which is expensive! Your server should ' + + 'support Range requests and CORS preflights.', + request.uris[0]); + request.headers = fullSegmentHeaders; + return networkingEngine.request(requestType, request); + }); }; @@ -1701,7 +1702,7 @@ shaka.hls.HlsParser.prototype.guessMimeType_ = headRequest.method = 'HEAD'; var requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; var networkingEngine = this.playerInterface_.networkingEngine; - return networkingEngine.request(requestType, headRequest) + return networkingEngine.request(requestType, headRequest).promise .then(function(response) { var mimeType = response.headers['content-type']; if (!mimeType) { @@ -1797,10 +1798,8 @@ shaka.hls.HlsParser.prototype.requestManifest_ = function(uri) { var request = shaka.net.NetworkingEngine.makeRequest( [uri], this.config_.retryParameters); var networkingEngine = this.playerInterface_.networkingEngine; - var isCanceled = (function() { - return !this.playerInterface_; - }).bind(this); - return networkingEngine.request(requestType, request, isCanceled); + // TODO(#829): abort these requests on destroy + return networkingEngine.request(requestType, request).promise; }; diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 626d7b8e5..1359f7355 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1149,7 +1149,7 @@ shaka.media.DrmEngine.prototype.sendLicenseRequest_ = function(event) { this.unpackPlayReadyRequest_(request); } - this.playerInterface_.netEngine.request(requestType, request) + this.playerInterface_.netEngine.request(requestType, request).promise .then(function(response) { if (this.destroyed_) return Promise.reject(); diff --git a/lib/media/manifest_parser.js b/lib/media/manifest_parser.js index 8123da065..d0f01ccf9 100644 --- a/lib/media/manifest_parser.js +++ b/lib/media/manifest_parser.js @@ -157,7 +157,7 @@ shaka.media.ManifestParser.getFactory = function( headRequest.method = 'HEAD'; var type = shaka.net.NetworkingEngine.RequestType.MANIFEST; - return netEngine.request(type, headRequest).then( + return netEngine.request(type, headRequest).promise.then( function(response) { var mimeType = response.headers['content-type']; // https://goo.gl/yzKDRx says this header should always be available, diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index 11a643d7f..f927d8d5c 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -2055,8 +2055,8 @@ shaka.media.StreamingEngine.prototype.fetch_ = function(reference) { } shaka.log.v2('fetching: reference=' + reference); - var p = this.playerInterface_.netEngine.request(requestType, request); - return p.then(function(response) { + var op = this.playerInterface_.netEngine.request(requestType, request); + return op.promise.then(function(response) { return response.data; }); }; diff --git a/lib/net/data_uri_plugin.js b/lib/net/data_uri_plugin.js index 3b059e542..ef5c732ec 100644 --- a/lib/net/data_uri_plugin.js +++ b/lib/net/data_uri_plugin.js @@ -19,6 +19,7 @@ goog.provide('shaka.net.DataUriPlugin'); goog.require('shaka.log'); goog.require('shaka.net.NetworkingEngine'); +goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Uint8ArrayUtils'); @@ -30,11 +31,11 @@ goog.require('shaka.util.Uint8ArrayUtils'); * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs * @param {string} uri * @param {shakaExtern.Request} request - * @return {!Promise.} + * @return {!shakaExtern.IAbortableOperation.} * @export */ shaka.net.DataUriPlugin = function(uri, request) { - return new Promise(function(resolve, reject) { + try { var parsed = shaka.net.DataUriPlugin.parse(uri); /** @type {shakaExtern.Response} */ @@ -46,8 +47,10 @@ shaka.net.DataUriPlugin = function(uri, request) { } }; - resolve(response); - }); + return shaka.util.AbortableOperation.completed(response); + } catch (error) { + return shaka.util.AbortableOperation.failed(error); + } }; diff --git a/lib/net/http_plugin.js b/lib/net/http_plugin.js index f56568491..14e96f68e 100644 --- a/lib/net/http_plugin.js +++ b/lib/net/http_plugin.js @@ -20,6 +20,7 @@ goog.provide('shaka.net.HttpPlugin'); goog.require('goog.asserts'); goog.require('shaka.log'); goog.require('shaka.net.NetworkingEngine'); +goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); @@ -29,13 +30,13 @@ goog.require('shaka.util.StringUtils'); * @summary A networking plugin to handle http and https URIs via XHR. * @param {string} uri * @param {shakaExtern.Request} request - * @return {!Promise.} + * @return {!shakaExtern.IAbortableOperation.} * @export */ shaka.net.HttpPlugin = function(uri, request) { - return new Promise(function(resolve, reject) { - var xhr = new shaka.net.HttpPlugin.xhr_(); + var xhr = new shaka.net.HttpPlugin.xhr_(); + var promise = new Promise(function(resolve, reject) { xhr.open(request.method, uri, true); xhr.responseType = 'arraybuffer'; xhr.timeout = request.retryParameters.timeout; @@ -109,6 +110,13 @@ shaka.net.HttpPlugin = function(uri, request) { } xhr.send(request.body); }); + + return new shaka.util.AbortableOperation( + promise, + () => { + xhr.abort(); + return Promise.resolve(); + }); }; diff --git a/lib/net/networking_engine.js b/lib/net/networking_engine.js index 27816db5a..8918710fa 100644 --- a/lib/net/networking_engine.js +++ b/lib/net/networking_engine.js @@ -19,7 +19,10 @@ goog.provide('shaka.net.NetworkingEngine'); goog.require('goog.Uri'); goog.require('goog.asserts'); +goog.require('shaka.log'); goog.require('shaka.net.Backoff'); +goog.require('shaka.util.AbortableOperation'); +goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.ConfigUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); @@ -45,8 +48,8 @@ shaka.net.NetworkingEngine = function(opt_onSegmentDownloaded) { /** @private {boolean} */ this.destroyed_ = false; - /** @private {!Array.} */ - this.requests_ = []; + /** @private {!Array.} */ + this.operations_ = []; /** @private {!Array.} */ this.requestFilters_ = []; @@ -167,11 +170,7 @@ shaka.net.NetworkingEngine.prototype.registerRequestFilter = function(filter) { */ shaka.net.NetworkingEngine.prototype.unregisterRequestFilter = function(filter) { - var filters = this.requestFilters_; - var i = filters.indexOf(filter); - if (i >= 0) { - filters.splice(i, 1); - } + shaka.util.ArrayUtils.remove(this.requestFilters_, filter); }; @@ -205,11 +204,7 @@ shaka.net.NetworkingEngine.prototype.registerResponseFilter = function(filter) { */ shaka.net.NetworkingEngine.prototype.unregisterResponseFilter = function(filter) { - var filters = this.responseFilters_; - var i = filters.indexOf(filter); - if (i >= 0) { - filters.splice(i, 1); - } + shaka.util.ArrayUtils.remove(this.responseFilters_, filter); }; @@ -266,30 +261,59 @@ shaka.net.NetworkingEngine.prototype.destroy = function() { this.responseFilters_ = []; var cleanup = []; - for (var i = 0; i < this.requests_.length; ++i) { - cleanup.push(this.requests_[i].catch(Functional.noop)); - } + this.operations_.forEach((op) => { + cleanup.push(op.promise.catch(Functional.noop)); + cleanup.push(op.abort()); + }); + this.operations_ = []; return Promise.all(cleanup); }; +/** + * Shim return values from requests to look like Promises, so that callers have + * time to update to the new operation-based API. + * + * @param {!shakaExtern.IAbortableOperation.} operation + * @return {!shakaExtern.IAbortableOperation.} + * @private + */ +shaka.net.NetworkingEngine.shimRequests_ = function(operation) { + // TODO: remove in v2.5 + operation.then = (onSuccess, onError) => { + shaka.log.alwaysWarn('The network request interface has changed! Please ' + + 'update your application to the new interface, ' + + 'which allows operations to be aborted. Support ' + + 'for the old API will be removed in v2.5.'); + return operation.promise.then(onSuccess, onError); + }; + operation.catch = (onError) => { + shaka.log.alwaysWarn('The network request interface has changed! Please ' + + 'update your application to the new interface, ' + + 'which allows operations to be aborted. Support ' + + 'for the old API will be removed in v2.5.'); + return operation.promise.catch(onError); + }; + return operation; +}; + + /** * Makes a network request and returns the resulting data. * * @param {shaka.net.NetworkingEngine.RequestType} type * @param {shakaExtern.Request} request - * @param {?function()=} opt_isCanceled - * @return {!Promise.} + * @return {!shakaExtern.IAbortableOperation.} * @export */ -shaka.net.NetworkingEngine.prototype.request = - function(type, request, opt_isCanceled) { - var isCanceled = opt_isCanceled || function() { return false; }; +shaka.net.NetworkingEngine.prototype.request = function(type, request) { var cloneObject = shaka.util.ConfigUtils.cloneObject; // Reject all requests made after destroy is called. - if (this.destroyed_) - return Promise.reject(); + if (this.destroyed_) { + return shaka.net.NetworkingEngine.shimRequests_( + shaka.util.AbortableOperation.aborted()); + } goog.asserts.assert(request.uris && request.uris.length, 'Request without URIs!'); @@ -307,63 +331,100 @@ shaka.net.NetworkingEngine.prototype.request = shaka.net.NetworkingEngine.defaultRetryParameters(); request.uris = cloneObject(request.uris); - var filterStartMs = Date.now(); + var requestFilterOperation = this.filterRequest_(type, request); + var requestOperation = requestFilterOperation.chain( + () => this.makeRequestWithRetry_(type, request)); + var responseFilterOperation = requestOperation.chain( + (response) => this.filterResponse_(type, response)); - // 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(requestFilter.bind(null, type, request)); + // Keep track of time spent in filters + var requestFilterStartTime = Date.now(); + var requestFilterMs = 0; + requestFilterOperation.promise.then(() => { + requestFilterMs = Date.now() - requestFilterStartTime; }); - // 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.Severity.CRITICAL, - shaka.util.Error.Category.NETWORK, - shaka.util.Error.Code.REQUEST_FILTER_ERROR, e); + var responseFilterStartTime = 0; + requestOperation.promise.then(() => { + responseFilterStartTime = Date.now(); }); - // 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 backoff = new shaka.net.Backoff( - request.retryParameters, /* autoReset */ false, opt_isCanceled); - var index = 0; - // Every call to send_ must have an associated attempt() so that the - // accounting in backoff is correct. - return backoff.attempt().then(function() { - return this.send_( - type, request, backoff, index, filterTimeMs, isCanceled); - }.bind(this)); - }.bind(this)); + var operation = responseFilterOperation.chain((response) => { + let responseFilterMs = Date.now() - responseFilterStartTime; + + response.timeMs += requestFilterMs; + response.timeMs += responseFilterMs; - // 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_ && !response.fromCache && type == shaka.net.NetworkingEngine.RequestType.SEGMENT) { this.onSegmentDownloaded_(response.timeMs, response.data.byteLength); } + return response; - }.bind(this)).catch(function(e) { - // Ignore if using |Promise.reject()| to signal destroy. + }, (e) => { + // Any error thrown from elsewhere should be recategorized as CRITICAL here. + // This is because by the time it gets here, we've exhausted retries. if (e) { goog.asserts.assert(e instanceof shaka.util.Error, 'Wrong error type'); e.severity = shaka.util.Error.Severity.CRITICAL; } - if (this.requests_.indexOf(p) >= 0) { - this.requests_.splice(this.requests_.indexOf(p), 1); + throw e; + }).finally(() => { + // Manage state when the operation is complete. + // TODO: move to Destroyable + shaka.util.ArrayUtils.remove(this.operations_, operation); + }); + + // Add the operation to the array. + this.operations_.push(operation); + return shaka.net.NetworkingEngine.shimRequests_(operation); +}; + + +/** + * @param {shaka.net.NetworkingEngine.RequestType} type + * @param {shakaExtern.Request} request + * @return {!shakaExtern.IAbortableOperation.} + * @private + */ +shaka.net.NetworkingEngine.prototype.filterRequest_ = function(type, request) { + var filterOperation = shaka.util.AbortableOperation.completed(undefined); + + this.requestFilters_.forEach((requestFilter) => { + // Request filters are run sequentially. + filterOperation = + filterOperation.chain(() => requestFilter(type, request)); + }); + + // Catch any errors thrown by request filters, and substitute + // them with a Shaka-native error. + return filterOperation.chain(undefined, (e) => { + if (e && e.code == shaka.util.Error.Code.OPERATION_ABORTED) { + // Don't change anything if the operation was aborted. + throw e; } - return Promise.reject(e); - }.bind(this)); + + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.REQUEST_FILTER_ERROR, e); + }); +}; + + +/** + * @param {shaka.net.NetworkingEngine.RequestType} type + * @param {shakaExtern.Request} request + * @return {!shakaExtern.IAbortableOperation.} + * @private + */ +shaka.net.NetworkingEngine.prototype.makeRequestWithRetry_ = + function(type, request) { + var backoff = new shaka.net.Backoff( + request.retryParameters, /* autoReset */ false); + var index = 0; + return this.send_(type, request, backoff, index); }; @@ -374,17 +435,11 @@ shaka.net.NetworkingEngine.prototype.request = * @param {shakaExtern.Request} request * @param {!shaka.net.Backoff} backoff * @param {number} index - * @param {number} requestFilterTime - * @param {function()} isCanceled - * @return {!Promise.} + * @return {!shakaExtern.IAbortableOperation.} * @private */ shaka.net.NetworkingEngine.prototype.send_ = function( - type, request, backoff, index, requestFilterTime, isCanceled) { - // Retries sent after destroy is called are rejected. - if (this.destroyed_ || isCanceled()) - return Promise.reject(); - + type, request, backoff, index) { var uri = new goog.Uri(request.uris[index]); var scheme = uri.getScheme(); @@ -404,67 +459,106 @@ shaka.net.NetworkingEngine.prototype.send_ = function( var object = shaka.net.NetworkingEngine.schemes_[scheme]; var plugin = object ? object.plugin : null; if (!plugin) { - return Promise.reject(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.NETWORK, - shaka.util.Error.Code.UNSUPPORTED_SCHEME, - uri)); + return shaka.util.AbortableOperation.failed( + new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + 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) + + // Every attempt must have an associated backoff.attempt() call so that the + // accounting is correct. + var backoffOperation = + shaka.util.AbortableOperation.notAbortable(backoff.attempt()); + + var startTimeMs; + var sendOperation = backoffOperation.chain(() => { + if (this.destroyed_) { + return shaka.util.AbortableOperation.aborted(); + } + + startTimeMs = Date.now(); + var operation = plugin(request.uris[index], request, type); + + // Backward compatibility with older scheme plugins. + // TODO: remove in v2.5 + if (operation.promise == undefined) { + shaka.log.alwaysWarn('The scheme plugin interface has changed! Please ' + + 'update your scheme plugins to the new interface ' + + 'to add support for abort(). Support for the old ' + + 'plugin interface will be removed in v2.5.'); + + // The return was just a promise, so wrap it into an operation. + var schemePromise = /** @type {!Promise} */(operation); + operation = shaka.util.AbortableOperation.notAbortable(schemePromise); + } + return operation; + }).chain((response) => { + if (response.timeMs == undefined) { response.timeMs = Date.now() - startTimeMs; - var filterStartMs = Date.now(); + } + return response; + }, (error) => { + if (error && error.code == shaka.util.Error.Code.OPERATION_ABORTED) { + // Don't change anything if the operation was aborted. + throw error; + } - 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)); - }.bind(this)); - - // Catch any errors thrown by response filters, and substitute - // them with a Shaka-native error. - p = p.catch(function(e) { - var severity = shaka.util.Error.Severity.CRITICAL; - if (e instanceof shaka.util.Error) - severity = e.severity; - - throw new shaka.util.Error( - severity, - 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)).catch(function(error) { if (error && error.severity == shaka.util.Error.Severity.RECOVERABLE) { // Move to the next URI. index = (index + 1) % request.uris.length; - - if (isCanceled()) - return Promise.reject(); - return backoff.attempt().then(function() { - // Delay has passed. Try again. - return this.send_( - type, request, backoff, index, requestFilterTime, isCanceled); - }.bind(this), function() { - // No more attempts are allowed. Fail with the most recent error. - throw error; - }); + return this.send_(type, request, backoff, index); } // The error was not recoverable, so do not try again. // Rethrow the error so the Promise chain stays rejected. throw error; - }.bind(this)); + }); + + return sendOperation; +}; + + +/** + * @param {shaka.net.NetworkingEngine.RequestType} type + * @param {shakaExtern.Response} response + * @return {!shakaExtern.IAbortableOperation.} + * @private + */ +shaka.net.NetworkingEngine.prototype.filterResponse_ = + function(type, response) { + var filterOperation = shaka.util.AbortableOperation.completed(undefined); + + this.responseFilters_.forEach((responseFilter) => { + // Response filters are run sequentially. + filterOperation = + filterOperation.chain(() => responseFilter(type, response)); + }); + + return filterOperation.chain(() => { + // If successful, return the filtered response. + return response; + }, (e) => { + // Catch any errors thrown by request filters, and substitute + // them with a Shaka-native error. + + if (e && e.code == shaka.util.Error.Code.OPERATION_ABORTED) { + // Don't change anything if the operation was aborted. + throw e; + } + + // Critical error, unless the original had some other severity. + var severity = shaka.util.Error.Severity.CRITICAL; + if (e instanceof shaka.util.Error) + severity = e.severity; + + throw new shaka.util.Error( + severity, + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.RESPONSE_FILTER_ERROR, e); + }); }; diff --git a/lib/offline/download_manager.js b/lib/offline/download_manager.js index 871b6377b..bd1a58785 100644 --- a/lib/offline/download_manager.js +++ b/lib/offline/download_manager.js @@ -224,7 +224,7 @@ shaka.offline.DownloadManager.prototype.downloadSegment_ = function(segment) { } var byteCount; - return this.netEngine_.request(type, request) + return this.netEngine_.request(type, request).promise .then(function(response) { if (!this.manifest_) { return Promise.reject(new shaka.util.Error( diff --git a/lib/offline/offline_scheme.js b/lib/offline/offline_scheme.js index 7b6e4cefa..34f5ea048 100644 --- a/lib/offline/offline_scheme.js +++ b/lib/offline/offline_scheme.js @@ -20,6 +20,7 @@ goog.provide('shaka.offline.OfflineScheme'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.offline.OfflineUri'); goog.require('shaka.offline.StorageEngineFactory'); +goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.Error'); @@ -28,7 +29,7 @@ goog.require('shaka.util.Error'); * @summary A plugin that handles requests for offline content. * @param {string} uri * @param {shakaExtern.Request} request - * @return {!Promise.} + * @return {!shakaExtern.IAbortableOperation.} * @export */ shaka.offline.OfflineScheme = function(uri, request) { @@ -42,17 +43,18 @@ shaka.offline.OfflineScheme = function(uri, request) { return shaka.offline.OfflineScheme.onSegment_(segmentId, uri); } - return Promise.reject(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.NETWORK, - shaka.util.Error.Code.MALFORMED_OFFLINE_URI, - uri)); + return shaka.util.AbortableOperation.failed( + new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.MALFORMED_OFFLINE_URI, + uri)); }; /** * @param {string} uri - * @return {!Promise} + * @return {!shakaExtern.IAbortableOperation.} * @private */ shaka.offline.OfflineScheme.onManifest_ = function(uri) { @@ -63,14 +65,14 @@ shaka.offline.OfflineScheme.onManifest_ = function(uri) { headers: {'content-type': 'application/x-offline-manifest'} }; - return Promise.resolve(response); + return shaka.util.AbortableOperation.completed(response); }; /** * @param {number} id * @param {string} uri - * @return {!Promise} + * @return {!shakaExtern.IAbortableOperation.} * @private */ shaka.offline.OfflineScheme.onSegment_ = function(id, uri) { @@ -78,7 +80,7 @@ shaka.offline.OfflineScheme.onSegment_ = function(id, uri) { var storageEngine; var segment; - return shaka.offline.StorageEngineFactory.createStorageEngine() + var promise = shaka.offline.StorageEngineFactory.createStorageEngine() .then(function(se) { storageEngine = se; return storageEngine.getSegment(id); @@ -105,6 +107,9 @@ shaka.offline.OfflineScheme.onSegment_ = function(id, uri) { return response; }); + + // TODO: support abort() in OfflineScheme + return shaka.util.AbortableOperation.notAbortable(promise); }; diff --git a/lib/util/error.js b/lib/util/error.js index d429cc468..40181433d 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -207,6 +207,18 @@ shaka.util.Error.Code = { */ 'RESPONSE_FILTER_ERROR': 1007, + /** + * A testing network request was made with a malformed URI. + * This error is only used by unit and integration tests. + */ + 'MALFORMED_TEST_URI': 1008, + + /** + * An unexpected network request was made to the FakeNetworkingEngine. + * This error is only used by unit and integration tests. + */ + 'UNEXPECTED_TEST_REQUEST': 1009, + /** The text parser failed to parse a text stream due to an invalid header. */ 'INVALID_TEXT_HEADER': 2000, diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js index 494e9fdf6..1a27aab2b 100644 --- a/shaka-player.uncompiled.js +++ b/shaka-player.uncompiled.js @@ -55,5 +55,4 @@ goog.require('shaka.text.SimpleTextDisplayer'); goog.require('shaka.text.TextEngine'); goog.require('shaka.text.TtmlTextParser'); goog.require('shaka.text.VttTextParser'); -goog.require('shaka.util.AbortableOperation'); // TODO(#829): remove when used goog.require('shaka.util.Error'); diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index 0bff480dc..4513a24a4 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -426,7 +426,10 @@ describe('DashParser Live', function() { // The initial manifest request will be redirected. fakeNetEngine.request.and.returnValue( - Promise.resolve({uri: redirectedUri, data: manifestData})); + shaka.util.AbortableOperation.completed({ + uri: redirectedUri, + data: manifestData, + })); parser.start(originalUri, playerInterface) .then(function(manifest) { @@ -461,8 +464,8 @@ describe('DashParser Live', function() { shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.NETWORK, shaka.util.Error.Code.BAD_HTTP_STATUS); - var promise = Promise.reject(error); - fakeNetEngine.request.and.returnValue(promise); + var operation = shaka.util.AbortableOperation.failed(error); + fakeNetEngine.request.and.returnValue(operation); delayForUpdatePeriod(); expect(onError.calls.count()).toBe(1); @@ -569,7 +572,7 @@ describe('DashParser Live', function() { parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(fakeNetEngine.request.calls.count()).toBe(1); - fakeNetEngine.expectCancelableRequest('dummy://foo', manifestRequest); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest); fakeNetEngine.request.calls.reset(); // Create a mock so we can verify it gives two URIs. @@ -577,7 +580,7 @@ describe('DashParser Live', function() { expect(type).toBe(manifestRequest); expect(request.uris).toEqual(['http://foobar', 'http://foobar2']); var data = shaka.util.StringUtils.toUTF8(manifestText); - return Promise.resolve( + return shaka.util.AbortableOperation.completed( {uri: request.uris[0], data: data, headers: {}}); }); @@ -774,8 +777,7 @@ describe('DashParser Live', function() { it('stops updates', function(done) { parser.start(manifestUri, playerInterface) .then(function(manifest) { - fakeNetEngine.expectCancelableRequest( - manifestUri, manifestRequestType); + fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); parser.stop(); @@ -789,8 +791,7 @@ describe('DashParser Live', function() { parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest).toBe(null); - fakeNetEngine.expectCancelableRequest( - manifestUri, manifestRequestType); + fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); delayForUpdatePeriod(); // An update should not occur. @@ -808,16 +809,14 @@ describe('DashParser Live', function() { parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest).toBeTruthy(); - fakeNetEngine.expectCancelableRequest( - manifestUri, manifestRequestType); + fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); var delay = fakeNetEngine.delayNextRequest(); delayForUpdatePeriod(); // The request was made but should not be resolved yet. expect(fakeNetEngine.request.calls.count()).toBe(1); - fakeNetEngine.expectCancelableRequest( - manifestUri, manifestRequestType); + fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); parser.stop(); delay.resolve(); @@ -838,7 +837,7 @@ describe('DashParser Live', function() { Util.delay(0.2, realTimeout).then(function() { // This is the initial manifest request. expect(fakeNetEngine.request.calls.count()).toBe(1); - fakeNetEngine.expectCancelableRequest(manifestUri, manifestRequestType); + fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); // Resolve the manifest request and wait on the UTCTiming request. delay.resolve(); diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index 06335abac..7072f6fdc 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -466,13 +466,17 @@ describe('DashParser Manifest', function() { fakeNetEngine.request.and.callFake(function(type, request) { if (request.uris[0] == 'http://foo.bar/manifest') { var data = shaka.util.StringUtils.toUTF8(source); - return Promise.resolve({data: data, headers: {}, uri: ''}); + return shaka.util.AbortableOperation.completed({ + data: data, + headers: {}, + uri: '', + }); } else { expect(request.uris[0]).toBe('http://foo.bar/date'); - return Promise.resolve({ + return shaka.util.AbortableOperation.completed({ data: new ArrayBuffer(0), headers: {'date': '1970-01-01T00:00:40Z'}, - uri: '' + uri: '', }); } }); @@ -685,7 +689,8 @@ describe('DashParser Manifest', function() { shaka.util.Error.Category.NETWORK, shaka.util.Error.Code.BAD_HTTP_STATUS); - fakeNetEngine.request.and.returnValue(Promise.reject(expectedError)); + fakeNetEngine.request.and.returnValue( + shaka.util.AbortableOperation.failed(expectedError)); parser.start('', playerInterface) .then(fail) .catch(function(error) { expect(error).toEqual(expectedError); }) diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index ce9b6eca4..565fa08c6 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -607,7 +607,7 @@ describe('HlsParser live', function() { // Only one request was made, and it was for the playlist. // No segment requests were needed to get the start time. expect(fakeNetEngine.request.calls.count()).toBe(1); - fakeNetEngine.expectCancelableRequest( + fakeNetEngine.expectRequest( 'test:/video', shaka.net.NetworkingEngine.RequestType.MANIFEST); }).catch(fail).then(done); diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 8d40fc9ac..3d85a803a 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -834,9 +834,10 @@ describe('DrmEngine', function() { mockVideo.on['encrypted']( { initDataType: 'webm', initData: initData, keyId: null }); - fakeNetEngine.request.and.returnValue(new shaka.util.PublicPromise()); + var operation = shaka.util.AbortableOperation.completed({}); + fakeNetEngine.request.and.returnValue(operation); var message = new Uint8Array(0); - session1.on['message']({ message: message }); + session1.on['message']({ target: session1, message: message }); expect(fakeNetEngine.request).toHaveBeenCalledWith( shaka.net.NetworkingEngine.RequestType.LICENSE, @@ -857,9 +858,10 @@ describe('DrmEngine', function() { mockVideo.on['encrypted']( { initDataType: 'webm', initData: initData, keyId: null }); - fakeNetEngine.request.and.returnValue(new shaka.util.PublicPromise()); + var operation = shaka.util.AbortableOperation.completed({}); + fakeNetEngine.request.and.returnValue(operation); var message = new Uint8Array(0); - session1.on['message']({ message: message }); + session1.on['message']({ target: session1, message: message }); expect(fakeNetEngine.request).toHaveBeenCalledWith( shaka.net.NetworkingEngine.RequestType.LICENSE, @@ -881,10 +883,11 @@ describe('DrmEngine', function() { shaka.util.Error.Category.NETWORK, shaka.util.Error.Code.BAD_HTTP_STATUS, 'http://abc.drm/license', 403); - fakeNetEngine.request.and.returnValue(Promise.reject(netError)); + var operation = shaka.util.AbortableOperation.failed(netError); + fakeNetEngine.request.and.returnValue(operation); var message = new Uint8Array(0); - session1.on['message']({ message: message }); + session1.on['message']({ target: session1, message: message }); return shaka.test.Util.delay(0.5); }).then(function() { expect(onErrorSpy).toHaveBeenCalled(); @@ -1398,7 +1401,8 @@ describe('DrmEngine', function() { it('interrupts successful license requests', function(done) { var p = new shaka.util.PublicPromise(); - fakeNetEngine.request.and.returnValue(p); + var operation = shaka.util.AbortableOperation.notAbortable(p); + fakeNetEngine.request.and.returnValue(operation); initAndAttach().then(function() { var initData1 = new Uint8Array(1); @@ -1428,7 +1432,8 @@ describe('DrmEngine', function() { it('interrupts failed license requests', function(done) { var p = new shaka.util.PublicPromise(); - fakeNetEngine.request.and.returnValue(p); + var operation = shaka.util.AbortableOperation.notAbortable(p); + fakeNetEngine.request.and.returnValue(operation); initAndAttach().then(function() { var initData1 = new Uint8Array(1); @@ -1627,9 +1632,10 @@ describe('DrmEngine', function() { mockVideo.on['encrypted']( { initDataType: 'webm', initData: initData, keyId: null }); - fakeNetEngine.request.and.returnValue(new shaka.util.PublicPromise()); + var operation = shaka.util.AbortableOperation.completed({}); + fakeNetEngine.request.and.returnValue(operation); var message = new Uint8Array(0); - session1.on['message']({ message: message }); + session1.on['message']({ target: session1, message: message }); expect(fakeNetEngine.request).not.toHaveBeenCalled(); @@ -1655,9 +1661,10 @@ describe('DrmEngine', function() { mockVideo.on['encrypted']( { initDataType: 'webm', initData: initData, keyId: null }); - fakeNetEngine.request.and.returnValue(new shaka.util.PublicPromise()); + var operation = shaka.util.AbortableOperation.completed({}); + fakeNetEngine.request.and.returnValue(operation); var message = new Uint8Array(0); - session1.on['message']({ message: message }); + session1.on['message']({ target: session1, message: message }); expect(fakeNetEngine.request).not.toHaveBeenCalled(); @@ -1674,7 +1681,7 @@ describe('DrmEngine', function() { fakeNetEngine.request.calls.reset(); mockVideo.paused = true; - session1.on['message']({ message: message }); + session1.on['message']({ target: session1, message: message }); expect(fakeNetEngine.request).toHaveBeenCalledWith( shaka.net.NetworkingEngine.RequestType.LICENSE, @@ -1881,6 +1888,7 @@ describe('DrmEngine', function() { }; session.generateRequest.and.returnValue(Promise.resolve()); session.close.and.returnValue(Promise.resolve()); + session.update.and.returnValue(Promise.resolve()); session.addEventListener.and.callFake(function(name, callback) { session.on[name] = callback; }); diff --git a/test/media/streaming_engine_unit.js b/test/media/streaming_engine_unit.js index 94dea3d0f..4002aee75 100644 --- a/test/media/streaming_engine_unit.js +++ b/test/media/streaming_engine_unit.js @@ -1007,7 +1007,7 @@ describe('StreamingEngine', function() { request: function(requestType, request) { var buffer = new ArrayBuffer(0); var response = { uri: request.uris[0], data: buffer, headers: {} }; - return Promise.resolve(response); + return shaka.util.AbortableOperation.completed(response); } }; @@ -1828,7 +1828,7 @@ describe('StreamingEngine', function() { }; netEngine.request.and.callFake(function(requestType, request) { if (request.uris[0] == textUri) { - return Promise.reject(new shaka.util.Error( + return shaka.util.AbortableOperation.failed(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.NETWORK, shaka.util.Error.Code.BAD_HTTP_STATUS, textUri, 404)); @@ -2824,7 +2824,7 @@ describe('StreamingEngine', function() { data.push(''); } - return Promise.reject(new shaka.util.Error( + return shaka.util.AbortableOperation.failed(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.NETWORK, errorCode, data)); diff --git a/test/net/data_uri_plugin_unit.js b/test/net/data_uri_plugin_unit.js index 92800f87d..b42bf5924 100644 --- a/test/net/data_uri_plugin_unit.js +++ b/test/net/data_uri_plugin_unit.js @@ -71,7 +71,7 @@ describe('DataUriPlugin', function() { function testSucceeds(uri, contentType, text, done) { var request = shaka.net.NetworkingEngine.makeRequest([uri], retryParameters); - shaka.net.DataUriPlugin(uri, request) + shaka.net.DataUriPlugin(uri, request).promise .then(function(response) { expect(response).toBeTruthy(); expect(response.uri).toBe(uri); @@ -87,7 +87,7 @@ describe('DataUriPlugin', function() { function testFails(uri, done, code) { var request = shaka.net.NetworkingEngine.makeRequest([uri], retryParameters); - shaka.net.DataUriPlugin(uri, request) + shaka.net.DataUriPlugin(uri, request).promise .then(fail) .catch(function(error) { expect(error.code).toBe(code); }) .then(function() { diff --git a/test/net/http_plugin_unit.js b/test/net/http_plugin_unit.js index d92a3f151..1e01df566 100644 --- a/test/net/http_plugin_unit.js +++ b/test/net/http_plugin_unit.js @@ -93,7 +93,7 @@ describe('HttpPlugin', function() { request.method = 'POST'; request.headers['BAZ'] = '123'; - shaka.net.HttpPlugin(request.uris[0], request) + shaka.net.HttpPlugin(request.uris[0], request).promise .then(function() { /** @type {!jasmine.Ajax.RequestStub} */ var actual = jasmine.Ajax.requests.mostRecent(); @@ -147,7 +147,7 @@ describe('HttpPlugin', function() { it('detects cache headers', function(done) { var request = shaka.net.NetworkingEngine.makeRequest( ['https://foo.bar/cache'], retryParameters); - shaka.net.HttpPlugin(request.uris[0], request) + shaka.net.HttpPlugin(request.uris[0], request).promise .catch(fail) .then(function(response) { expect(response).toBeTruthy(); @@ -156,6 +156,24 @@ describe('HttpPlugin', function() { .then(done); }); + it('aborts the request when the operation is aborted', function(done) { + var request = shaka.net.NetworkingEngine.makeRequest( + ['https://foo.bar/'], retryParameters); + var operation = shaka.net.HttpPlugin(request.uris[0], request); + + /** @type {!jasmine.Ajax.RequestStub} */ + var actual = jasmine.Ajax.requests.mostRecent(); + actual.abort = shaka.test.Util.spyFunc(jasmine.createSpy('abort')); + + var requestPromise = operation.promise.catch(() => {}); + + expect(actual.abort).not.toHaveBeenCalled(); + var abortPromise = operation.abort(); + expect(actual.abort).toHaveBeenCalled(); + + Promise.all([abortPromise, requestPromise]).catch(fail).then(done); + }); + /** * @param {string} uri * @param {function()} done @@ -164,7 +182,7 @@ describe('HttpPlugin', function() { function testSucceeds(uri, done, opt_overrideUri) { var request = shaka.net.NetworkingEngine.makeRequest( [uri], retryParameters); - shaka.net.HttpPlugin(uri, request) + shaka.net.HttpPlugin(uri, request).promise .catch(fail) .then(function(response) { expect(jasmine.Ajax.requests.mostRecent().url).toBe(uri); @@ -188,7 +206,7 @@ describe('HttpPlugin', function() { function testFails(uri, done, opt_severity) { var request = shaka.net.NetworkingEngine.makeRequest( [uri], retryParameters); - shaka.net.HttpPlugin(uri, request) + shaka.net.HttpPlugin(uri, request).promise .then(fail) .catch(function(error) { expect(error).toBeTruthy(); @@ -211,7 +229,7 @@ describe('HttpPlugin', function() { function testSucceedsWithEmptyLine(uri, done, opt_overrideUri) { var request = shaka.net.NetworkingEngine.makeRequest( [uri], retryParameters); - shaka.net.HttpPlugin(uri, request) + shaka.net.HttpPlugin(uri, request).promise .catch(fail) .then(function(response) { expect(jasmine.Ajax.requests.mostRecent().url).toBe(uri); diff --git a/test/net/networking_engine_unit.js b/test/net/networking_engine_unit.js index 62f3917e5..b7a0d782a 100644 --- a/test/net/networking_engine_unit.js +++ b/test/net/networking_engine_unit.js @@ -44,12 +44,11 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { }); function makeResolveScheme(spyName) { - return jasmine.createSpy(spyName).and.callFake( - function() { - return Promise.resolve({ - uri: '', data: new ArrayBuffer(5), headers: {} - }); - }); + return jasmine.createSpy(spyName).and.callFake(function() { + return shaka.util.AbortableOperation.completed({ + uri: '', data: new ArrayBuffer(5), headers: {}, + }); + }); } beforeEach(function() { @@ -61,8 +60,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { networkingEngine = new shaka.net.NetworkingEngine(); resolveScheme = makeResolveScheme('resolve scheme'); - rejectScheme = jasmine.createSpy('reject scheme') - .and.callFake(function() { return Promise.reject(error); }); + rejectScheme = jasmine.createSpy('reject scheme').and.callFake(() => + shaka.util.AbortableOperation.failed(error)); shaka.net.NetworkingEngine.registerScheme( 'resolve', Util.spyFunc(resolveScheme), shaka.net.NetworkingEngine.PluginPriority.FALLBACK); @@ -92,13 +91,13 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { }); rejectScheme.and.callFake(function() { if (rejectScheme.calls.count() == 1) - return Promise.reject(error); + return shaka.util.AbortableOperation.failed(error); else - return Promise.resolve({ + return shaka.util.AbortableOperation.completed({ uri: '', data: new ArrayBuffer(0), headers: {} }); }); - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .catch(fail) .then(function() { expect(rejectScheme.calls.count()).toBe(2); @@ -116,13 +115,13 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { }); rejectScheme.and.callFake(function() { if (rejectScheme.calls.count() < 3) - return Promise.reject(error); + return shaka.util.AbortableOperation.failed(error); else - return Promise.resolve({ + return shaka.util.AbortableOperation.completed({ uri: '', data: new ArrayBuffer(0), headers: {} }); }); - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .catch(fail) .then(function() { expect(rejectScheme.calls.count()).toBe(3); @@ -138,7 +137,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { fuzzFactor: 0, timeout: 0 }); - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .then(fail) .catch(function() { expect(rejectScheme.calls.count()).toBe(3); }) .then(done); @@ -179,7 +178,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { backoffFactor: 2, timeout: 0 }); - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .then(fail) .catch(function() { expect(setTimeoutSpy.calls.count()).toBe(1); @@ -197,7 +196,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { backoffFactor: 2, timeout: 0 }); - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .then(fail) .catch(function() { expect(setTimeoutSpy.calls.count()).toBe(2); @@ -217,7 +216,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { backoffFactor: 1, timeout: 0 }); - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .then(fail) .catch(function() { // (rand * 2.0) - 1.0 = (0.75 * 2.0) - 1.0 = 0.5 @@ -229,7 +228,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { }) .then(done); }); - }); + }); // describe('backoff') it('uses multiple URIs', function(done) { var request = createRequest('', { @@ -240,7 +239,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { timeout: 0 }); request.uris = ['reject://foo', 'resolve://foo']; - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .catch(fail) .then(function() { expect(rejectScheme.calls.count()).toBe(1); @@ -259,19 +258,19 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { }); error.severity = shaka.util.Error.Severity.CRITICAL; - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .then(fail) .catch(function() { expect(rejectScheme.calls.count()).toBe(1); done(); }); }); - }); + }); // describe('retry') describe('request', function() { function testResolve(schemeSpy) { return networkingEngine.request( - requestType, createRequest('resolve://foo')) + requestType, createRequest('resolve://foo')).promise .catch(fail) .then(function() { expect(schemeSpy).toHaveBeenCalled(); @@ -322,6 +321,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('can unregister scheme', function(done) { shaka.net.NetworkingEngine.unregisterScheme('resolve'); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .then(fail) .catch(function() { expect(resolveScheme).not.toHaveBeenCalled(); }) .then(done); @@ -336,6 +336,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { shaka.net.NetworkingEngine.unregisterScheme('resolve'); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .then(fail) .catch(function() { expect(resolveScheme).not.toHaveBeenCalled(); @@ -345,6 +346,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('rejects if scheme does not exist', function(done) { networkingEngine.request(requestType, createRequest('foo://foo')) + .promise .then(fail) .catch(function() { expect(resolveScheme).not.toHaveBeenCalled(); }) .then(done); @@ -352,6 +354,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('returns the response object', function(done) { networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .catch(fail) .then(function(response) { expect(response).toBeTruthy(); @@ -371,14 +374,15 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { expect(uri).toBe(request.uris[0]); expect(requestPassed).toEqual(request); expect(requestTypePassed).toEqual(requestType); - return Promise.resolve({}); + return shaka.util.AbortableOperation.completed({}); }); - networkingEngine.request(requestType, request).catch(fail).then(done); + networkingEngine.request(requestType, request) + .promise.catch(fail).then(done); }); it('infers a scheme for // URIs', function(done) { fakeProtocol = 'resolve:'; - networkingEngine.request(requestType, createRequest('//foo')) + networkingEngine.request(requestType, createRequest('//foo')).promise .catch(fail) .then(function() { expect(resolveScheme).toHaveBeenCalled(); @@ -398,91 +402,12 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { expect(request.headers).toBeTruthy(); expect(request.retryParameters).toBeTruthy(); - return Promise.resolve({}); + return shaka.util.AbortableOperation.completed({}); }); - networkingEngine.request(requestType, originalRequest) + networkingEngine.request(requestType, originalRequest).promise .catch(fail).then(done); }); - }); - - describe('request cancel', function() { - /** @const */ - var origSetTimeout = shaka.net.Backoff.setTimeout_; - - /** @type {!jasmine.Spy} */ - var setTimeoutSpy; - - beforeAll(function() { - setTimeoutSpy = jasmine.createSpy('setTimeout'); - setTimeoutSpy.and.callFake(origSetTimeout); - shaka.net.Backoff.setTimeout_ = Util.spyFunc(setTimeoutSpy); - }); - - afterAll(function() { - shaka.net.Backoff.setTimeout_ = origSetTimeout; - }); - - beforeEach(function() { - setTimeoutSpy.calls.reset(); - }); - - it('cancels instantly if isCanceled is true from start', function(done) { - var isCanceled = function() { return true; }; - var request = createRequest('resolve://foo'); - networkingEngine.request(requestType, request, isCanceled) - .then(fail) - .catch(function() { - expect(setTimeoutSpy.calls.count()).toBe(0); - expect(resolveScheme.calls.count()).toBe(0); - done(); - }); - }); - - it('cancels when isCanceled becomes true', function(done) { - var request = createRequest('reject://foo', { - maxAttempts: 3, - baseDelay: 1000, - backoffFactor: 0, - fuzzFactor: 0, - timeout: 0 - }); - - var cancelToken = false; - var isCanceled = function() { return cancelToken; }; - networkingEngine.request(requestType, request, isCanceled) - .then(fail) - .catch(function() { - // Cancel at 500 MS means it will have gone through two 200 MS - // setTimeouts and will be halfway through the third. - expect(setTimeoutSpy.calls.count()).toBe(3); - expect(cancelToken).toBe(true); - expect(rejectScheme.calls.count()).toBe(1); - done(); - }); - origSetTimeout(function() { - cancelToken = true; - }, 500); - }); - - it('does single timeouts when isCanceled is not provided', function(done) { - var request = createRequest('reject://foo', { - maxAttempts: 2, - baseDelay: 400, - backoffFactor: 0, - fuzzFactor: 0, - timeout: 0 - }); - networkingEngine.request(requestType, request) - .then(fail) - .catch(function() { - // If this is broken into 200 MS segments, this would have more than - // one call to setTimeoutSpy. - expect(setTimeoutSpy.calls.count()).toBe(1); - expect(rejectScheme.calls.count()).toBe(2); - done(); - }); - }); - }); + }); // describe('request') describe('request filter', function() { /** @type {!jasmine.Spy} */ @@ -499,6 +424,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('can be called', function(done) { networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .catch(fail) .then(function() { expect(filter).toHaveBeenCalled(); @@ -508,6 +434,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('called on failure', function(done) { networkingEngine.request(requestType, createRequest('reject://foo')) + .promise .then(fail) .catch(function() { expect(filter).toHaveBeenCalled(); }) .then(done); @@ -515,7 +442,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('is given correct arguments', function(done) { var request = createRequest('resolve://foo'); - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .catch(fail) .then(function() { expect(filter.calls.argsFor(0)[0]).toBe(requestType); @@ -534,7 +461,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { filter.and.returnValue(p); responseFilter.and.returnValue(p2); var request = createRequest('resolve://foo'); - var r = new StatusPromise(networkingEngine.request(requestType, request)); + var r = new StatusPromise( + networkingEngine.request(requestType, request).promise); Util.delay(0.1).then(function() { expect(filter).toHaveBeenCalled(); @@ -563,6 +491,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { throw fakeError; }); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .then(fail) .catch(function(e) { expect(e.severity).toBe(shaka.util.Error.Severity.CRITICAL); @@ -577,6 +506,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { request.uris = ['resolve://foo']; }); networkingEngine.request(requestType, createRequest('reject://foo')) + .promise .catch(fail) .then(function() { expect(filter).toHaveBeenCalled(); @@ -599,6 +529,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { }); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .catch(fail) .then(done); }); @@ -612,6 +543,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { }); }); networkingEngine.request(requestType, createRequest('reject://foo')) + .promise .catch(fail) .then(function() { expect(resolveScheme).toHaveBeenCalled(); @@ -633,6 +565,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { request.allowCrossSiteCredentials = true; }); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .catch(fail) .then(function() { expect(filter).toHaveBeenCalled(); @@ -652,7 +585,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { timeout: 0 }); filter.and.returnValue(Promise.reject()); - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .then(fail) .catch(function() { expect(resolveScheme).not.toHaveBeenCalled(); @@ -670,7 +603,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { timeout: 0 }); filter.and.throwError(error); - networkingEngine.request(requestType, request) + networkingEngine.request(requestType, request).promise .then(fail) .catch(function() { expect(resolveScheme).not.toHaveBeenCalled(); @@ -683,7 +616,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { var unusedFilter = jasmine.createSpy('unused filter'); networkingEngine.unregisterRequestFilter(Util.spyFunc(unusedFilter)); }); - }); + }); // describe('request filter') describe('response filter', function() { /** @type {!jasmine.Spy} */ @@ -696,7 +629,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { var response = { uri: '', data: new ArrayBuffer(100), headers: {} }; - return Promise.resolve(response); + return shaka.util.AbortableOperation.completed(response); }); }); @@ -706,6 +639,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('can be called', function(done) { networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .catch(fail) .then(function() { expect(filter).toHaveBeenCalled(); @@ -715,6 +649,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('not called on failure', function(done) { networkingEngine.request(requestType, createRequest('reject://foo')) + .promise .then(fail) .catch(function() { expect(filter).not.toHaveBeenCalled(); }) .then(done); @@ -723,6 +658,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('is given correct arguments', function(done) { var request = createRequest('resolve://foo'); networkingEngine.request(requestType, request) + .promise .catch(fail) .then(function() { expect(filter.calls.argsFor(0)[0]).toBe(requestType); @@ -738,6 +674,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { response.data = new ArrayBuffer(5); }); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .catch(fail) .then(function(response) { expect(filter).toHaveBeenCalled(); @@ -753,6 +690,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { response.headers['DATE'] = 'CAT'; }); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .catch(fail) .then(function(response) { expect(filter).toHaveBeenCalled(); @@ -777,6 +715,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { }); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .catch(fail) .then(done); }); @@ -787,6 +726,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { throw fakeError; }); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .then(fail) .catch(function(e) { expect(e.code).toBe(shaka.util.Error.Code.RESPONSE_FILTER_ERROR); @@ -807,6 +747,7 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { var request = createRequest('resolve://foo'); var r = new StatusPromise(networkingEngine.request(requestType, request) + .promise .catch(fail) .then(function(response) { expect(response).toBeTruthy(); @@ -826,49 +767,29 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('if throws will stop requests', function(done) { filter.and.callFake(function() { throw error; }); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .then(fail) .catch(function() { expect(filter).toHaveBeenCalled(); }) .then(done); }); - it('if throws will retry', function(done) { - var request = createRequest('resolve://foo', { - maxAttempts: 2, - baseDelay: 0, - backoffFactor: 0, - fuzzFactor: 0, - timeout: 0 - }); - error.severity = shaka.util.Error.Severity.RECOVERABLE; - filter.and.callFake(function() { - if (filter.calls.count() == 1) throw error; - }); - - networkingEngine.request(requestType, request) - .catch(fail) - .then(function() { - expect(resolveScheme.calls.count()).toBe(2); - expect(filter.calls.count()).toBe(2); - done(); - }); - }); - it('causes no errors to remove an unused filter', function() { var unusedFilter = jasmine.createSpy('unused filter'); networkingEngine.unregisterResponseFilter(Util.spyFunc(unusedFilter)); }); - }); + }); // describe('response filter') describe('destroy', function() { it('waits for all operations to complete', function(done) { var request = createRequest('resolve://foo'); var p = new shaka.util.PublicPromise(); - resolveScheme.and.returnValue(p); + resolveScheme.and.returnValue( + shaka.util.AbortableOperation.notAbortable(p)); - var r1 = - new StatusPromise(networkingEngine.request(requestType, request)); - var r2 = - new StatusPromise(networkingEngine.request(requestType, request)); + var r1 = new StatusPromise( + networkingEngine.request(requestType, request).promise); + var r2 = new StatusPromise( + networkingEngine.request(requestType, request).promise); expect(r1.status).toBe('pending'); expect(r2.status).toBe('pending'); @@ -907,7 +828,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { fuzzFactor: 0, timeout: 0 }); - var r = new StatusPromise(networkingEngine.request(requestType, request)); + var r = new StatusPromise( + networkingEngine.request(requestType, request).promise); /** @type {!shaka.test.StatusPromise} */ var d; @@ -929,12 +851,13 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('resolves even when a request fails', function(done) { var request = createRequest('reject://foo'); var p = new shaka.util.PublicPromise(); - rejectScheme.and.returnValue(p); + rejectScheme.and.returnValue( + shaka.util.AbortableOperation.notAbortable(p)); - var r1 = - new StatusPromise(networkingEngine.request(requestType, request)); - var r2 = - new StatusPromise(networkingEngine.request(requestType, request)); + var r1 = new StatusPromise( + networkingEngine.request(requestType, request).promise); + var r2 = new StatusPromise( + networkingEngine.request(requestType, request).promise); expect(r1.status).toBe('pending'); expect(r2.status).toBe('pending'); @@ -961,43 +884,21 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { it('prevents new requests', function(done) { var request = createRequest('resolve://foo'); - var p = new shaka.util.PublicPromise(); - resolveScheme.and.returnValue(p); - var r1 = - new StatusPromise(networkingEngine.request(requestType, request)); - /** @type {!shaka.test.StatusPromise} */ - var r2; - /** @type {!shaka.test.StatusPromise} */ - var d; - expect(r1.status).toBe('pending'); - Util.delay(0.1).then(function() { - // The request has already been made. - expect(resolveScheme.calls.count()).toBe(1); + var d = new StatusPromise(networkingEngine.destroy()); + expect(d.status).toBe('pending'); - d = new StatusPromise(networkingEngine.destroy()); - expect(d.status).toBe('pending'); + var r = new StatusPromise( + networkingEngine.request(requestType, request).promise); - r2 = new StatusPromise(networkingEngine.request(requestType, request)); - expect(r2.status).toBe('pending'); + Util.delay(0.1).then(() => { // A new request has not been made. - expect(resolveScheme.calls.count()).toBe(1); + expect(resolveScheme.calls.count()).toBe(0); - return Util.delay(0.1); - }).then(function() { - expect(r1.status).toBe('pending'); - expect(r2.status).toBe('rejected'); - expect(d.status).toBe('pending'); - p.resolve({}); - return d; - }).then(function() { - return Util.delay(0.1); - }).then(function() { - expect(r1.status).toBe('resolved'); - expect(r2.status).toBe('rejected'); expect(d.status).toBe('resolved'); - expect(resolveScheme.calls.count()).toBe(1); - }).catch(fail).then(done); + expect(r.status).toBe('rejected'); + done(); + }); }); it('does not allow further retries', function(done) { @@ -1012,14 +913,16 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { var p1 = new shaka.util.PublicPromise(); var p2 = new shaka.util.PublicPromise(); rejectScheme.and.callFake(function() { - return (rejectScheme.calls.count() == 1) ? p1 : p2; + // Return p1 the first time, then p2 the second time. + return (rejectScheme.calls.count() == 1) ? + shaka.util.AbortableOperation.notAbortable(p1) : + shaka.util.AbortableOperation.notAbortable(p2); }); - var r1 = - new StatusPromise(networkingEngine.request(requestType, request)); + var r = new StatusPromise( + networkingEngine.request(requestType, request).promise); /** @type {shaka.test.StatusPromise} */ var d; - expect(r1.status).toBe('pending'); Util.delay(0.1).then(function() { expect(rejectScheme.calls.count()).toBe(1); @@ -1028,7 +931,6 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { return Util.delay(0.1); }).then(function() { - expect(r1.status).toBe('pending'); expect(d.status).toBe('pending'); expect(rejectScheme.calls.count()).toBe(1); // Reject the initial request. @@ -1042,11 +944,11 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { }).then(function() { expect(d.status).toBe('resolved'); // The request was never retried. - expect(r1.status).toBe('rejected'); + expect(r.status).toBe('rejected'); expect(rejectScheme.calls.count()).toBe(1); }).catch(fail).then(done); }); - }); + }); // describe('destroy') it('ignores cache hits', function(done) { var onSegmentDownloaded = jasmine.createSpy('onSegmentDownloaded'); @@ -1054,12 +956,13 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { new shaka.net.NetworkingEngine(Util.spyFunc(onSegmentDownloaded)); networkingEngine.request(requestType, createRequest('resolve://foo')) + .promise .then(function() { expect(onSegmentDownloaded).toHaveBeenCalled(); onSegmentDownloaded.calls.reset(); resolveScheme.and.callFake(function() { - return Promise.resolve({ + return shaka.util.AbortableOperation.completed({ uri: '', data: new ArrayBuffer(5), headers: {}, @@ -1076,6 +979,169 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { .then(done); }); + describe('abort', function() { + /** @type {!shaka.util.Error} */ + var abortError; + + beforeEach(function() { + abortError = new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.PLAYER, + shaka.util.Error.Code.OPERATION_ABORTED); + }); + + it('interrupts request filters', function(done) { + var filter1Promise = new shaka.util.PublicPromise(); + var filter1Spy = jasmine.createSpy('filter 1') + .and.returnValue(filter1Promise); + var filter1 = Util.spyFunc(filter1Spy); + networkingEngine.registerRequestFilter(filter1); + + var filter2Promise = new shaka.util.PublicPromise(); + var filter2Spy = jasmine.createSpy('filter 2') + .and.returnValue(filter2Promise); + var filter2 = Util.spyFunc(filter2Spy); + networkingEngine.registerRequestFilter(filter2); + + var request = createRequest('resolve://foo'); + var operation = networkingEngine.request(requestType, request); + var r = new StatusPromise(operation.promise); + + Util.delay(0.1).then(() => { + // The first filter has been called, but not the second, and not the + // scheme plugin. + expect(filter1Spy).toHaveBeenCalled(); + expect(filter2Spy).not.toHaveBeenCalled(); + expect(resolveScheme).not.toHaveBeenCalled(); + + // The operation is still pending. + expect(r.status).toBe('pending'); + + operation.abort(); + + filter1Promise.resolve(); + return Util.delay(0.1); + }).then(() => { + // The second filter has not been called, nor has the scheme plugin. + // The filter chain was interrupted by abort(). + expect(filter2Spy).not.toHaveBeenCalled(); + expect(resolveScheme).not.toHaveBeenCalled(); + + // The operation has been aborted. + expect(r.status).toBe('rejected'); + return operation.promise.catch((e) => { + Util.expectToEqualError(e, abortError); + }); + }).catch(fail).then(done); + }); + + it('interrupts scheme plugins', function(done) { + var p = new shaka.util.PublicPromise(); + var abortSpy = jasmine.createSpy('abort'); + var abort = Util.spyFunc(abortSpy); + + resolveScheme.and.returnValue( + new shaka.util.AbortableOperation(p, abort)); + expect(resolveScheme).not.toHaveBeenCalled(); + + var request = createRequest('resolve://foo'); + var operation = networkingEngine.request(requestType, request); + var r = new StatusPromise(operation.promise); + + Util.delay(0.1).then(() => { + // A request has been made, but not completed yet. + expect(resolveScheme).toHaveBeenCalled(); + expect(r.status).toBe('pending'); + + expect(abortSpy).not.toHaveBeenCalled(); + return operation.abort(); + }).then(() => { + expect(abortSpy).toHaveBeenCalled(); + p.resolve(); + + // The operation has been aborted. + expect(r.status).toBe('rejected'); + return operation.promise.catch((e) => { + Util.expectToEqualError(e, abortError); + }); + }).catch(fail).then(done); + }); + + it('interrupts response filters', function(done) { + var filter1Promise = new shaka.util.PublicPromise(); + var filter1Spy = jasmine.createSpy('filter 1') + .and.returnValue(filter1Promise); + var filter1 = Util.spyFunc(filter1Spy); + networkingEngine.registerResponseFilter(filter1); + + var filter2Promise = new shaka.util.PublicPromise(); + var filter2Spy = jasmine.createSpy('filter 2') + .and.returnValue(filter2Promise); + var filter2 = Util.spyFunc(filter2Spy); + networkingEngine.registerResponseFilter(filter2); + + var request = createRequest('resolve://foo'); + var operation = networkingEngine.request(requestType, request); + var r = new StatusPromise(operation.promise); + + Util.delay(0.1).then(() => { + // The scheme plugin has been called, and the first filter has been + // called, but not the second. + expect(resolveScheme).toHaveBeenCalled(); + expect(filter1Spy).toHaveBeenCalled(); + expect(filter2Spy).not.toHaveBeenCalled(); + + // The operation is still pending. + expect(r.status).toBe('pending'); + + operation.abort(); + + filter1Promise.resolve(); + return Util.delay(0.1); + }).then(() => { + // The second filter has still not been called. + // The filter chain was interrupted by abort(). + expect(filter2Spy).not.toHaveBeenCalled(); + + // The operation has been aborted. + expect(r.status).toBe('rejected'); + return operation.promise.catch((e) => { + Util.expectToEqualError(e, abortError); + }); + }).catch(fail).then(done); + }); + + it('is called by destroy', function(done) { + var p = new shaka.util.PublicPromise(); + var abortSpy = jasmine.createSpy('abort'); + var abort = Util.spyFunc(abortSpy); + + resolveScheme.and.returnValue( + new shaka.util.AbortableOperation(p, abort)); + expect(resolveScheme).not.toHaveBeenCalled(); + + var request = createRequest('resolve://foo'); + var operation = networkingEngine.request(requestType, request); + var r = new StatusPromise(operation.promise); + + Util.delay(0.1).then(() => { + // A request has been made, but not completed yet. + expect(resolveScheme).toHaveBeenCalled(); + expect(abortSpy).not.toHaveBeenCalled(); + return networkingEngine.destroy(); + }).then(() => { + expect(abortSpy).toHaveBeenCalled(); + p.resolve(); + + // The operation has been aborted. + expect(r.status).toBe('rejected'); + return operation.promise.catch((e) => { + Util.expectToEqualError(e, abortError); + }); + }).catch(fail).then(done); + }); + }); + /** * @param {string} uri * @param {shakaExtern.RetryParameters=} opt_retryParameters @@ -1086,4 +1152,4 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ function() { shaka.net.NetworkingEngine.defaultRetryParameters(); return shaka.net.NetworkingEngine.makeRequest([uri], retryParameters); } -}); +}); // describe('NetworkingEngine') diff --git a/test/offline/offline_scheme_unit.js b/test/offline/offline_scheme_unit.js index 8cafd1816..e64fd4b53 100644 --- a/test/offline/offline_scheme_unit.js +++ b/test/offline/offline_scheme_unit.js @@ -65,7 +65,7 @@ describe('OfflineScheme', function() { }) .then(function(id) { uri = OfflineUri.manifestIdToUri(id); - return OfflineScheme(uri, request); + return OfflineScheme(uri, request).promise; }) .then(function(response) { expect(response).toBeTruthy(); @@ -92,7 +92,7 @@ describe('OfflineScheme', function() { }) .then(function(id) { uri = OfflineUri.segmentIdToUri(id); - return OfflineScheme(uri, request); + return OfflineScheme(uri, request).promise; }) .then(function(response) { expect(response).toBeTruthy(); @@ -113,7 +113,7 @@ describe('OfflineScheme', function() { /** @const {string} */ var uri = OfflineUri.segmentIdToUri(id); - OfflineScheme(uri, request) + OfflineScheme(uri, request).promise .then(fail) .catch(function(err) { shaka.test.Util.expectToEqualError( @@ -132,7 +132,7 @@ describe('OfflineScheme', function() { /** @type {string} */ var uri = 'offline:this-is-invalid'; - OfflineScheme(uri, request) + OfflineScheme(uri, request).promise .then(fail) .catch(function(err) { shaka.test.Util.expectToEqualError( diff --git a/test/player_integration.js b/test/player_integration.js index cd6c5e379..6e5bb6d81 100644 --- a/test/player_integration.js +++ b/test/player_integration.js @@ -312,7 +312,9 @@ describe('Player', function() { } }); - describe('cancel', function() { + // TODO(#829): re-enable these tests after AbortableOperation is used in the + // manifest parsers. + xdescribe('cancel', function() { /** @type {!jasmine.Spy} */ var schemeSpy; @@ -324,7 +326,7 @@ describe('Player', function() { shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.NETWORK, shaka.util.Error.Code.HTTP_ERROR); - return Promise.reject(error); + return shaka.util.AbortableOperation.failed(error); }); compiledShaka.net.NetworkingEngine.registerScheme('reject', Util.spyFunc(schemeSpy)); diff --git a/test/test/util/fake_networking_engine.js b/test/test/util/fake_networking_engine.js index 7d40402f3..edbcff26d 100644 --- a/test/test/util/fake_networking_engine.js +++ b/test/test/util/fake_networking_engine.js @@ -108,20 +108,6 @@ shaka.test.FakeNetworkingEngine.expectNoRequest = function( }; -/** - * Expects that a cancelable request for the given segment has occurred. - * - * @param {!Object} requestSpy - * @param {string} uri - * @param {shaka.net.NetworkingEngine.RequestType} type - */ -shaka.test.FakeNetworkingEngine.expectCancelableRequest = function( - requestSpy, uri, type) { - expect(requestSpy).toHaveBeenCalledWith( - type, jasmine.objectContaining({uris: [uri]}), jasmine.any(Function)); -}; - - /** * Expects that a range request for the given segment has occurred. * @@ -147,7 +133,7 @@ shaka.test.FakeNetworkingEngine.expectRangeRequest = function( /** * @param {shaka.net.NetworkingEngine.RequestType} type * @param {shakaExtern.Request} request - * @return {!Promise.} + * @return {!shakaExtern.IAbortableOperation.} * @private */ shaka.test.FakeNetworkingEngine.prototype.requestImpl_ = function( @@ -160,7 +146,11 @@ shaka.test.FakeNetworkingEngine.prototype.requestImpl_ = function( if (!result && request.method != 'HEAD') { // Give a more helpful error message to jasmine. expect(request.uris[0]).toBe('in the response map'); - return Promise.reject(); + var error = new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.UNEXPECTED_TEST_REQUEST); + return shaka.util.AbortableOperation.failed(error); } /** @type {shakaExtern.Response} */ @@ -173,10 +163,11 @@ shaka.test.FakeNetworkingEngine.prototype.requestImpl_ = function( if (this.delayNextRequestPromise_) { var delay = this.delayNextRequestPromise_; this.delayNextRequestPromise_ = null; - return delay.then(function() { return response; }); + return shaka.util.AbortableOperation.notAbortable( + delay.then(function() { return response; })); + } else { + return shaka.util.AbortableOperation.completed(response); } - else - return Promise.resolve(response); }; @@ -237,19 +228,6 @@ shaka.test.FakeNetworkingEngine.prototype.expectNoRequest = }; -/** - * Expects that a cancelable request for the given segment has occurred. - * - * @param {string} uri - * @param {shaka.net.NetworkingEngine.RequestType} type - */ -shaka.test.FakeNetworkingEngine.prototype.expectCancelableRequest = - function(uri, type) { - shaka.test.FakeNetworkingEngine.expectCancelableRequest( - this.request, uri, type); -}; - - /** * Expects that a range request for the given segment has occurred. * diff --git a/test/test/util/streaming_engine_util.js b/test/test/util/streaming_engine_util.js index 581b47415..a24a230cb 100644 --- a/test/test/util/streaming_engine_util.js +++ b/test/test/util/streaming_engine_util.js @@ -70,7 +70,7 @@ shaka.test.StreamingEngineUtil.createFakeNetworkingEngine = function( } var response = {uri: request.uris[0], data: buffer, headers: {}}; - return Promise.resolve(response); + return shaka.util.AbortableOperation.completed(response); }); netEngine.expectRequest = function(uri, type) { diff --git a/test/test/util/test_scheme.js b/test/test/util/test_scheme.js index e4671e3a9..ae7db4a37 100644 --- a/test/test/util/test_scheme.js +++ b/test/test/util/test_scheme.js @@ -24,7 +24,7 @@ goog.provide('shaka.test.TestScheme'); * * @param {string} uri * @param {shakaExtern.Request} request - * @return {!Promise.} + * @return {!shakaExtern.IAbortableOperation.} */ shaka.test.TestScheme = function(uri, request) { var manifestParts = /^test:([^/]+)$/.exec(uri); @@ -35,14 +35,20 @@ shaka.test.TestScheme = function(uri, request) { data: new ArrayBuffer(0), headers: {'content-type': 'application/x-test-manifest'} }; - return Promise.resolve(response); + return shaka.util.AbortableOperation.completed(response); } + + var malformed = new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.MALFORMED_TEST_URI); + var re = /^test:([^/]+)\/(video|audio)\/(init|[0-9]+)$/; var segmentParts = re.exec(uri); if (!segmentParts) { // Use expect so the URI is printed on errors. expect(uri).toMatch(re); - return Promise.reject(); + return shaka.util.AbortableOperation.failed(malformed); } var name = segmentParts[1]; @@ -50,11 +56,15 @@ shaka.test.TestScheme = function(uri, request) { var generators = shaka.test.TestScheme.GENERATORS[name]; expect(generators).toBeTruthy(); - if (!generators) return Promise.reject(); + if (!generators) { + return shaka.util.AbortableOperation.failed(malformed); + } var generator = generators[type]; expect(generator).toBeTruthy(); - if (!generator) return Promise.reject(); + if (!generator) { + return shaka.util.AbortableOperation.failed(malformed); + } var responseData; if (segmentParts[3] === 'init') { @@ -64,10 +74,9 @@ shaka.test.TestScheme = function(uri, request) { responseData = generator.getSegment(index + 1, 0, 0); } + /** @type {shakaExtern.Response} */ var ret = {uri: uri, data: responseData, headers: {}}; - // Cannot use |Promise.resolve(ret)| because of a compiler bug. - // https://goo.gl/4TdteC - return Promise.resolve().then(function() { return ret; }); + return shaka.util.AbortableOperation.completed(ret); };