diff --git a/src/components/image.ts b/src/components/image.ts index a8f5a743..2975f33e 100644 --- a/src/components/image.ts +++ b/src/components/image.ts @@ -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; 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, + }); } }} />` @@ -263,7 +273,7 @@ export class FrigateCardImage extends LitElement { } declare global { - interface HTMLElementTagNameMap { - "frigate-card-image": FrigateCardImage - } + interface HTMLElementTagNameMap { + 'frigate-card-image': FrigateCardImage; + } } diff --git a/src/components/live/live-ha.ts b/src/components/live/live-ha.ts index d09a689a..2a9e8c0c 100644 --- a/src/components/live/live-ha.ts +++ b/src/components/live/live-ha.ts @@ -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` + `; + } + + /** + * Get styles. + */ + static get styles(): CSSResultGroup { + return unsafeCSS(liveFrigateStyle); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'frigate-card-live-image': FrigateCardLiveImage; + } +} diff --git a/src/components/live/live.ts b/src/components/live/live.ts index e1fbc602..01e13e8b 100644 --- a/src/components/live/live.ts +++ b/src/components/live/live.ts @@ -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) { - return 'ha'; + 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` { + 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(); + } + }} > - ` + ` : html``} ${provider === 'ha' ? html` ` - : html` - `} + ` + : html``} `; } diff --git a/src/editor.ts b/src/editor.ts index cb6d7434..cf6a5a39 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -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'), diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 8a71c1d2..7f9c3dd4 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -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)", diff --git a/src/types.ts b/src/types.ts index 8d2b4721..dd5f7d59 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 = [