From 2405ae5022e059807e4128bf95879a5340b38422 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Feb 2022 17:26:38 -0800 Subject: [PATCH 1/3] Improve robustness of hls media player Improve robustness of hls media player by retrying errors following the recommended approach in the HLS docs: https://github.com/video-dev/hls.js/blob/master/docs/API.md#fatal-error-recovery This change introduces two types of errors: - Fatal errors that make the video element disappear (the old way all errors are handled) - Retryable errors that show a banner on the top of the screen while a retry is invoked The error state is reset in a couple places based on HLS player making progress (e.g. loading a manifest or loading an actual fragment). The error messages are simplified a bit too while we are here since the existing error codes are jarring, and don't actually provide that much extra detail. --- src/components/ha-hls-player.ts | 85 +++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts index fd16491efa18..5f972604f175 100644 --- a/src/components/ha-hls-player.ts +++ b/src/components/ha-hls-player.ts @@ -43,6 +43,8 @@ class HaHLSPlayer extends LitElement { @state() private _error?: string; + @state() private _errorIsFatal: bool; // Determines how error is rendered + private _hlsPolyfillInstance?: HlsLite; private _exoPlayer = false; @@ -53,6 +55,7 @@ class HaHLSPlayer extends LitElement { super.connectedCallback(); HaHLSPlayer.streamCount += 1; if (this.hasUpdated) { + this._resetError(); this._startHls(); } } @@ -64,16 +67,23 @@ class HaHLSPlayer extends LitElement { } protected render(): TemplateResult { - if (this._error) { - return html`${this._error}`; - } return html` - + ${this._error + ? html` + ${this._error} + ` + : ""} + ${!this._errorIsFatal + ? html`` + : ""} `; } @@ -87,12 +97,11 @@ class HaHLSPlayer extends LitElement { } this._cleanUp(); + this._resetError(); this._startHls(); } private async _startHls(): Promise { - this._error = undefined; - const masterPlaylistPromise = fetch(this.url); const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min")) @@ -110,8 +119,8 @@ class HaHLSPlayer extends LitElement { } if (!hlsSupported) { - this._error = this.hass.localize( - "ui.components.media-browser.video_not_supported" + this._fatalError( + this.hass.localize("ui.components.media-browser.video_not_supported") ); return; } @@ -219,9 +228,16 @@ class HaHLSPlayer extends LitElement { this._hlsPolyfillInstance = hls; hls.attachMedia(videoEl); hls.on(Hls.Events.MEDIA_ATTACHED, () => { + this._resetError(); hls.loadSource(url); }); - hls.on(Hls.Events.ERROR, (_, data: any) => { + hls.on(Hls.Events.FRAG_LOADED, (_event, _data: any) => { + this._resetError(); + }); + hls.on(Hls.Events.ERROR, (_event, data: any) => { + // Some errors are recovered automatically by the hls player itself, and the others handled + // in this function require special actions to recover. Errors retried in this function + // are done with backoff to not cause unecessary failures. if (!data.fatal) { return; } @@ -241,22 +257,22 @@ class HaHLSPlayer extends LitElement { error += " (" + data.response.code + ")"; } } - this._error = error; - return; + this._setRetryableError(error); + break; } case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT: - this._error = "Timeout while starting stream"; - return; + this._setRetryableError("Timeout while starting stream"); + break; default: - this._error = "Unknown stream network error (" + data.details + ")"; - return; + this._setRetryableError("Stream network error"); + break; } - this._error = "Error with media stream contents (" + data.details + ")"; + hls.startLoad(); } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { - this._error = "Error with media stream contents (" + data.details + ")"; + this._setRetryableError("Error with media stream contents"); + hls.recoverMediaError(); } else { - this._error = - "Unknown error with stream (" + data.type + ", " + data.details + ")"; + this._setFatalError("Error playing stream"); } }); } @@ -284,6 +300,21 @@ class HaHLSPlayer extends LitElement { } } + private _resetError() { + this._error = undefined; + this._errorIsFatal = false; + } + + private _setFatalError(errorMessage: string) { + this._error = errorMessage; + this._errorIsFatal = true; + } + + private _setRetryableError(errorMessage: string) { + this._error = errorMessage; + this._errorIsFatal = false; + } + static get styles(): CSSResultGroup { return css` :host, @@ -296,10 +327,14 @@ class HaHLSPlayer extends LitElement { max-height: var(--video-max-height, calc(100vh - 97px)); } - ha-alert { + .fatal { display: block; padding: 100px 16px; } + + .retry { + display: block; + } `; } } From fd4e22c7366467f9a9d33fc9efecd0ed0d0d14f8 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Feb 2022 18:15:31 -0800 Subject: [PATCH 2/3] Address errors found by lint --- src/components/ha-hls-player.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts index 5f972604f175..c03815504351 100644 --- a/src/components/ha-hls-player.ts +++ b/src/components/ha-hls-player.ts @@ -43,7 +43,7 @@ class HaHLSPlayer extends LitElement { @state() private _error?: string; - @state() private _errorIsFatal: bool; // Determines how error is rendered + @state() private _errorIsFatal: boolean; // Determines how error is rendered private _hlsPolyfillInstance?: HlsLite; @@ -119,7 +119,7 @@ class HaHLSPlayer extends LitElement { } if (!hlsSupported) { - this._fatalError( + this._setFatalError( this.hass.localize("ui.components.media-browser.video_not_supported") ); return; From 59e2eba511a27a97ea37fd0039128c10cdd1f03a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Feb 2022 18:21:51 -0800 Subject: [PATCH 3/3] Address error found by tsc --- src/components/ha-hls-player.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts index c03815504351..0be5f97535b1 100644 --- a/src/components/ha-hls-player.ts +++ b/src/components/ha-hls-player.ts @@ -43,7 +43,7 @@ class HaHLSPlayer extends LitElement { @state() private _error?: string; - @state() private _errorIsFatal: boolean; // Determines how error is rendered + @state() private _errorIsFatal = false; private _hlsPolyfillInstance?: HlsLite;