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'} .nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
.autoplayClip=${this.config.autoplay_clip} .autoplayClip=${this.config.autoplay_clip}
.resolvedMediaCache=${this._resolvedMediaCache} .resolvedMediaCache=${this._resolvedMediaCache}
.lazyLoad=${this.config.event_viewer?.lazy_load ?? true}
class="${classMap(viewerClasses)}" class="${classMap(viewerClasses)}"
@frigate-card:change-view=${this._changeViewHandler} @frigate-card:change-view=${this._changeViewHandler}
@frigate-card:media-load=${this._mediaLoadHandler} @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 { HomeAssistant } from 'custom-card-helpers';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { until } from 'lit/directives/until.js'; import { until } from 'lit/directives/until.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import dayjs_custom_parse_format from 'dayjs/plugin/customParseFormat.js'; 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'; import viewerStyle from '../scss/viewer.scss';
const IMG_TRANSPARENT_1x1 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
// Load dayjs plugin(s). // Load dayjs plugin(s).
dayjs.extend(dayjs_custom_parse_format); dayjs.extend(dayjs_custom_parse_format);
@@ -64,6 +68,9 @@ export class FrigateCardViewer extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected resolvedMediaCache?: ResolvedMediaCache; protected resolvedMediaCache?: ResolvedMediaCache;
@property({ attribute: false })
protected lazyLoad?: boolean;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html`${until(this._render(), renderProgressIndicator())}`; return html`${until(this._render(), renderProgressIndicator())}`;
} }
@@ -73,7 +80,9 @@ export class FrigateCardViewer extends LitElement {
* @param target The target to resolve media from. * @param target The target to resolve media from.
* @returns True if the resolutions were all error free. * @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) { if (!this.hass) {
return false; return false;
} }
@@ -145,6 +154,7 @@ export class FrigateCardViewer extends LitElement {
.autoplayClip=${autoplay} .autoplayClip=${autoplay}
.hass=${this.hass} .hass=${this.hass}
.browseMediaQueryParameters=${this.browseMediaQueryParameters} .browseMediaQueryParameters=${this.browseMediaQueryParameters}
.lazyLoad=${this.lazyLoad}
> >
</frigate-card-viewer-core>`; </frigate-card-viewer-core>`;
} }
@@ -177,6 +187,9 @@ export class FrigateCardViewerCore extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected browseMediaQueryParameters?: BrowseMediaQueryParameters; protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
@property({ attribute: false })
protected lazyLoad?: boolean;
// Media carousel object. // Media carousel object.
protected _carousel?: EmblaCarouselType; protected _carousel?: EmblaCarouselType;
protected _loadedCarousel = false; protected _loadedCarousel = false;
@@ -220,7 +233,15 @@ export class FrigateCardViewerCore extends LitElement {
this._carousel = EmblaCarousel(carouselNode, { this._carousel = EmblaCarousel(carouselNode, {
startIndex: isNaN(startIndex) ? undefined : startIndex, startIndex: isNaN(startIndex) ? undefined : startIndex,
}); });
// Update views based on slide selections.
this._carousel.on('select', this._slideSelectHandler.bind(this)); 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( protected async _findRelatedClipView(
snapshot: BrowseMediaSource, snapshot: BrowseMediaSource,
): Promise<View | null> { ): Promise<View | null> {
if (!this.hass || if (
!this.hass ||
!this.view || !this.view ||
!this.view.target || !this.view.target ||
!this.view.target.children || !this.view.target.children ||
!this.view.target.children.length || !this.view.target.children.length ||
!this.browseMediaQueryParameters) { !this.browseMediaQueryParameters
) {
return null; return null;
} }
@@ -321,7 +344,8 @@ export class FrigateCardViewerCore extends LitElement {
// currently viewing and mirrors that range into the clips view. Then, // 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 // within the results see if there's a clip that matches the same time as
// the snapshot. // 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++) { for (let i = 0; i < this.view.target.children.length; i++) {
const child = this.view.target.children[i]; const child = this.view.target.children[i];
if (!isTrueMedia(child)) { if (!isTrueMedia(child)) {
@@ -351,10 +375,9 @@ export class FrigateCardViewerCore extends LitElement {
}); });
} catch (e) { } catch (e) {
// This is best effort. // This is best effort.
return null return null;
} }
console.info(clips);
if (!clips || !clips.children || !clips.children.length) { if (!clips || !clips.children || !clips.children.length) {
return null; return null;
} }
@@ -370,7 +393,7 @@ export class FrigateCardViewerCore extends LitElement {
view: 'clip', view: 'clip',
target: clips, target: clips,
childIndex: i, 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. * @param direction The direction requested, previous or next.
*/ */
protected _nextPreviousHandler(direction: 'previous' | 'next'): void { protected _nextPreviousHandler(direction: 'previous' | 'next'): void {
console.info(`next/prev handler: ${direction}`);
if (direction == 'previous') { if (direction == 'previous') {
this._carousel?.scrollPrev(); this._carousel?.scrollPrev();
} else if (direction == 'next') { } 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. * Render the element.
* @returns A template to display to the user. * @returns A template to display to the user.
@@ -489,7 +565,8 @@ export class FrigateCardViewerCore extends LitElement {
? resolvedMedia?.mime_type.toLowerCase() == 'application/x-mpegurl' ? resolvedMedia?.mime_type.toLowerCase() == 'application/x-mpegurl'
? html`<frigate-card-ha-hls-player ? html`<frigate-card-ha-hls-player
.hass=${this.hass} .hass=${this.hass}
.url=${resolvedMedia.url} url=${ifDefined(this.lazyLoad ? undefined : resolvedMedia.url)}
data-url=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
title="${mediaToRender.title}" title="${mediaToRender.title}"
muted muted
controls controls
@@ -508,10 +585,15 @@ export class FrigateCardViewerCore extends LitElement {
@play=${() => dispatchPlayEvent(this)} @play=${() => dispatchPlayEvent(this)}
@pause=${() => dispatchPauseEvent(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>` </video>`
: html`<img : 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}" title="${mediaToRender.title}"
@click=${() => { @click=${() => {
if (this._carousel?.clickAllowed()) { if (this._carousel?.clickAllowed()) {
+3
View File
@@ -283,6 +283,9 @@ export const frigateCardConfigSchema = z.object({
label: z.string().optional(), label: z.string().optional(),
zone: z.string().optional(), zone: z.string().optional(),
autoplay_clip: z.boolean().default(false), 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_mode: z.enum(FRIGATE_MENU_MODES).optional().default('hidden-top'),
menu_buttons: z menu_buttons: z
.object({ .object({