Initial lazy loading support for carousel.

This commit is contained in:
Dermot Duffy
2021-10-26 11:07:21 -07:00
parent 30adb637a8
commit 8d7e5e9b2e
3 changed files with 102 additions and 16 deletions
+1
View File
@@ -729,6 +729,7 @@ export class FrigateCard extends LitElement {
.nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
.autoplayClip=${this.config.autoplay_clip}
.resolvedMediaCache=${this._resolvedMediaCache}
.lazyLoad=${this.config.event_viewer?.lazy_load ?? true}
class="${classMap(viewerClasses)}"
@frigate-card:change-view=${this._changeViewHandler}
@frigate-card:media-load=${this._mediaLoadHandler}
+93 -11
View File
@@ -10,6 +10,7 @@ import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel';
import { HomeAssistant } from 'custom-card-helpers';
import { customElement, property } from 'lit/decorators.js';
import { until } from 'lit/directives/until.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import dayjs from 'dayjs';
import dayjs_custom_parse_format from 'dayjs/plugin/customParseFormat.js';
@@ -41,6 +42,9 @@ import './next-prev-control.js';
import viewerStyle from '../scss/viewer.scss';
const IMG_TRANSPARENT_1x1 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
// Load dayjs plugin(s).
dayjs.extend(dayjs_custom_parse_format);
@@ -64,6 +68,9 @@ export class FrigateCardViewer extends LitElement {
@property({ attribute: false })
protected resolvedMediaCache?: ResolvedMediaCache;
@property({ attribute: false })
protected lazyLoad?: boolean;
protected render(): TemplateResult | void {
return html`${until(this._render(), renderProgressIndicator())}`;
}
@@ -73,7 +80,9 @@ export class FrigateCardViewer extends LitElement {
* @param target The target to resolve media from.
* @returns True if the resolutions were all error free.
*/
protected async _resolveAllMediaForTarget(target: BrowseMediaSource): Promise<boolean> {
protected async _resolveAllMediaForTarget(
target: BrowseMediaSource,
): Promise<boolean> {
if (!this.hass) {
return false;
}
@@ -145,6 +154,7 @@ export class FrigateCardViewer extends LitElement {
.autoplayClip=${autoplay}
.hass=${this.hass}
.browseMediaQueryParameters=${this.browseMediaQueryParameters}
.lazyLoad=${this.lazyLoad}
>
</frigate-card-viewer-core>`;
}
@@ -177,6 +187,9 @@ export class FrigateCardViewerCore extends LitElement {
@property({ attribute: false })
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
@property({ attribute: false })
protected lazyLoad?: boolean;
// Media carousel object.
protected _carousel?: EmblaCarouselType;
protected _loadedCarousel = false;
@@ -220,7 +233,15 @@ export class FrigateCardViewerCore extends LitElement {
this._carousel = EmblaCarousel(carouselNode, {
startIndex: isNaN(startIndex) ? undefined : startIndex,
});
// Update views based on slide selections.
this._carousel.on('select', this._slideSelectHandler.bind(this));
// Lazily load media that is displayed. These handlers are registered
// regardless of the value of this.lazyLoad to allow that value to change
// after the carousel has been initialized.
this._carousel.on('init', this._lazyLoadMediaHandler.bind(this));
this._carousel.on('select', this._lazyLoadMediaHandler.bind(this));
this._carousel.on('resize', this._lazyLoadMediaHandler.bind(this));
}
}
@@ -298,12 +319,14 @@ export class FrigateCardViewerCore extends LitElement {
protected async _findRelatedClipView(
snapshot: BrowseMediaSource,
): Promise<View | null> {
if (!this.hass ||
if (
!this.hass ||
!this.view ||
!this.view.target ||
!this.view.target.children ||
!this.view.target.children.length ||
!this.browseMediaQueryParameters) {
!this.browseMediaQueryParameters
) {
return null;
}
@@ -321,7 +344,8 @@ export class FrigateCardViewerCore extends LitElement {
// currently viewing and mirrors that range into the clips view. Then,
// within the results see if there's a clip that matches the same time as
// the snapshot.
let earliest: number | null = null, latest: number | null = null;
let earliest: number | null = null;
let latest: number | null = null;
for (let i = 0; i < this.view.target.children.length; i++) {
const child = this.view.target.children[i];
if (!isTrueMedia(child)) {
@@ -351,10 +375,9 @@ export class FrigateCardViewerCore extends LitElement {
});
} catch (e) {
// This is best effort.
return null
return null;
}
console.info(clips);
if (!clips || !clips.children || !clips.children.length) {
return null;
}
@@ -370,7 +393,7 @@ export class FrigateCardViewerCore extends LitElement {
view: 'clip',
target: clips,
childIndex: i,
previous: this.view
previous: this.view,
});
}
}
@@ -402,7 +425,6 @@ export class FrigateCardViewerCore extends LitElement {
* @param direction The direction requested, previous or next.
*/
protected _nextPreviousHandler(direction: 'previous' | 'next'): void {
console.info(`next/prev handler: ${direction}`);
if (direction == 'previous') {
this._carousel?.scrollPrev();
} else if (direction == 'next') {
@@ -410,6 +432,60 @@ export class FrigateCardViewerCore extends LitElement {
}
}
/**
* Lazily load media in the carousel.
* @param eventName The Embla event name that triggered this load.
* // TODO delete eventName above?
*/
protected _lazyLoadMediaHandler(): void {
if (!this.lazyLoad || !this._carousel) {
return;
}
const slides = this._carousel.slideNodes();
const slidesInView = this._carousel.slidesInView(true);
const slidesToLoad = new Set<number>();
// Lazily load the selected slide and the one on each side of it to improve
// the user navigation experience.
for (let i = 0; i < slidesInView.length; i++) {
const index = slidesInView[i];
if (index > 0) {
slidesToLoad.add(index - 1);
}
slidesToLoad.add(index);
if (index < slides.length - 1) {
slidesToLoad.add(index + 1);
}
}
slidesToLoad.forEach((index) => {
const slide = slides[index];
// Snapshots.
const img = slide.querySelector('img');
// Frigate >= 0.9.0+ clips.
const hls_player = slide.querySelector(
'frigate-card-ha-hls-player',
) as HTMLElement & { url: string };
// Frigate < 0.9.0 clips. frigate-card-ha-hls-player will also have a
// video source element, so search for that first.
const video_source = slide.querySelector('video source') as HTMLElement & {
src: string;
};
if (img) {
img.src = img.getAttribute('data-src') || img.src;
} else if (hls_player) {
console.info(hls_player);
hls_player.url = hls_player.getAttribute('data-url') || hls_player.url;
} else if (video_source) {
video_source.src = video_source.getAttribute('data-src') || video_source.src;
}
});
}
/**
* Render the element.
* @returns A template to display to the user.
@@ -489,7 +565,8 @@ export class FrigateCardViewerCore extends LitElement {
? resolvedMedia?.mime_type.toLowerCase() == 'application/x-mpegurl'
? html`<frigate-card-ha-hls-player
.hass=${this.hass}
.url=${resolvedMedia.url}
url=${ifDefined(this.lazyLoad ? undefined : resolvedMedia.url)}
data-url=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
title="${mediaToRender.title}"
muted
controls
@@ -508,10 +585,15 @@ export class FrigateCardViewerCore extends LitElement {
@play=${() => dispatchPlayEvent(this)}
@pause=${() => dispatchPauseEvent(this)}
>
<source src="${resolvedMedia.url}" type="${resolvedMedia.mime_type}" />
<source
src=${ifDefined(this.lazyLoad ? undefined : resolvedMedia.url)}
data-src=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
type="${resolvedMedia.mime_type}"
/>
</video>`
: html`<img
src=${resolvedMedia.url}
src=${ifDefined(this.lazyLoad ? IMG_TRANSPARENT_1x1 : resolvedMedia.url)}
data-src=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
title="${mediaToRender.title}"
@click=${() => {
if (this._carousel?.clickAllowed()) {
+3
View File
@@ -283,6 +283,9 @@ export const frigateCardConfigSchema = z.object({
label: z.string().optional(),
zone: z.string().optional(),
autoplay_clip: z.boolean().default(false),
event_viewer: z.object({
lazy_load: z.boolean().default(true),
}).optional(),
menu_mode: z.enum(FRIGATE_MENU_MODES).optional().default('hidden-top'),
menu_buttons: z
.object({