Enable smooth panning between past/live.
This commit is contained in:
@@ -75,17 +75,17 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
||||
return this._player?.video?.play();
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
public async pause(): Promise<void> {
|
||||
this._player?.video?.pause();
|
||||
}
|
||||
|
||||
public mute(): void {
|
||||
public async mute(): Promise<void> {
|
||||
if (this._player?.video) {
|
||||
this._player.video.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public unmute(): void {
|
||||
public async unmute(): Promise<void> {
|
||||
if (this._player?.video) {
|
||||
this._player.video.muted = false;
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
||||
return this._player?.video.muted ?? true;
|
||||
}
|
||||
|
||||
public seek(seconds: number): void {
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
if (this._player?.video) {
|
||||
this._player.video.currentTime = seconds;
|
||||
}
|
||||
|
||||
@@ -23,15 +23,15 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
||||
return this._playerRef.value?.play();
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
public async pause(): Promise<void> {
|
||||
this._playerRef.value?.pause();
|
||||
}
|
||||
|
||||
public mute(): void {
|
||||
public async mute(): Promise<void> {
|
||||
this._playerRef.value?.mute();
|
||||
}
|
||||
|
||||
public unmute(): void {
|
||||
public async unmute(): Promise<void> {
|
||||
this._playerRef.value?.unmute();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
||||
return this._playerRef.value?.isMuted() ?? true;
|
||||
}
|
||||
|
||||
public seek(seconds: number): void {
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
this._playerRef.value?.seek(seconds);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,15 +21,15 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
||||
this._playing = true;
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
public async pause(): Promise<void> {
|
||||
this._playing = false;
|
||||
}
|
||||
|
||||
public mute(): void {
|
||||
public async mute(): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
public unmute(): void {
|
||||
public async unmute(): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public seek(_seconds: number): void {
|
||||
public async seek(_seconds: number): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
||||
|
||||
return html` <frigate-card-image
|
||||
.imageConfig=${{
|
||||
mode: this.cameraConfig.image.url ? 'url' as const : 'camera' as const,
|
||||
mode: this.cameraConfig.image.url ? ('url' as const) : ('camera' as const),
|
||||
refresh_seconds: this._playing ? this.cameraConfig.image.refresh_seconds : 0,
|
||||
url: this.cameraConfig.image.url,
|
||||
// Don't need to pass layout options as FrigateCardLiveProvider has
|
||||
|
||||
@@ -43,18 +43,18 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
||||
return this._jsmpegVideoPlayer?.play();
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
public async pause(): Promise<void> {
|
||||
this._jsmpegVideoPlayer?.stop();
|
||||
}
|
||||
|
||||
public mute(): void {
|
||||
public async mute(): Promise<void> {
|
||||
const player = this._jsmpegVideoPlayer?.player;
|
||||
if (player) {
|
||||
player.volume = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public unmute(): void {
|
||||
public async unmute(): Promise<void> {
|
||||
const player = this._jsmpegVideoPlayer?.player;
|
||||
if (player) {
|
||||
player.volume = 1;
|
||||
@@ -66,7 +66,7 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public seek(_seconds: number): void {
|
||||
public async seek(_seconds: number): Promise<void> {
|
||||
// JSMPEG does not support seeking.
|
||||
}
|
||||
|
||||
|
||||
@@ -44,18 +44,18 @@ export class FrigateCardLiveWebRTCCard
|
||||
return this._getPlayer()?.play();
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
public async pause(): Promise<void> {
|
||||
this._getPlayer()?.pause();
|
||||
}
|
||||
|
||||
public mute(): void {
|
||||
public async mute(): Promise<void> {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public unmute(): void {
|
||||
public async unmute(): Promise<void> {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.muted = false;
|
||||
@@ -66,7 +66,7 @@ export class FrigateCardLiveWebRTCCard
|
||||
return this._getPlayer()?.muted ?? true;
|
||||
}
|
||||
|
||||
public seek(seconds: number): void {
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.currentTime = seconds;
|
||||
@@ -78,7 +78,7 @@ export class FrigateCardLiveWebRTCCard
|
||||
|
||||
// Reset the player when reconnected to the DOM.
|
||||
// https://github.com/dermotduffy/frigate-hass-card/issues/996
|
||||
this.requestUpdate();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+100
-67
@@ -69,6 +69,11 @@ declare module 'view' {
|
||||
}
|
||||
}
|
||||
|
||||
interface LastMediaLoadedInfo {
|
||||
mediaLoadedInfo: MediaLoadedInfo;
|
||||
source: EventTarget;
|
||||
}
|
||||
|
||||
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
|
||||
|
||||
/**
|
||||
@@ -141,9 +146,11 @@ export class FrigateCardLive extends LitElement {
|
||||
// foreground and background (in preload mode).
|
||||
protected _intersectionObserver: IntersectionObserver;
|
||||
|
||||
// MediaLoadedInfo object and message from the underlying live object. In the
|
||||
// case of pre-loading these may be propagated upwards later.
|
||||
protected _backgroundMediaLoadedInfo: MediaLoadedInfo | null = null;
|
||||
// MediaLoadedInfo object and target from the underlying live object. In the
|
||||
// case of pre-loading these may be propagated later (from the original
|
||||
// source).
|
||||
protected _lastMediaLoadedInfo: LastMediaLoadedInfo | null = null;
|
||||
|
||||
protected _messageReceivedPostRender = false;
|
||||
protected _renderKey = 0;
|
||||
|
||||
@@ -164,12 +171,17 @@ export class FrigateCardLive extends LitElement {
|
||||
if (
|
||||
!this._inBackground &&
|
||||
!this._messageReceivedPostRender &&
|
||||
this._backgroundMediaLoadedInfo
|
||||
this._lastMediaLoadedInfo
|
||||
) {
|
||||
// If this isn't being rendered in the background, the last render did not
|
||||
// generate a message and there's a saved MediaInfo, dispatch it upwards.
|
||||
dispatchExistingMediaLoadedInfoAsEvent(this, this._backgroundMediaLoadedInfo);
|
||||
this._backgroundMediaLoadedInfo = null;
|
||||
dispatchExistingMediaLoadedInfoAsEvent(
|
||||
// Specifically dispatch the event "where it came from", as otherwise
|
||||
// the intermediate layers (e.g. media-carousel which controls the title
|
||||
// popups) will not re-receive the events.
|
||||
this._lastMediaLoadedInfo.source,
|
||||
this._lastMediaLoadedInfo.mediaLoadedInfo,
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger a re-render which may be necessary if the prior render resulted
|
||||
@@ -217,18 +229,10 @@ export class FrigateCardLive extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = getOverriddenConfig(
|
||||
this.liveConfig,
|
||||
this.liveOverrides,
|
||||
this.conditionState,
|
||||
) as LiveConfig;
|
||||
|
||||
// Notes:
|
||||
// - See use of liveConfig and not config below -- the carousel will
|
||||
// independently override the liveConfig to reflect the camera in the
|
||||
// carousel (not necessarily the selected camera).
|
||||
// - Fetching of thumbnails is disabled as long as live view is the
|
||||
// background.
|
||||
// - 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
|
||||
@@ -236,34 +240,7 @@ export class FrigateCardLive extends LitElement {
|
||||
// is received when the card is in the background).
|
||||
const result = html`${keyed(
|
||||
this._renderKey,
|
||||
html`<frigate-card-surround
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.fetchMedia=${config.controls.thumbnails.media}
|
||||
.thumbnailConfig=${config.controls.thumbnails}
|
||||
.timelineConfig=${config.controls.timeline}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.inBackground=${this._inBackground}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
@frigate-card:message=${(ev: CustomEvent<Message>) => {
|
||||
this._renderKey++;
|
||||
this._messageReceivedPostRender = true;
|
||||
if (this._inBackground) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}}
|
||||
@frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||
if (this._inBackground) {
|
||||
this._backgroundMediaLoadedInfo = ev.detail;
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}}
|
||||
@frigate-card:view:change=${(ev: CustomEvent<View>) => {
|
||||
if (this._inBackground) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
html`
|
||||
<frigate-card-live-carousel
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
@@ -273,9 +250,30 @@ export class FrigateCardLive extends LitElement {
|
||||
.liveOverrides=${this.liveOverrides}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.cameraManager=${this.cameraManager}
|
||||
@frigate-card:message=${(ev: CustomEvent<Message>) => {
|
||||
this._renderKey++;
|
||||
this._messageReceivedPostRender = true;
|
||||
if (this._inBackground) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}}
|
||||
@frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||
this._lastMediaLoadedInfo = {
|
||||
source: ev.composedPath()[0],
|
||||
mediaLoadedInfo: ev.detail,
|
||||
};
|
||||
if (this._inBackground) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}}
|
||||
@frigate-card:view:change=${(ev: CustomEvent<View>) => {
|
||||
if (this._inBackground) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
</frigate-card-live-carousel>
|
||||
</frigate-card-surround>`,
|
||||
`,
|
||||
)}`;
|
||||
|
||||
this._messageReceivedPostRender = false;
|
||||
@@ -327,18 +325,22 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const frigateCardMediaCarousel = this._refMediaCarousel.value;
|
||||
|
||||
if (frigateCardMediaCarousel && changedProperties.has('inBackground')) {
|
||||
// If this has changed to be in the background (i.e. preloaded but not
|
||||
// visible) take the appropriate play/pause/mute/unmute actions.
|
||||
if (this.inBackground) {
|
||||
frigateCardMediaCarousel.autoPause();
|
||||
frigateCardMediaCarousel.autoMute();
|
||||
} else {
|
||||
frigateCardMediaCarousel.autoPlay();
|
||||
frigateCardMediaCarousel.autoUnmute();
|
||||
}
|
||||
if (changedProperties.has('inBackground')) {
|
||||
this.updateComplete.then(async () => {
|
||||
const frigateCardMediaCarousel = this._refMediaCarousel.value;
|
||||
if (frigateCardMediaCarousel) {
|
||||
await frigateCardMediaCarousel.updateComplete;
|
||||
// If this has changed to be in the background (i.e. preloaded but not
|
||||
// visible) take the appropriate play/pause/mute/unmute actions.
|
||||
if (this.inBackground) {
|
||||
frigateCardMediaCarousel.autoPause();
|
||||
frigateCardMediaCarousel.autoMute();
|
||||
} else {
|
||||
frigateCardMediaCarousel.autoPlay();
|
||||
frigateCardMediaCarousel.autoUnmute();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,21 +724,41 @@ export class FrigateCardLiveProvider
|
||||
@state()
|
||||
protected _isVideoMediaLoaded = false;
|
||||
|
||||
protected _refProvider: Ref<Element & FrigateCardMediaPlayer> = createRef();
|
||||
protected _refProvider: Ref<LitElement & FrigateCardMediaPlayer> = createRef();
|
||||
|
||||
// A note on dynamic imports:
|
||||
//
|
||||
// We gather the dynamic live provider import promises and do not consider the
|
||||
// update of the element complete until these imports have returned. Without
|
||||
// this behavior calls to the media methods (e.g. `mute()`) may throw if the
|
||||
// underlying code is not yet loaded.
|
||||
//
|
||||
// Test case: A card with a non-live view, but live pre-loaded, attempts to
|
||||
// call mute() when the <frigate-card-live> element first renders in the
|
||||
// background. These calls fail without waiting for loading here.
|
||||
protected _importPromises: Promise<unknown>[] = [];
|
||||
|
||||
public async play(): Promise<void> {
|
||||
playMediaMutingIfNecessary(this, this._refProvider.value);
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
await playMediaMutingIfNecessary(this, this._refProvider.value);
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
public async pause(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
this._refProvider.value?.pause();
|
||||
}
|
||||
|
||||
public mute(): void {
|
||||
public async mute(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
this._refProvider.value?.mute();
|
||||
}
|
||||
|
||||
public unmute(): void {
|
||||
public async unmute(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
this._refProvider.value?.unmute();
|
||||
}
|
||||
|
||||
@@ -744,7 +766,9 @@ export class FrigateCardLiveProvider
|
||||
return this._refProvider.value?.isMuted() ?? true;
|
||||
}
|
||||
|
||||
public seek(seconds: number): void {
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
this._refProvider.value?.seek(seconds);
|
||||
}
|
||||
|
||||
@@ -813,25 +837,34 @@ export class FrigateCardLiveProvider
|
||||
if (changedProps.has('liveConfig')) {
|
||||
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
|
||||
if (this.liveConfig?.show_image_during_load) {
|
||||
import('./live-image.js');
|
||||
this._importPromises.push(import('./live-image.js'));
|
||||
}
|
||||
}
|
||||
if (changedProps.has('cameraConfig')) {
|
||||
const provider = this._getResolvedProvider();
|
||||
if (provider === 'jsmpeg') {
|
||||
import('./live-jsmpeg.js');
|
||||
this._importPromises.push(import('./live-jsmpeg.js'));
|
||||
} else if (provider === 'ha') {
|
||||
import('./live-ha.js');
|
||||
this._importPromises.push(import('./live-ha.js'));
|
||||
} else if (provider === 'webrtc-card') {
|
||||
import('./live-webrtc-card.js');
|
||||
this._importPromises.push(import('./live-webrtc-card.js'));
|
||||
} else if (provider === 'image') {
|
||||
import('./live-image.js');
|
||||
this._importPromises.push(import('./live-image.js'));
|
||||
} else if (provider === 'go2rtc') {
|
||||
import('./live-go2rtc.js');
|
||||
this._importPromises.push(import('./live-go2rtc.js'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override async getUpdateComplete(): Promise<boolean> {
|
||||
// See 'A note on dynamic imports' above for explanation of why this is
|
||||
// necessary.
|
||||
const result = await super.getUpdateComplete();
|
||||
await Promise.all(this._importPromises);
|
||||
this._importPromises = [];
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
|
||||
@@ -49,9 +49,6 @@ export class FrigateCardSurround extends LitElement {
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public timelineConfig?: MiniTimelineControlConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public inBackground?: boolean;
|
||||
|
||||
// If fetchMedia is not specified, no fetching is done.
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public fetchMedia?: ClipsOrSnapshotsOrAll;
|
||||
@@ -75,7 +72,6 @@ export class FrigateCardSurround extends LitElement {
|
||||
!this.cameraManager ||
|
||||
!this.cardWideConfig ||
|
||||
!this.fetchMedia ||
|
||||
this.inBackground ||
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
this.view.query ||
|
||||
@@ -131,9 +127,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
// do so if properties relevant to the request have changed (as per their
|
||||
// hasChanged).
|
||||
if (
|
||||
['view', 'fetch', 'browseMediaParams', 'inBackground'].some((prop) =>
|
||||
changedProperties.has(prop),
|
||||
)
|
||||
['view', 'fetch', 'browseMediaParams'].some((prop) => changedProperties.has(prop))
|
||||
) {
|
||||
this._fetchMedia();
|
||||
}
|
||||
@@ -162,7 +156,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || !this.view || !this.thumbnailConfig) {
|
||||
if (!this.hass || !this.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -183,9 +177,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
@frigate-card:thumbnails:open=${(ev: CustomEvent) => changeDrawer(ev, 'open')}
|
||||
@frigate-card:thumbnails:close=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
|
||||
>
|
||||
${this.thumbnailConfig &&
|
||||
this.thumbnailConfig.mode !== 'none' &&
|
||||
!this.inBackground
|
||||
${this.thumbnailConfig && this.thumbnailConfig.mode !== 'none'
|
||||
? html` <frigate-card-thumbnail-carousel
|
||||
slot=${this.thumbnailConfig.mode}
|
||||
.hass=${this.hass}
|
||||
@@ -215,15 +207,14 @@ export class FrigateCardSurround extends LitElement {
|
||||
>
|
||||
</frigate-card-thumbnail-carousel>`
|
||||
: ''}
|
||||
${this.timelineConfig?.mode &&
|
||||
this.timelineConfig.mode !== 'none' &&
|
||||
!this.inBackground
|
||||
${this.timelineConfig && this.timelineConfig.mode !== 'none'
|
||||
? html` <frigate-card-timeline-core
|
||||
slot=${this.timelineConfig.mode}
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.itemClickAction=${this.view.isViewerView() ||
|
||||
this.thumbnailConfig.mode === 'none'
|
||||
!this.thumbnailConfig ||
|
||||
this.thumbnailConfig?.mode === 'none'
|
||||
? 'play'
|
||||
: 'select'}
|
||||
.cameraIDs=${this._cameraIDsForTimeline}
|
||||
|
||||
@@ -284,8 +284,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
this._panBehavior === 'pan'
|
||||
? 'mdi:pan-horizontal'
|
||||
: this._panBehavior === 'seek'
|
||||
? 'mdi:movie-search'
|
||||
: 'mdi:file-find';
|
||||
? 'mdi:filmstrip'
|
||||
: 'mdi:lock';
|
||||
|
||||
return html` ${capabilities?.supportsTimeline
|
||||
? html` <div
|
||||
|
||||
@@ -7,11 +7,6 @@ import { View } from '../view/view';
|
||||
import './surround.js';
|
||||
import './timeline-core.js';
|
||||
|
||||
// This file is kept separate from timeline-core.ts to avoid a circular dependency:
|
||||
// FrigateCardTimeline ->
|
||||
// FrigateCardSurround ->
|
||||
// FrigateCardTimelineCore
|
||||
|
||||
@customElement('frigate-card-timeline')
|
||||
export class FrigateCardTimeline extends LitElement {
|
||||
@property({ attribute: false })
|
||||
@@ -38,12 +33,7 @@ export class FrigateCardTimeline extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html` <frigate-card-surround
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
|
||||
.cameraManager=${this.cameraManager}
|
||||
>
|
||||
return html`
|
||||
<frigate-card-timeline-core
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
@@ -56,7 +46,7 @@ export class FrigateCardTimeline extends LitElement {
|
||||
: 'select'}
|
||||
>
|
||||
</frigate-card-timeline-core>
|
||||
</frigate-card-surround>`;
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -142,14 +142,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
|
||||
}
|
||||
|
||||
return html` <frigate-card-surround
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
|
||||
.timelineConfig=${this.viewerConfig.controls.timeline}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
return html`
|
||||
<frigate-card-viewer-carousel
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
@@ -159,7 +152,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</frigate-card-viewer-carousel>
|
||||
</frigate-card-surround>`;
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -565,17 +558,17 @@ export class FrigateCardViewerProvider
|
||||
protected _refVideoProvider: Ref<HTMLVideoElement> = createRef();
|
||||
|
||||
public async play(): Promise<void> {
|
||||
playMediaMutingIfNecessary(
|
||||
await playMediaMutingIfNecessary(
|
||||
this,
|
||||
this._refFrigateCardMediaPlayer.value ?? this._refVideoProvider.value,
|
||||
);
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
public async pause(): Promise<void> {
|
||||
(this._refFrigateCardMediaPlayer.value || this._refVideoProvider.value)?.pause();
|
||||
}
|
||||
|
||||
public mute(): void {
|
||||
public async mute(): Promise<void> {
|
||||
if (this._refFrigateCardMediaPlayer.value) {
|
||||
this._refFrigateCardMediaPlayer.value?.mute();
|
||||
} else if (this._refVideoProvider.value) {
|
||||
@@ -583,7 +576,7 @@ export class FrigateCardViewerProvider
|
||||
}
|
||||
}
|
||||
|
||||
public unmute(): void {
|
||||
public async unmute(): Promise<void> {
|
||||
if (this._refFrigateCardMediaPlayer.value) {
|
||||
this._refFrigateCardMediaPlayer.value?.mute();
|
||||
} else if (this._refVideoProvider.value) {
|
||||
@@ -600,7 +593,7 @@ export class FrigateCardViewerProvider
|
||||
return true;
|
||||
}
|
||||
|
||||
public seek(seconds: number): void {
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
if (this._refFrigateCardMediaPlayer.value) {
|
||||
return this._refFrigateCardMediaPlayer.value.seek(seconds);
|
||||
} else if (this._refVideoProvider.value) {
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { ConditionState, getOverridesByKey } from '../card-condition';
|
||||
import viewsStyle from '../scss/views.scss';
|
||||
import { CardWideConfig, ExtendedHomeAssistant, FrigateCardConfig } from '../types.js';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import { View } from '../view/view.js';
|
||||
import './surround.js';
|
||||
|
||||
@customElement('frigate-card-views')
|
||||
export class FrigateCardViews extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public view?: Readonly<View>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public config?: FrigateCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public nonOverriddenConfig?: FrigateCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public resolvedMediaCache?: ResolvedMediaCache;
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditionState?: ConditionState;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameras?: ConditionState;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hide?: boolean;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('view') || changedProps.has('config')) {
|
||||
if (this.view?.is('live') || this._shouldLivePreload()) {
|
||||
import('./live/live.js');
|
||||
}
|
||||
if (this.view?.isGalleryView()) {
|
||||
import('./gallery.js');
|
||||
} else if (this.view?.isViewerView()) {
|
||||
import('./viewer.js');
|
||||
} else if (this.view?.is('image')) {
|
||||
import('./image.js');
|
||||
} else if (this.view?.is('timeline')) {
|
||||
import('./timeline.js');
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has('hide')) {
|
||||
if (this.hide) {
|
||||
this.setAttribute('hidden', '');
|
||||
} else {
|
||||
this.removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected shouldUpdate(_: PropertyValues): boolean {
|
||||
// Future: Updates to `hass` and `conditionState` here will be frequent.
|
||||
// Throttling here may be necessary if users report performance degradation
|
||||
// > v5.0.0-beta1 .
|
||||
//
|
||||
// These updates are necessary in these cases:
|
||||
// - conditionState: Required to let `frigate-card-live` calculate its own
|
||||
// overrides.
|
||||
// - hass: Required for anything that needs to sign URLs. Of note is
|
||||
// anything that renders an image (e.g. a thumbnail -- almost everything,
|
||||
// or the main `frigate-card-image` view).
|
||||
//
|
||||
// It should instead be possible to pass conditionState to live only (every
|
||||
// update required), and pass hass only once / 5 minutes (see
|
||||
// HASS_REJECTION_CUTOFF_MS).
|
||||
return true;
|
||||
}
|
||||
|
||||
protected _shouldLivePreload(): boolean {
|
||||
return !!this.config?.live.preload;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
// Only essential items should be added to the below list, since we want the
|
||||
// overall views pane to render in ~almost all cases (e.g. for a camera
|
||||
// initialization error to display, `view` and `cameraConfig` may both be
|
||||
// undefined, but we still want to render).
|
||||
if (!this.hass || !this.config || !this.nonOverriddenConfig) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
// Render but hide the live view if there's a message, or if it's preload
|
||||
// mode and the view is not live.
|
||||
const liveClasses = {
|
||||
hidden: this._shouldLivePreload() && !this.view?.is('live'),
|
||||
};
|
||||
const overallClasses = {
|
||||
hidden: !!this.hide,
|
||||
};
|
||||
|
||||
const thumbnailConfig = this.view?.is('live')
|
||||
? this.config.live.controls.thumbnails
|
||||
: this.view?.isViewerView()
|
||||
? this.config.media_viewer.controls.thumbnails
|
||||
: this.view?.is('timeline')
|
||||
? this.config.timeline.controls.thumbnails
|
||||
: undefined;
|
||||
|
||||
const miniTimelineConfig = this.view?.is('live')
|
||||
? this.config.live.controls.timeline
|
||||
: this.view?.isViewerView()
|
||||
? this.config.media_viewer.controls.timeline
|
||||
: undefined;
|
||||
|
||||
const cameraConfig = this.view
|
||||
? this.cameraManager?.getStore().getCameraConfig(this.view.camera) ?? null
|
||||
: null;
|
||||
|
||||
return html` <frigate-card-surround
|
||||
class="${classMap(overallClasses)}"
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.fetchMedia=${this.view?.is('live')
|
||||
? this.config.live.controls.thumbnails.media
|
||||
: undefined}
|
||||
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
||||
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
${!this.hide && this.view?.is('image') && cameraConfig
|
||||
? html` <frigate-card-image
|
||||
.imageConfig=${this.config.image}
|
||||
.view=${this.view}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${cameraConfig}
|
||||
>
|
||||
</frigate-card-image>`
|
||||
: ``}
|
||||
${!this.hide && this.view?.isGalleryView()
|
||||
? html` <frigate-card-gallery
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.galleryConfig=${this.config.media_gallery}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</frigate-card-gallery>`
|
||||
: ``}
|
||||
${!this.hide && this.view?.isViewerView()
|
||||
? html`
|
||||
<frigate-card-viewer
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.viewerConfig=${this.config.media_viewer}
|
||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</frigate-card-viewer>
|
||||
`
|
||||
: ``}
|
||||
${!this.hide && this.view?.is('timeline')
|
||||
? html` <frigate-card-timeline
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.timelineConfig=${this.config.timeline}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</frigate-card-timeline>`
|
||||
: ``}
|
||||
${
|
||||
// Note: Subtle difference in condition below vs the other views in order
|
||||
// to always render the live view for live.preload mode.
|
||||
|
||||
// Note: <frigate-card-live> uses nonOverriddenConfig rather than the
|
||||
// overriden config as it does it's own overriding as part of the camera
|
||||
// carousel.
|
||||
this._shouldLivePreload() || (!this.hide && this.view?.is('live'))
|
||||
? html`
|
||||
<frigate-card-live
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.liveConfig=${this.nonOverriddenConfig.live}
|
||||
.conditionState=${this.conditionState}
|
||||
.liveOverrides=${getOverridesByKey(this.config.overrides, 'live')}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
class="${classMap(liveClasses)}"
|
||||
>
|
||||
</frigate-card-live>
|
||||
`
|
||||
: ``
|
||||
}
|
||||
</frigate-card-surround>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(viewsStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-views': FrigateCardViews;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user