Add live view lazy unloading support.

This commit is contained in:
Dermot Duffy
2022-01-25 20:06:02 -08:00
parent da6b9f2ad1
commit 4bba7ae5fb
8 changed files with 89 additions and 48 deletions
+2 -1
View File
@@ -190,7 +190,8 @@ live:
| Option | Default | Overridable | Description |
| - | - | - | - |
| `preload` | `false` | :heavy_multiplication_x: | Whether or not to preload the live view. Preloading causes the live view to render in the background regardless of what view is actually shown, so it's instantly available when requested. This consumes additional network/CPU resources continually. |
| `lazy_load` | `true` | :heavy_multiplication_x: | Whether or not to lazily load camera views in the camera carousel. Setting this will `false` will cause all cameras to load simultaneously when the `live` carousel is opened (or cause all cameras to load continually if both `lazy_load` and `preload` are `true`). This will result in a smoother carousel experience at a cost of (potentially) a substantial amount of continually streamed data. |
| `lazy_load` | `true` | :heavy_multiplication_x: | Whether or not to lazily load cameras in the camera carousel. Setting this will `false` will cause all cameras to load simultaneously when the `live` carousel is opened (or cause all cameras to load continually if both `lazy_load` and `preload` are `true`). This will result in a smoother carousel experience at a cost of (potentially) a substantial amount of continually streamed data. |
| `lazy_unload` | `false` | :heavy_multiplication_x: | Whether or not to lazily **un**load cameras in the camera carousel, or just leave the camera paused. Setting this to `true` will cause cameras to be entirely unloaded when they are no longer visible. This will cause a reloading delay on revisiting that camera in the carousel but will save the streaming network resources that are otherwise consumed. |
| `draggable` | `true` | :heavy_multiplication_x: | Whether or not the live carousel can be dragged left or right, via touch/swipe and mouse dragging. |
| `provider` | `frigate` | :white_check_mark: | The means through which the live camera view is displayed. See [Live Provider](#live-provider) below.|
| `actions` | | :white_check_mark: | Actions to use for the `live` view. See [actions](#actions) below.|
+50 -23
View File
@@ -1,12 +1,15 @@
import { EmblaCarouselType, EmblaPluginType } from 'embla-carousel';
import { EmblaCarouselType, EmblaEventType, EmblaPluginType } from 'embla-carousel';
export type LazyloadOptionsType = {
count?: number;
lazyloadCallback: (index: number, slide: HTMLElement) => void;
// Number of slides to lazyload left/right of selected (0 == only selected slide).
lazyloadCount?: number;
lazyloadCallback?: (index: number, slide: HTMLElement) => void;
lazyunloadCallback?: (index: number, slide: HTMLElement) => void;
};
export const defaultOptions: Partial<LazyloadOptionsType> = {
count: 0,
lazyloadCount: 0,
};
export type LazyloadType = EmblaPluginType<LazyloadOptionsType> & {
@@ -20,6 +23,9 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
let slides: HTMLElement[];
const isSlideLazyloaded: Record<number, boolean> = {};
const loadEvents: EmblaEventType[] = ['init', 'select', 'resize'];
const unloadEvents: EmblaEventType[] = ['select'];
/**
* Initialize the plugin.
*/
@@ -27,18 +33,24 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
carousel = embla;
slides = carousel.slideNodes();
carousel.on('init', lazyLoadHandler);
carousel.on('select', lazyLoadHandler);
carousel.on('resize', lazyLoadHandler);
if (options.lazyloadCallback) {
loadEvents.forEach((evt) => carousel.on(evt, lazyloadHandler));
}
if (options.lazyunloadCallback) {
unloadEvents.forEach((evt) => carousel.on(evt, lazyunloadHandler));
}
}
/**
* Destroy the plugin.
*/
function destroy(): void {
carousel.off('init', lazyLoadHandler);
carousel.off('select', lazyLoadHandler);
carousel.off('resize', lazyLoadHandler);
if (options.lazyloadCallback) {
loadEvents.forEach((evt) => carousel.off(evt, lazyloadHandler));
}
if (options.lazyunloadCallback) {
unloadEvents.forEach((evt) => carousel.off(evt, lazyunloadHandler));
}
}
/**
@@ -53,21 +65,18 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
/**
* Lazily load media in the carousel.
*/
function lazyLoadHandler(): void {
const lazyLoadCount = options.count ?? 0;
const slidesInView = carousel.slidesInView(true);
function lazyloadHandler(): void {
const lazyLoadCount = options.lazyloadCount ?? 0;
const currentIndex = carousel.selectedScrollSnap();
const slidesToLoad = new Set<number>();
const minSlide = Math.min(...slidesInView);
const maxSlide = Math.max(...slidesInView);
// Lazily load 'count' slides on either side of the slides in view.
for (let i = 1; i <= lazyLoadCount && minSlide - i >= 0; i++) {
slidesToLoad.add(minSlide - i);
for (let i = 1; i <= lazyLoadCount && currentIndex - i >= 0; i++) {
slidesToLoad.add(currentIndex - i);
}
slidesInView.forEach((index) => slidesToLoad.add(index));
for (let i = 1; i <= lazyLoadCount && maxSlide + i < slides.length; i++) {
slidesToLoad.add(maxSlide + i);
slidesToLoad.add(currentIndex);
for (let i = 1; i <= lazyLoadCount && currentIndex + i < slides.length; i++) {
slidesToLoad.add(currentIndex + i);
}
slidesToLoad.forEach((index) => {
@@ -75,11 +84,29 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
if (isSlideLazyloaded[index]) {
return;
}
isSlideLazyloaded[index] = true;
options.lazyloadCallback(index, slides[index]);
if (options.lazyloadCallback) {
isSlideLazyloaded[index] = true;
options.lazyloadCallback(index, slides[index]);
}
});
}
/**
* Lazily unload media in the carousel.
*/
function lazyunloadHandler(): void {
const index = carousel.previousScrollSnap();
// Only lazy unload slides that are loaded.
if (!isSlideLazyloaded[index]) {
return;
}
if (options.lazyunloadCallback) {
options.lazyunloadCallback(index, slides[index]);
isSlideLazyloaded[index] = false;
}
}
const self: LazyloadType = {
name: 'Lazyload',
options,
+21 -14
View File
@@ -297,13 +297,14 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
*/
protected _getPlugins(): EmblaPluginType[] | undefined {
return [
...(this.liveConfig?.lazy_load
? [
Lazyload({
lazyloadCallback: this._lazyLoadSlide.bind(this),
}),
]
: []),
Lazyload({
lazyloadCallback: this.liveConfig?.lazy_load
? (...args) => this._lazyloadOrUnloadSlide('load', ...args)
: undefined,
lazyunloadCallback: this.liveConfig?.lazy_unload
? (...args) => this._lazyloadOrUnloadSlide('unload', ...args)
: undefined,
}),
MediaAutoPlayPause({
playerSelector: 'frigate-card-live-provider',
}),
@@ -367,12 +368,16 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
* @param _index The slide number to lazy load.
* @param slide The slide to lazy load.
*/
protected _lazyLoadSlide(_index: number, slide: HTMLElement): void {
protected _lazyloadOrUnloadSlide(
action: 'load' | 'unload',
_index: number,
slide: HTMLElement,
): void {
const liveProvider = slide.querySelector(
'frigate-card-live-provider',
) as FrigateCardLiveProvider;
if (liveProvider) {
liveProvider.disabled = false;
liveProvider.disabled = action == 'load' ? false : true;
}
}
@@ -680,11 +685,13 @@ export class FrigateCardLiveWebRTC extends LitElement {
* Play the video.
*/
public play(): void {
this._getPlayer()?.play().catch(() => {
// WebRTC appears to generate additional spurious load events, which may
// result in loads after a play() call, which causes the browser to spam
// the logs unless the promise rejection is handled here.
})
this._getPlayer()
?.play()
.catch(() => {
// WebRTC appears to generate additional spurious load events, which may
// result in loads after a play() call, which causes the browser to spam
// the logs unless the promise rejection is handled here.
});
}
/**
+6 -9
View File
@@ -311,14 +311,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
*/
protected _getPlugins(): EmblaPluginType[] | undefined {
return [
...(this.viewerConfig?.lazy_load
? [
Lazyload({
lazyloadCallback: this._lazyLoadSlide.bind(this),
}),
]
: []),
Lazyload({
lazyloadCallback: this.viewerConfig?.lazy_load
? this._lazyloadSlide.bind(this)
: undefined,
}),
// Don't need autoplay/pause for snapshots.
...(this.view?.is('clip')
? [
@@ -493,7 +490,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
* @param slide The slide to lazy load.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected _lazyLoadSlide(index: number, slide: HTMLElement): void {
protected _lazyloadSlide(index: number, slide: HTMLElement): void {
const childIndex: number | undefined = this._slideToChild[index];
if (
+1
View File
@@ -57,6 +57,7 @@ export const CONF_LIVE_CONTROLS_THUMBNAILS_SIZE =
export const CONF_LIVE_DRAGGABLE = `${CONF_LIVE}.draggable` as const;
export const CONF_LIVE_JSMPEG = `${CONF_LIVE}.jsmpeg` as const;
export const CONF_LIVE_LAZY_LOAD = `${CONF_LIVE}.lazy_load` as const;
export const CONF_LIVE_LAZY_UNLOAD = `${CONF_LIVE}.lazy_unload` as const;
export const CONF_LIVE_PRELOAD = `${CONF_LIVE}.preload` as const;
export const CONF_LIVE_WEBRTC = `${CONF_LIVE}.webrtc` as const;
export const CONF_LIVE_WEBRTC_ENTITY = `${CONF_LIVE_WEBRTC}.entity` as const;
+5
View File
@@ -44,6 +44,7 @@ import {
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
CONF_LIVE_DRAGGABLE,
CONF_LIVE_LAZY_LOAD,
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_PRELOAD,
CONF_MENU_BUTTONS_CLIPS,
CONF_MENU_BUTTONS_FRIGATE,
@@ -728,6 +729,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_LAZY_LOAD,
defaults.live.lazy_load,
)}
${this._renderSwitch(
CONF_LIVE_LAZY_UNLOAD,
defaults.live.lazy_unload,
)}
${this._renderDropdown(
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
liveNextPreviousControlStyles,
+2 -1
View File
@@ -77,7 +77,8 @@
"live": {
"preload": "Preload live view in the background",
"draggable": "Live cameras view can be dragged/swiped",
"lazy_load": "Live cameras are lazily loaded in carousel",
"lazy_load": "Live cameras are lazily loaded",
"lazy_unload": "Live cameras are lazily unloaded",
"controls": {
"next_previous": {
"style": "Live view next & previous control style",
+2
View File
@@ -432,6 +432,7 @@ export type NextPreviousControlConfig = z.infer<typeof nextPreviousControlConfig
const liveConfigDefault = {
preload: false,
lazy_load: true,
lazy_unload: false,
draggable: true,
controls: {
next_previous: {
@@ -516,6 +517,7 @@ const liveConfigSchema = liveOverridableConfigSchema
// Non-overrideable parameters.
preload: z.boolean().default(liveConfigDefault.preload),
lazy_load: z.boolean().default(liveConfigDefault.lazy_load),
lazy_unload: z.boolean().default(liveConfigDefault.lazy_unload),
draggable: z.boolean().default(liveConfigDefault.draggable),
})
.default(liveConfigDefault);