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');