Add live preload option.

This commit is contained in:
Dermot Duffy
2021-10-16 15:39:32 -07:00
parent 83641ca6a7
commit 8f86e7fcd7
8 changed files with 140 additions and 92 deletions
+1
View File
@@ -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.| | `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. | | `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.| | `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 #### Live Provider
+47 -22
View File
@@ -33,10 +33,7 @@ import type {
import { CARD_VERSION, REPO_URL } from './const'; import { CARD_VERSION, REPO_URL } from './const';
import { FrigateCardMenu, MENU_HEIGHT } from './components/menu'; import { FrigateCardMenu, MENU_HEIGHT } from './components/menu';
import { View } from './view'; import { View } from './view';
import { import { homeAssistantWSRequest, shouldUpdateBasedOnHass } from './common';
homeAssistantWSRequest,
shouldUpdateBasedOnHass,
} from './common';
import { localize } from './localize/localize'; import { localize } from './localize/localize';
import { renderMessage, renderProgressIndicator } from './components/message'; import { renderMessage, renderProgressIndicator } from './components/message';
@@ -108,7 +105,7 @@ export class FrigateCard extends LitElement {
_menu!: FrigateCardMenu; _menu!: FrigateCardMenu;
@query('frigate-card-elements') @query('frigate-card-elements')
_elements!: FrigateCardElements; _elements?: FrigateCardElements;
// Whether or not media is actively playing (live or clip). // Whether or not media is actively playing (live or clip).
protected _mediaPlaying = false; protected _mediaPlaying = false;
@@ -122,7 +119,11 @@ export class FrigateCard extends LitElement {
// The frigate camera name to use (may be manually specified or automatically // The frigate camera name to use (may be manually specified or automatically
// derived). // 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. // Error/info message to render.
protected _message: Message | null = null; protected _message: Message | null = null;
@@ -153,7 +154,7 @@ export class FrigateCard extends LitElement {
_hass: HomeAssistant, _hass: HomeAssistant,
entities: string[], entities: string[],
): FrigateCardConfig { ): FrigateCardConfig {
const cameraEntity = entities.find(element => element.startsWith('camera.')); const cameraEntity = entities.find((element) => element.startsWith('camera.'));
return { return {
camera_entity: cameraEntity, camera_entity: cameraEntity,
} as FrigateCardConfig; } as FrigateCardConfig;
@@ -297,7 +298,7 @@ export class FrigateCard extends LitElement {
getLovelace().setEditMode(true); getLovelace().setEditMode(true);
} }
this._frigateCameraName = null; this._frigateCameraName = undefined;
this.config = config; this.config = config;
this._entitiesToMonitor = this.config.update_entities || []; this._entitiesToMonitor = this.config.update_entities || [];
@@ -441,13 +442,15 @@ export class FrigateCard extends LitElement {
this._mediaPlaying = false; 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. // Register the first message, or prioritize errors if there's pre-render competition.
if (!this._message || (message.type == 'error' && this._message.type != 'error')) { if (!this._message || (message.type == 'error' && this._message.type != 'error')) {
this._message = message; this._message = message;
if (!skipUpdate) {
this.requestUpdate(); this.requestUpdate();
} }
} }
}
protected _messageHandler(e: CustomEvent<Message>): void { protected _messageHandler(e: CustomEvent<Message>): void {
return this._setMessageAndUpdate(e.detail); return this._setMessageAndUpdate(e.detail);
@@ -581,51 +584,70 @@ export class FrigateCard extends LitElement {
${this.config.menu_mode == 'above' ? this._renderMenu() : ''} ${this.config.menu_mode == 'above' ? this._renderMenu() : ''}
<div class="container outer" style="${styleMap(outerStyle)}"> <div class="container outer" style="${styleMap(outerStyle)}">
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}"> <div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
${this._message ${this._frigateCameraName == undefined
? renderMessage(this._message) ? until(
: until(this._render(), renderProgressIndicator())} (async () => {
this._frigateCameraName = await this._getFrigateCameraName();
return this._render();
})(),
renderProgressIndicator(),
)
: this._render()}
</div> </div>
</div> </div>
${this.config.menu_mode != 'above' ? this._renderMenu() : ''} ${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
</ha-card>`; </ha-card>`;
} }
protected async _render(): Promise<TemplateResult | void> { protected _render(): TemplateResult | void {
if (!this._frigateCameraName) {
this._frigateCameraName = await this._getFrigateCameraName();
}
const mediaQueryParameters = this._getBrowseMediaQueryParameters(); const mediaQueryParameters = this._getBrowseMediaQueryParameters();
if (!this._hass || !this._frigateCameraName || !mediaQueryParameters) { if (!this._hass || !this._frigateCameraName || !mediaQueryParameters) {
return this._setMessageAndUpdate({ this._setMessageAndUpdate(
{
message: localize('error.no_frigate_camera_name'), message: localize('error.no_frigate_camera_name'),
type: 'error', type: 'error',
}); },
true,
);
} }
const pictureElementsClasses = { const pictureElementsClasses = {
'picture-elements': true, 'picture-elements': true,
gallery: this._view.isGalleryView(), 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` return html`
<div class="${classMap(pictureElementsClasses)}"> <div class="${classMap(pictureElementsClasses)}">
${this._view.is('clips') || this._view.is('snapshots') ${this._message ? renderMessage(this._message) : ``}
${!this._message && this._view.isGalleryView()
? html` <frigate-card-gallery ? html` <frigate-card-gallery
.hass=${this._hass} .hass=${this._hass}
.view=${this._view} .view=${this._view}
.browseMediaQueryParameters=${mediaQueryParameters} .browseMediaQueryParameters=${mediaQueryParameters}
class="${classMap(galleryClasses)}"
@frigate-card:change-view=${this._changeViewHandler} @frigate-card:change-view=${this._changeViewHandler}
@frigate-card:message=${this._messageHandler} @frigate-card:message=${this._messageHandler}
> >
</frigate-card-gallery>` </frigate-card-gallery>`
: ``} : ``}
${this._view.is('clip') || this._view.is('snapshot') ${!this._message && (this._view.is('clip') || this._view.is('snapshot'))
? html` <frigate-card-viewer ? html` <frigate-card-viewer
.hass=${this._hass} .hass=${this._hass}
.view=${this._view} .view=${this._view}
.browseMediaQueryParameters=${mediaQueryParameters} .browseMediaQueryParameters=${mediaQueryParameters}
.nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'} .nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
.autoplayClip=${this.config.autoplay_clip} .autoplayClip=${this.config.autoplay_clip}
class="${classMap(viewerClasses)}"
@frigate-card:change-view=${this._changeViewHandler} @frigate-card:change-view=${this._changeViewHandler}
@frigate-card:media-load=${this._mediaLoadHandler} @frigate-card:media-load=${this._mediaLoadHandler}
@frigate-card:pause=${this._pauseHandler} @frigate-card:pause=${this._pauseHandler}
@@ -634,12 +656,15 @@ export class FrigateCard extends LitElement {
> >
</frigate-card-viewer>` </frigate-card-viewer>`
: ``} : ``}
${this._view.is('live') <!-- Note the subtle difference in condition below vs the other views in order
to always render the live view for live_preload mode -->
${(!this._message && this._view.is('live')) || this.config.live_preload
? html` ? html`
<frigate-card-live <frigate-card-live
.hass=${this._hass} .hass=${this._hass}
.config=${this.config} .config=${this.config}
.frigateCameraName=${this._frigateCameraName} .frigateCameraName=${this._frigateCameraName}
class="${classMap(liveClasses)}"
@frigate-card:media-load=${this._mediaLoadHandler} @frigate-card:media-load=${this._mediaLoadHandler}
@frigate-card:pause=${this._pauseHandler} @frigate-card:pause=${this._pauseHandler}
@frigate-card:play=${this._playHandler} @frigate-card:play=${this._playHandler}
@@ -648,7 +673,7 @@ export class FrigateCard extends LitElement {
</frigate-card-live> </frigate-card-live>
` `
: ``} : ``}
${this.config.elements ${!this._message && this.config.elements
? html` ? html`
<frigate-card-elements <frigate-card-elements
.hass=${this._hass} .hass=${this._hass}
+47 -41
View File
@@ -22,7 +22,7 @@ import JSMpeg from '@cycjimmy/jsmpeg-player';
import liveStyle from '../scss/live.scss'; import liveStyle from '../scss/live.scss';
@customElement('frigate-card-live') @customElement('frigate-card-live')
export class FrigateCardViewer extends LitElement { export class FrigateCardLive extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected hass!: HomeAssistant & ExtendedHomeAssistant; protected hass!: HomeAssistant & ExtendedHomeAssistant;
@@ -33,10 +33,6 @@ export class FrigateCardViewer extends LitElement {
protected frigateCameraName!: string; protected frigateCameraName!: string;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html`${until(this._render(), renderProgressIndicator())}`;
}
protected async _render(): Promise<TemplateResult> {
return html` ${this.config.live_provider == 'frigate' return html` ${this.config.live_provider == 'frigate'
? html` <frigate-card-live-frigate ? html` <frigate-card-live-frigate
.hass=${this.hass} .hass=${this.hass}
@@ -63,7 +59,7 @@ export class FrigateCardViewer extends LitElement {
} }
@customElement('frigate-card-live-frigate') @customElement('frigate-card-live-frigate')
export class FrigateCardViewerFrigate extends LitElement { export class FrigateCardLiveFrigate extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected hass!: HomeAssistant & ExtendedHomeAssistant; protected hass!: HomeAssistant & ExtendedHomeAssistant;
@@ -95,13 +91,11 @@ export class FrigateCardViewerFrigate extends LitElement {
// Create a wrapper for the WebRTC element // Create a wrapper for the WebRTC element
// - https://github.com/AlexxIT/WebRTC // - https://github.com/AlexxIT/WebRTC
@customElement('frigate-card-live-webrtc') @customElement('frigate-card-live-webrtc')
export class FrigateCardViewerWebRTC extends LitElement { export class FrigateCardLiveWebRTC extends LitElement {
@property({ attribute: false })
protected hass!: HomeAssistant & ExtendedHomeAssistant;
@property({ attribute: false }) @property({ attribute: false })
protected webRTCConfig!: Record<string, unknown>; protected webRTCConfig!: Record<string, unknown>;
protected hass!: HomeAssistant & ExtendedHomeAssistant;
protected _webRTCElement: HTMLElement | null = null; protected _webRTCElement: HTMLElement | null = null;
protected _createWebRTC(): TemplateResult | void { protected _createWebRTC(): TemplateResult | void {
@@ -166,20 +160,17 @@ export class FrigateCardViewerWebRTC extends LitElement {
} }
@customElement('frigate-card-live-jsmpeg') @customElement('frigate-card-live-jsmpeg')
export class FrigateCardViewerJSMPEG extends LitElement { export class FrigateCardLiveJSMPEG extends LitElement {
@property({ attribute: false })
protected hass!: HomeAssistant & ExtendedHomeAssistant;
@property({ attribute: false }) @property({ attribute: false })
protected cameraName!: string; protected cameraName!: string;
@property({ attribute: false }) @property({ attribute: false })
protected clientId!: string; protected clientId!: string;
protected _jsmpegCanvasElement: HTMLCanvasElement | null = null; protected hass!: HomeAssistant & ExtendedHomeAssistant;
protected _jsmpegCanvasElement?: HTMLCanvasElement;
// eslint-disable-next-line @typescript-eslint/no-explicit-any protected _jsmpegVideoPlayer?;
protected _jsmpegVideoPlayer: any | null = null; protected _jsmpegURL?: string | null;
protected async _getURL(): Promise<string | null> { protected async _getURL(): Promise<string | null> {
if (!this.hass) { if (!this.hass) {
@@ -202,32 +193,14 @@ export class FrigateCardViewerJSMPEG extends LitElement {
return url.replace(/^http/i, 'ws'); return url.replace(/^http/i, 'ws');
} }
protected render(): TemplateResult | void { protected async _createJSMPEGPlayer(): Promise<void> {
return html`${until(this._render(), renderProgressIndicator())}`;
}
protected async _render(): Promise<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; let videoDecoded = false;
return new Promise<TemplateResult>((resolve) => { return new Promise<void>((resolve) => {
this._jsmpegVideoPlayer = new JSMpeg.VideoElement( this._jsmpegVideoPlayer = new JSMpeg.VideoElement(
this, this,
jsmpeg_url, this._jsmpegURL,
{ {
preserveDrawingBuffer: true,
canvas: this._jsmpegCanvasElement, canvas: this._jsmpegCanvasElement,
hooks: { hooks: {
// Don't resolve the promise until it's playing to minimize the // Don't resolve the promise until it's playing to minimize the
@@ -235,7 +208,7 @@ export class FrigateCardViewerJSMPEG extends LitElement {
// instead). // instead).
play: () => { play: () => {
dispatchPlayEvent(this); dispatchPlayEvent(this);
resolve(html`${this._jsmpegCanvasElement}`); resolve();
}, },
pause: () => { pause: () => {
dispatchPauseEvent(this); dispatchPauseEvent(this);
@@ -258,6 +231,39 @@ export class FrigateCardViewerJSMPEG extends LitElement {
); );
}); });
} }
protected render(): TemplateResult | void {
if (!this._jsmpegCanvasElement) {
this._jsmpegCanvasElement = document.createElement('canvas');
this._jsmpegCanvasElement.className = 'media';
}
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}`; return html`${this._jsmpegCanvasElement}`;
} }
+7
View File
@@ -255,6 +255,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield .label=${localize('editor.live_preload')}>
<ha-switch
.checked=${this._config?.live_preload === true}
.configValue=${'live_preload'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
</div>` </div>`
: ''} : ''}
<div class="option" @click=${this._toggleOption} .option=${'appearance'}> <div class="option" @click=${this._toggleOption} .option=${'appearance'}>
+2 -1
View File
@@ -32,7 +32,8 @@
"show_button": "Show button", "show_button": "Show button",
"zone": "Zone", "zone": "Zone",
"label": "Frigate label/object filter (Optional)", "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": { "menu": {
"frigate": "Frigate Menu / Default View", "frigate": "Frigate Menu / Default View",
+3
View File
@@ -75,6 +75,9 @@ frigate-card-gallery, frigate-card-viewer, frigate-card-live, frigate-card-messa
frigate-card-gallery { frigate-card-gallery {
height: 100%; 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 // Browsers will reject invalid whole CSS selectors if one selector is bad, so
// need to use mixin here instead of just comma-separated selectors. // need to use mixin here instead of just comma-separated selectors.
+4
View File
@@ -1,3 +1,7 @@
:host {
height: 100%;
}
.message { .message {
height: 100%; height: 100%;
display: flex; display: flex;
+1
View File
@@ -262,6 +262,7 @@ export const frigateCardConfigSchema = z.object({
.optional() .optional()
.default(180), .default(180),
live_provider: z.enum(LIVE_PROVIDERS).default('frigate'), live_provider: z.enum(LIVE_PROVIDERS).default('frigate'),
live_preload: z.boolean().default(false),
webrtc: z webrtc: z
.object({ .object({
entity: z.string().optional(), entity: z.string().optional(),