Add an 'image' live provider.
This commit is contained in:
+20
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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``}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user