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(),