Add live preload option.
This commit is contained in:
+50
-25
@@ -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() : ''}
|
||||
<div class="container outer" style="${styleMap(outerStyle)}">
|
||||
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
|
||||
${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()}
|
||||
</div>
|
||||
</div>
|
||||
${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
protected async _render(): Promise<TemplateResult | void> {
|
||||
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`
|
||||
<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
|
||||
.hass=${this._hass}
|
||||
.view=${this._view}
|
||||
.browseMediaQueryParameters=${mediaQueryParameters}
|
||||
class="${classMap(galleryClasses)}"
|
||||
@frigate-card:change-view=${this._changeViewHandler}
|
||||
@frigate-card:message=${this._messageHandler}
|
||||
>
|
||||
</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
|
||||
.hass=${this._hass}
|
||||
.view=${this._view}
|
||||
.browseMediaQueryParameters=${mediaQueryParameters}
|
||||
.nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
|
||||
.autoplayClip=${this.config.autoplay_clip}
|
||||
class="${classMap(viewerClasses)}"
|
||||
@frigate-card:change-view=${this._changeViewHandler}
|
||||
@frigate-card:media-load=${this._mediaLoadHandler}
|
||||
@frigate-card:pause=${this._pauseHandler}
|
||||
@@ -634,12 +656,15 @@ export class FrigateCard extends LitElement {
|
||||
>
|
||||
</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`
|
||||
<frigate-card-live
|
||||
.hass=${this._hass}
|
||||
.config=${this.config}
|
||||
.frigateCameraName=${this._frigateCameraName}
|
||||
class="${classMap(liveClasses)}"
|
||||
@frigate-card:media-load=${this._mediaLoadHandler}
|
||||
@frigate-card:pause=${this._pauseHandler}
|
||||
@frigate-card:play=${this._playHandler}
|
||||
@@ -648,7 +673,7 @@ export class FrigateCard extends LitElement {
|
||||
</frigate-card-live>
|
||||
`
|
||||
: ``}
|
||||
${this.config.elements
|
||||
${!this._message && this.config.elements
|
||||
? html`
|
||||
<frigate-card-elements
|
||||
.hass=${this._hass}
|
||||
|
||||
+72
-66
@@ -22,7 +22,7 @@ import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||
import liveStyle from '../scss/live.scss';
|
||||
|
||||
@customElement('frigate-card-live')
|
||||
export class FrigateCardViewer extends LitElement {
|
||||
export class FrigateCardLive extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected hass!: HomeAssistant & ExtendedHomeAssistant;
|
||||
|
||||
@@ -33,10 +33,6 @@ export class FrigateCardViewer extends LitElement {
|
||||
protected frigateCameraName!: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`${until(this._render(), renderProgressIndicator())}`;
|
||||
}
|
||||
|
||||
protected async _render(): Promise<TemplateResult> {
|
||||
return html` ${this.config.live_provider == 'frigate'
|
||||
? html` <frigate-card-live-frigate
|
||||
.hass=${this.hass}
|
||||
@@ -63,7 +59,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
}
|
||||
|
||||
@customElement('frigate-card-live-frigate')
|
||||
export class FrigateCardViewerFrigate extends LitElement {
|
||||
export class FrigateCardLiveFrigate extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected hass!: HomeAssistant & ExtendedHomeAssistant;
|
||||
|
||||
@@ -95,13 +91,11 @@ export class FrigateCardViewerFrigate extends LitElement {
|
||||
// Create a wrapper for the WebRTC element
|
||||
// - https://github.com/AlexxIT/WebRTC
|
||||
@customElement('frigate-card-live-webrtc')
|
||||
export class FrigateCardViewerWebRTC extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected hass!: HomeAssistant & ExtendedHomeAssistant;
|
||||
|
||||
export class FrigateCardLiveWebRTC extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected webRTCConfig!: Record<string, unknown>;
|
||||
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
let videoDecoded = false;
|
||||
return new Promise<void>((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<TemplateResult | void> {
|
||||
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<TemplateResult>((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}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
@change=${this._valueChanged}
|
||||
></ha-switch>
|
||||
</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 class="option" @click=${this._toggleOption} .option=${'appearance'}>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.message {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user