Split live providers into chunks.
This commit is contained in:
+2
-2
@@ -27,8 +27,8 @@ import { FrigateCardElements } from './components/elements.js';
|
||||
import './components/gallery.js';
|
||||
import './components/image.js';
|
||||
import { FrigateCardImage } from './components/image.js';
|
||||
import './components/live.js';
|
||||
import { FrigateCardLive } from './components/live.js';
|
||||
import './components/live/live.js';
|
||||
import type { FrigateCardLive } from './components/live/live.js';
|
||||
import './components/menu.js';
|
||||
import { FrigateCardMenu, FRIGATE_BUTTON_MENU_ICON } from './components/menu.js';
|
||||
import './components/message.js';
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import liveFrigateStyle from '../../scss/live-frigate.scss';
|
||||
import { CameraConfig, FrigateCardMediaPlayer } from '../../types.js';
|
||||
import { getCameraTitle } from '../../utils/camera.js';
|
||||
import { dispatchErrorMessageEvent, dispatchMessageEvent } from '../message.js';
|
||||
import '../../patches/ha-camera-stream';
|
||||
|
||||
@customElement('frigate-card-live-ha')
|
||||
export class FrigateCardLiveHA extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
protected _playerRef: Ref<Element & FrigateCardMediaPlayer> = createRef();
|
||||
|
||||
/**
|
||||
* Play the video.
|
||||
*/
|
||||
public play(): void {
|
||||
this._playerRef.value?.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the video.
|
||||
*/
|
||||
public pause(): void {
|
||||
this._playerRef.value?.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the video.
|
||||
*/
|
||||
public mute(): void {
|
||||
this._playerRef.value?.mute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the video.
|
||||
*/
|
||||
public unmute(): void {
|
||||
this._playerRef.value?.unmute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek the video.
|
||||
*/
|
||||
public seek(seconds: number): void {
|
||||
this._playerRef.value?.seek(seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.cameraConfig?.camera_entity) {
|
||||
return dispatchErrorMessageEvent(this, localize('error.no_live_camera'), {
|
||||
context: this.cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
|
||||
if (!stateObj) {
|
||||
return dispatchErrorMessageEvent(this, localize('error.live_camera_not_found'), {
|
||||
context: this.cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
if (stateObj.state === 'unavailable') {
|
||||
// Don't treat state unavailability as an error per se.
|
||||
return dispatchMessageEvent(
|
||||
this,
|
||||
localize('error.live_camera_unavailable'),
|
||||
'info',
|
||||
{
|
||||
icon: 'mdi:connection',
|
||||
context: getCameraTitle(this.hass, this.cameraConfig),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return html` <frigate-card-ha-camera-stream
|
||||
${ref(this._playerRef)}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.controls=${true}
|
||||
.muted=${true}
|
||||
>
|
||||
</frigate-card-ha-camera-stream>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveFrigateStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-live-ha': FrigateCardLiveHA;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
import { renderProgressIndicator } from '../../components/message.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import liveJSMPEGStyle from '../../scss/live-jsmpeg.scss';
|
||||
import {
|
||||
CameraConfig,
|
||||
CardWideConfig,
|
||||
ExtendedHomeAssistant,
|
||||
JSMPEGConfig,
|
||||
} from '../../types.js';
|
||||
import { homeAssistantSignPath } from '../../utils/ha';
|
||||
import { dispatchMediaLoadedEvent } from '../../utils/media-info.js';
|
||||
import { dispatchErrorMessageEvent } from '../message.js';
|
||||
import { contentsChanged, errorToConsole } from '../../utils/basic.js';
|
||||
|
||||
// Number of seconds a signed URL is valid for.
|
||||
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||
|
||||
// Number of seconds before the expiry to trigger a refresh.
|
||||
const URL_SIGN_REFRESH_THRESHOLD_SECONDS = 1 * 60 * 60;
|
||||
|
||||
@customElement('frigate-card-live-jsmpeg')
|
||||
export class FrigateCardLiveJSMPEG extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public jsmpegConfig?: JSMPEGConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected hass?: ExtendedHomeAssistant;
|
||||
|
||||
protected _jsmpegCanvasElement?: HTMLCanvasElement;
|
||||
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
||||
protected _refreshPlayerTimerID?: number;
|
||||
|
||||
/**
|
||||
* Play the video.
|
||||
*/
|
||||
public play(): void {
|
||||
this._jsmpegVideoPlayer?.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the video.
|
||||
*/
|
||||
public pause(): void {
|
||||
this._jsmpegVideoPlayer?.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the video (included for completeness, JSMPEG live disables audio as
|
||||
* Frigate does not encode it).
|
||||
*/
|
||||
public mute(): void {
|
||||
const player = this._jsmpegVideoPlayer?.player;
|
||||
if (player) {
|
||||
player.volume = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the video (included for completeness, JSMPEG live disables audio as
|
||||
* Frigate does not encode it).
|
||||
*/
|
||||
public unmute(): void {
|
||||
const player = this._jsmpegVideoPlayer?.player;
|
||||
if (player) {
|
||||
player.volume = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek the video (unsupported).
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public seek(_seconds: number): void {
|
||||
// JSMPEG does not support seeking.
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a signed player URL.
|
||||
* @returns A URL or null.
|
||||
*/
|
||||
protected async _getURL(): Promise<string | null> {
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.cameraConfig?.frigate.client_id ||
|
||||
!this.cameraConfig?.frigate.camera_name
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let response: string | null | undefined;
|
||||
try {
|
||||
response = await homeAssistantSignPath(
|
||||
this.hass,
|
||||
`/api/frigate/${this.cameraConfig.frigate.client_id}` +
|
||||
`/jsmpeg/${this.cameraConfig.frigate.camera_name}`,
|
||||
URL_SIGN_EXPIRY_SECONDS,
|
||||
);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
return null;
|
||||
}
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
return response.replace(/^http/i, 'ws');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSMPEG player.
|
||||
* @param url The URL for the player to connect to.
|
||||
* @returns A JSMPEG player.
|
||||
*/
|
||||
protected async _createJSMPEGPlayer(url: string): Promise<JSMpeg.VideoElement> {
|
||||
return new Promise<JSMpeg.VideoElement>((resolve) => {
|
||||
let videoDecoded = false;
|
||||
const player = new JSMpeg.VideoElement(
|
||||
this,
|
||||
url,
|
||||
{
|
||||
canvas: this._jsmpegCanvasElement,
|
||||
},
|
||||
{
|
||||
// The media carousel may automatically pause when the browser tab is
|
||||
// inactive, JSMPEG does not need to also do so independently.
|
||||
pauseWhenHidden: false,
|
||||
autoplay: false,
|
||||
protocols: [],
|
||||
audio: false,
|
||||
videoBufferSize: 1024 * 1024 * 4,
|
||||
|
||||
// Override with user-specified options.
|
||||
...this.jsmpegConfig?.options,
|
||||
|
||||
// Don't allow the player to internally reconnect, as it may re-use a
|
||||
// URL with a (newly) invalid signature, e.g. during a Home Assistant
|
||||
// restart.
|
||||
reconnectInterval: 0,
|
||||
onVideoDecode: () => {
|
||||
// This is the only callback that is called after the dimensions
|
||||
// are available. It's called on every frame decode, so just
|
||||
// ignore any subsequent calls.
|
||||
if (!videoDecoded && this._jsmpegCanvasElement) {
|
||||
videoDecoded = true;
|
||||
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement);
|
||||
resolve(player);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset / destroy the player.
|
||||
*/
|
||||
protected _resetPlayer(): void {
|
||||
if (this._refreshPlayerTimerID) {
|
||||
window.clearTimeout(this._refreshPlayerTimerID);
|
||||
this._refreshPlayerTimerID = undefined;
|
||||
}
|
||||
if (this._jsmpegVideoPlayer) {
|
||||
try {
|
||||
this._jsmpegVideoPlayer.destroy();
|
||||
} catch (err) {
|
||||
// Pass.
|
||||
}
|
||||
this._jsmpegVideoPlayer = undefined;
|
||||
}
|
||||
if (this._jsmpegCanvasElement) {
|
||||
this._jsmpegCanvasElement.remove();
|
||||
this._jsmpegCanvasElement = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component connected callback.
|
||||
*/
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.isConnected) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
if (!this.isConnected) {
|
||||
this._resetPlayer();
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the JSMPEG player.
|
||||
*/
|
||||
protected async _refreshPlayer(): Promise<void> {
|
||||
this._resetPlayer();
|
||||
|
||||
this._jsmpegCanvasElement = document.createElement('canvas');
|
||||
this._jsmpegCanvasElement.className = 'media';
|
||||
|
||||
if (!this.cameraConfig?.frigate.camera_name) {
|
||||
return dispatchErrorMessageEvent(this, localize('error.no_camera_name'), {
|
||||
context: this.cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const url = await this._getURL();
|
||||
if (url) {
|
||||
this._jsmpegVideoPlayer = await this._createJSMPEGPlayer(url);
|
||||
this._refreshPlayerTimerID = window.setTimeout(() => {
|
||||
this.requestUpdate();
|
||||
}, (URL_SIGN_EXPIRY_SECONDS - URL_SIGN_REFRESH_THRESHOLD_SECONDS) * 1000);
|
||||
} else {
|
||||
dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_sign'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
const _render = async (): Promise<TemplateResult | void> => {
|
||||
await this._refreshPlayer();
|
||||
|
||||
if (!this._jsmpegVideoPlayer || !this._jsmpegCanvasElement) {
|
||||
return dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_player'));
|
||||
}
|
||||
return html`${this._jsmpegCanvasElement}`;
|
||||
};
|
||||
return html`${until(
|
||||
_render(),
|
||||
renderProgressIndicator({
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
}),
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveJSMPEGStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-live-jsmpeg': FrigateCardLiveJSMPEG;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { Task } from '@lit-labs/task';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import liveWebRTCStyle from '../../scss/live-webrtc.scss';
|
||||
import {
|
||||
CameraConfig,
|
||||
CardWideConfig,
|
||||
FrigateCardError,
|
||||
WebRTCCardConfig,
|
||||
} from '../../types.js';
|
||||
import { contentsChanged } from '../../utils/basic.js';
|
||||
import { dispatchMediaLoadedEvent } from '../../utils/media-info.js';
|
||||
import { dispatchErrorMessageEvent, renderProgressIndicator } from '../message.js';
|
||||
import { renderTask } from '../../utils/task.js';
|
||||
|
||||
// Create a wrapper for AlexxIT's WebRTC card
|
||||
// - https://github.com/AlexxIT/WebRTC
|
||||
@customElement('frigate-card-live-webrtc-card')
|
||||
export class FrigateCardLiveWebRTCCard extends LitElement {
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public webRTCConfig?: WebRTCCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected hass?: HomeAssistant;
|
||||
|
||||
// A task to await the load of the WebRTC component.
|
||||
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
||||
|
||||
/**
|
||||
* Play the video.
|
||||
*/
|
||||
public play(): void {
|
||||
this._getPlayer()
|
||||
?.play()
|
||||
.catch(() => {
|
||||
// WebRTC appears to generate additional spurious load events, which may
|
||||
// result in loads after a play() call, which causes the browser to spam
|
||||
// the logs unless the promise rejection is handled here.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the video.
|
||||
*/
|
||||
public pause(): void {
|
||||
this._getPlayer()?.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the video.
|
||||
*/
|
||||
public mute(): void {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the video.
|
||||
*/
|
||||
public unmute(): void {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek the video.
|
||||
*/
|
||||
public seek(seconds: number): void {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.currentTime = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying video player.
|
||||
* @returns The player or `null` if not found.
|
||||
*/
|
||||
protected _getPlayer(): HTMLVideoElement | null {
|
||||
return this.renderRoot?.querySelector('#video') as HTMLVideoElement | null;
|
||||
}
|
||||
|
||||
protected async _getWebRTCCardElement(): Promise<
|
||||
CustomElementConstructor | undefined
|
||||
> {
|
||||
await customElements.whenDefined('webrtc-camera');
|
||||
return customElements.get('webrtc-camera');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the WebRTC element. May throw.
|
||||
*/
|
||||
protected _createWebRTC(): HTMLElement | null {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const webrtcElement = this._webrtcTask.value;
|
||||
if (webrtcElement && this.hass) {
|
||||
const webrtc = new webrtcElement() as HTMLElement & {
|
||||
hass: HomeAssistant;
|
||||
setConfig: (config: Record<string, unknown>) => void;
|
||||
};
|
||||
const config = { ...this.webRTCConfig };
|
||||
|
||||
// If the live WebRTC configuration does not specify a URL/entity to use,
|
||||
// then take values from the camera configuration instead (if there are
|
||||
// any).
|
||||
if (!config.url) {
|
||||
config.url = this.cameraConfig?.webrtc_card?.url;
|
||||
}
|
||||
if (!config.entity) {
|
||||
config.entity = this.cameraConfig?.webrtc_card?.entity;
|
||||
}
|
||||
webrtc.setConfig(config);
|
||||
webrtc.hass = this.hass;
|
||||
return webrtc;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
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 },
|
||||
);
|
||||
}
|
||||
if (webrtcElement) {
|
||||
// Set the id to ensure that the relevant CSS styles will have
|
||||
// sufficient specifity to overcome some styles that are otherwise
|
||||
// applied to <ha-card> in Safari.
|
||||
webrtcElement.id = 'webrtc';
|
||||
}
|
||||
return html`${webrtcElement}`;
|
||||
};
|
||||
|
||||
// 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, {
|
||||
inProgressFunc: () =>
|
||||
renderProgressIndicator({
|
||||
message: localize('error.webrtc_card_waiting'),
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated lifecycle callback.
|
||||
*/
|
||||
public updated(): void {
|
||||
// Extract the video component after it has been rendered and generate the
|
||||
// media load event.
|
||||
this.updateComplete.then(() => {
|
||||
const video = this._getPlayer();
|
||||
if (video) {
|
||||
const onloadeddata = video.onloadeddata;
|
||||
|
||||
video.onloadeddata = (e) => {
|
||||
if (onloadeddata) {
|
||||
onloadeddata.call(video, e);
|
||||
}
|
||||
dispatchMediaLoadedEvent(this, video);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveWebRTCStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-live-webrtc-card': FrigateCardLiveWebRTCCard;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||
import { Task } from '@lit-labs/task';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { EmblaOptionsType } from 'embla-carousel';
|
||||
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
||||
import {
|
||||
@@ -15,67 +12,49 @@ import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
import { ConditionState, getOverriddenConfig } from '../card-condition.js';
|
||||
import { dispatchMessageEvent, renderProgressIndicator } from '../components/message.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import liveFrigateStyle from '../scss/live-frigate.scss';
|
||||
import liveJSMPEGStyle from '../scss/live-jsmpeg.scss';
|
||||
import liveWebRTCStyle from '../scss/live-webrtc.scss';
|
||||
import liveStyle from '../scss/live.scss';
|
||||
import liveCarouselStyle from '../scss/live-carousel.scss';
|
||||
import liveProviderStyle from '../scss/live-provider.scss';
|
||||
import { ConditionState, getOverriddenConfig } from '../../card-condition.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import liveStyle from '../../scss/live.scss';
|
||||
import liveCarouselStyle from '../../scss/live-carousel.scss';
|
||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||
import {
|
||||
CameraConfig,
|
||||
CardWideConfig,
|
||||
ExtendedHomeAssistant,
|
||||
frigateCardConfigDefaults,
|
||||
FrigateCardError,
|
||||
FrigateCardMediaPlayer,
|
||||
JSMPEGConfig,
|
||||
LiveConfig,
|
||||
LiveOverrides,
|
||||
LiveProvider,
|
||||
MediaLoadedInfo,
|
||||
Message,
|
||||
TransitionEffect,
|
||||
WebRTCCardConfig,
|
||||
} from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { contentsChanged, errorToConsole } from '../utils/basic.js';
|
||||
import { getCameraIcon, getCameraTitle } from '../utils/camera.js';
|
||||
import { homeAssistantSignPath } from '../utils/ha';
|
||||
import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js';
|
||||
} from '../../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||
import { contentsChanged } from '../../utils/basic.js';
|
||||
import { getCameraIcon, getCameraTitle } from '../../utils/camera.js';
|
||||
import { getFullDependentBrowseMediaQueryParameters } from '../../utils/ha/browse-media.js';
|
||||
import {
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaUnloadedEvent,
|
||||
} from '../utils/media-info.js';
|
||||
import { dispatchViewContextChangeEvent, View } from '../view.js';
|
||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||
import { Lazyload } from './embla-plugins/lazyload.js';
|
||||
} from '../../utils/media-info.js';
|
||||
import { dispatchViewContextChangeEvent, View } from '../../view.js';
|
||||
import { AutoMediaPlugin } from './../embla-plugins/automedia.js';
|
||||
import { Lazyload } from './../embla-plugins/lazyload.js';
|
||||
import {
|
||||
FrigateCardMediaCarousel,
|
||||
wrapMediaLoadedEventForCarousel,
|
||||
wrapMediaUnloadedEventForCarousel,
|
||||
} from './media-carousel.js';
|
||||
import { dispatchErrorMessageEvent } from './message.js';
|
||||
import './next-prev-control.js';
|
||||
import './title-control.js';
|
||||
import './surround.js';
|
||||
import '../patches/ha-camera-stream';
|
||||
import { EmblaCarouselPlugins } from './carousel.js';
|
||||
import { renderTask } from '../utils/task.js';
|
||||
} from '../media-carousel.js';
|
||||
import '../next-prev-control.js';
|
||||
import '../title-control.js';
|
||||
import '../surround.js';
|
||||
import '../../patches/ha-camera-stream';
|
||||
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import './image';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||
import { DataManager } from '../utils/data-manager.js';
|
||||
|
||||
// Number of seconds a signed URL is valid for.
|
||||
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||
|
||||
// Number of seconds before the expiry to trigger a refresh.
|
||||
const URL_SIGN_REFRESH_THRESHOLD_SECONDS = 1 * 60 * 60;
|
||||
import '../image';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||
import { DataManager } from '../../utils/data-manager.js';
|
||||
|
||||
@customElement('frigate-card-live')
|
||||
export class FrigateCardLive extends LitElement {
|
||||
@@ -492,7 +471,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
|
||||
const liveProvider = slide?.querySelector(
|
||||
'frigate-card-live-provider',
|
||||
) as FrigateCardLiveProvider;
|
||||
) as FrigateCardLiveProvider | null;
|
||||
if (liveProvider) {
|
||||
liveProvider.disabled = action !== 'load';
|
||||
}
|
||||
@@ -782,6 +761,15 @@ export class FrigateCardLiveProvider extends LitElement {
|
||||
if (changedProps.has('liveConfig')) {
|
||||
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
|
||||
}
|
||||
if (changedProps.has('cameraConfig')) {
|
||||
if (this._getResolvedProvider() === 'frigate-jsmpeg') {
|
||||
import('./live-jsmpeg.js');
|
||||
} else if (this._getResolvedProvider() === 'ha') {
|
||||
import('./live-ha.js');
|
||||
} else if (this._getResolvedProvider() === 'webrtc-card') {
|
||||
import('./live-webrtc.js');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -856,522 +844,8 @@ export class FrigateCardLiveProvider extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('frigate-card-live-ha')
|
||||
export class FrigateCardLiveFrigate extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
protected _playerRef: Ref<FrigateCardLiveFrigate> = createRef();
|
||||
|
||||
/**
|
||||
* Play the video.
|
||||
*/
|
||||
public play(): void {
|
||||
this._playerRef.value?.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the video.
|
||||
*/
|
||||
public pause(): void {
|
||||
this._playerRef.value?.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the video.
|
||||
*/
|
||||
public mute(): void {
|
||||
this._playerRef.value?.mute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the video.
|
||||
*/
|
||||
public unmute(): void {
|
||||
this._playerRef.value?.unmute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek the video.
|
||||
*/
|
||||
public seek(seconds: number): void {
|
||||
this._playerRef.value?.seek(seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.cameraConfig?.camera_entity) {
|
||||
return dispatchErrorMessageEvent(this, localize('error.no_live_camera'), {
|
||||
context: this.cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
|
||||
if (!stateObj) {
|
||||
return dispatchErrorMessageEvent(this, localize('error.live_camera_not_found'), {
|
||||
context: this.cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
if (stateObj.state === 'unavailable') {
|
||||
// Don't treat state unavailability as an error per se.
|
||||
return dispatchMessageEvent(
|
||||
this,
|
||||
localize('error.live_camera_unavailable'),
|
||||
'info',
|
||||
{
|
||||
icon: 'mdi:connection',
|
||||
context: getCameraTitle(this.hass, this.cameraConfig),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return html` <frigate-card-ha-camera-stream
|
||||
${ref(this._playerRef)}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.controls=${true}
|
||||
.muted=${true}
|
||||
>
|
||||
</frigate-card-ha-camera-stream>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveFrigateStyle);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a wrapper for AlexxIT's WebRTC card
|
||||
// - https://github.com/AlexxIT/WebRTC
|
||||
@customElement('frigate-card-live-webrtc-card')
|
||||
export class FrigateCardLiveWebRTCCard extends LitElement {
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public webRTCConfig?: WebRTCCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected hass?: HomeAssistant;
|
||||
|
||||
// A task to await the load of the WebRTC component.
|
||||
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
||||
|
||||
/**
|
||||
* Play the video.
|
||||
*/
|
||||
public play(): void {
|
||||
this._getPlayer()
|
||||
?.play()
|
||||
.catch(() => {
|
||||
// WebRTC appears to generate additional spurious load events, which may
|
||||
// result in loads after a play() call, which causes the browser to spam
|
||||
// the logs unless the promise rejection is handled here.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the video.
|
||||
*/
|
||||
public pause(): void {
|
||||
this._getPlayer()?.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the video.
|
||||
*/
|
||||
public mute(): void {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the video.
|
||||
*/
|
||||
public unmute(): void {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek the video.
|
||||
*/
|
||||
public seek(seconds: number): void {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.currentTime = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying video player.
|
||||
* @returns The player or `null` if not found.
|
||||
*/
|
||||
protected _getPlayer(): HTMLVideoElement | null {
|
||||
return this.renderRoot?.querySelector('#video') as HTMLVideoElement | null;
|
||||
}
|
||||
|
||||
protected async _getWebRTCCardElement(): Promise<
|
||||
CustomElementConstructor | undefined
|
||||
> {
|
||||
await customElements.whenDefined('webrtc-camera');
|
||||
return customElements.get('webrtc-camera');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the WebRTC element. May throw.
|
||||
*/
|
||||
protected _createWebRTC(): HTMLElement | null {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const webrtcElement = this._webrtcTask.value;
|
||||
if (webrtcElement && this.hass) {
|
||||
const webrtc = new webrtcElement() as HTMLElement & {
|
||||
hass: HomeAssistant;
|
||||
setConfig: (config: Record<string, unknown>) => void;
|
||||
};
|
||||
const config = { ...this.webRTCConfig };
|
||||
|
||||
// If the live WebRTC configuration does not specify a URL/entity to use,
|
||||
// then take values from the camera configuration instead (if there are
|
||||
// any).
|
||||
if (!config.url) {
|
||||
config.url = this.cameraConfig?.webrtc_card?.url;
|
||||
}
|
||||
if (!config.entity) {
|
||||
config.entity = this.cameraConfig?.webrtc_card?.entity;
|
||||
}
|
||||
webrtc.setConfig(config);
|
||||
webrtc.hass = this.hass;
|
||||
return webrtc;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
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 },
|
||||
);
|
||||
}
|
||||
if (webrtcElement) {
|
||||
// Set the id to ensure that the relevant CSS styles will have
|
||||
// sufficient specifity to overcome some styles that are otherwise
|
||||
// applied to <ha-card> in Safari.
|
||||
webrtcElement.id = 'webrtc';
|
||||
}
|
||||
return html`${webrtcElement}`;
|
||||
};
|
||||
|
||||
// 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, {
|
||||
inProgressFunc: () =>
|
||||
renderProgressIndicator({
|
||||
message: localize('error.webrtc_card_waiting'),
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated lifecycle callback.
|
||||
*/
|
||||
public updated(): void {
|
||||
// Extract the video component after it has been rendered and generate the
|
||||
// media load event.
|
||||
this.updateComplete.then(() => {
|
||||
const video = this._getPlayer();
|
||||
if (video) {
|
||||
const onloadeddata = video.onloadeddata;
|
||||
|
||||
video.onloadeddata = (e) => {
|
||||
if (onloadeddata) {
|
||||
onloadeddata.call(video, e);
|
||||
}
|
||||
dispatchMediaLoadedEvent(this, video);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveWebRTCStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('frigate-card-live-jsmpeg')
|
||||
export class FrigateCardLiveJSMPEG extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public jsmpegConfig?: JSMPEGConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected hass?: ExtendedHomeAssistant;
|
||||
|
||||
protected _jsmpegCanvasElement?: HTMLCanvasElement;
|
||||
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
||||
protected _refreshPlayerTimerID?: number;
|
||||
|
||||
/**
|
||||
* Play the video.
|
||||
*/
|
||||
public play(): void {
|
||||
this._jsmpegVideoPlayer?.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the video.
|
||||
*/
|
||||
public pause(): void {
|
||||
this._jsmpegVideoPlayer?.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the video (included for completeness, JSMPEG live disables audio as
|
||||
* Frigate does not encode it).
|
||||
*/
|
||||
public mute(): void {
|
||||
const player = this._jsmpegVideoPlayer?.player;
|
||||
if (player) {
|
||||
player.volume = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the video (included for completeness, JSMPEG live disables audio as
|
||||
* Frigate does not encode it).
|
||||
*/
|
||||
public unmute(): void {
|
||||
const player = this._jsmpegVideoPlayer?.player;
|
||||
if (player) {
|
||||
player.volume = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek the video (unsupported).
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public seek(_seconds: number): void {
|
||||
// JSMPEG does not support seeking.
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a signed player URL.
|
||||
* @returns A URL or null.
|
||||
*/
|
||||
protected async _getURL(): Promise<string | null> {
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.cameraConfig?.frigate.client_id ||
|
||||
!this.cameraConfig?.frigate.camera_name
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let response: string | null | undefined;
|
||||
try {
|
||||
response = await homeAssistantSignPath(
|
||||
this.hass,
|
||||
`/api/frigate/${this.cameraConfig.frigate.client_id}` +
|
||||
`/jsmpeg/${this.cameraConfig.frigate.camera_name}`,
|
||||
URL_SIGN_EXPIRY_SECONDS,
|
||||
);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
return null;
|
||||
}
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
return response.replace(/^http/i, 'ws');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSMPEG player.
|
||||
* @param url The URL for the player to connect to.
|
||||
* @returns A JSMPEG player.
|
||||
*/
|
||||
protected async _createJSMPEGPlayer(url: string): Promise<JSMpeg.VideoElement> {
|
||||
return new Promise<JSMpeg.VideoElement>((resolve) => {
|
||||
let videoDecoded = false;
|
||||
const player = new JSMpeg.VideoElement(
|
||||
this,
|
||||
url,
|
||||
{
|
||||
canvas: this._jsmpegCanvasElement,
|
||||
},
|
||||
{
|
||||
// The media carousel may automatically pause when the browser tab is
|
||||
// inactive, JSMPEG does not need to also do so independently.
|
||||
pauseWhenHidden: false,
|
||||
autoplay: false,
|
||||
protocols: [],
|
||||
audio: false,
|
||||
videoBufferSize: 1024 * 1024 * 4,
|
||||
|
||||
// Override with user-specified options.
|
||||
...this.jsmpegConfig?.options,
|
||||
|
||||
// Don't allow the player to internally reconnect, as it may re-use a
|
||||
// URL with a (newly) invalid signature, e.g. during a Home Assistant
|
||||
// restart.
|
||||
reconnectInterval: 0,
|
||||
onVideoDecode: () => {
|
||||
// This is the only callback that is called after the dimensions
|
||||
// are available. It's called on every frame decode, so just
|
||||
// ignore any subsequent calls.
|
||||
if (!videoDecoded && this._jsmpegCanvasElement) {
|
||||
videoDecoded = true;
|
||||
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement);
|
||||
resolve(player);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset / destroy the player.
|
||||
*/
|
||||
protected _resetPlayer(): void {
|
||||
if (this._refreshPlayerTimerID) {
|
||||
window.clearTimeout(this._refreshPlayerTimerID);
|
||||
this._refreshPlayerTimerID = undefined;
|
||||
}
|
||||
if (this._jsmpegVideoPlayer) {
|
||||
try {
|
||||
this._jsmpegVideoPlayer.destroy();
|
||||
} catch (err) {
|
||||
// Pass.
|
||||
}
|
||||
this._jsmpegVideoPlayer = undefined;
|
||||
}
|
||||
if (this._jsmpegCanvasElement) {
|
||||
this._jsmpegCanvasElement.remove();
|
||||
this._jsmpegCanvasElement = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component connected callback.
|
||||
*/
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.isConnected) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
if (!this.isConnected) {
|
||||
this._resetPlayer();
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the JSMPEG player.
|
||||
*/
|
||||
protected async _refreshPlayer(): Promise<void> {
|
||||
this._resetPlayer();
|
||||
|
||||
this._jsmpegCanvasElement = document.createElement('canvas');
|
||||
this._jsmpegCanvasElement.className = 'media';
|
||||
|
||||
if (!this.cameraConfig?.frigate.camera_name) {
|
||||
return dispatchErrorMessageEvent(this, localize('error.no_camera_name'), {
|
||||
context: this.cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const url = await this._getURL();
|
||||
if (url) {
|
||||
this._jsmpegVideoPlayer = await this._createJSMPEGPlayer(url);
|
||||
this._refreshPlayerTimerID = window.setTimeout(() => {
|
||||
this.requestUpdate();
|
||||
}, (URL_SIGN_EXPIRY_SECONDS - URL_SIGN_REFRESH_THRESHOLD_SECONDS) * 1000);
|
||||
} else {
|
||||
dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_sign'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
const _render = async (): Promise<TemplateResult | void> => {
|
||||
await this._refreshPlayer();
|
||||
|
||||
if (!this._jsmpegVideoPlayer || !this._jsmpegCanvasElement) {
|
||||
return dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_player'));
|
||||
}
|
||||
return html`${this._jsmpegCanvasElement}`;
|
||||
};
|
||||
return html`${until(
|
||||
_render(),
|
||||
renderProgressIndicator({
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
}),
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveJSMPEGStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-live-jsmpeg': FrigateCardLiveJSMPEG;
|
||||
'frigate-card-live-webrtc-card': FrigateCardLiveWebRTCCard;
|
||||
'frigate-card-live-ha': FrigateCardLiveFrigate;
|
||||
'frigate-card-live-provider': FrigateCardLiveProvider;
|
||||
'frigate-card-live-carousel': FrigateCardLiveCarousel;
|
||||
'frigate-card-live': FrigateCardLive;
|
||||
@@ -29,7 +29,6 @@ import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||
|
||||
import './surround-basic.js';
|
||||
import './timeline-core.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
|
||||
interface ThumbnailViewContext {
|
||||
@@ -122,6 +121,10 @@ export class FrigateCardSurround extends LitElement {
|
||||
* Called before each update.
|
||||
*/
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (this.timelineConfig?.mode && this.timelineConfig.mode !== 'none') {
|
||||
import('./timeline-core.js');
|
||||
}
|
||||
|
||||
// Once the component will certainly update, dispatch a media request. Only
|
||||
// do so if properties relevant to the request have changed (as per their
|
||||
// hasChanged).
|
||||
|
||||
@@ -19,9 +19,8 @@ import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { isEqual, throttle } from 'lodash-es';
|
||||
import { ViewContext } from 'view';
|
||||
import { DataView, DataSet } from 'vis-data/esnext';
|
||||
import type { DataGroupCollectionType, IdType } from 'vis-timeline/esnext';
|
||||
import {
|
||||
DataGroupCollectionType,
|
||||
IdType,
|
||||
Timeline,
|
||||
TimelineEventPropertiesResult,
|
||||
TimelineItem,
|
||||
@@ -458,7 +457,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
this.hass,
|
||||
this.dataManager,
|
||||
this.cameras,
|
||||
this.view, {
|
||||
this.view,
|
||||
{
|
||||
cameraIDs: new Set([String(properties.group)]),
|
||||
targetTime:
|
||||
properties.what === 'background' ? properties.time : window.end,
|
||||
@@ -471,7 +471,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
this.hass,
|
||||
this.dataManager,
|
||||
this.cameras,
|
||||
this.view, {
|
||||
this.view,
|
||||
{
|
||||
targetTime: window.end,
|
||||
},
|
||||
);
|
||||
@@ -483,12 +484,13 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
this.hass,
|
||||
this.dataManager,
|
||||
this.cameras,
|
||||
this.view, {
|
||||
this.view,
|
||||
{
|
||||
cameraIDs: this._getAllCameraIDs(),
|
||||
start: startOfHour(properties.time),
|
||||
end: endOfHour(properties.time),
|
||||
targetTime: properties.time,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
|
||||
@@ -5,7 +5,6 @@ import { CameraConfig, ExtendedHomeAssistant, TimelineConfig } from '../types';
|
||||
import { DataManager } from '../utils/data-manager';
|
||||
import { View } from '../view';
|
||||
import './surround.js';
|
||||
import './timeline-core.js';
|
||||
|
||||
// This file is kept separate from timeline-core.ts to avoid a circular dependency:
|
||||
// FrigateCardTimeline ->
|
||||
@@ -29,6 +28,13 @@ export class FrigateCardTimeline extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public dataManager?: DataManager;
|
||||
|
||||
/**
|
||||
* Called on first update.
|
||||
*/
|
||||
protected firstUpdated(): void {
|
||||
import('./timeline-core.js');
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
"info": "This card configuration has manually specified overrides configured which may override values shown in the visual editor, please consult the code editor to view/modify these overrides"
|
||||
},
|
||||
"performance": {
|
||||
"warning": "This card is in low profile mode so many defaults have changed to optimize performance",
|
||||
"warning": "This card is in low profile mode so defaults have changed to optimize performance",
|
||||
"features": {
|
||||
"editor_label": "Feature Options",
|
||||
"animated_progress_indicator": "Animated Progress Indicator"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { DataSet, DataView } from 'vis-data/esnext';
|
||||
import { IdType, TimelineItem } from 'vis-timeline/esnext';
|
||||
import type { IdType, TimelineItem } from 'vis-timeline/esnext';
|
||||
import { CAMERA_BIRDSEYE } from '../const.js';
|
||||
import {
|
||||
CameraConfig,
|
||||
|
||||
Reference in New Issue
Block a user