From 089ed9bfd2c0a34e53d5a1e73a92d61dd2acf88f Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 31 Mar 2023 14:52:21 -0700 Subject: [PATCH 1/2] Relax TTFB timeout on manifest request (Add support for LoadPolicy `maxTimeToFirstByteMs` of Infinity and similarly ignore value of 0) --- src/config.ts | 2 +- src/utils/fetch-loader.ts | 10 +++++++--- src/utils/xhr-loader.ts | 24 ++++++++++++++--------- tests/unit/controller/error-controller.ts | 6 ++++-- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/config.ts b/src/config.ts index f4b70f40f87..e7a58a1d470 100644 --- a/src/config.ts +++ b/src/config.ts @@ -405,7 +405,7 @@ export const hlsDefaultConfig: HlsConfig = { }, manifestLoadPolicy: { default: { - maxTimeToFirstByteMs: 10000, + maxTimeToFirstByteMs: Infinity, maxLoadTimeMs: 20000, timeoutRetry: { maxNumRetry: 2, diff --git a/src/utils/fetch-loader.ts b/src/utils/fetch-loader.ts index 0f48050e4a5..12ac10583f0 100644 --- a/src/utils/fetch-loader.ts +++ b/src/utils/fetch-loader.ts @@ -84,13 +84,17 @@ class FetchLoader implements Loader { callbacks.onProgress; const isArrayBuffer = context.responseType === 'arraybuffer'; const LENGTH = isArrayBuffer ? 'byteLength' : 'length'; + const { maxTimeToFirstByteMs, maxLoadTimeMs } = config.loadPolicy; this.context = context; this.config = config; this.callbacks = callbacks; this.request = this.fetchSetup(context, initParams); self.clearTimeout(this.requestTimeout); - config.timeout = config.loadPolicy.maxTimeToFirstByteMs; + config.timeout = + maxTimeToFirstByteMs && Number.isFinite(maxTimeToFirstByteMs) + ? maxTimeToFirstByteMs + : maxLoadTimeMs; this.requestTimeout = self.setTimeout(() => { this.abortInternal(); callbacks.onTimeout(stats, context, this.response); @@ -104,11 +108,11 @@ class FetchLoader implements Loader { const first = Math.max(self.performance.now(), stats.loading.start); self.clearTimeout(this.requestTimeout); - config.timeout = config.loadPolicy.maxLoadTimeMs; + config.timeout = maxLoadTimeMs; this.requestTimeout = self.setTimeout(() => { this.abortInternal(); callbacks.onTimeout(stats, context, this.response); - }, config.loadPolicy.maxLoadTimeMs - (first - stats.loading.start)); + }, maxLoadTimeMs - (first - stats.loading.start)); if (!response.ok) { const { status, statusText } = response; diff --git a/src/utils/xhr-loader.ts b/src/utils/xhr-loader.ts index 0b80f663aad..463db98df07 100644 --- a/src/utils/xhr-loader.ts +++ b/src/utils/xhr-loader.ts @@ -127,6 +127,7 @@ class XhrLoader implements Loader { } const headers = this.context.headers; + const { maxTimeToFirstByteMs, maxLoadTimeMs } = config.loadPolicy; if (headers) { for (const header in headers) { xhr.setRequestHeader(header, headers[header]); @@ -145,10 +146,13 @@ class XhrLoader implements Loader { xhr.responseType = context.responseType as XMLHttpRequestResponseType; // setup timeout before we perform request self.clearTimeout(this.requestTimeout); - config.timeout = config.loadPolicy.maxTimeToFirstByteMs; + config.timeout = + maxTimeToFirstByteMs && Number.isFinite(maxTimeToFirstByteMs) + ? maxTimeToFirstByteMs + : maxLoadTimeMs; this.requestTimeout = self.setTimeout( this.loadtimeout.bind(this), - config.loadPolicy.maxTimeToFirstByteMs + config.timeout ); xhr.send(); } @@ -174,13 +178,15 @@ class XhrLoader implements Loader { stats.loading.start ); // readyState >= 2 AND readyState !==4 (readyState = HEADERS_RECEIVED || LOADING) rearm timeout as xhr not finished yet - self.clearTimeout(this.requestTimeout); - config.timeout = config.loadPolicy.maxLoadTimeMs; - this.requestTimeout = self.setTimeout( - this.loadtimeout.bind(this), - config.loadPolicy.maxLoadTimeMs - - (stats.loading.first - stats.loading.start) - ); + if (config.timeout !== config.loadPolicy.maxLoadTimeMs) { + self.clearTimeout(this.requestTimeout); + config.timeout = config.loadPolicy.maxLoadTimeMs; + this.requestTimeout = self.setTimeout( + this.loadtimeout.bind(this), + config.loadPolicy.maxLoadTimeMs - + (stats.loading.first - stats.loading.start) + ); + } } if (readyState === 4) { diff --git a/tests/unit/controller/error-controller.ts b/tests/unit/controller/error-controller.ts index ff33f873a8e..e47998c2945 100644 --- a/tests/unit/controller/error-controller.ts +++ b/tests/unit/controller/error-controller.ts @@ -203,8 +203,10 @@ describe('ErrorController Integration Tests', function () { ) ) ); - timers.tick(hls.config.playlistLoadPolicy.default.maxLoadTimeMs); - timers.tick(hls.config.playlistLoadPolicy.default.maxLoadTimeMs); + // tick 3 times to trigger 2 retries and then an error + timers.tick(hls.config.manifestLoadPolicy.default.maxLoadTimeMs + 1); + timers.tick(hls.config.manifestLoadPolicy.default.maxLoadTimeMs + 1); + timers.tick(hls.config.manifestLoadPolicy.default.maxLoadTimeMs); }).then( expectFatalErrorEventToStopPlayer( hls, From e8649bf172f0d3124e68f39587535c6b448ba540 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Mon, 3 Apr 2023 18:11:30 -0700 Subject: [PATCH 2/2] Add LoadPolicy API documentation --- docs/API.md | 200 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 185 insertions(+), 15 deletions(-) diff --git a/docs/API.md b/docs/API.md index c43c9fc8486..96caca20694 100644 --- a/docs/API.md +++ b/docs/API.md @@ -50,10 +50,21 @@ See [API Reference](https://hlsjs-dev.video-dev.org/api-docs/) for a complete li - [`workerPath`](#workerpath) - [`enableSoftwareAES`](#enablesoftwareaes) - [`startLevel`](#startlevel) - - [`fragLoadingTimeOut` / `manifestLoadingTimeOut` / `levelLoadingTimeOut`](#fragloadingtimeout--manifestloadingtimeout--levelloadingtimeout) - - [`fragLoadingMaxRetry` / `manifestLoadingMaxRetry` / `levelLoadingMaxRetry`](#fragloadingmaxretry--manifestloadingmaxretry--levelloadingmaxretry) - - [`fragLoadingMaxRetryTimeout` / `manifestLoadingMaxRetryTimeout` / `levelLoadingMaxRetryTimeout`](#fragloadingmaxretrytimeout--manifestloadingmaxretrytimeout--levelloadingmaxretrytimeout) - - [`fragLoadingRetryDelay` / `manifestLoadingRetryDelay` / `levelLoadingRetryDelay`](#fragloadingretrydelay--manifestloadingretrydelay--levelloadingretrydelay) + - [`fragLoadingTimeOut` / `manifestLoadingTimeOut` / `levelLoadingTimeOut` (deprecated)](#fragloadingtimeout--manifestloadingtimeout--levelloadingtimeout-deprecated) + - [`fragLoadingMaxRetry` / `manifestLoadingMaxRetry` / `levelLoadingMaxRetry` (deprecated)](#fragloadingmaxretry--manifestloadingmaxretry--levelloadingmaxretry-deprecated) + - [`fragLoadingMaxRetryTimeout` / `manifestLoadingMaxRetryTimeout` / `levelLoadingMaxRetryTimeout` (deprecated)](#fragloadingmaxretrytimeout--manifestloadingmaxretrytimeout--levelloadingmaxretrytimeout-deprecated) + - [`fragLoadingRetryDelay` / `manifestLoadingRetryDelay` / `levelLoadingRetryDelay` (deprecated)](#fragloadingretrydelay--manifestloadingretrydelay--levelloadingretrydelay-deprecated) + - [`fragLoadPolicy` / `keyLoadPolicy` / `certLoadPolicy` / `playlistLoadPolicy` / `manifestLoadPolicy` / `steeringManifestLoadPolicy`](#fragloadpolicy--keyloadpolicy--certloadpolicy--playlistloadpolicy--manifestloadpolicy--steeringmanifestloadpolicy) + - [`LoaderConfig`](#loaderconfig) + - [`maxTimeToFirstByteMs: number`](#maxtimetofirstbytems-number) + - [`maxLoadTimeMs: number`](#maxloadtimems-number) + - [`timeoutRetry: RetryConfig | null`](#timeoutretry-retryconfig--null) + - [`errorRetry: RetryConfig | null`](#errorretry-retryconfig--null) + - [`RetryConfig`](#retryconfig) + - [`maxNumRetry: number`](#maxnumretry-number) + - [`retryDelayMs: number`](#retrydelayms-number) + - [`maxRetryDelayMs: number`](#maxretrydelayms-number) + - [`backoff?: 'exponential' | 'linear'`](#backoff-exponential--linear) - [`startFragPrefetch`](#startfragprefetch) - [`testBandwidth`](#testbandwidth) - [`progressive`](#progressive) @@ -668,36 +679,195 @@ Enable to use JavaScript version AES decryption for fallback of WebCrypto API. When set, use this level as the default hls.startLevel. Keep in mind that the startLevel set with the API takes precedence over config.startLevel configuration parameter. -### `fragLoadingTimeOut` / `manifestLoadingTimeOut` / `levelLoadingTimeOut` +### `fragLoadingTimeOut` / `manifestLoadingTimeOut` / `levelLoadingTimeOut` (deprecated) (default: 20000ms for fragment / 10000ms for level and manifest) -URL Loader timeout. -A timeout callback will be triggered if loading duration exceeds this timeout. -no further action will be done : the load operation will not be cancelled/aborted. -It is up to the application to catch this event and treat it as needed. +x-LoadingTimeOut settings have been deprecated. Use one of the LoadPolicy settings instead. -### `fragLoadingMaxRetry` / `manifestLoadingMaxRetry` / `levelLoadingMaxRetry` +### `fragLoadingMaxRetry` / `manifestLoadingMaxRetry` / `levelLoadingMaxRetry` (deprecated) (default: `6` / `1` / `4`) -Max number of load retries. +x-LoadingMaxRetry settings have been deprecated. Use one of the LoadPolicy settings instead. -### `fragLoadingMaxRetryTimeout` / `manifestLoadingMaxRetryTimeout` / `levelLoadingMaxRetryTimeout` +### `fragLoadingMaxRetryTimeout` / `manifestLoadingMaxRetryTimeout` / `levelLoadingMaxRetryTimeout` (deprecated) (default: `64000` ms) -Maximum frag/manifest/key retry timeout (in milliseconds) in case I/O errors are met. +x-LoadingMaxRetryTimeout settings have been deprecated. Use one of the LoadPolicy settings instead. + +Maximum frag/manifest/key retry timeout (in milliseconds). This value is used as capping value for exponential grow of `loading retry delays`, i.e. the retry delay can not be bigger than this value, but overall time will be based on the overall number of retries. -### `fragLoadingRetryDelay` / `manifestLoadingRetryDelay` / `levelLoadingRetryDelay` +### `fragLoadingRetryDelay` / `manifestLoadingRetryDelay` / `levelLoadingRetryDelay` (deprecated) (default: `1000` ms) +x-LoadingRetryDelay settings have been deprecated. Use one of the LoadPolicy settings instead. + Initial delay between `XMLHttpRequest` error and first load retry (in ms). Any I/O error will trigger retries every 500ms,1s,2s,4s,8s, ... capped to `fragLoadingMaxRetryTimeout` / `manifestLoadingMaxRetryTimeout` / `levelLoadingMaxRetryTimeout` value (exponential backoff). -Prefetch start fragment although media not attached. +### `fragLoadPolicy` / `keyLoadPolicy` / `certLoadPolicy` / `playlistLoadPolicy` / `manifestLoadPolicy` / `steeringManifestLoadPolicy` + +LoadPolicies specify the default settings for request timeouts and the timing and number of retries after a request error or timeout for a particular type of asset. + +- `manifestLoadPolicy`: The `LoadPolicy` for Multivariant Playlist requests +- `playlistLoadPolicy`: The `LoadPolicy` for Media Playlist requests +- `fragLoadPolicy`: The `LoadPolicy` for Segment and Part\* requests +- `keyLoadPolicy`: The `LoadPolicy` for Key requests +- `certLoadPolicy`: The `LoadPolicy` for License Server certificate requests +- `steeringManifestLoadPolicy`: The `LoadPolicy` for Content Steering manifest requests + +\*Some timeout settings are adjusted for Low-Latency Part requests based on Part duration or target. + +Each `LoadPolicy` contains a set of contexts. The `default` property is the only context supported at this time. It contains the `LoaderConfig` for that asset type. Future releases may include support for other policy contexts besides `default`. + +HLS.js config defines the following default policies. Each can be overridden on player instantiation in the user configuration: + +```js +manifestLoadPolicy: { + default: { + maxTimeToFirstByteMs: Infinity, + maxLoadTimeMs: 20000, + timeoutRetry: { + maxNumRetry: 2, + retryDelayMs: 0, + maxRetryDelayMs: 0, + }, + errorRetry: { + maxNumRetry: 1, + retryDelayMs: 1000, + maxRetryDelayMs: 8000, + }, + }, +}, +playlistLoadPolicy: { + default: { + maxTimeToFirstByteMs: 10000, + maxLoadTimeMs: 20000, + timeoutRetry: { + maxNumRetry: 2, + retryDelayMs: 0, + maxRetryDelayMs: 0, + }, + errorRetry: { + maxNumRetry: 2, + retryDelayMs: 1000, + maxRetryDelayMs: 8000, + }, + }, +}, +fragLoadPolicy: { + default: { + maxTimeToFirstByteMs: 10000, + maxLoadTimeMs: 120000, + timeoutRetry: { + maxNumRetry: 4, + retryDelayMs: 0, + maxRetryDelayMs: 0, + }, + errorRetry: { + maxNumRetry: 6, + retryDelayMs: 1000, + maxRetryDelayMs: 8000, + }, + }, +}, +keyLoadPolicy: { + default: { + maxTimeToFirstByteMs: 8000, + maxLoadTimeMs: 20000, + timeoutRetry: { + maxNumRetry: 1, + retryDelayMs: 1000, + maxRetryDelayMs: 20000, + backoff: 'linear', + }, + errorRetry: { + maxNumRetry: 8, + retryDelayMs: 1000, + maxRetryDelayMs: 20000, + backoff: 'linear', + }, + }, +}, +certLoadPolicy: { + default: { + maxTimeToFirstByteMs: 8000, + maxLoadTimeMs: 20000, + timeoutRetry: null, + errorRetry: null, + }, +}, +steeringManifestLoadPolicy: { + default: { + maxTimeToFirstByteMs: 10000, + maxLoadTimeMs: 20000, + timeoutRetry: { + maxNumRetry: 2, + retryDelayMs: 0, + maxRetryDelayMs: 0, + }, + errorRetry: { + maxNumRetry: 1, + retryDelayMs: 1000, + maxRetryDelayMs: 8000, + }, + } +} +``` + +#### `LoaderConfig` + +Each `LoaderConfig` has the following properties: + +##### `maxTimeToFirstByteMs: number` + +Maximum time-to-first-byte in milliseconds. If no bytes or readyState change happens in this time, a network timeout error will be triggered for the asset. + +Non-finite values and 0 will be ignored, resulting in only a single `maxLoadTimeMs` timeout timer for the entire request. + +##### `maxLoadTimeMs: number` + +Maximum time to load the asset in milliseconds. If the request is not completed in time, a network timeout error will be triggered for the asset. + +##### `timeoutRetry: RetryConfig | null` + +Retry rules for timeout errors. Specifying null results in no retries after a timeout error for the asset type. + +##### `errorRetry: RetryConfig | null` + +Retry rules for network I/O errors. Specifying null results in no retries after a timeout error for the asset type. + +#### `RetryConfig` + +Each `RetryConfig` has the following properties: + +##### `maxNumRetry: number` + +Maximum number of retries. After an error, the request will be retried this many times before other recovery +measures are taken. For example, after having retried a segment or playlist request this number of times\*, if it continues to error, the player will try switching to another level or fall back to another Pathway to recover playback. + +When no valid recovery options are available, the error will escalate to fatal, and the player will stop loading all media and asset types. + +\*Requests resulting in a stall may trigger a level switch before all retries are performed. + +##### `retryDelayMs: number` + +The time to wait before performing a retry in milliseconds. Delays are added to prevent the player from overloading +servers having trouble responding to requests. + +Retry delay = 2^retryCount _ retryDelayMs (exponential) or retryCount _ retryDelayMs (linear) + +##### `maxRetryDelayMs: number` + +Maximum delay between retries in milliseconds. With each retry, the delay is increased up to `maxRetryDelayMs`. + +##### `backoff?: 'exponential' | 'linear'` + +Used to determine retry backoff duration: Retry delay = 2^retryCount \* retryDelayMs (exponential). ### `startFragPrefetch`