Enable smooth panning between past/live.

This commit is contained in:
Dermot Duffy
2023-04-09 19:23:34 -07:00
parent b3bb56298a
commit e1d74dc35a
26 changed files with 474 additions and 339 deletions
+4 -4
View File
@@ -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;
}
+4 -4
View File
@@ -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);
}
+5 -5
View File
@@ -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
+4 -4
View File
@@ -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.
}
+5 -5
View File
@@ -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
View File
@@ -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.