From 4bba7ae5fbc3a2837032f8ea5196d77c2c002338 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 25 Jan 2022 20:06:02 -0800 Subject: [PATCH] Add live view lazy unloading support. --- README.md | 3 +- src/components/embla-plugins/lazyload.ts | 73 ++++++++++++++++-------- src/components/live.ts | 35 +++++++----- src/components/viewer.ts | 15 ++--- src/const.ts | 1 + src/editor.ts | 5 ++ src/localize/languages/en.json | 3 +- src/types.ts | 2 + 8 files changed, 89 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index eb9dc9fd..665f26a1 100644 --- a/README.md +++ b/README.md @@ -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.| diff --git a/src/components/embla-plugins/lazyload.ts b/src/components/embla-plugins/lazyload.ts index a78e6ca9..a973e73b 100644 --- a/src/components/embla-plugins/lazyload.ts +++ b/src/components/embla-plugins/lazyload.ts @@ -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 = { - count: 0, + lazyloadCount: 0, }; export type LazyloadType = EmblaPluginType & { @@ -20,6 +23,9 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType { let slides: HTMLElement[]; const isSlideLazyloaded: Record = {}; + 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(); - 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, diff --git a/src/components/live.ts b/src/components/live.ts index 59aeffe1..dcf6fe5a 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -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. + }); } /** diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 406dd764..299ac2f8 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -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 ( diff --git a/src/const.ts b/src/const.ts index 4ccb4c6d..876017be 100644 --- a/src/const.ts +++ b/src/const.ts @@ -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; diff --git a/src/editor.ts b/src/editor.ts index c3c60d2b..40bac4aa 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -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, diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 6b7d709b..2a89bc00 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -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", diff --git a/src/types.ts b/src/types.ts index 34579767..c8865038 100644 --- a/src/types.ts +++ b/src/types.ts @@ -432,6 +432,7 @@ export type NextPreviousControlConfig = z.infer