fix: Substantially reduce cases where errors block whole card (#1764)

This commit is contained in:
Dermot Duffy
2024-12-15 20:39:18 -08:00
committed by GitHub
parent b6fb821074
commit dab9ff3045
25 changed files with 329 additions and 407 deletions
+19 -5
View File
@@ -8,7 +8,7 @@ import {
TemplateResult,
unsafeCSS,
} 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 { createRef, ref, Ref } from 'lit/directives/ref.js';
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 { localize } from '../localize/localize.js';
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 { isHassDifferent } from '../utils/ha';
import {
@@ -28,7 +28,7 @@ import {
dispatchMediaPlayEvent,
} from '../utils/media-info.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 .
const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000;
@@ -53,6 +53,9 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
@property({ attribute: false, hasChanged: contentsChanged })
public imageConfig?: ImageViewConfig;
@state()
protected _message: Message | null = null;
protected _refImage: Ref<HTMLImageElement> = createRef();
protected _cachedValueController?: CachedValueController<string>;
@@ -175,6 +178,10 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
if (!this._cachedValueController?.value) {
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 {
this._cachedValueController?.stopTimer();
this._message = null;
document.removeEventListener('visibilitychange', this._boundVisibilityHandler);
super.disconnectedCallback();
}
@@ -329,6 +337,10 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
}
protected render(): TemplateResult | void {
if (this._message) {
return renderMessage(this._message);
}
const src = this._cachedValueController?.value;
// Note the use of live() below to ensure the update will restore the image
// src if it's been changed via _forceSafeImage().
@@ -363,9 +375,11 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
} else if (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'), {
this._message = {
type: 'error',
message: localize('error.image_load_error'),
context: this.imageConfig,
});
};
}
}}
/>
+17 -40
View File
@@ -1,13 +1,5 @@
import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS,
} from 'lit';
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { keyed } from 'lit/directives/keyed.js';
import { CameraManager } from '../../camera-manager/manager.js';
import { ConditionsManagerEpoch } from '../../card-controller/conditions-manager.js';
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
@@ -53,15 +45,6 @@ export class FrigateCardLive extends LitElement {
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 {
if (!this.hass || !this.nonOverriddenLiveConfig || !this.cameraManager) {
return;
@@ -73,28 +56,22 @@ export class FrigateCardLive extends LitElement {
// carousel (not necessarily the selected camera).
// - Various events are captured to prevent them propagating upwards if the
// card is in the background.
// - The entire returned template is keyed to allow for the whole template
// to be re-rendered in certain circumstances (specifically: if a message
// is received when the card is in the background).
return html`${keyed(
this._controller.getRenderEpoch(),
html`
<frigate-card-live-grid
.hass=${this.hass}
.viewManagerEpoch=${this.viewManagerEpoch}
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
.overriddenLiveConfig=${this.overriddenLiveConfig}
.inBackground=${this._controller.isInBackground()}
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
.overrides=${this.overrides}
.cardWideConfig=${this.cardWideConfig}
.cameraManager=${this.cameraManager}
.microphoneManager=${this.microphoneManager}
.triggeredCameraIDs=${this.triggeredCameraIDs}
>
</frigate-card-live-grid>
`,
)}`;
return html`
<frigate-card-live-grid
.hass=${this.hass}
.viewManagerEpoch=${this.viewManagerEpoch}
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
.overriddenLiveConfig=${this.overriddenLiveConfig}
.inBackground=${this._controller.isInBackground()}
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
.overrides=${this.overrides}
.cardWideConfig=${this.cardWideConfig}
.cameraManager=${this.cameraManager}
.microphoneManager=${this.microphoneManager}
.triggeredCameraIDs=${this.triggeredCameraIDs}
>
</frigate-card-live-grid>
`;
}
static get styles(): CSSResultGroup {
+38 -11
View File
@@ -23,7 +23,6 @@ import { localize } from '../../localize/localize.js';
import liveProviderStyle from '../../scss/live-provider.scss';
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
import { aspectRatioToString } from '../../utils/basic.js';
import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
import { playMediaMutingIfNecessary } from '../../utils/media.js';
@@ -31,6 +30,7 @@ import { renderMessage } from '../message.js';
import '../next-prev-control.js';
import '../ptz.js';
import '../surround.js';
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
@customElement('frigate-card-live-provider')
export class FrigateCardLiveProvider
@@ -71,6 +71,9 @@ export class FrigateCardLiveProvider
@state()
protected _isVideoMediaLoaded = false;
@state()
protected _hasProviderError = false;
protected _refProvider: Ref<LitElement & FrigateCardMediaPlayer> = createRef();
// A note on dynamic imports:
@@ -165,7 +168,9 @@ export class FrigateCardLiveProvider
return (
!!this.cameraConfig?.camera_entity &&
!!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;
}
protected _providerErrorHandler(): void {
this._hasProviderError = true;
}
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('load')) {
if (!this.load) {
@@ -263,17 +272,30 @@ export class FrigateCardLiveProvider
};
if (provider === 'ha' || provider === 'image') {
const stateObj = getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
if (!stateObj) {
return;
if (!this.cameraConfig?.camera_entity) {
dispatchLiveErrorEvent(this);
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,
// as this may be a common occurrence (e.g. Frigate cameras that stop
// receiving frames). Otherwise a single temporarily unavailable camera
// would render a whole carousel inoperable.
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
if (!stateObj) {
dispatchLiveErrorEvent(this);
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({
message: `${localize('error.live_camera_unavailable')}${
this.label ? `: ${this.label}` : ''
@@ -291,6 +313,7 @@ export class FrigateCardLiveProvider
${ref(this._refProvider)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
@frigate-card:live:error=${() => this._providerErrorHandler()}
@frigate-card:media:loaded=${(ev: Event) => {
if (provider === 'image') {
// Only count the media has loaded if the required provider is
@@ -311,6 +334,7 @@ export class FrigateCardLiveProvider
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
?controls=${this.liveConfig.controls.builtin}
@frigate-card:live:error=${() => this._providerErrorHandler()}
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
>
</frigate-card-live-ha>`
@@ -324,6 +348,7 @@ export class FrigateCardLiveProvider
.microphoneStream=${this.microphoneStream}
.microphoneConfig=${this.liveConfig.microphone}
?controls=${this.liveConfig.controls.builtin}
@frigate-card:live:error=${() => this._providerErrorHandler()}
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
>
</frigate-card-live-go2rtc>`
@@ -336,6 +361,7 @@ export class FrigateCardLiveProvider
.cameraEndpoints=${this.cameraEndpoints}
.cardWideConfig=${this.cardWideConfig}
?controls=${this.liveConfig.controls.builtin}
@frigate-card:live:error=${() => this._providerErrorHandler()}
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
>
</frigate-card-live-webrtc-card>`
@@ -347,6 +373,7 @@ export class FrigateCardLiveProvider
.cameraConfig=${this.cameraConfig}
.cameraEndpoints=${this.cameraEndpoints}
.cardWideConfig=${this.cardWideConfig}
@frigate-card:live:error=${() => this._providerErrorHandler()}
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
>
</frigate-card-live-jsmpeg>`
+34 -9
View File
@@ -6,17 +6,22 @@ import {
TemplateResult,
unsafeCSS,
} 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 { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
import { CameraConfig, MicrophoneConfig } from '../../../../config/types.js';
import { localize } from '../../../../localize/localize.js';
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../../../types.js';
import { getEndpointAddressOrDispatchError } from '../../../../utils/endpoint.js';
import {
ExtendedHomeAssistant,
FrigateCardMediaPlayer,
Message,
} from '../../../../types.js';
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
import { setControlsOnVideo } from '../../../../utils/media.js';
import { screenshotMedia } from '../../../../utils/screenshot.js';
import '../../../image.js';
import { dispatchErrorMessageEvent } from '../../../message.js';
import { renderMessage } from '../../../message.js';
import { VideoRTC } from './video-rtc.js';
customElements.define('frigate-card-live-go2rtc-player', VideoRTC);
@@ -48,6 +53,9 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
@property({ attribute: true, type: Boolean })
public controls = false;
@state()
protected _message: Message | null = null;
protected _player?: VideoRTC;
public async play(): Promise<void> {
@@ -96,6 +104,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
disconnectedCallback(): void {
this._player = undefined;
this._message = null;
}
connectedCallback(): void {
@@ -113,18 +122,27 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
const endpoint = this.cameraEndpoints?.go2rtc;
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,
});
};
dispatchLiveErrorEvent(this);
return;
}
const address = await getEndpointAddressOrDispatchError(
this,
const address = await convertEndpointAddressToSignedWebsocket(
this.hass,
endpoint,
GO2RTC_URL_SIGN_EXPIRY_SECONDS,
);
if (!address) {
this._message = {
type: 'error',
message: localize('error.failed_sign'),
context: this.cameraConfig,
};
dispatchLiveErrorEvent(this);
return;
}
@@ -143,7 +161,11 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
}
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();
}
@@ -163,6 +185,9 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
}
protected render(): TemplateResult | void {
if (this._message) {
return renderMessage(this._message);
}
return html`${this._player}`;
}
+3 -16
View File
@@ -3,14 +3,11 @@ 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 { CameraConfig } from '../../../config/types';
import { localize } from '../../../localize/localize';
import '../../../patches/ha-camera-stream';
import '../../../patches/ha-hls-player.js';
import '../../../patches/ha-web-rtc-player.js';
import liveHAStyle from '../../../scss/live-ha.scss';
import { FrigateCardMediaPlayer } from '../../../types.js';
import { renderMessage } from '../../message';
import { getStateObjOrDispatchError } from '../../../utils/get-state-obj';
@customElement('frigate-card-live-ha')
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
@@ -66,22 +63,12 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
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
${ref(this._playerRef)}
.hass=${this.hass}
.stateObj=${stateObj}
.stateObj=${this.cameraConfig?.camera_entity
? this.hass.states[this.cameraConfig.camera_entity]
: undefined}
.controls=${this.controls}
.muted=${true}
>
-3
View File
@@ -5,7 +5,6 @@ import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { CameraConfig } from '../../../config/types';
import basicBlockStyle from '../../../scss/basic-block.scss';
import { FrigateCardMediaPlayer } from '../../../types.js';
import { getStateObjOrDispatchError } from '../../../utils/get-state-obj';
import '../../image.js';
@customElement('frigate-card-live-image')
@@ -59,8 +58,6 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
return;
}
getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
return html`
<frigate-card-image
${ref(this._refImage)}
+55 -11
View File
@@ -1,21 +1,32 @@
import JSMpeg from '@cycjimmy/jsmpeg-player';
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS,
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { until } from 'lit/directives/until.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 { localize } from '../../../localize/localize.js';
import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../../types.js';
import { getEndpointAddressOrDispatchError } from '../../../utils/endpoint.js';
import {
ExtendedHomeAssistant,
FrigateCardMediaPlayer,
Message,
} from '../../../types.js';
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
import {
dispatchMediaLoadedEvent,
dispatchMediaPauseEvent,
dispatchMediaPlayEvent,
} from '../../../utils/media-info.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.
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 _refreshPlayerTimer = new Timer();
@state()
protected _message: Message | null = null;
public async play(): Promise<void> {
return this._jsmpegVideoPlayer?.play();
}
@@ -84,6 +98,14 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
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.
* @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.
*/
protected _resetPlayer(): void {
this._message = null;
this._refreshPlayerTimer.stop();
if (this._jsmpegVideoPlayer) {
try {
@@ -199,18 +222,27 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
const endpoint = this.cameraEndpoints?.jsmpeg;
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,
});
};
dispatchLiveErrorEvent(this);
return;
}
const address = await getEndpointAddressOrDispatchError(
this,
const address = await convertEndpointAddressToSignedWebsocket(
this.hass,
endpoint,
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
);
if (!address) {
this._message = {
type: 'error',
message: localize('error.failed_sign'),
context: this.cameraConfig,
};
dispatchLiveErrorEvent(this);
return;
}
@@ -225,11 +257,23 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
* Master render method.
*/
protected render(): TemplateResult | void {
if (this._message) {
return renderMessage(this._message);
}
const _render = async (): Promise<TemplateResult | void> => {
await this._refreshPlayer();
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}`;
};
+45 -22
View File
@@ -1,13 +1,21 @@
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
import { Task } from '@lit-labs/task';
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS,
} from 'lit';
import { customElement, property, state } from 'lit/decorators.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 { CameraConfig, CardWideConfig } from '../../../config/types.js';
import { localize } from '../../../localize/localize.js';
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 {
dispatchMediaLoadedEvent,
@@ -22,7 +30,7 @@ import {
} from '../../../utils/media.js';
import { screenshotMedia } from '../../../utils/screenshot.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';
// Create a wrapper for AlexxIT's WebRTC card
@@ -44,6 +52,9 @@ export class FrigateCardLiveWebRTCCard
@property({ attribute: true, type: Boolean })
public controls = false;
@state()
protected _message: Message | null = null;
protected hass?: HomeAssistant;
// A task to await the load of the WebRTC component.
@@ -106,6 +117,19 @@ export class FrigateCardLiveWebRTCCard
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 {
return (this.renderRoot?.querySelector('#webrtc') ?? null) as VideoRTC | null;
}
@@ -162,23 +186,28 @@ export class FrigateCardLiveWebRTCCard
return null;
}
/**
* Master render method.
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
if (this._message) {
return renderMessage(this._message);
}
const render = (): TemplateResult | void => {
let webrtcElement: HTMLElement | null;
try {
webrtcElement = this._createWebRTC();
} catch (e) {
return dispatchErrorMessageEvent(
this,
e instanceof FrigateCardError
? e.message
: localize('error.webrtc_card_reported_error') + ': ' + (e as Error).message,
{ context: (e as FrigateCardError).context },
);
this._message = {
type: 'error',
message:
e instanceof FrigateCardError
? e.message
: localize('error.webrtc_card_reported_error') +
': ' +
(e as Error).message,
context: (e as FrigateCardError).context,
};
dispatchLiveErrorEvent(this);
return;
}
if (webrtcElement) {
// 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
// load, but yet still have the card load be followed by the updated()
// lifecycle callback (unlike just using `until`).
return renderTask(this, this._webrtcTask, render, {
return renderTask(this._webrtcTask, render, {
inProgressFunc: () =>
renderProgressIndicator({
message: localize('error.webrtc_card_waiting'),
@@ -201,9 +230,6 @@ export class FrigateCardLiveWebRTCCard
});
}
/**
* Updated lifecycle callback.
*/
public updated(): void {
// Extract the video component after it has been rendered and generate the
// media load event.
@@ -232,9 +258,6 @@ export class FrigateCardLiveWebRTCCard
});
}
/**
* Get styles.
*/
static get styles(): CSSResultGroup {
return unsafeCSS(liveWebRTCCardStyle);
}
+14 -5
View File
@@ -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 message The message to show.
* @param options Optional icon and context to include.
*/
export function dispatchMessageEvent(
function dispatchMessageEvent(
element: EventTarget,
message: string,
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 message The message to show.
* @param options Optional context to include.
*/
export function dispatchErrorMessageEvent(
function dispatchErrorMessageEvent(
element: EventTarget,
message: string,
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 message The message to show.
*/
+17 -4
View File
@@ -37,6 +37,9 @@ export class FrigateCardNextPreviousControl extends LitElement {
// Label that is used for ARIA support and as tooltip.
@property() label = '';
@state()
protected _thumbnailError = false;
protected _embedThumbnailTask = createFetchThumbnailTask(
this,
() => this.hass,
@@ -49,7 +52,9 @@ export class FrigateCardNextPreviousControl extends LitElement {
}
const renderIcon =
!this.thumbnail || ['chevrons', 'icons'].includes(this._controlConfig.style);
!this.thumbnail ||
['chevrons', 'icons'].includes(this._controlConfig.style) ||
this._thumbnailError;
const classes = {
controls: true,
@@ -62,7 +67,10 @@ export class FrigateCardNextPreviousControl extends LitElement {
if (renderIcon) {
const icon =
!this.thumbnail || !this.icon || this._controlConfig.style === 'chevrons'
!this.thumbnail ||
!this.icon ||
this._controlConfig.style === 'chevrons' ||
this._thumbnailError
? this.side === 'left'
? 'mdi:chevron-left'
: 'mdi:chevron-right'
@@ -74,7 +82,6 @@ export class FrigateCardNextPreviousControl extends LitElement {
}
return renderTask(
this,
this._embedThumbnailTask,
(embeddedThumbnail: string | null) =>
embeddedThumbnail
@@ -85,7 +92,13 @@ export class FrigateCardNextPreviousControl extends LitElement {
aria-label="${this.label}"
/>`
: 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;
},
},
);
}
+11 -4
View File
@@ -8,7 +8,7 @@ import {
TemplateResult,
unsafeCSS,
} 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 { CameraManager } from '../camera-manager/manager.js';
import { CameraManagerCameraMetadata } from '../camera-manager/types.js';
@@ -45,6 +45,9 @@ export class FrigateCardThumbnailFeatureThumbnail extends LitElement {
@property({ attribute: false })
public hass?: ExtendedHomeAssistant;
@state()
protected _thumbnailError = false;
protected _embedThumbnailTask?: Task<FetchThumbnailTaskArgs, string | null>;
// 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')}
></ha-icon> `;
if (!this._embedThumbnailTask) {
if (!this._embedThumbnailTask || this._thumbnailError) {
return imageOff;
}
return html`${this.thumbnail
? renderTask(
this,
this._embedThumbnailTask,
(embeddedThumbnail: string | null) =>
embeddedThumbnail ? html`<img src="${embeddedThumbnail}" />` : html``,
{ inProgressFunc: () => imageOff },
{
inProgressFunc: () => imageOff,
errorFunc: () => {
this._thumbnailError = true;
},
},
)
: imageOff} `;
}
+9 -2
View File
@@ -38,7 +38,7 @@ import { getTextDirection } from '../../utils/text-direction.js';
import { ViewMedia } from '../../view/media.js';
import '../carousel';
import type { EmblaCarouselPlugins } from '../carousel.js';
import { dispatchMessageEvent } from '../message.js';
import { renderMessage } from '../message.js';
import '../next-prev-control.js';
import '../ptz.js';
import './provider.js';
@@ -348,8 +348,15 @@ export class FrigateCardViewerCarousel extends LitElement {
protected render(): TemplateResult | void {
const mediaCount = this._media?.length ?? 0;
if (!this._media || !mediaCount) {
return dispatchMessageEvent(this, localize('common.no_media'), 'info', {
return renderMessage({
message: localize('common.no_media'),
type: 'info',
icon: 'mdi:multimedia',
...(this.viewFilterCameraID && {
context: {
camera_id: this.viewFilterCameraID,
},
}),
});
}