fix: Substantially reduce cases where errors block whole card (#1764)
This commit is contained in:
+19
-5
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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,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}
|
||||
>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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} `;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user