Add an 'image' live provider.

This commit is contained in:
Dermot Duffy
2022-10-09 16:11:38 -07:00
parent 9af8eedb8a
commit 0366ad9736
7 changed files with 195 additions and 59 deletions
+20 -10
View File
@@ -6,7 +6,7 @@ import {
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS
unsafeCSS,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { live } from 'lit/directives/live.js';
@@ -15,13 +15,17 @@ import { CachedValueController } from '../cached-value-controller.js';
import defaultImage from '../images/frigate-bird-in-sky.jpg';
import { localize } from '../localize/localize.js';
import imageStyle from '../scss/image.scss';
import { CameraConfig, ImageViewConfig } from '../types.js';
import { CameraConfig, ImageViewConfig, MediaLoadedInfo } from '../types.js';
import { isHassDifferent } from '../utils/ha';
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
import {
createMediaLoadedInfo,
dispatchExistingMediaLoadedInfoAsEvent,
} from '../utils/media-info.js';
import { View } from '../view.js';
import { dispatchErrorMessageEvent } from './message.js';
import { contentsChanged } from '../utils/basic.js';
import isEqual from 'lodash-es/isEqual';
// See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py .
const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000;
@@ -48,6 +52,8 @@ export class FrigateCardImage extends LitElement {
protected _cachedValueController?: CachedValueController<string>;
protected _boundVisibilityHandler = this._visibilityHandler.bind(this);
protected _mediaLoadedInfo: MediaLoadedInfo | null = null;
/**
* Get the camera entity for the current camera configuration.
* @returns The entity or undefined if no camera entity is available.
@@ -234,7 +240,13 @@ export class FrigateCardImage extends LitElement {
${ref(this._refImage)}
src=${live(src)}
@load=${(ev: Event) => {
dispatchMediaLoadedEvent(this, ev);
const mediaLoadedInfo = createMediaLoadedInfo(ev);
// Avoid the media being reported as repeatedly loading unless the
// media info changes.
if (mediaLoadedInfo && !isEqual(this._mediaLoadedInfo, mediaLoadedInfo)) {
this._mediaLoadedInfo = mediaLoadedInfo;
dispatchExistingMediaLoadedInfoAsEvent(this, mediaLoadedInfo);
}
}}
@error=${() => {
if (this.imageConfig?.mode === 'camera') {
@@ -246,11 +258,9 @@ export class FrigateCardImage extends LitElement {
} else if (this.imageConfig?.mode === 'url') {
// In url mode, the user likely specified a URL that cannot be
// resolved. Show an error message.
dispatchErrorMessageEvent(
this,
localize('error.image_load_error'),
{ context: this.imageConfig },
);
dispatchErrorMessageEvent(this, localize('error.image_load_error'), {
context: this.imageConfig,
});
}
}}
/>`
@@ -264,6 +274,6 @@ export class FrigateCardImage extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"frigate-card-image": FrigateCardImage
'frigate-card-image': FrigateCardImage;
}
}
+3 -26
View File
@@ -2,11 +2,9 @@ import { HomeAssistant } from 'custom-card-helpers';
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { localize } from '../../localize/localize.js';
import liveFrigateStyle from '../../scss/live-frigate.scss';
import { CameraConfig, FrigateCardMediaPlayer } from '../../types.js';
import { getCameraTitle } from '../../utils/camera.js';
import { dispatchErrorMessageEvent, dispatchMessageEvent } from '../message.js';
import { getStateObjOrDispatchError } from './live.js';
import '../../patches/ha-camera-stream';
import '../../patches/ha-hls-player.js';
import '../../patches/ha-web-rtc-player.ts';
@@ -66,30 +64,9 @@ export class FrigateCardLiveHA extends LitElement {
return;
}
if (!this.cameraConfig?.camera_entity) {
return dispatchErrorMessageEvent(this, localize('error.no_live_camera'), {
context: this.cameraConfig,
});
}
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
const stateObj = getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
if (!stateObj) {
return dispatchErrorMessageEvent(this, localize('error.live_camera_not_found'), {
context: this.cameraConfig,
});
}
if (stateObj.state === 'unavailable') {
// Don't treat state unavailability as an error per se.
return dispatchMessageEvent(
this,
localize('error.live_camera_unavailable'),
'info',
{
icon: 'mdi:connection',
context: getCameraTitle(this.hass, this.cameraConfig),
},
);
return;
}
return html` <frigate-card-ha-camera-stream
+90
View File
@@ -0,0 +1,90 @@
import { HomeAssistant } from 'custom-card-helpers';
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import liveFrigateStyle from '../../scss/live-frigate.scss';
import { CameraConfig } from '../../types.js';
import { getStateObjOrDispatchError } from './live.js';
import '../image.js';
@customElement('frigate-card-live-image')
export class FrigateCardLiveImage extends LitElement {
@property({ attribute: false })
public hass?: HomeAssistant;
@property({ attribute: false })
public cameraConfig?: CameraConfig;
@state()
protected _playing = true;
/**
* Play the video.
*/
public play(): void {
this._playing = true;
}
/**
* Pause the video.
*/
public pause(): void {
this._playing = false;
}
/**
* Mute the video.
*/
public mute(): void {
// Not implemented.
}
/**
* Unmute the video.
*/
public unmute(): void {
// Not implemented.
}
/**
* Seek the video.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public seek(_seconds: number): void {
// Not implemented.
}
/**
* Master render method.
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
if (!this.hass) {
return;
}
getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
return html` <frigate-card-image
.imageConfig=${{
mode: 'camera' as const,
refresh_seconds: this._playing ? 1 : 0,
}}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
>
</frigate-card-image>`;
}
/**
* Get styles.
*/
static get styles(): CSSResultGroup {
return unsafeCSS(liveFrigateStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-live-image': FrigateCardLiveImage;
}
}
+72 -18
View File
@@ -53,6 +53,47 @@ import { EmblaCarouselPlugins } from '../carousel.js';
import { classMap } from 'lit/directives/class-map.js';
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
import { DataManager } from '../../utils/data-manager.js';
import { HomeAssistant } from 'custom-card-helpers';
import { dispatchMessageEvent, dispatchErrorMessageEvent } from '../message.js';
import { HassEntity } from 'home-assistant-js-websocket';
/**
* Get the state object or dispatch an error. Used in `ha` and `image` live
* providers.
* @param element HTMLElement to dispatch errors from.
* @param hass Home Assistant object.
* @param cameraConfig Camera configuration.
* @returns
*/
export const getStateObjOrDispatchError = (
element: HTMLElement,
hass: HomeAssistant,
cameraConfig?: CameraConfig,
): HassEntity | null => {
if (!cameraConfig?.camera_entity) {
dispatchErrorMessageEvent(element, localize('error.no_live_camera'), {
context: cameraConfig,
});
return null;
}
const stateObj = hass.states[cameraConfig.camera_entity];
if (!stateObj) {
dispatchErrorMessageEvent(element, localize('error.live_camera_not_found'), {
context: cameraConfig,
});
return null;
}
if (stateObj.state === 'unavailable') {
dispatchMessageEvent(element, localize('error.live_camera_unavailable'), 'info', {
icon: 'mdi:connection',
context: getCameraTitle(hass, cameraConfig),
});
return null;
}
return stateObj;
};
@customElement('frigate-card-live')
export class FrigateCardLive extends LitElement {
@@ -708,15 +749,17 @@ export class FrigateCardLiveProvider extends LitElement {
) {
return 'webrtc-card';
} else if (this.cameraConfig?.camera_entity) {
if (this.cardWideConfig?.performance?.profile === 'low') {
return 'image';
} else {
return 'ha';
}
} else if (this.cameraConfig?.frigate.camera_name) {
return 'frigate-jsmpeg';
}
return frigateCardConfigDefaults.cameras.live_provider;
}
return (
this.cameraConfig?.live_provider || frigateCardConfigDefaults.cameras.live_provider
);
return this.cameraConfig?.live_provider || 'image';
}
/**
@@ -759,16 +802,19 @@ export class FrigateCardLiveProvider extends LitElement {
if (changedProps.has('liveConfig')) {
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
if (this.liveConfig?.show_image_during_load) {
import('../image.js');
import('./live-image.js');
}
}
if (changedProps.has('cameraConfig')) {
if (this._getResolvedProvider() === 'frigate-jsmpeg') {
const provider = this._getResolvedProvider();
if (provider === 'frigate-jsmpeg') {
import('./live-jsmpeg.js');
} else if (this._getResolvedProvider() === 'ha') {
} else if (provider === 'ha') {
import('./live-ha.js');
} else if (this._getResolvedProvider() === 'webrtc-card') {
} else if (provider === 'webrtc-card') {
import('./live-webrtc.js');
} else if (provider === 'image') {
import('./live-image.js');
}
}
}
@@ -787,22 +833,28 @@ export class FrigateCardLiveProvider extends LitElement {
this.ariaLabel = this.label;
const provider = this._getResolvedProvider();
const showImage = !this._isVideoMediaLoaded && this._shouldShowImageDuringLoading();
const showImageDuringLoading =
!this._isVideoMediaLoaded && this._shouldShowImageDuringLoading();
const providerClasses = {
hidden: showImage,
hidden: showImageDuringLoading,
};
return html`
${showImage
? html`<frigate-card-image
.imageConfig=${{
mode: 'camera' as const,
refresh_seconds: 1,
}}
${showImageDuringLoading || provider === 'image'
? html`<frigate-card-live-image
${ref(this._providerRef)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
@frigate-card:media:loaded=${() => {
if (provider === 'image') {
// Only count the media has loaded if the required provider is
// the image (not just the temporary image shown during
// loading).
this._videoMediaShowHandler();
}
}}
>
</frigate-card-image>`
</frigate-card-live-image>`
: html``}
${provider === 'ha'
? html` <frigate-card-live-ha
@@ -824,7 +876,8 @@ export class FrigateCardLiveProvider extends LitElement {
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
>
</frigate-card-live-webrtc-card>`
: html` <frigate-card-live-jsmpeg
: provider === 'frigate-jsmpeg'
? html` <frigate-card-live-jsmpeg
${ref(this._providerRef)}
class=${classMap(providerClasses)}
.hass=${this.hass}
@@ -833,7 +886,8 @@ export class FrigateCardLiveProvider extends LitElement {
.cardWideConfig=${this.cardWideConfig}
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
>
</frigate-card-live-jsmpeg>`}
</frigate-card-live-jsmpeg>`
: html``}
`;
}
+4
View File
@@ -1107,6 +1107,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: '', label: '' },
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
{
value: 'image',
label: localize('config.cameras.live_providers.image'),
},
{
value: 'frigate-jsmpeg',
label: localize('config.cameras.live_providers.frigate-jsmpeg'),
+2 -1
View File
@@ -33,7 +33,8 @@
"live_providers": {
"auto": "Automatic",
"frigate-jsmpeg": "Frigate JSMpeg",
"ha": "Home Assistant (i.e. HLS, LL-HLS, WebRTC native)",
"ha": "Home Assistant video stream (i.e. HLS, LL-HLS, WebRTC native)",
"image": "Home Assistant images",
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
},
"title": "Title for this camera (Autodetected from entity)",
+1 -1
View File
@@ -59,7 +59,7 @@ const FRIGATE_MENU_ALIGNMENTS = FRIGATE_MENU_POSITIONS;
export const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
export const FRIGATE_MENU_PRIORITY_MAX = 100;
const LIVE_PROVIDERS = ['auto', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
const LIVE_PROVIDERS = ['auto', 'image', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
export type LiveProvider = typeof LIVE_PROVIDERS[number];
const MEDIA_ACTION_NEGATIVE_CONDITIONS = [