Initial lazy loading support for carousel.
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
+97
-15
@@ -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.view ||
|
!this.hass ||
|
||||||
!this.view.target ||
|
!this.view ||
|
||||||
!this.view.target.children ||
|
!this.view.target ||
|
||||||
!this.view.target.children.length ||
|
!this.view.target.children ||
|
||||||
!this.browseMediaQueryParameters) {
|
!this.view.target.children.length ||
|
||||||
|
!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()) {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user