diff --git a/docs/api.md b/docs/api.md index 5b5d4d25..18a0c15d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -91,6 +91,7 @@ In multipart mode, `duration` `filesize` `url` field in `MediaDataSource` struct | `reuseRedirectedURL?` | `boolean` | `false` | Reuse 301/302 redirected url for subsequence request like seek, reconnect, etc. | | `referrerPolicy?` | `string` | `no-referrer-when-downgrade` | Indicates the [Referrer Policy][] when using FetchStreamLoader | | `headers?` | `object` | `undefined` | Indicates additional headers that will be added to request | +| `requestTimeout?` | `number` | `Infinity` | Timeout setting for IO Connection, in **milliseconds**. `requestTimeout` must greater than zero. | [Referrer Policy]: https://w3c.github.io/webappsec-referrer-policy/#referrer-policy diff --git a/src/config.js b/src/config.js index 7dd6a79d..e6ea56c3 100644 --- a/src/config.js +++ b/src/config.js @@ -50,7 +50,8 @@ export const defaultConfig = { // referrerPolicy: leave as unspecified headers: undefined, - customLoader: undefined + customLoader: undefined, + requestTimeout: Infinity, }; export function createDefaultConfig() { diff --git a/src/io/fetch-stream-loader.js b/src/io/fetch-stream-loader.js index c04da5b0..a087fb32 100644 --- a/src/io/fetch-stream-loader.js +++ b/src/io/fetch-stream-loader.js @@ -121,6 +121,12 @@ class FetchStreamLoader extends BaseLoader { if (self.AbortController) { this._abortController = new self.AbortController(); params.signal = this._abortController.signal; + + if (this._config.requestTimeout !== Infinity && this._config.requestTimeout > 0) { + this._timeoutId = self.setTimeout(() => { + this.abort('timeout'); + }, this._config.requestTimeout); + } } this._status = LoaderStatus.kConnecting; @@ -130,6 +136,9 @@ class FetchStreamLoader extends BaseLoader { res.body.cancel(); return; } + + this.clearFetchTimeout(); + if (res.ok && (res.status >= 200 && res.status <= 299)) { if (res.url !== seekConfig.url) { if (this._onURLRedirect) { @@ -159,9 +168,19 @@ class FetchStreamLoader extends BaseLoader { } }).catch((e) => { if (this._abortController && this._abortController.signal.aborted) { + if (this._abortController.signal.reason === 'timeout') { + this._status = LoaderStatus.kError; + if (this._onError) { + this._onError(LoaderErrors.CONNECTING_TIMEOUT, {code: -1, msg: 'fetch stream loader connecting timeout'}); + } else { + throw new RuntimeException('FetchStreamLoader: connecting timeout'); + } + } return; } + this.clearFetchTimeout(); + this._status = LoaderStatus.kError; if (this._onError) { this._onError(LoaderErrors.EXCEPTION, {code: -1, msg: e.message}); @@ -171,14 +190,16 @@ class FetchStreamLoader extends BaseLoader { }); } - abort() { + abort(reason) { this._requestAbort = true; + this.clearFetchTimeout(); + if (this._status !== LoaderStatus.kBuffering || !Browser.chrome) { // Chrome may throw Exception-like things here, avoid using if is buffering if (this._abortController) { try { - this._abortController.abort(); + this._abortController.abort(reason); } catch (e) {} } } @@ -261,6 +282,12 @@ class FetchStreamLoader extends BaseLoader { }); } + clearFetchTimeout() { + if (this._timeoutId) { + self.clearTimeout(this._timeoutId); + this._timeoutId = null; + } + } } export default FetchStreamLoader; diff --git a/src/io/io-controller.js b/src/io/io-controller.js index 5ccf1a96..fbe66d38 100644 --- a/src/io/io-controller.js +++ b/src/io/io-controller.js @@ -21,7 +21,6 @@ import SpeedSampler from './speed-sampler.js'; import {LoaderStatus, LoaderErrors} from './loader.js'; import FetchStreamLoader from './fetch-stream-loader.js'; import MozChunkedLoader from './xhr-moz-chunked-loader.js'; -import MSStreamLoader from './xhr-msstream-loader.js'; import RangeLoader from './xhr-range-loader.js'; import WebSocketLoader from './websocket-loader.js'; import RangeSeekHandler from './range-seek-handler.js'; diff --git a/src/io/xhr-moz-chunked-loader.js b/src/io/xhr-moz-chunked-loader.js index e6605046..45ddda9c 100644 --- a/src/io/xhr-moz-chunked-loader.js +++ b/src/io/xhr-moz-chunked-loader.js @@ -112,6 +112,19 @@ class MozChunkedLoader extends BaseLoader { } } + if (this._config.requestTimeout !== Infinity && this._config.requestTimeout > 0) { + xhr.requestTimeoutId = window.setTimeout(() => { + xhr.abort(); + + this._status = LoaderStatus.kError; + if (this._onError) { + this._onError(LoaderErrors.CONNECTING_TIMEOUT, {code: -1, msg: 'MozChunkedLoader connecting timeout'}); + } else { + throw new RuntimeException('MozChunkedLoader: connecting timeout'); + } + }, this._config.requestTimeout); + } + this._status = LoaderStatus.kConnecting; xhr.send(); } @@ -119,6 +132,7 @@ class MozChunkedLoader extends BaseLoader { abort() { this._requestAbort = true; if (this._xhr) { + this._clearRequestTimeout(); this._xhr.abort(); } this._status = LoaderStatus.kComplete; @@ -128,6 +142,8 @@ class MozChunkedLoader extends BaseLoader { let xhr = e.target; if (xhr.readyState === 2) { // HEADERS_RECEIVED + this._clearRequestTimeout(); + if (xhr.responseURL != undefined && xhr.responseURL !== this._requestURL) { if (this._onURLRedirect) { let redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL); @@ -187,6 +203,8 @@ class MozChunkedLoader extends BaseLoader { } _onXhrError(e) { + this._clearRequestTimeout(); + this._status = LoaderStatus.kError; let type = 0; let info = null; @@ -206,6 +224,12 @@ class MozChunkedLoader extends BaseLoader { } } + _clearRequestTimeout() { + if (this._xhr && this._xhr.requestTimeoutId) { + clearTimeout(this._xhr.requestTimeoutId); + this._xhr.requestTimeoutId = undefined; + } + } } export default MozChunkedLoader; \ No newline at end of file diff --git a/src/io/xhr-range-loader.js b/src/io/xhr-range-loader.js index 342a3808..5a403cf7 100644 --- a/src/io/xhr-range-loader.js +++ b/src/io/xhr-range-loader.js @@ -170,6 +170,19 @@ class RangeLoader extends BaseLoader { } } + if (this._config.requestTimeout !== Infinity && this._config.requestTimeout > 0) { + xhr.requestTimeoutId = window.setTimeout(() => { + xhr.abort(); + + this._status = LoaderStatus.kError; + if (this._onError) { + this._onError(LoaderErrors.CONNECTING_TIMEOUT, {code: -1, msg: 'RangeLoader connecting timeout'}); + } else { + throw new RuntimeException('RangeLoader: connecting timeout'); + } + }, this._config.requestTimeout); + } + xhr.send(); } @@ -181,6 +194,7 @@ class RangeLoader extends BaseLoader { _internalAbort() { if (this._xhr) { + this._clearRequestTimeout(); this._xhr.onreadystatechange = null; this._xhr.onprogress = null; this._xhr.onload = null; @@ -194,6 +208,8 @@ class RangeLoader extends BaseLoader { let xhr = e.target; if (xhr.readyState === 2) { // HEADERS_RECEIVED + this._clearRequestTimeout(); + if (xhr.responseURL != undefined) { // if the browser support this property let redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL); if (xhr.responseURL !== this._currentRequestURL && redirectedURL !== this._currentRedirectedURL) { @@ -341,6 +357,8 @@ class RangeLoader extends BaseLoader { } _onXhrError(e) { + this._clearRequestTimeout(); + this._status = LoaderStatus.kError; let type = 0; let info = null; @@ -361,6 +379,12 @@ class RangeLoader extends BaseLoader { } } + _clearRequestTimeout() { + if (this._xhr && this._xhr.requestTimeoutId) { + clearTimeout(this._xhr.requestTimeoutId); + this._xhr.requestTimeoutId = undefined; + } + } } export default RangeLoader; \ No newline at end of file