From 8f86e7fcd7724b6e6bdf0c4816c3685e2411eaa1 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 16 Oct 2021 15:39:32 -0700 Subject: [PATCH] Add live preload option. --- README.md | 1 + src/card.ts | 75 ++++++++++++------ src/components/live.ts | 138 +++++++++++++++++---------------- src/editor.ts | 7 ++ src/localize/languages/en.json | 3 +- src/scss/card.scss | 3 + src/scss/message.scss | 4 + src/types.ts | 1 + 8 files changed, 140 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index cd745d4b..56d1763a 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ lovelace: | `view_timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.| | `frigate_url` | | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. | | `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.| +| `live_preload` | `false` | Whether or not to preload the live view. Preloading causes the live view to render in the background so it's instantly available when requested. This consumes additional network/CPU resources continually.| #### Live Provider diff --git a/src/card.ts b/src/card.ts index 25718ac0..73e4d38d 100644 --- a/src/card.ts +++ b/src/card.ts @@ -33,10 +33,7 @@ import type { import { CARD_VERSION, REPO_URL } from './const'; import { FrigateCardMenu, MENU_HEIGHT } from './components/menu'; import { View } from './view'; -import { - homeAssistantWSRequest, - shouldUpdateBasedOnHass, -} from './common'; +import { homeAssistantWSRequest, shouldUpdateBasedOnHass } from './common'; import { localize } from './localize/localize'; import { renderMessage, renderProgressIndicator } from './components/message'; @@ -108,7 +105,7 @@ export class FrigateCard extends LitElement { _menu!: FrigateCardMenu; @query('frigate-card-elements') - _elements!: FrigateCardElements; + _elements?: FrigateCardElements; // Whether or not media is actively playing (live or clip). protected _mediaPlaying = false; @@ -122,7 +119,11 @@ export class FrigateCard extends LitElement { // The frigate camera name to use (may be manually specified or automatically // derived). - protected _frigateCameraName: string | null = null; + // Values: + // - string: Camera name on the Frigate backend. + // - null: Attempted to find name, but failed. + // - undefined: Have not yet attempted to find name. + protected _frigateCameraName?: string | null; // Error/info message to render. protected _message: Message | null = null; @@ -153,7 +154,7 @@ export class FrigateCard extends LitElement { _hass: HomeAssistant, entities: string[], ): FrigateCardConfig { - const cameraEntity = entities.find(element => element.startsWith('camera.')); + const cameraEntity = entities.find((element) => element.startsWith('camera.')); return { camera_entity: cameraEntity, } as FrigateCardConfig; @@ -297,7 +298,7 @@ export class FrigateCard extends LitElement { getLovelace().setEditMode(true); } - this._frigateCameraName = null; + this._frigateCameraName = undefined; this.config = config; this._entitiesToMonitor = this.config.update_entities || []; @@ -441,11 +442,13 @@ export class FrigateCard extends LitElement { this._mediaPlaying = false; } - protected _setMessageAndUpdate(message: Message): void { + protected _setMessageAndUpdate(message: Message, skipUpdate?: boolean): void { // Register the first message, or prioritize errors if there's pre-render competition. if (!this._message || (message.type == 'error' && this._message.type != 'error')) { this._message = message; - this.requestUpdate(); + if (!skipUpdate) { + this.requestUpdate(); + } } } @@ -581,51 +584,70 @@ export class FrigateCard extends LitElement { ${this.config.menu_mode == 'above' ? this._renderMenu() : ''}
- ${this._message - ? renderMessage(this._message) - : until(this._render(), renderProgressIndicator())} + ${this._frigateCameraName == undefined + ? until( + (async () => { + this._frigateCameraName = await this._getFrigateCameraName(); + return this._render(); + })(), + renderProgressIndicator(), + ) + : this._render()}
${this.config.menu_mode != 'above' ? this._renderMenu() : ''} `; } - protected async _render(): Promise { - if (!this._frigateCameraName) { - this._frigateCameraName = await this._getFrigateCameraName(); - } + protected _render(): TemplateResult | void { const mediaQueryParameters = this._getBrowseMediaQueryParameters(); if (!this._hass || !this._frigateCameraName || !mediaQueryParameters) { - return this._setMessageAndUpdate({ - message: localize('error.no_frigate_camera_name'), - type: 'error', - }); + this._setMessageAndUpdate( + { + message: localize('error.no_frigate_camera_name'), + type: 'error', + }, + true, + ); } const pictureElementsClasses = { 'picture-elements': true, gallery: this._view.isGalleryView(), }; + const galleryClasses = { + hidden: this.config.live_preload && !this._view.isGalleryView(), + }; + const viewerClasses = { + hidden: + this.config.live_preload && !['clip', 'snapshot'].includes(this._view.view), + }; + const liveClasses = { + hidden: this.config.live_preload && this._view.view != 'live', + }; return html`
- ${this._view.is('clips') || this._view.is('snapshots') + ${this._message ? renderMessage(this._message) : ``} + ${!this._message && this._view.isGalleryView() ? html` ` : ``} - ${this._view.is('clip') || this._view.is('snapshot') + ${!this._message && (this._view.is('clip') || this._view.is('snapshot')) ? html` ` : ``} - ${this._view.is('live') + + ${(!this._message && this._view.is('live')) || this.config.live_preload ? html` ` : ``} - ${this.config.elements + ${!this._message && this.config.elements ? html` { return html` ${this.config.live_provider == 'frigate' ? html` ; + protected hass!: HomeAssistant & ExtendedHomeAssistant; protected _webRTCElement: HTMLElement | null = null; protected _createWebRTC(): TemplateResult | void { @@ -166,20 +160,17 @@ export class FrigateCardViewerWebRTC extends LitElement { } @customElement('frigate-card-live-jsmpeg') -export class FrigateCardViewerJSMPEG extends LitElement { - @property({ attribute: false }) - protected hass!: HomeAssistant & ExtendedHomeAssistant; - +export class FrigateCardLiveJSMPEG extends LitElement { @property({ attribute: false }) protected cameraName!: string; @property({ attribute: false }) protected clientId!: string; - protected _jsmpegCanvasElement: HTMLCanvasElement | null = null; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - protected _jsmpegVideoPlayer: any | null = null; + protected hass!: HomeAssistant & ExtendedHomeAssistant; + protected _jsmpegCanvasElement?: HTMLCanvasElement; + protected _jsmpegVideoPlayer?; + protected _jsmpegURL?: string | null; protected async _getURL(): Promise { if (!this.hass) { @@ -202,62 +193,77 @@ export class FrigateCardViewerJSMPEG extends LitElement { return url.replace(/^http/i, 'ws'); } - protected render(): TemplateResult | void { - return html`${until(this._render(), renderProgressIndicator())}`; + protected async _createJSMPEGPlayer(): Promise { + let videoDecoded = false; + return new Promise((resolve) => { + this._jsmpegVideoPlayer = new JSMpeg.VideoElement( + this, + this._jsmpegURL, + { + preserveDrawingBuffer: true, + canvas: this._jsmpegCanvasElement, + hooks: { + // Don't resolve the promise until it's playing to minimize the + // amount of time the canvas is empty (and show the spinner + // instead). + play: () => { + dispatchPlayEvent(this); + resolve(); + }, + pause: () => { + dispatchPauseEvent(this); + }, + }, + }, + { + protocols: [], + videoBufferSize: 1024 * 1024 * 4, + onVideoDecode: () => { + // This is the only callback that is called after the dimensions + // are available. It's called on every frame decode, so just + // ignore any subsequent calls. + if (!videoDecoded && this._jsmpegCanvasElement) { + videoDecoded = true; + dispatchMediaLoadEvent(this, this._jsmpegCanvasElement); + } + }, + }, + ); + }); } - protected async _render(): Promise { + protected render(): TemplateResult | void { if (!this._jsmpegCanvasElement) { this._jsmpegCanvasElement = document.createElement('canvas'); this._jsmpegCanvasElement.className = 'media'; } - if (!this._jsmpegVideoPlayer) { - const jsmpeg_url = await this._getURL(); - - if (!jsmpeg_url) { - return dispatchErrorMessageEvent( - this, - 'Could not retrieve or sign JSMPEG websocket path', - ); - } - - let videoDecoded = false; - return new Promise((resolve) => { - this._jsmpegVideoPlayer = new JSMpeg.VideoElement( - this, - jsmpeg_url, - { - canvas: this._jsmpegCanvasElement, - hooks: { - // Don't resolve the promise until it's playing to minimize the - // amount of time the canvas is empty (and show the spinner - // instead). - play: () => { - dispatchPlayEvent(this); - resolve(html`${this._jsmpegCanvasElement}`); - }, - pause: () => { - dispatchPauseEvent(this); - }, - }, - }, - { - protocols: [], - videoBufferSize: 1024 * 1024 * 4, - onVideoDecode: () => { - // This is the only callback that is called after the dimensions - // are available. It's called on every frame decode, so just - // ignore any subsequent calls. - if (!videoDecoded && this._jsmpegCanvasElement) { - videoDecoded = true; - dispatchMediaLoadEvent(this, this._jsmpegCanvasElement); - } - }, - }, - ); - }); + if (this._jsmpegURL === undefined) { + return html`${until( + (async () => { + this._jsmpegURL = await this._getURL(); + this.requestUpdate(); + })(), + renderProgressIndicator(), + )}`; } + if (!this._jsmpegURL) { + return dispatchErrorMessageEvent( + this, + 'Could not retrieve or sign JSMPEG websocket path', + ); + } + + if (!this._jsmpegVideoPlayer) { + return html`${until( + (async () => { + await this._createJSMPEGPlayer(); + this.requestUpdate(); + })(), + renderProgressIndicator(), + )}`; + } + return html`${this._jsmpegCanvasElement}`; } diff --git a/src/editor.ts b/src/editor.ts index fc2a1378..b2a2b615 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -255,6 +255,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor @change=${this._valueChanged} > + + +
` : ''}
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index ff693f27..a37348ae 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -32,7 +32,8 @@ "show_button": "Show button", "zone": "Zone", "label": "Frigate label/object filter (Optional)", - "live_provider": "Live view provider (Optional)" + "live_provider": "Live view provider (Optional)", + "live_preload": "Preload live view (Optional)" }, "menu": { "frigate": "Frigate Menu / Default View", diff --git a/src/scss/card.scss b/src/scss/card.scss index f1d9006d..2a1d6445 100644 --- a/src/scss/card.scss +++ b/src/scss/card.scss @@ -75,6 +75,9 @@ frigate-card-gallery, frigate-card-viewer, frigate-card-live, frigate-card-messa frigate-card-gallery { height: 100%; } +frigate-card-gallery.hidden,frigate-card-viewer.hidden,frigate-card-live.hidden { + display: none; +} // Browsers will reject invalid whole CSS selectors if one selector is bad, so // need to use mixin here instead of just comma-separated selectors. diff --git a/src/scss/message.scss b/src/scss/message.scss index 6becbe0c..54727410 100644 --- a/src/scss/message.scss +++ b/src/scss/message.scss @@ -1,3 +1,7 @@ +:host { + height: 100%; +} + .message { height: 100%; display: flex; diff --git a/src/types.ts b/src/types.ts index 1ceb8b55..54e76324 100644 --- a/src/types.ts +++ b/src/types.ts @@ -262,6 +262,7 @@ export const frigateCardConfigSchema = z.object({ .optional() .default(180), live_provider: z.enum(LIVE_PROVIDERS).default('frigate'), + live_preload: z.boolean().default(false), webrtc: z .object({ entity: z.string().optional(),