fix: Substantially reduce cases where errors block whole card (#1764)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { LitElement, ReactiveController } from 'lit';
|
import { LitElement, ReactiveController } from 'lit';
|
||||||
import { FrigateCardMessageEventTarget } from '../../components/message.js';
|
import { FrigateCardMessageEventTarget } from '../../components/message.js';
|
||||||
import { MediaLoadedInfo, Message } from '../../types.js';
|
import { MediaLoadedInfo } from '../../types.js';
|
||||||
import {
|
import {
|
||||||
FrigateCardMediaLoadedEventTarget,
|
FrigateCardMediaLoadedEventTarget,
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
@@ -38,16 +38,11 @@ export class LiveController implements ReactiveController {
|
|||||||
// foreground and background (in preload mode).
|
// foreground and background (in preload mode).
|
||||||
protected _intersectionObserver: IntersectionObserver;
|
protected _intersectionObserver: IntersectionObserver;
|
||||||
|
|
||||||
// Whether or not to allow updates.
|
|
||||||
protected _messageReceived = false;
|
|
||||||
|
|
||||||
// MediaLoadedInfo object and target from the underlying live media. In the
|
// MediaLoadedInfo object and target from the underlying live media. In the
|
||||||
// case of pre-loading these may be propagated later (from the original
|
// case of pre-loading these may be propagated later (from the original
|
||||||
// source).
|
// source).
|
||||||
protected _lastMediaLoadedInfo: LastMediaLoadedInfo | null = null;
|
protected _lastMediaLoadedInfo: LastMediaLoadedInfo | null = null;
|
||||||
|
|
||||||
protected _renderEpoch = 0;
|
|
||||||
|
|
||||||
constructor(host: LiveControllerHost) {
|
constructor(host: LiveControllerHost) {
|
||||||
this._host = host;
|
this._host = host;
|
||||||
|
|
||||||
@@ -58,50 +53,22 @@ export class LiveController implements ReactiveController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldUpdate(): boolean {
|
|
||||||
// Don't process updates if it's in the background and a message was
|
|
||||||
// received (otherwise an error message thrown by the background live
|
|
||||||
// component may continually be re-spammed hitting performance).
|
|
||||||
return !(this._inBackground && this._messageReceived);
|
|
||||||
}
|
|
||||||
|
|
||||||
public hostConnected(): void {
|
public hostConnected(): void {
|
||||||
this._intersectionObserver.observe(this._host);
|
this._intersectionObserver.observe(this._host);
|
||||||
|
|
||||||
this._host.addEventListener('frigate-card:media:loaded', this._handleMediaLoaded);
|
this._host.addEventListener('frigate-card:media:loaded', this._handleMediaLoaded);
|
||||||
this._host.addEventListener('frigate-card:message', this._handleMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public hostDisconnected(): void {
|
public hostDisconnected(): void {
|
||||||
this._intersectionObserver.disconnect();
|
this._intersectionObserver.disconnect();
|
||||||
|
|
||||||
this._host.removeEventListener('frigate-card:media:loaded', this._handleMediaLoaded);
|
this._host.removeEventListener('frigate-card:media:loaded', this._handleMediaLoaded);
|
||||||
this._host.removeEventListener('frigate-card:message', this._handleMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearMessageReceived(): void {
|
|
||||||
this._messageReceived = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public isInBackground(): boolean {
|
public isInBackground(): boolean {
|
||||||
return this._inBackground;
|
return this._inBackground;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRenderEpoch(): number {
|
|
||||||
return this._renderEpoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _handleMessage = (ev: CustomEvent<Message>): void => {
|
|
||||||
this._messageReceived = true;
|
|
||||||
|
|
||||||
if (this._inBackground) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
// Force the whole DOM to re-render next time.
|
|
||||||
this._renderEpoch++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected _handleMediaLoaded = (ev: CustomEvent<MediaLoadedInfo>): void => {
|
protected _handleMediaLoaded = (ev: CustomEvent<MediaLoadedInfo>): void => {
|
||||||
this._lastMediaLoadedInfo = {
|
this._lastMediaLoadedInfo = {
|
||||||
source: ev.composedPath()[0],
|
source: ev.composedPath()[0],
|
||||||
@@ -117,7 +84,7 @@ export class LiveController implements ReactiveController {
|
|||||||
const wasInBackground = this._inBackground;
|
const wasInBackground = this._inBackground;
|
||||||
this._inBackground = !entries.some((entry) => entry.isIntersecting);
|
this._inBackground = !entries.some((entry) => entry.isIntersecting);
|
||||||
|
|
||||||
if (!this._inBackground && !this._messageReceived && this._lastMediaLoadedInfo) {
|
if (!this._inBackground && this._lastMediaLoadedInfo) {
|
||||||
// If this isn't being rendered in the background, the last render did not
|
// If this isn't being rendered in the background, the last render did not
|
||||||
// generate a message and there's a saved MediaInfo, dispatch it upwards.
|
// generate a message and there's a saved MediaInfo, dispatch it upwards.
|
||||||
dispatchExistingMediaLoadedInfoAsEvent(
|
dispatchExistingMediaLoadedInfoAsEvent(
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { dispatchFrigateCardEvent } from '../../../utils/basic';
|
||||||
|
|
||||||
|
export function dispatchLiveErrorEvent(element: EventTarget): void {
|
||||||
|
dispatchFrigateCardEvent(element, 'live:error');
|
||||||
|
}
|
||||||
+19
-5
@@ -8,7 +8,7 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { live } from 'lit/directives/live.js';
|
import { live } from 'lit/directives/live.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
@@ -18,7 +18,7 @@ import { CameraConfig, ImageMode, ImageViewConfig } from '../config/types.js';
|
|||||||
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import imageStyle from '../scss/image.scss';
|
import imageStyle from '../scss/image.scss';
|
||||||
import { FrigateCardMediaPlayer, MediaLoadedInfo } from '../types.js';
|
import { FrigateCardMediaPlayer, MediaLoadedInfo, Message } from '../types.js';
|
||||||
import { contentsChanged } from '../utils/basic.js';
|
import { contentsChanged } from '../utils/basic.js';
|
||||||
import { isHassDifferent } from '../utils/ha';
|
import { isHassDifferent } from '../utils/ha';
|
||||||
import {
|
import {
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
dispatchMediaPlayEvent,
|
dispatchMediaPlayEvent,
|
||||||
} from '../utils/media-info.js';
|
} from '../utils/media-info.js';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
import { dispatchErrorMessageEvent } from './message.js';
|
import { renderMessage } from './message.js';
|
||||||
|
|
||||||
// See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py .
|
// 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;
|
const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000;
|
||||||
@@ -53,6 +53,9 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
|||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
public imageConfig?: ImageViewConfig;
|
public imageConfig?: ImageViewConfig;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _message: Message | null = null;
|
||||||
|
|
||||||
protected _refImage: Ref<HTMLImageElement> = createRef();
|
protected _refImage: Ref<HTMLImageElement> = createRef();
|
||||||
|
|
||||||
protected _cachedValueController?: CachedValueController<string>;
|
protected _cachedValueController?: CachedValueController<string>;
|
||||||
@@ -175,6 +178,10 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
|||||||
if (!this._cachedValueController?.value) {
|
if (!this._cachedValueController?.value) {
|
||||||
this._cachedValueController?.updateValue();
|
this._cachedValueController?.updateValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['imageConfig', 'view'].some((prop) => changedProps.has(prop))) {
|
||||||
|
this._message = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -211,6 +218,7 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
|||||||
*/
|
*/
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
this._cachedValueController?.stopTimer();
|
this._cachedValueController?.stopTimer();
|
||||||
|
this._message = null;
|
||||||
document.removeEventListener('visibilitychange', this._boundVisibilityHandler);
|
document.removeEventListener('visibilitychange', this._boundVisibilityHandler);
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
@@ -329,6 +337,10 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
if (this._message) {
|
||||||
|
return renderMessage(this._message);
|
||||||
|
}
|
||||||
|
|
||||||
const src = this._cachedValueController?.value;
|
const src = this._cachedValueController?.value;
|
||||||
// Note the use of live() below to ensure the update will restore the image
|
// Note the use of live() below to ensure the update will restore the image
|
||||||
// src if it's been changed via _forceSafeImage().
|
// src if it's been changed via _forceSafeImage().
|
||||||
@@ -363,9 +375,11 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
|||||||
} else if (mode === 'url') {
|
} else if (mode === 'url') {
|
||||||
// In url mode, the user likely specified a URL that cannot be
|
// In url mode, the user likely specified a URL that cannot be
|
||||||
// resolved. Show an error message.
|
// resolved. Show an error message.
|
||||||
dispatchErrorMessageEvent(this, localize('error.image_load_error'), {
|
this._message = {
|
||||||
|
type: 'error',
|
||||||
|
message: localize('error.image_load_error'),
|
||||||
context: this.imageConfig,
|
context: this.imageConfig,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import {
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
unsafeCSS,
|
|
||||||
} from 'lit';
|
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { keyed } from 'lit/directives/keyed.js';
|
|
||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { ConditionsManagerEpoch } from '../../card-controller/conditions-manager.js';
|
import { ConditionsManagerEpoch } from '../../card-controller/conditions-manager.js';
|
||||||
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
|
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
|
||||||
@@ -53,15 +45,6 @@ export class FrigateCardLive extends LitElement {
|
|||||||
|
|
||||||
protected _controller = new LiveController(this);
|
protected _controller = new LiveController(this);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
protected shouldUpdate(_changedProps: PropertyValues): boolean {
|
|
||||||
return this._controller.shouldUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected willUpdate(): void {
|
|
||||||
this._controller.clearMessageReceived();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.nonOverriddenLiveConfig || !this.cameraManager) {
|
if (!this.hass || !this.nonOverriddenLiveConfig || !this.cameraManager) {
|
||||||
return;
|
return;
|
||||||
@@ -73,28 +56,22 @@ export class FrigateCardLive extends LitElement {
|
|||||||
// carousel (not necessarily the selected camera).
|
// carousel (not necessarily the selected camera).
|
||||||
// - Various events are captured to prevent them propagating upwards if the
|
// - Various events are captured to prevent them propagating upwards if the
|
||||||
// card is in the background.
|
// card is in the background.
|
||||||
// - The entire returned template is keyed to allow for the whole template
|
return html`
|
||||||
// to be re-rendered in certain circumstances (specifically: if a message
|
<frigate-card-live-grid
|
||||||
// is received when the card is in the background).
|
.hass=${this.hass}
|
||||||
return html`${keyed(
|
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||||
this._controller.getRenderEpoch(),
|
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
||||||
html`
|
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
||||||
<frigate-card-live-grid
|
.inBackground=${this._controller.isInBackground()}
|
||||||
.hass=${this.hass}
|
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
.overrides=${this.overrides}
|
||||||
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
.cameraManager=${this.cameraManager}
|
||||||
.inBackground=${this._controller.isInBackground()}
|
.microphoneManager=${this.microphoneManager}
|
||||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
.triggeredCameraIDs=${this.triggeredCameraIDs}
|
||||||
.overrides=${this.overrides}
|
>
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
</frigate-card-live-grid>
|
||||||
.cameraManager=${this.cameraManager}
|
`;
|
||||||
.microphoneManager=${this.microphoneManager}
|
|
||||||
.triggeredCameraIDs=${this.triggeredCameraIDs}
|
|
||||||
>
|
|
||||||
</frigate-card-live-grid>
|
|
||||||
`,
|
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { localize } from '../../localize/localize.js';
|
|||||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||||
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
|
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
|
||||||
import { aspectRatioToString } from '../../utils/basic.js';
|
import { aspectRatioToString } from '../../utils/basic.js';
|
||||||
import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
|
|
||||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||||
import { playMediaMutingIfNecessary } from '../../utils/media.js';
|
import { playMediaMutingIfNecessary } from '../../utils/media.js';
|
||||||
@@ -31,6 +30,7 @@ import { renderMessage } from '../message.js';
|
|||||||
import '../next-prev-control.js';
|
import '../next-prev-control.js';
|
||||||
import '../ptz.js';
|
import '../ptz.js';
|
||||||
import '../surround.js';
|
import '../surround.js';
|
||||||
|
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
|
|
||||||
@customElement('frigate-card-live-provider')
|
@customElement('frigate-card-live-provider')
|
||||||
export class FrigateCardLiveProvider
|
export class FrigateCardLiveProvider
|
||||||
@@ -71,6 +71,9 @@ export class FrigateCardLiveProvider
|
|||||||
@state()
|
@state()
|
||||||
protected _isVideoMediaLoaded = false;
|
protected _isVideoMediaLoaded = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _hasProviderError = false;
|
||||||
|
|
||||||
protected _refProvider: Ref<LitElement & FrigateCardMediaPlayer> = createRef();
|
protected _refProvider: Ref<LitElement & FrigateCardMediaPlayer> = createRef();
|
||||||
|
|
||||||
// A note on dynamic imports:
|
// A note on dynamic imports:
|
||||||
@@ -165,7 +168,9 @@ export class FrigateCardLiveProvider
|
|||||||
return (
|
return (
|
||||||
!!this.cameraConfig?.camera_entity &&
|
!!this.cameraConfig?.camera_entity &&
|
||||||
!!this.hass &&
|
!!this.hass &&
|
||||||
!!this.liveConfig?.show_image_during_load
|
!!this.liveConfig?.show_image_during_load &&
|
||||||
|
// Do not continue to show image during loading if an error has occurred.
|
||||||
|
!this._hasProviderError
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +182,10 @@ export class FrigateCardLiveProvider
|
|||||||
this._isVideoMediaLoaded = true;
|
this._isVideoMediaLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _providerErrorHandler(): void {
|
||||||
|
this._hasProviderError = true;
|
||||||
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has('load')) {
|
if (changedProps.has('load')) {
|
||||||
if (!this.load) {
|
if (!this.load) {
|
||||||
@@ -263,17 +272,30 @@ export class FrigateCardLiveProvider
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (provider === 'ha' || provider === 'image') {
|
if (provider === 'ha' || provider === 'image') {
|
||||||
const stateObj = getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
|
if (!this.cameraConfig?.camera_entity) {
|
||||||
if (!stateObj) {
|
dispatchLiveErrorEvent(this);
|
||||||
return;
|
return renderMessage({
|
||||||
|
message: localize('error.no_live_camera'),
|
||||||
|
type: 'error',
|
||||||
|
icon: 'mdi:camera',
|
||||||
|
context: this.cameraConfig,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (stateObj.state === 'unavailable') {
|
|
||||||
dispatchMediaUnloadedEvent(this);
|
|
||||||
|
|
||||||
// An unavailable camera gets a message rendered in place vs dispatched,
|
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
|
||||||
// as this may be a common occurrence (e.g. Frigate cameras that stop
|
if (!stateObj) {
|
||||||
// receiving frames). Otherwise a single temporarily unavailable camera
|
dispatchLiveErrorEvent(this);
|
||||||
// would render a whole carousel inoperable.
|
return renderMessage({
|
||||||
|
message: localize('error.live_camera_not_found'),
|
||||||
|
type: 'error',
|
||||||
|
icon: 'mdi:camera',
|
||||||
|
context: this.cameraConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateObj.state === 'unavailable') {
|
||||||
|
dispatchLiveErrorEvent(this);
|
||||||
|
dispatchMediaUnloadedEvent(this);
|
||||||
return renderMessage({
|
return renderMessage({
|
||||||
message: `${localize('error.live_camera_unavailable')}${
|
message: `${localize('error.live_camera_unavailable')}${
|
||||||
this.label ? `: ${this.label}` : ''
|
this.label ? `: ${this.label}` : ''
|
||||||
@@ -291,6 +313,7 @@ export class FrigateCardLiveProvider
|
|||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
|
@frigate-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@frigate-card:media:loaded=${(ev: Event) => {
|
@frigate-card:media:loaded=${(ev: Event) => {
|
||||||
if (provider === 'image') {
|
if (provider === 'image') {
|
||||||
// Only count the media has loaded if the required provider is
|
// Only count the media has loaded if the required provider is
|
||||||
@@ -311,6 +334,7 @@ export class FrigateCardLiveProvider
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
|
@frigate-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-live-ha>`
|
</frigate-card-live-ha>`
|
||||||
@@ -324,6 +348,7 @@ export class FrigateCardLiveProvider
|
|||||||
.microphoneStream=${this.microphoneStream}
|
.microphoneStream=${this.microphoneStream}
|
||||||
.microphoneConfig=${this.liveConfig.microphone}
|
.microphoneConfig=${this.liveConfig.microphone}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
|
@frigate-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-live-go2rtc>`
|
</frigate-card-live-go2rtc>`
|
||||||
@@ -336,6 +361,7 @@ export class FrigateCardLiveProvider
|
|||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
|
@frigate-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-live-webrtc-card>`
|
</frigate-card-live-webrtc-card>`
|
||||||
@@ -347,6 +373,7 @@ export class FrigateCardLiveProvider
|
|||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
|
@frigate-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-live-jsmpeg>`
|
</frigate-card-live-jsmpeg>`
|
||||||
|
|||||||
@@ -6,17 +6,22 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { CameraEndpoints } from '../../../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../../../camera-manager/types.js';
|
||||||
|
import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { CameraConfig, MicrophoneConfig } from '../../../../config/types.js';
|
import { CameraConfig, MicrophoneConfig } from '../../../../config/types.js';
|
||||||
import { localize } from '../../../../localize/localize.js';
|
import { localize } from '../../../../localize/localize.js';
|
||||||
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
||||||
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../../../types.js';
|
import {
|
||||||
import { getEndpointAddressOrDispatchError } from '../../../../utils/endpoint.js';
|
ExtendedHomeAssistant,
|
||||||
|
FrigateCardMediaPlayer,
|
||||||
|
Message,
|
||||||
|
} from '../../../../types.js';
|
||||||
|
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
|
||||||
import { setControlsOnVideo } from '../../../../utils/media.js';
|
import { setControlsOnVideo } from '../../../../utils/media.js';
|
||||||
import { screenshotMedia } from '../../../../utils/screenshot.js';
|
import { screenshotMedia } from '../../../../utils/screenshot.js';
|
||||||
import '../../../image.js';
|
import '../../../image.js';
|
||||||
import { dispatchErrorMessageEvent } from '../../../message.js';
|
import { renderMessage } from '../../../message.js';
|
||||||
import { VideoRTC } from './video-rtc.js';
|
import { VideoRTC } from './video-rtc.js';
|
||||||
|
|
||||||
customElements.define('frigate-card-live-go2rtc-player', VideoRTC);
|
customElements.define('frigate-card-live-go2rtc-player', VideoRTC);
|
||||||
@@ -48,6 +53,9 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
@property({ attribute: true, type: Boolean })
|
@property({ attribute: true, type: Boolean })
|
||||||
public controls = false;
|
public controls = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _message: Message | null = null;
|
||||||
|
|
||||||
protected _player?: VideoRTC;
|
protected _player?: VideoRTC;
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async play(): Promise<void> {
|
||||||
@@ -96,6 +104,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
this._player = undefined;
|
this._player = undefined;
|
||||||
|
this._message = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
@@ -113,18 +122,27 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
|
|
||||||
const endpoint = this.cameraEndpoints?.go2rtc;
|
const endpoint = this.cameraEndpoints?.go2rtc;
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
return dispatchErrorMessageEvent(this, localize('error.live_camera_no_endpoint'), {
|
this._message = {
|
||||||
|
type: 'error',
|
||||||
|
message: localize('error.live_camera_no_endpoint'),
|
||||||
context: this.cameraConfig,
|
context: this.cameraConfig,
|
||||||
});
|
};
|
||||||
|
dispatchLiveErrorEvent(this);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const address = await getEndpointAddressOrDispatchError(
|
const address = await convertEndpointAddressToSignedWebsocket(
|
||||||
this,
|
|
||||||
this.hass,
|
this.hass,
|
||||||
endpoint,
|
endpoint,
|
||||||
GO2RTC_URL_SIGN_EXPIRY_SECONDS,
|
GO2RTC_URL_SIGN_EXPIRY_SECONDS,
|
||||||
);
|
);
|
||||||
if (!address) {
|
if (!address) {
|
||||||
|
this._message = {
|
||||||
|
type: 'error',
|
||||||
|
message: localize('error.failed_sign'),
|
||||||
|
context: this.cameraConfig,
|
||||||
|
};
|
||||||
|
dispatchLiveErrorEvent(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +161,11 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (!this._player || changedProps.has('cameraEndpoints')) {
|
if (changedProps.has('cameraEndpoints')) {
|
||||||
|
this._message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._message && (!this._player || changedProps.has('cameraEndpoints'))) {
|
||||||
this._createPlayer();
|
this._createPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +185,9 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
if (this._message) {
|
||||||
|
return renderMessage(this._message);
|
||||||
|
}
|
||||||
return html`${this._player}`;
|
return html`${this._player}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,11 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { CameraConfig } from '../../../config/types';
|
import { CameraConfig } from '../../../config/types';
|
||||||
import { localize } from '../../../localize/localize';
|
|
||||||
import '../../../patches/ha-camera-stream';
|
import '../../../patches/ha-camera-stream';
|
||||||
import '../../../patches/ha-hls-player.js';
|
import '../../../patches/ha-hls-player.js';
|
||||||
import '../../../patches/ha-web-rtc-player.js';
|
import '../../../patches/ha-web-rtc-player.js';
|
||||||
import liveHAStyle from '../../../scss/live-ha.scss';
|
import liveHAStyle from '../../../scss/live-ha.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../../../types.js';
|
import { FrigateCardMediaPlayer } from '../../../types.js';
|
||||||
import { renderMessage } from '../../message';
|
|
||||||
import { getStateObjOrDispatchError } from '../../../utils/get-state-obj';
|
|
||||||
|
|
||||||
@customElement('frigate-card-live-ha')
|
@customElement('frigate-card-live-ha')
|
||||||
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
|
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
|
||||||
@@ -66,22 +63,12 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateObj = getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
|
|
||||||
if (!stateObj) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (stateObj.state === 'unavailable') {
|
|
||||||
return renderMessage({
|
|
||||||
message: localize('error.live_camera_unavailable'),
|
|
||||||
type: 'error',
|
|
||||||
icon: 'mdi:connection',
|
|
||||||
context: this.cameraConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return html` <frigate-card-ha-camera-stream
|
return html` <frigate-card-ha-camera-stream
|
||||||
${ref(this._playerRef)}
|
${ref(this._playerRef)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.stateObj=${stateObj}
|
.stateObj=${this.cameraConfig?.camera_entity
|
||||||
|
? this.hass.states[this.cameraConfig.camera_entity]
|
||||||
|
: undefined}
|
||||||
.controls=${this.controls}
|
.controls=${this.controls}
|
||||||
.muted=${true}
|
.muted=${true}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
|||||||
import { CameraConfig } from '../../../config/types';
|
import { CameraConfig } from '../../../config/types';
|
||||||
import basicBlockStyle from '../../../scss/basic-block.scss';
|
import basicBlockStyle from '../../../scss/basic-block.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../../../types.js';
|
import { FrigateCardMediaPlayer } from '../../../types.js';
|
||||||
import { getStateObjOrDispatchError } from '../../../utils/get-state-obj';
|
|
||||||
import '../../image.js';
|
import '../../image.js';
|
||||||
|
|
||||||
@customElement('frigate-card-live-image')
|
@customElement('frigate-card-live-image')
|
||||||
@@ -59,8 +58,6 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<frigate-card-image
|
<frigate-card-image
|
||||||
${ref(this._refImage)}
|
${ref(this._refImage)}
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import {
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
unsafeCSS,
|
||||||
|
} from 'lit';
|
||||||
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { until } from 'lit/directives/until.js';
|
import { until } from 'lit/directives/until.js';
|
||||||
import { CameraEndpoints } from '../../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../../camera-manager/types.js';
|
||||||
import { renderProgressIndicator } from '../../message.js';
|
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
||||||
import { localize } from '../../../localize/localize.js';
|
import { localize } from '../../../localize/localize.js';
|
||||||
import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
|
import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
|
||||||
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../../types.js';
|
import {
|
||||||
import { getEndpointAddressOrDispatchError } from '../../../utils/endpoint.js';
|
ExtendedHomeAssistant,
|
||||||
|
FrigateCardMediaPlayer,
|
||||||
|
Message,
|
||||||
|
} from '../../../types.js';
|
||||||
|
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
dispatchMediaPlayEvent,
|
dispatchMediaPlayEvent,
|
||||||
} from '../../../utils/media-info.js';
|
} from '../../../utils/media-info.js';
|
||||||
import { Timer } from '../../../utils/timer.js';
|
import { Timer } from '../../../utils/timer.js';
|
||||||
import { dispatchErrorMessageEvent } from '../../message.js';
|
import { renderMessage, renderProgressIndicator } from '../../message.js';
|
||||||
|
|
||||||
// Number of seconds a signed URL is valid for.
|
// Number of seconds a signed URL is valid for.
|
||||||
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||||
@@ -40,6 +51,9 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
|||||||
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
||||||
protected _refreshPlayerTimer = new Timer();
|
protected _refreshPlayerTimer = new Timer();
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _message: Message | null = null;
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async play(): Promise<void> {
|
||||||
return this._jsmpegVideoPlayer?.play();
|
return this._jsmpegVideoPlayer?.play();
|
||||||
}
|
}
|
||||||
@@ -84,6 +98,14 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
|||||||
return this._jsmpegCanvasElement?.toDataURL('image/jpeg') ?? null;
|
return this._jsmpegCanvasElement?.toDataURL('image/jpeg') ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
if (
|
||||||
|
['cameraConfig', 'cameraEndpoints'].some((prop) => changedProperties.has(prop))
|
||||||
|
) {
|
||||||
|
this._message = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSMPEG player.
|
* Create a JSMPEG player.
|
||||||
* @param url The URL for the player to connect to.
|
* @param url The URL for the player to connect to.
|
||||||
@@ -150,6 +172,7 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
|||||||
* Reset / destroy the player.
|
* Reset / destroy the player.
|
||||||
*/
|
*/
|
||||||
protected _resetPlayer(): void {
|
protected _resetPlayer(): void {
|
||||||
|
this._message = null;
|
||||||
this._refreshPlayerTimer.stop();
|
this._refreshPlayerTimer.stop();
|
||||||
if (this._jsmpegVideoPlayer) {
|
if (this._jsmpegVideoPlayer) {
|
||||||
try {
|
try {
|
||||||
@@ -199,18 +222,27 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
|||||||
|
|
||||||
const endpoint = this.cameraEndpoints?.jsmpeg;
|
const endpoint = this.cameraEndpoints?.jsmpeg;
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
return dispatchErrorMessageEvent(this, localize('error.live_camera_no_endpoint'), {
|
this._message = {
|
||||||
|
message: localize('error.live_camera_no_endpoint'),
|
||||||
|
type: 'error',
|
||||||
context: this.cameraConfig,
|
context: this.cameraConfig,
|
||||||
});
|
};
|
||||||
|
dispatchLiveErrorEvent(this);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const address = await getEndpointAddressOrDispatchError(
|
const address = await convertEndpointAddressToSignedWebsocket(
|
||||||
this,
|
|
||||||
this.hass,
|
this.hass,
|
||||||
endpoint,
|
endpoint,
|
||||||
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
|
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
|
||||||
);
|
);
|
||||||
if (!address) {
|
if (!address) {
|
||||||
|
this._message = {
|
||||||
|
type: 'error',
|
||||||
|
message: localize('error.failed_sign'),
|
||||||
|
context: this.cameraConfig,
|
||||||
|
};
|
||||||
|
dispatchLiveErrorEvent(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,11 +257,23 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
|||||||
* Master render method.
|
* Master render method.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
if (this._message) {
|
||||||
|
return renderMessage(this._message);
|
||||||
|
}
|
||||||
|
|
||||||
const _render = async (): Promise<TemplateResult | void> => {
|
const _render = async (): Promise<TemplateResult | void> => {
|
||||||
await this._refreshPlayer();
|
await this._refreshPlayer();
|
||||||
|
|
||||||
if (!this._jsmpegVideoPlayer || !this._jsmpegCanvasElement) {
|
if (!this._jsmpegVideoPlayer || !this._jsmpegCanvasElement) {
|
||||||
return dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_player'));
|
if (!this._message) {
|
||||||
|
this._message = {
|
||||||
|
message: localize('error.jsmpeg_no_player'),
|
||||||
|
type: 'error',
|
||||||
|
context: this.cameraConfig,
|
||||||
|
};
|
||||||
|
dispatchLiveErrorEvent(this);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return html`${this._jsmpegCanvasElement}`;
|
return html`${this._jsmpegCanvasElement}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
import { Task } from '@lit-labs/task';
|
import { Task } from '@lit-labs/task';
|
||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import {
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
unsafeCSS,
|
||||||
|
} from 'lit';
|
||||||
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { CameraEndpoints } from '../../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../../camera-manager/types.js';
|
||||||
|
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get-technology-for-video-rtc.js';
|
import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get-technology-for-video-rtc.js';
|
||||||
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
||||||
import { localize } from '../../../localize/localize.js';
|
import { localize } from '../../../localize/localize.js';
|
||||||
import liveWebRTCCardStyle from '../../../scss/live-webrtc-card.scss';
|
import liveWebRTCCardStyle from '../../../scss/live-webrtc-card.scss';
|
||||||
import { FrigateCardError, FrigateCardMediaPlayer } from '../../../types.js';
|
import { FrigateCardError, FrigateCardMediaPlayer, Message } from '../../../types.js';
|
||||||
import { mayHaveAudio } from '../../../utils/audio.js';
|
import { mayHaveAudio } from '../../../utils/audio.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
@@ -22,7 +30,7 @@ import {
|
|||||||
} from '../../../utils/media.js';
|
} from '../../../utils/media.js';
|
||||||
import { screenshotMedia } from '../../../utils/screenshot.js';
|
import { screenshotMedia } from '../../../utils/screenshot.js';
|
||||||
import { renderTask } from '../../../utils/task.js';
|
import { renderTask } from '../../../utils/task.js';
|
||||||
import { dispatchErrorMessageEvent, renderProgressIndicator } from '../../message.js';
|
import { renderMessage, renderProgressIndicator } from '../../message.js';
|
||||||
import { VideoRTC } from './go2rtc/video-rtc.js';
|
import { VideoRTC } from './go2rtc/video-rtc.js';
|
||||||
|
|
||||||
// Create a wrapper for AlexxIT's WebRTC card
|
// Create a wrapper for AlexxIT's WebRTC card
|
||||||
@@ -44,6 +52,9 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
@property({ attribute: true, type: Boolean })
|
@property({ attribute: true, type: Boolean })
|
||||||
public controls = false;
|
public controls = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _message: Message | null = null;
|
||||||
|
|
||||||
protected hass?: HomeAssistant;
|
protected hass?: HomeAssistant;
|
||||||
|
|
||||||
// A task to await the load of the WebRTC component.
|
// A task to await the load of the WebRTC component.
|
||||||
@@ -106,6 +117,19 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
this._message = null;
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
if (
|
||||||
|
['cameraConfig', 'cameraEndpoints'].some((prop) => changedProperties.has(prop))
|
||||||
|
) {
|
||||||
|
this._message = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected _getVideoRTC(): VideoRTC | null {
|
protected _getVideoRTC(): VideoRTC | null {
|
||||||
return (this.renderRoot?.querySelector('#webrtc') ?? null) as VideoRTC | null;
|
return (this.renderRoot?.querySelector('#webrtc') ?? null) as VideoRTC | null;
|
||||||
}
|
}
|
||||||
@@ -162,23 +186,28 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Master render method.
|
|
||||||
* @returns A rendered template.
|
|
||||||
*/
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
if (this._message) {
|
||||||
|
return renderMessage(this._message);
|
||||||
|
}
|
||||||
|
|
||||||
const render = (): TemplateResult | void => {
|
const render = (): TemplateResult | void => {
|
||||||
let webrtcElement: HTMLElement | null;
|
let webrtcElement: HTMLElement | null;
|
||||||
try {
|
try {
|
||||||
webrtcElement = this._createWebRTC();
|
webrtcElement = this._createWebRTC();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(
|
this._message = {
|
||||||
this,
|
type: 'error',
|
||||||
e instanceof FrigateCardError
|
message:
|
||||||
? e.message
|
e instanceof FrigateCardError
|
||||||
: localize('error.webrtc_card_reported_error') + ': ' + (e as Error).message,
|
? e.message
|
||||||
{ context: (e as FrigateCardError).context },
|
: localize('error.webrtc_card_reported_error') +
|
||||||
);
|
': ' +
|
||||||
|
(e as Error).message,
|
||||||
|
context: (e as FrigateCardError).context,
|
||||||
|
};
|
||||||
|
dispatchLiveErrorEvent(this);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (webrtcElement) {
|
if (webrtcElement) {
|
||||||
// Set the id to ensure that the relevant CSS styles will have
|
// Set the id to ensure that the relevant CSS styles will have
|
||||||
@@ -192,7 +221,7 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
// Use a task to allow us to asynchronously wait for the WebRTC card to
|
// Use a task to allow us to asynchronously wait for the WebRTC card to
|
||||||
// load, but yet still have the card load be followed by the updated()
|
// load, but yet still have the card load be followed by the updated()
|
||||||
// lifecycle callback (unlike just using `until`).
|
// lifecycle callback (unlike just using `until`).
|
||||||
return renderTask(this, this._webrtcTask, render, {
|
return renderTask(this._webrtcTask, render, {
|
||||||
inProgressFunc: () =>
|
inProgressFunc: () =>
|
||||||
renderProgressIndicator({
|
renderProgressIndicator({
|
||||||
message: localize('error.webrtc_card_waiting'),
|
message: localize('error.webrtc_card_waiting'),
|
||||||
@@ -201,9 +230,6 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updated lifecycle callback.
|
|
||||||
*/
|
|
||||||
public updated(): void {
|
public updated(): void {
|
||||||
// Extract the video component after it has been rendered and generate the
|
// Extract the video component after it has been rendered and generate the
|
||||||
// media load event.
|
// media load event.
|
||||||
@@ -232,9 +258,6 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get styles.
|
|
||||||
*/
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(liveWebRTCCardStyle);
|
return unsafeCSS(liveWebRTCCardStyle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,12 +152,15 @@ export function renderProgressIndicator(options?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch an event with a message to show to the user.
|
* Dispatch an event with a message to show to the user. Calling this method
|
||||||
|
* will grind the card to a halt, so should only be used for "global" / critical
|
||||||
|
* errors (i.e. not for individual errors with a given camera, since there may
|
||||||
|
* be multiple correctly functioning cameras in a grid).
|
||||||
* @param element The element to send the event.
|
* @param element The element to send the event.
|
||||||
* @param message The message to show.
|
* @param message The message to show.
|
||||||
* @param options Optional icon and context to include.
|
* @param options Optional icon and context to include.
|
||||||
*/
|
*/
|
||||||
export function dispatchMessageEvent(
|
function dispatchMessageEvent(
|
||||||
element: EventTarget,
|
element: EventTarget,
|
||||||
message: string,
|
message: string,
|
||||||
type: MessageType,
|
type: MessageType,
|
||||||
@@ -175,12 +178,15 @@ export function dispatchMessageEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch an event with an error message to show to the user.
|
* Dispatch an event with an error message to show to the user. Calling this
|
||||||
|
* method will grind the card to a halt, so should only be used for "global" /
|
||||||
|
* critical errors (i.e. not for individual errors with a given camera, since
|
||||||
|
* there may be multiple correctly functioning cameras in a grid).
|
||||||
* @param element The element to send the event.
|
* @param element The element to send the event.
|
||||||
* @param message The message to show.
|
* @param message The message to show.
|
||||||
* @param options Optional context to include.
|
* @param options Optional context to include.
|
||||||
*/
|
*/
|
||||||
export function dispatchErrorMessageEvent(
|
function dispatchErrorMessageEvent(
|
||||||
element: EventTarget,
|
element: EventTarget,
|
||||||
message: string,
|
message: string,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -193,7 +199,10 @@ export function dispatchErrorMessageEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch an event with an error message to show to the user.
|
* Dispatch an event with an error message to show to the user. Calling this
|
||||||
|
* method will grind the card to a halt, so should only be used for "global" /
|
||||||
|
* critical errors (i.e. not for individual errors with a given camera, since
|
||||||
|
* there may be multiple correctly functioning cameras in a grid).
|
||||||
* @param element The element to send the event.
|
* @param element The element to send the event.
|
||||||
* @param message The message to show.
|
* @param message The message to show.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
// Label that is used for ARIA support and as tooltip.
|
// Label that is used for ARIA support and as tooltip.
|
||||||
@property() label = '';
|
@property() label = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _thumbnailError = false;
|
||||||
|
|
||||||
protected _embedThumbnailTask = createFetchThumbnailTask(
|
protected _embedThumbnailTask = createFetchThumbnailTask(
|
||||||
this,
|
this,
|
||||||
() => this.hass,
|
() => this.hass,
|
||||||
@@ -49,7 +52,9 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderIcon =
|
const renderIcon =
|
||||||
!this.thumbnail || ['chevrons', 'icons'].includes(this._controlConfig.style);
|
!this.thumbnail ||
|
||||||
|
['chevrons', 'icons'].includes(this._controlConfig.style) ||
|
||||||
|
this._thumbnailError;
|
||||||
|
|
||||||
const classes = {
|
const classes = {
|
||||||
controls: true,
|
controls: true,
|
||||||
@@ -62,7 +67,10 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
|
|
||||||
if (renderIcon) {
|
if (renderIcon) {
|
||||||
const icon =
|
const icon =
|
||||||
!this.thumbnail || !this.icon || this._controlConfig.style === 'chevrons'
|
!this.thumbnail ||
|
||||||
|
!this.icon ||
|
||||||
|
this._controlConfig.style === 'chevrons' ||
|
||||||
|
this._thumbnailError
|
||||||
? this.side === 'left'
|
? this.side === 'left'
|
||||||
? 'mdi:chevron-left'
|
? 'mdi:chevron-left'
|
||||||
: 'mdi:chevron-right'
|
: 'mdi:chevron-right'
|
||||||
@@ -74,7 +82,6 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return renderTask(
|
return renderTask(
|
||||||
this,
|
|
||||||
this._embedThumbnailTask,
|
this._embedThumbnailTask,
|
||||||
(embeddedThumbnail: string | null) =>
|
(embeddedThumbnail: string | null) =>
|
||||||
embeddedThumbnail
|
embeddedThumbnail
|
||||||
@@ -85,7 +92,13 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
aria-label="${this.label}"
|
aria-label="${this.label}"
|
||||||
/>`
|
/>`
|
||||||
: html``,
|
: html``,
|
||||||
{ inProgressFunc: () => html`<div class=${classMap(classes)}></div>` },
|
{
|
||||||
|
inProgressFunc: () => html`<div class=${classMap(classes)}></div>`,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
errorFunc: (_e: Error) => {
|
||||||
|
this._thumbnailError = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
import { CameraManagerCameraMetadata } from '../camera-manager/types.js';
|
import { CameraManagerCameraMetadata } from '../camera-manager/types.js';
|
||||||
@@ -45,6 +45,9 @@ export class FrigateCardThumbnailFeatureThumbnail extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _thumbnailError = false;
|
||||||
|
|
||||||
protected _embedThumbnailTask?: Task<FetchThumbnailTaskArgs, string | null>;
|
protected _embedThumbnailTask?: Task<FetchThumbnailTaskArgs, string | null>;
|
||||||
|
|
||||||
// Only load thumbnails on view in case there is a very large number of them.
|
// Only load thumbnails on view in case there is a very large number of them.
|
||||||
@@ -107,17 +110,21 @@ export class FrigateCardThumbnailFeatureThumbnail extends LitElement {
|
|||||||
title=${localize('thumbnail.no_thumbnail')}
|
title=${localize('thumbnail.no_thumbnail')}
|
||||||
></ha-icon> `;
|
></ha-icon> `;
|
||||||
|
|
||||||
if (!this._embedThumbnailTask) {
|
if (!this._embedThumbnailTask || this._thumbnailError) {
|
||||||
return imageOff;
|
return imageOff;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`${this.thumbnail
|
return html`${this.thumbnail
|
||||||
? renderTask(
|
? renderTask(
|
||||||
this,
|
|
||||||
this._embedThumbnailTask,
|
this._embedThumbnailTask,
|
||||||
(embeddedThumbnail: string | null) =>
|
(embeddedThumbnail: string | null) =>
|
||||||
embeddedThumbnail ? html`<img src="${embeddedThumbnail}" />` : html``,
|
embeddedThumbnail ? html`<img src="${embeddedThumbnail}" />` : html``,
|
||||||
{ inProgressFunc: () => imageOff },
|
{
|
||||||
|
inProgressFunc: () => imageOff,
|
||||||
|
errorFunc: () => {
|
||||||
|
this._thumbnailError = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
: imageOff} `;
|
: imageOff} `;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { getTextDirection } from '../../utils/text-direction.js';
|
|||||||
import { ViewMedia } from '../../view/media.js';
|
import { ViewMedia } from '../../view/media.js';
|
||||||
import '../carousel';
|
import '../carousel';
|
||||||
import type { EmblaCarouselPlugins } from '../carousel.js';
|
import type { EmblaCarouselPlugins } from '../carousel.js';
|
||||||
import { dispatchMessageEvent } from '../message.js';
|
import { renderMessage } from '../message.js';
|
||||||
import '../next-prev-control.js';
|
import '../next-prev-control.js';
|
||||||
import '../ptz.js';
|
import '../ptz.js';
|
||||||
import './provider.js';
|
import './provider.js';
|
||||||
@@ -348,8 +348,15 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
const mediaCount = this._media?.length ?? 0;
|
const mediaCount = this._media?.length ?? 0;
|
||||||
if (!this._media || !mediaCount) {
|
if (!this._media || !mediaCount) {
|
||||||
return dispatchMessageEvent(this, localize('common.no_media'), 'info', {
|
return renderMessage({
|
||||||
|
message: localize('common.no_media'),
|
||||||
|
type: 'info',
|
||||||
icon: 'mdi:multimedia',
|
icon: 'mdi:multimedia',
|
||||||
|
...(this.viewFilterCameraID && {
|
||||||
|
context: {
|
||||||
|
camera_id: this.viewFilterCameraID,
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card' as const;
|
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card' as const;
|
||||||
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting` as const;
|
export const TROUBLESHOOTING_URL = `https://card.camera/#/troubleshooting` as const;
|
||||||
|
|
||||||
export const CONF_AUTOMATIONS = 'automations' as const;
|
export const CONF_AUTOMATIONS = 'automations' as const;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
import { css, CSSResultGroup, html, TemplateResult, unsafeCSS } from 'lit';
|
import { css, CSSResultGroup, html, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
import { query } from 'lit/decorators/query.js';
|
import { query } from 'lit/decorators/query.js';
|
||||||
import { dispatchErrorMessageEvent } from '../components/message.js';
|
import { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
||||||
|
import { renderMessage } from '../components/message.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../types.js';
|
import { FrigateCardMediaPlayer } from '../types.js';
|
||||||
import { mayHaveAudio } from '../utils/audio.js';
|
import { mayHaveAudio } from '../utils/audio.js';
|
||||||
@@ -97,8 +98,14 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (this._error) {
|
if (this._error) {
|
||||||
if (this._errorIsFatal) {
|
if (this._errorIsFatal) {
|
||||||
// Use native Frigate card error handling for fatal errors.
|
dispatchLiveErrorEvent(this);
|
||||||
return dispatchErrorMessageEvent(this, this._error);
|
return renderMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: this._error,
|
||||||
|
context: {
|
||||||
|
entity_id: this.entityid,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
errorToConsole(this._error, console.error);
|
errorToConsole(this._error, console.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
import { css, CSSResultGroup, html, TemplateResult, unsafeCSS } from 'lit';
|
import { css, CSSResultGroup, html, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
import { query } from 'lit/decorators/query.js';
|
import { query } from 'lit/decorators/query.js';
|
||||||
import { screenshotMedia } from '../utils/screenshot.js';
|
import { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { dispatchErrorMessageEvent } from '../components/message.js';
|
import { renderMessage } from '../components/message.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../types.js';
|
import { FrigateCardMediaPlayer } from '../types.js';
|
||||||
import { mayHaveAudio } from '../utils/audio.js';
|
import { mayHaveAudio } from '../utils/audio.js';
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
setControlsOnVideo,
|
setControlsOnVideo,
|
||||||
} from '../utils/media.js';
|
} from '../utils/media.js';
|
||||||
|
import { screenshotMedia } from '../utils/screenshot.js';
|
||||||
|
|
||||||
customElements.whenDefined('ha-web-rtc-player').then(() => {
|
customElements.whenDefined('ha-web-rtc-player').then(() => {
|
||||||
@customElement('frigate-card-ha-web-rtc-player')
|
@customElement('frigate-card-ha-web-rtc-player')
|
||||||
@@ -94,9 +95,14 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
|||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (this._error) {
|
if (this._error) {
|
||||||
// Use native Frigate card error handling, and attach the entityid to
|
dispatchLiveErrorEvent(this);
|
||||||
// clarify which camera the error refers to.
|
return renderMessage({
|
||||||
return dispatchErrorMessageEvent(this, `${this._error} (${this.entityid})`);
|
type: 'error',
|
||||||
|
message: this._error,
|
||||||
|
context: {
|
||||||
|
entity_id: this.entityid,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<video
|
<video
|
||||||
|
|||||||
+2
-11
@@ -1,12 +1,9 @@
|
|||||||
import { CameraEndpoint } from '../camera-manager/types';
|
import { CameraEndpoint } from '../camera-manager/types';
|
||||||
import { dispatchErrorMessageEvent } from '../components/message';
|
|
||||||
import { localize } from '../localize/localize';
|
|
||||||
import { ExtendedHomeAssistant } from '../types';
|
import { ExtendedHomeAssistant } from '../types';
|
||||||
import { errorToConsole } from './basic';
|
import { errorToConsole } from './basic';
|
||||||
import { homeAssistantSignPath } from './ha';
|
import { homeAssistantSignPath } from './ha';
|
||||||
|
|
||||||
export const getEndpointAddressOrDispatchError = async (
|
export const convertEndpointAddressToSignedWebsocket = async (
|
||||||
element: HTMLElement,
|
|
||||||
hass: ExtendedHomeAssistant,
|
hass: ExtendedHomeAssistant,
|
||||||
endpoint: CameraEndpoint,
|
endpoint: CameraEndpoint,
|
||||||
expires?: number,
|
expires?: number,
|
||||||
@@ -20,13 +17,7 @@ export const getEndpointAddressOrDispatchError = async (
|
|||||||
response = await homeAssistantSignPath(hass, endpoint.endpoint, expires);
|
response = await homeAssistantSignPath(hass, endpoint.endpoint, expires);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response) {
|
return response ? response.replace(/^http/i, 'ws') : null;
|
||||||
dispatchErrorMessageEvent(element, localize('error.failed_sign'));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.replace(/^http/i, 'ws');
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
|
||||||
import { HassEntity } from 'home-assistant-js-websocket';
|
|
||||||
import { CameraConfig } from '../config/types.js';
|
|
||||||
import { dispatchErrorMessageEvent } from '../components/message.js';
|
|
||||||
import { localize } from '../localize/localize.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stateObj;
|
|
||||||
};
|
|
||||||
+5
-6
@@ -1,9 +1,6 @@
|
|||||||
import { Task } from '@lit-labs/task';
|
import { Task } from '@lit-labs/task';
|
||||||
import { html, TemplateResult } from 'lit';
|
import { html, TemplateResult } from 'lit';
|
||||||
import {
|
import { renderProgressIndicator } from '../components/message';
|
||||||
dispatchFrigateCardErrorEvent,
|
|
||||||
renderProgressIndicator,
|
|
||||||
} from '../components/message';
|
|
||||||
import { CardWideConfig } from '../config/types';
|
import { CardWideConfig } from '../config/types';
|
||||||
import { errorToConsole } from './basic';
|
import { errorToConsole } from './basic';
|
||||||
|
|
||||||
@@ -16,12 +13,12 @@ import { errorToConsole } from './basic';
|
|||||||
* @returns A template.
|
* @returns A template.
|
||||||
*/
|
*/
|
||||||
export const renderTask = <R>(
|
export const renderTask = <R>(
|
||||||
host: EventTarget,
|
|
||||||
task: Task<unknown[], R>,
|
task: Task<unknown[], R>,
|
||||||
completeFunc: (result: R) => TemplateResult | void,
|
completeFunc: (result: R) => TemplateResult | void,
|
||||||
options?: {
|
options?: {
|
||||||
cardWideConfig?: CardWideConfig;
|
cardWideConfig?: CardWideConfig;
|
||||||
inProgressFunc?: () => TemplateResult | void;
|
inProgressFunc?: () => TemplateResult | void;
|
||||||
|
errorFunc?: (e: Error) => void;
|
||||||
},
|
},
|
||||||
): TemplateResult => {
|
): TemplateResult => {
|
||||||
const progressConfig = {
|
const progressConfig = {
|
||||||
@@ -34,7 +31,9 @@ export const renderTask = <R>(
|
|||||||
options?.inProgressFunc?.() ?? renderProgressIndicator(progressConfig),
|
options?.inProgressFunc?.() ?? renderProgressIndicator(progressConfig),
|
||||||
error: (e: unknown) => {
|
error: (e: unknown) => {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
dispatchFrigateCardErrorEvent(host, e as Error);
|
if (options?.errorFunc) {
|
||||||
|
options.errorFunc(e as Error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
complete: completeFunc,
|
complete: completeFunc,
|
||||||
})}`;
|
})}`;
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ const fetchThumbnail = async (
|
|||||||
};
|
};
|
||||||
reader.onerror = (e) => reject(e);
|
reader.onerror = (e) => reject(e);
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
});
|
})
|
||||||
|
.catch((e) => reject(e));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { LiveController } from '../../../src/components-lib/live/live-controller';
|
import { LiveController } from '../../../src/components-lib/live/live-controller';
|
||||||
import { dispatchMessageEvent } from '../../../src/components/message';
|
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../../src/utils/media-info';
|
||||||
import {
|
import {
|
||||||
IntersectionObserverMock,
|
IntersectionObserverMock,
|
||||||
callIntersectionHandler,
|
callIntersectionHandler,
|
||||||
@@ -26,7 +26,7 @@ describe('LiveController', () => {
|
|||||||
const host = createLitElement();
|
const host = createLitElement();
|
||||||
const parent = createParent({ children: [host] });
|
const parent = createParent({ children: [host] });
|
||||||
const eventListener = vi.fn();
|
const eventListener = vi.fn();
|
||||||
parent.addEventListener('frigate-card:message', eventListener);
|
parent.addEventListener('frigate-card:media:loaded', eventListener);
|
||||||
|
|
||||||
const controller = new LiveController(host);
|
const controller = new LiveController(host);
|
||||||
expect(host.addController).toBeCalled();
|
expect(host.addController).toBeCalled();
|
||||||
@@ -34,12 +34,13 @@ describe('LiveController', () => {
|
|||||||
controller.hostConnected();
|
controller.hostConnected();
|
||||||
|
|
||||||
callIntersectionHandler(false);
|
callIntersectionHandler(false);
|
||||||
dispatchMessageEvent(host, 'message', 'info');
|
|
||||||
|
dispatchExistingMediaLoadedInfoAsEvent(host, createMediaLoadedInfo());
|
||||||
|
|
||||||
expect(eventListener).toBeCalledTimes(0);
|
expect(eventListener).toBeCalledTimes(0);
|
||||||
|
|
||||||
controller.hostDisconnected();
|
controller.hostDisconnected();
|
||||||
dispatchMessageEvent(host, 'message', 'info');
|
dispatchExistingMediaLoadedInfoAsEvent(host, createMediaLoadedInfo());
|
||||||
|
|
||||||
expect(eventListener).toBeCalledTimes(1);
|
expect(eventListener).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -92,70 +93,6 @@ describe('LiveController', () => {
|
|||||||
|
|
||||||
host.dispatchEvent(createMediaLoadedInfoEvent(mediaLoadedInfo));
|
host.dispatchEvent(createMediaLoadedInfoEvent(mediaLoadedInfo));
|
||||||
expect(eventListener).toBeCalledTimes(2);
|
expect(eventListener).toBeCalledTimes(2);
|
||||||
|
|
||||||
callIntersectionHandler(false);
|
|
||||||
dispatchMessageEvent(host, 'message', 'info');
|
|
||||||
callIntersectionHandler(true);
|
|
||||||
expect(eventListener).toBeCalledTimes(2);
|
|
||||||
|
|
||||||
controller.clearMessageReceived();
|
|
||||||
callIntersectionHandler(true);
|
|
||||||
expect(eventListener).toBeCalledTimes(3);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should correctly allow updates', () => {
|
|
||||||
it('when not in background', () => {
|
|
||||||
const controller = new LiveController(createLitElement());
|
|
||||||
expect(controller.shouldUpdate()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when in background without message', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const controller = new LiveController(host);
|
|
||||||
controller.hostConnected();
|
|
||||||
|
|
||||||
callIntersectionHandler(false);
|
|
||||||
|
|
||||||
expect(controller.shouldUpdate()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when in background with message', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const controller = new LiveController(host);
|
|
||||||
controller.hostConnected();
|
|
||||||
|
|
||||||
callIntersectionHandler(false);
|
|
||||||
dispatchMessageEvent(host, 'message', 'info');
|
|
||||||
|
|
||||||
expect(controller.shouldUpdate()).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle message', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const parent = createParent({ children: [host] });
|
|
||||||
const eventListener = vi.fn();
|
|
||||||
parent.addEventListener('frigate-card:message', eventListener);
|
|
||||||
|
|
||||||
const controller = new LiveController(host);
|
|
||||||
controller.hostConnected();
|
|
||||||
|
|
||||||
callIntersectionHandler(false);
|
|
||||||
expect(controller.isInBackground()).toBeTruthy();
|
|
||||||
|
|
||||||
const firstRenderEpoch = controller.getRenderEpoch();
|
|
||||||
|
|
||||||
dispatchMessageEvent(host, 'message', 'info');
|
|
||||||
expect(eventListener).toBeCalledTimes(0);
|
|
||||||
|
|
||||||
const secondRenderEpoch = controller.getRenderEpoch();
|
|
||||||
expect(secondRenderEpoch).not.toBe(firstRenderEpoch);
|
|
||||||
|
|
||||||
callIntersectionHandler(true);
|
|
||||||
|
|
||||||
dispatchMessageEvent(host, 'message', 'info');
|
|
||||||
expect(eventListener).toBeCalledTimes(1);
|
|
||||||
expect(controller.getRenderEpoch()).toBe(secondRenderEpoch);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { expect, it, vi } from 'vitest';
|
||||||
|
import { dispatchLiveErrorEvent } from '../../../../src/components-lib/live/utils/dispatch-live-error';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
it('should dispatch live error event', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const handler = vi.fn();
|
||||||
|
element.addEventListener('frigate-card:live:error', handler);
|
||||||
|
|
||||||
|
dispatchLiveErrorEvent(element);
|
||||||
|
expect(handler).toBeCalled();
|
||||||
|
});
|
||||||
@@ -1,22 +1,17 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { getEndpointAddressOrDispatchError } from '../../src/utils/endpoint';
|
import { convertEndpointAddressToSignedWebsocket } from '../../src/utils/endpoint';
|
||||||
import { homeAssistantSignPath } from '../../src/utils/ha';
|
import { homeAssistantSignPath } from '../../src/utils/ha';
|
||||||
import { createHASS } from '../test-utils';
|
import { createHASS } from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/utils/ha');
|
vi.mock('../../src/utils/ha');
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
describe('convertEndpointAddressToSignedWebsocket', () => {
|
||||||
describe('getEndpointAddressOrDispatchError', () => {
|
|
||||||
it('without signing', async () => {
|
it('without signing', async () => {
|
||||||
expect(
|
expect(
|
||||||
await getEndpointAddressOrDispatchError(
|
await convertEndpointAddressToSignedWebsocket(createHASS(), {
|
||||||
document.createElement('div'),
|
endpoint: 'http://example.com',
|
||||||
createHASS(),
|
sign: false,
|
||||||
{
|
}),
|
||||||
endpoint: 'http://example.com',
|
|
||||||
sign: false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).toBe('http://example.com');
|
).toBe('http://example.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,51 +24,31 @@ describe('getEndpointAddressOrDispatchError', () => {
|
|||||||
vi.mocked(homeAssistantSignPath).mockResolvedValue('http://signed.com');
|
vi.mocked(homeAssistantSignPath).mockResolvedValue('http://signed.com');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await getEndpointAddressOrDispatchError(
|
await convertEndpointAddressToSignedWebsocket(createHASS(), {
|
||||||
document.createElement('div'),
|
endpoint: 'http://example.com',
|
||||||
createHASS(),
|
sign: true,
|
||||||
{
|
}),
|
||||||
endpoint: 'http://example.com',
|
|
||||||
sign: true,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).toBe('ws://signed.com');
|
).toBe('ws://signed.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with null response', async () => {
|
it('with null response', async () => {
|
||||||
const element = document.createElement('div');
|
|
||||||
const listener = vi.fn();
|
|
||||||
element.addEventListener('frigate-card:message', listener);
|
|
||||||
|
|
||||||
vi.mocked(homeAssistantSignPath).mockResolvedValue(null);
|
vi.mocked(homeAssistantSignPath).mockResolvedValue(null);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await getEndpointAddressOrDispatchError(element, createHASS(), {
|
await convertEndpointAddressToSignedWebsocket(createHASS(), {
|
||||||
endpoint: 'http://example.com',
|
endpoint: 'http://example.com',
|
||||||
sign: true,
|
sign: true,
|
||||||
}),
|
}),
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
|
|
||||||
expect(listener).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
message: 'Could not sign Home Assistant URL',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with exception on signing', async () => {
|
it('with exception on signing', async () => {
|
||||||
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
||||||
|
|
||||||
const element = document.createElement('div');
|
|
||||||
const listener = vi.fn();
|
|
||||||
element.addEventListener('frigate-card:message', listener);
|
|
||||||
|
|
||||||
vi.mocked(homeAssistantSignPath).mockRejectedValue(new Error());
|
vi.mocked(homeAssistantSignPath).mockRejectedValue(new Error());
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await getEndpointAddressOrDispatchError(element, createHASS(), {
|
await convertEndpointAddressToSignedWebsocket(createHASS(), {
|
||||||
endpoint: 'http://example.com',
|
endpoint: 'http://example.com',
|
||||||
sign: true,
|
sign: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
|
||||||
import { createCameraConfig, createHASS, createStateEntity } from '../test-utils';
|
|
||||||
import { getStateObjOrDispatchError } from '../../src/utils/get-state-obj';
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('getStateObjOrDispatchError', () => {
|
|
||||||
it('should retrieve valid state object', () => {
|
|
||||||
const messageHandler = vi.fn();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
element.addEventListener('frigate-card:message', messageHandler);
|
|
||||||
const state = createStateEntity();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
getStateObjOrDispatchError(
|
|
||||||
element,
|
|
||||||
createHASS({
|
|
||||||
'camera.test': state,
|
|
||||||
}),
|
|
||||||
createCameraConfig({
|
|
||||||
camera_entity: 'camera.test',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toBe(state);
|
|
||||||
|
|
||||||
expect(messageHandler).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dispatch unspecified entity', () => {
|
|
||||||
const messageHandler = vi.fn();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
element.addEventListener('frigate-card:message', messageHandler);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
getStateObjOrDispatchError(element, createHASS(), createCameraConfig()),
|
|
||||||
).toBeNull();
|
|
||||||
|
|
||||||
expect(messageHandler).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
message:
|
|
||||||
'The camera_entity parameter must be set and valid for this live provider',
|
|
||||||
type: 'error',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dispatch not found state', () => {
|
|
||||||
const messageHandler = vi.fn();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
element.addEventListener('frigate-card:message', messageHandler);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
getStateObjOrDispatchError(
|
|
||||||
element,
|
|
||||||
createHASS(),
|
|
||||||
createCameraConfig({
|
|
||||||
camera_entity: 'camera.will-not-be-found',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toBeNull();
|
|
||||||
|
|
||||||
expect(messageHandler).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
message: 'The configured camera_entity was not found',
|
|
||||||
type: 'error',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user