diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index ef8b6d2778..5738b19499 100644 --- a/demo/common/message_ids.js +++ b/demo/common/message_ids.js @@ -152,6 +152,7 @@ shakaDemo.MessageIds = { BUFFER_BEHIND: 'DEMO_BUFFER_BEHIND', BUFFERING_GOAL: 'DEMO_BUFFERING_GOAL', CLOCK_SYNC_URI: 'DEMO_CLOCK_SYNC_URI', + CONNECTION_TIMEOUT: 'DEMO_CONNECTION_TIMEOUT', DEFAULT_PRESENTATION_DELAY: 'DEMO_DEFAULT_PRESENTATION_DELAY', DELAY_LICENSE: 'DEMO_DELAY_LICENSE', DISABLE_AUDIO: 'DEMO_DISABLE_AUDIO', @@ -215,6 +216,7 @@ shakaDemo.MessageIds = { SHAKA_CONTROLS: 'DEMO_SHAKA_CONTROLS', STALL_DETECTOR_ENABLED: 'DEMO_STALL_DETECTOR_ENABLED', STALL_THRESHOLD: 'DEMO_STALL_THRESHOLD', + STALL_TIMEOUT: 'DEMO_STALL_TIMEOUT', START_AT_SEGMENT_BOUNDARY: 'DEMO_START_AT_SEGMENT_BOUNDARY', STREAMING_RETRY_SECTION_HEADER: 'DEMO_STREAMING_RETRY_SECTION_HEADER', STREAMING_SECTION_HEADER: 'DEMO_STREAMING_SECTION_HEADER', diff --git a/demo/config.js b/demo/config.js index cb0be5fa9f..f377312bee 100644 --- a/demo/config.js +++ b/demo/config.js @@ -280,6 +280,11 @@ shakaDemo.Config = class { .addNumberInput_(MessageIds.FUZZ_FACTOR, prefix + 'fuzzFactor', /* canBeDecimal= */ true) .addNumberInput_(MessageIds.TIMEOUT, prefix + 'timeout', + /* canBeDecimal= */ true) + .addNumberInput_(MessageIds.STALL_TIMEOUT, prefix + 'stallTimeout', + /* canBeDecimal= */ true) + .addNumberInput_(MessageIds.CONNECTION_TIMEOUT, + prefix + 'connectionTimeout', /* canBeDecimal= */ true); } diff --git a/demo/locales/en.json b/demo/locales/en.json index eff08d008c..7c971659e5 100644 --- a/demo/locales/en.json +++ b/demo/locales/en.json @@ -35,6 +35,7 @@ "DEMO_CLOCK_SYNC_URI": "Clock Sync URI", "DEMO_COMPILED_DEBUG": "Compiled (Debug)", "DEMO_COMPILED_RELEASE": "Compiled (Release)", + "DEMO_CONNECTION_TIMEOUT": "Connection timeout", "DEMO_CONFIG": "Shaka Player Demo Config", "DEMO_CONTAINER_SEARCH": "Container", "DEMO_CUSTOM": "Custom", @@ -168,6 +169,7 @@ "DEMO_SOURCE_SEARCH": "Source", "DEMO_STALL_DETECTOR_ENABLED": "Stall Detector Enabled", "DEMO_STALL_THRESHOLD": "Stall Threshold", + "DEMO_STALL_TIMEOUT": "Stall timeout", "DEMO_START_AT_SEGMENT_BOUNDARY": "Start At Segment Boundary", "DEMO_STORED": "Downloaded", "DEMO_STORED_SEARCH": "Filters for assets that have been stored offline.", diff --git a/demo/locales/source.json b/demo/locales/source.json index 16aea1d167..f72ca837db 100644 --- a/demo/locales/source.json +++ b/demo/locales/source.json @@ -143,6 +143,10 @@ "description": "A link in the footer, to the release build of the demo.", "message": "Compiled (Release)" }, + "DEMO_CONNECTION_TIMEOUT": { + "description": "The name of a configuration value.", + "message": "Connection timeout" + }, "DEMO_CONFIG": { "description": "A title on the configuration panel, labeling it as configuration.", "message": "[PROPER_NAME:Shaka Player] Demo Config" @@ -675,6 +679,10 @@ "description": "The name of a configuration value.", "message": "Stall Threshold" }, + "DEMO_STALL_TIMEOUT": { + "description": "The name of a configuration value.", + "message": "Stall timeout" + }, "DEMO_START_AT_SEGMENT_BOUNDARY": { "description": "The name of a configuration value.", "message": "Start At Segment Boundary" diff --git a/docs/tutorials/network-and-buffering-config.md b/docs/tutorials/network-and-buffering-config.md index 10d1d4edf0..82b3cedc73 100644 --- a/docs/tutorials/network-and-buffering-config.md +++ b/docs/tutorials/network-and-buffering-config.md @@ -14,7 +14,9 @@ identical: ```js retryParameters: { - timeout: 0, // timeout in ms, after which we abort; 0 means never + timeout: 30000, // timeout in ms, after which we abort + stallTimeout: 5000, // stall timeout in ms, after which we abort + connectionTimeout: 10000, // connection timeout in ms, after which we abort maxAttempts: 2, // the maximum number of requests before we fail baseDelay: 1000, // the base delay in ms between retries backoffFactor: 2, // the multiplicative backoff factor between retries diff --git a/externs/shaka/net.js b/externs/shaka/net.js index 38ba8c518e..58047b63d4 100644 --- a/externs/shaka/net.js +++ b/externs/shaka/net.js @@ -16,7 +16,9 @@ * baseDelay: number, * backoffFactor: number, * fuzzFactor: number, - * timeout: number + * timeout: number, + * stallTimeout: number, + * connectionTimeout: number * }} * * @description @@ -34,6 +36,12 @@ * @property {number} timeout * The request timeout, in milliseconds. Zero means "unlimited". * Defaults to 30000 milliseconds. + * @property {number} stallTimeout + * The request stall timeout, in milliseconds. Zero means "unlimited". + * Defaults to 5000 milliseconds. + * @property {number} connectionTimeout + * The request connection timeout, in milliseconds. Zero means "unlimited". + * Defaults to 10000 milliseconds. * * @tutorial network-and-buffering-config * diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index 7ca99dfe10..380463528a 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -157,6 +157,8 @@ shaka.media.StreamingEngine = class { backoffFactor: config.retryParameters.backoffFactor, fuzzFactor: config.retryParameters.fuzzFactor, timeout: 0, // irrelevant + stallTimeout: 0, // irrelevant + connectionTimeout: 0, // irrelevant }; // We don't want to ever run out of attempts. The application should be diff --git a/lib/net/backoff.js b/lib/net/backoff.js index 46bbf3611a..03c2c1692b 100644 --- a/lib/net/backoff.js +++ b/lib/net/backoff.js @@ -140,6 +140,8 @@ shaka.net.Backoff = class { backoffFactor: 2, fuzzFactor: 0.5, timeout: 30000, + stallTimeout: 5000, + connectionTimeout: 10000, }; } diff --git a/lib/net/http_fetch_plugin.js b/lib/net/http_fetch_plugin.js index f6d7ee3b84..e199ceebf8 100644 --- a/lib/net/http_fetch_plugin.js +++ b/lib/net/http_fetch_plugin.js @@ -284,8 +284,10 @@ shaka.net.HttpFetchPlugin.Headers_ = window.Headers; if (shaka.net.HttpFetchPlugin.isSupported()) { shaka.net.NetworkingEngine.registerScheme( 'http', shaka.net.HttpFetchPlugin.parse, - shaka.net.NetworkingEngine.PluginPriority.PREFERRED); + shaka.net.NetworkingEngine.PluginPriority.PREFERRED, + /* progressSupport= */ true); shaka.net.NetworkingEngine.registerScheme( 'https', shaka.net.HttpFetchPlugin.parse, - shaka.net.NetworkingEngine.PluginPriority.PREFERRED); + shaka.net.NetworkingEngine.PluginPriority.PREFERRED, + /* progressSupport= */ true); } diff --git a/lib/net/http_xhr_plugin.js b/lib/net/http_xhr_plugin.js index b8764ad29e..65f174b4a6 100644 --- a/lib/net/http_xhr_plugin.js +++ b/lib/net/http_xhr_plugin.js @@ -130,8 +130,10 @@ shaka.net.HttpXHRPlugin.Xhr_ = window.XMLHttpRequest; shaka.net.NetworkingEngine.registerScheme( 'http', shaka.net.HttpXHRPlugin.parse, - shaka.net.NetworkingEngine.PluginPriority.FALLBACK); + shaka.net.NetworkingEngine.PluginPriority.FALLBACK, + /* progressSupport= */ true); shaka.net.NetworkingEngine.registerScheme( 'https', shaka.net.HttpXHRPlugin.parse, - shaka.net.NetworkingEngine.PluginPriority.FALLBACK); + shaka.net.NetworkingEngine.PluginPriority.FALLBACK, + /* progressSupport= */ true); diff --git a/lib/net/networking_engine.js b/lib/net/networking_engine.js index 18b8e68e21..e03cc37484 100644 --- a/lib/net/networking_engine.js +++ b/lib/net/networking_engine.js @@ -19,6 +19,7 @@ goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.ObjectUtils'); goog.require('shaka.util.OperationManager'); +goog.require('shaka.util.Timer'); /** @@ -88,9 +89,10 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget { * @param {string} scheme * @param {shaka.extern.SchemePlugin} plugin * @param {number=} priority + * @param {boolean=} progressSupport * @export */ - static registerScheme(scheme, plugin, priority) { + static registerScheme(scheme, plugin, priority, progressSupport = false) { goog.asserts.assert( priority == undefined || priority > 0, 'explicit priority must be > 0'); priority = @@ -100,6 +102,7 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget { shaka.net.NetworkingEngine.schemes_[scheme] = { priority: priority, plugin: plugin, + progressSupport: progressSupport, }; } } @@ -428,6 +431,7 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget { shaka.util.Error.Code.UNSUPPORTED_SCHEME, uri)); } + const progressSupport = object.progressSupport; // Every attempt must have an associated backoff.attempt() call so that the @@ -435,6 +439,14 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget { const backoffOperation = shaka.util.AbortableOperation.notAbortable(backoff.attempt()); + /** @type {?shaka.util.Timer} */ + let connectionTimer = null; + + /** @type {?shaka.util.Timer} */ + let stallTimer = null; + + let aborted = false; + let startTimeMs; const sendOperation = backoffOperation.chain(() => { if (this.destroyed_) { @@ -444,18 +456,54 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget { startTimeMs = Date.now(); const segment = shaka.net.NetworkingEngine.RequestType.SEGMENT; - return plugin(request.uris[index], + const requestPlugin = plugin(request.uris[index], request, type, // The following function is passed to plugin. (time, bytes, numBytesRemaining) => { + if (connectionTimer) { + connectionTimer.stop(); + } + if (stallTimer) { + stallTimer.tickAfter(stallTimeoutMs / 1000); + } if (this.onProgressUpdated_ && type == segment) { this.onProgressUpdated_(time, bytes); gotProgress = true; numBytesRemainingObj.setBytes(numBytesRemaining); } }); + + if (!progressSupport) { + return requestPlugin; + } + + const connectionTimeoutMs = request.retryParameters.connectionTimeout; + if (connectionTimeoutMs) { + connectionTimer = new shaka.util.Timer(() => { + aborted = true; + requestPlugin.abort(); + }); + + connectionTimer.tickAfter(connectionTimeoutMs / 1000); + } + + const stallTimeoutMs = request.retryParameters.stallTimeout; + if (stallTimeoutMs) { + stallTimer = new shaka.util.Timer(() => { + aborted = true; + requestPlugin.abort(); + }); + } + + return requestPlugin; }).chain((response) => { + if (connectionTimer) { + connectionTimer.stop(); + } + if (stallTimer) { + stallTimer.stop(); + } if (response.timeMs == undefined) { response.timeMs = Date.now() - startTimeMs; } @@ -466,10 +514,26 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget { return responseAndGotProgress; }, (error) => { + if (connectionTimer) { + connectionTimer.stop(); + } + if (stallTimer) { + stallTimer.stop(); + } if (this.destroyed_) { return shaka.util.AbortableOperation.aborted(); } + if (aborted) { + // It is necessary to change the error code to the correct one because + // otherwise the retry logic would not work. + error = new shaka.util.Error( + shaka.util.Error.Severity.RECOVERABLE, + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.TIMEOUT, + request.uris[index], type); + } + if (error instanceof shaka.util.Error) { if (error.code == shaka.util.Error.Code.OPERATION_ABORTED) { // Don't change anything if the operation was aborted. @@ -663,12 +727,15 @@ shaka.net.NetworkingEngine.PluginPriority = { /** * @typedef {{ * plugin: shaka.extern.SchemePlugin, - * priority: number + * priority: number, + * progressSupport: boolean * }} * @property {shaka.extern.SchemePlugin} plugin * The associated plugin. * @property {number} priority * The plugin's priority. + * @property {boolean} progressSupport + * The plugin's supports progress events */ shaka.net.NetworkingEngine.SchemeObject; diff --git a/test/net/networking_engine_unit.js b/test/net/networking_engine_unit.js index 9acbd2e0e9..d6db85a518 100644 --- a/test/net/networking_engine_unit.js +++ b/test/net/networking_engine_unit.js @@ -90,6 +90,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); rejectScheme.and.callFake(() => { if (rejectScheme.calls.count() == 1) { @@ -109,6 +111,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); rejectScheme.and.callFake(() => { if (rejectScheme.calls.count() < 3) { @@ -128,6 +132,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); // It is expected to fail with the most recent error, but at a CRITICAL @@ -166,6 +172,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { fuzzFactor: 0, backoffFactor: 2, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); await expectAsync( @@ -182,6 +190,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { fuzzFactor: 0, backoffFactor: 2, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); await expectAsync( @@ -201,6 +211,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { fuzzFactor: 1, backoffFactor: 1, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); await expectAsync( @@ -222,6 +234,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); request.uris = ['reject://foo', 'resolve://foo']; await networkingEngine.request(requestType, request).promise; @@ -236,6 +250,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); error.severity = shaka.util.Error.Severity.CRITICAL; @@ -542,6 +558,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); filter.and.returnValue(Promise.reject(new Error(''))); @@ -558,6 +576,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); filter.and.throwError(error); @@ -765,6 +785,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); /** @type {!shaka.test.StatusPromise} */ const r = new StatusPromise( @@ -844,6 +866,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); /** @type {!shaka.util.PublicPromise} */ @@ -917,6 +941,8 @@ describe('NetworkingEngine', /** @suppress {accessControls} */ () => { backoffFactor: 0, fuzzFactor: 0, timeout: 0, + stallTimeout: 0, + connectionTimeout: 0, }); retrySpy = jasmine.createSpy('retry listener');