Add new lazily unload criteria .

This commit is contained in:
Dermot Duffy
2022-05-03 22:30:43 -07:00
parent 328a1088b8
commit 26b7bbbb9e
7 changed files with 105 additions and 51 deletions
+1 -1
View File
@@ -225,7 +225,7 @@ live:
| `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. |
| `auto_unmute` | `false` | :heavy_multiplication_x: | Whether or not to automatically unmute live cameras. Note that some browsers will not allow automated unmute until the user has interacted with the page in some way -- if the user has not then the browser may pause the media instead. |
| `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 lazyily-loaded 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 (either because the carousel has scrolled past them, or because the document has been marked hidden/inactive by the browser). This will cause a reloading delay on revisiting that camera in the carousel but will save the streaming network resources that are otherwise consumed. This option has no effect if `lazy_load` is false. |
| `lazy_unload` | `never` | :heavy_multiplication_x: | When to lazily **un**load lazyily-loaded cameras. `never` will never lazily-unload, `unselected` will lazy-unload a camera when it is unselected in the carousel, `hidden` will lazy-unload all cameras when the browser/tab is hidden or `all` on any opportunity to lazily unload (i.e. either case). This will cause a reloading delay on revisiting that camera in the carousel but will save the streaming network resources that are otherwise consumed. This option has no effect if `lazy_load` is false. Some live providers (e.g. `webrtc-card`) implement their own lazy unloading independently which may occur regardless of the value of this setting.|
| `draggable` | `true` | :heavy_multiplication_x: | Whether or not the live carousel can be dragged left or right, via touch/swipe and mouse dragging. |
| `transition_effect` | `slide` | :heavy_multiplication_x: | Effect to apply as a transition between live cameras. Accepted values: `slide` or `none`. |
| `actions` | | :white_check_mark: | Actions to use for the `live` view. See [actions](#actions) below.|
+51 -36
View File
@@ -1,16 +1,18 @@
import { EmblaCarouselType, EmblaEventType, EmblaPluginType } from 'embla-carousel';
import { LazyUnloadCondition } from '../../types';
export type LazyloadOptionsType = {
// Number of slides to lazyload left/right of selected (0 == only selected
// slide).
lazyloadCount?: number;
lazyLoadCount?: number;
lazyUnloadCondition?: LazyUnloadCondition;
lazyloadCallback?: (index: number, slide: HTMLElement) => void;
lazyunloadCallback?: (index: number, slide: HTMLElement) => void;
lazyLoadCallback?: (index: number, slide: HTMLElement) => void;
lazyUnloadCallback?: (index: number, slide: HTMLElement) => void;
};
export const defaultOptions: Partial<LazyloadOptionsType> = {
lazyloadCount: 0,
lazyLoadCount: 0,
};
export type LazyloadType = EmblaPluginType<LazyloadOptionsType> & {
@@ -22,7 +24,7 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
let carousel: EmblaCarouselType;
let slides: HTMLElement[];
const isSlideLazyloaded: Record<number, boolean> = {};
const lazyLoadedSlides: Set<number> = new Set();
const loadEvents: EmblaEventType[] = ['init', 'select', 'resize'];
const unloadEvents: EmblaEventType[] = ['select'];
@@ -34,11 +36,15 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
carousel = embla;
slides = carousel.slideNodes();
if (options.lazyloadCallback) {
loadEvents.forEach((evt) => carousel.on(evt, lazyloadHandler));
if (options.lazyLoadCallback) {
loadEvents.forEach((evt) => carousel.on(evt, lazyLoadHandler));
}
if (options.lazyunloadCallback) {
unloadEvents.forEach((evt) => carousel.on(evt, lazyunloadHandler));
if (
options.lazyUnloadCallback &&
options.lazyUnloadCondition &&
['all', 'unselected'].includes(options.lazyUnloadCondition)
) {
unloadEvents.forEach((evt) => carousel.on(evt, lazyUnloadPreviousHandler));
}
document.addEventListener('visibilitychange', visibilityHandler);
}
@@ -47,11 +53,11 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
* Destroy the plugin.
*/
function destroy(): void {
if (options.lazyloadCallback) {
loadEvents.forEach((evt) => carousel.off(evt, lazyloadHandler));
if (options.lazyLoadCallback) {
loadEvents.forEach((evt) => carousel.off(evt, lazyLoadHandler));
}
if (options.lazyunloadCallback) {
unloadEvents.forEach((evt) => carousel.off(evt, lazyunloadHandler));
if (options.lazyUnloadCallback) {
unloadEvents.forEach((evt) => carousel.off(evt, lazyUnloadPreviousHandler));
}
document.removeEventListener('visibilitychange', visibilityHandler);
}
@@ -60,10 +66,15 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
* Handle document visibility changes.
*/
function visibilityHandler(): void {
if (document.visibilityState == 'hidden' && lazyunloadHandler) {
lazyunloadHandler();
} else if (document.visibilityState == 'visible' && lazyloadHandler) {
lazyloadHandler();
if (
document.visibilityState === 'hidden' &&
options.lazyUnloadCallback &&
options.lazyUnloadCondition &&
['all', 'hidden'].includes(options.lazyUnloadCondition)
) {
lazyUnloadAllHandler();
} else if (document.visibilityState === 'visible' && options.lazyLoadCallback) {
lazyLoadHandler();
}
}
@@ -73,14 +84,14 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
* @returns `true` if the slide has been lazily loaded.
*/
function hasLazyloaded(index: number): boolean {
return !!isSlideLazyloaded[index];
return lazyLoadedSlides.has(index);
}
/**
* Lazily load media in the carousel.
*/
function lazyloadHandler(): void {
const lazyLoadCount = options.lazyloadCount ?? 0;
function lazyLoadHandler(): void {
const lazyLoadCount = options.lazyLoadCount ?? 0;
const currentIndex = carousel.selectedScrollSnap();
const slidesToLoad = new Set<number>();
@@ -94,30 +105,34 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
}
slidesToLoad.forEach((index) => {
// Only lazy load slides that are not already loaded.
if (isSlideLazyloaded[index]) {
return;
}
if (options.lazyloadCallback) {
isSlideLazyloaded[index] = true;
options.lazyloadCallback(index, slides[index]);
if (!hasLazyloaded(index) && options.lazyLoadCallback) {
lazyLoadedSlides.add(index);
options.lazyLoadCallback(index, slides[index]);
}
});
}
/**
* Lazily unload media in the carousel.
* Lazily unload all media in the carousel.
*/
function lazyunloadHandler(): void {
function lazyUnloadAllHandler(): void {
lazyLoadedSlides.forEach((index) => {
if (options.lazyUnloadCallback) {
options.lazyUnloadCallback(index, slides[index]);
lazyLoadedSlides.delete(index);
}
});
}
/**
* Lazily unload the previously selected media in the carousel.
*/
function lazyUnloadPreviousHandler(): void {
const index = carousel.previousScrollSnap();
// Only lazy unload slides that are lazy loaded.
if (!isSlideLazyloaded[index]) {
return;
}
if (options.lazyunloadCallback) {
options.lazyunloadCallback(index, slides[index]);
isSlideLazyloaded[index] = false;
if (hasLazyloaded(index) && options.lazyUnloadCallback) {
options.lazyUnloadCallback(index, slides[index]);
lazyLoadedSlides.delete(index);
}
}
+13 -11
View File
@@ -288,12 +288,14 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
return [
...super._getPlugins(),
Lazyload({
lazyloadCallback: this.liveConfig?.lazy_load
? (...args) => this._lazyloadOrUnloadSlide('load', ...args)
: undefined,
lazyunloadCallback: this.liveConfig?.lazy_unload
? (...args) => this._lazyloadOrUnloadSlide('unload', ...args)
: undefined,
...(this.liveConfig?.lazy_load && {
lazyLoadCallback: (index, slide) =>
this._lazyloadOrUnloadSlide('load', index, slide),
}),
lazyUnloadCondition: this.liveConfig?.lazy_unload,
lazyUnloadCallback: (index, slide) =>
this._lazyloadOrUnloadSlide('unload', index, slide),
}),
AutoMediaPlugin({
playerSelector: 'frigate-card-live-provider',
@@ -380,7 +382,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
'frigate-card-live-provider',
) as FrigateCardLiveProvider;
if (liveProvider) {
liveProvider.disabled = action == 'load' ? false : true;
liveProvider.disabled = action !== 'load';
}
}
@@ -795,7 +797,7 @@ export class FrigateCardLiveWebRTCCard extends LitElement {
/**
* Create the WebRTC element. May throw.
*/
protected _createWebRTC(): HTMLElement | undefined {
protected _createWebRTC(): HTMLElement | null {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const webrtcElement = this._webrtcTask.value;
if (webrtcElement && this.hass) {
@@ -818,7 +820,7 @@ export class FrigateCardLiveWebRTCCard extends LitElement {
webrtc.hass = this.hass;
return webrtc;
}
return undefined;
return null;
}
/**
@@ -827,7 +829,7 @@ export class FrigateCardLiveWebRTCCard extends LitElement {
*/
protected render(): TemplateResult | void {
const render = (): TemplateResult | void => {
let webrtcElement: HTMLElement | undefined;
let webrtcElement: HTMLElement | null;
try {
webrtcElement = this._createWebRTC();
} catch (e) {
@@ -973,7 +975,7 @@ export class FrigateCardLiveJSMPEG extends LitElement {
canvas: this._jsmpegCanvasElement,
},
{
// The media carousel automatically pauses when the browser tab is
// The media carousel may automatically pause when the browser tab is
// inactive, JSMPEG does not need to also do so independently.
pauseWhenHidden: false,
protocols: [],
+17
View File
@@ -15,6 +15,7 @@ import {
CONF_IMAGE_URL,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_PRELOAD,
CONF_LIVE_WEBRTC_CARD,
CONF_MENU,
@@ -322,6 +323,19 @@ const upgradeWithOverrides = function (
return upgradeMoveToWithOverrides(path, path, transform);
};
/**
* Upgrade a property in place without overrides.
* @param path The old property path.
* @param transform An optional transform for the value.
* @returns A function that returns `true` if the configuration was modified.
*/
const upgrade = function (
path: string,
transform?: (valueIn: unknown) => unknown,
): (obj: RawFrigateCardConfig) => boolean {
return upgradeMoveTo(path, path, transform);
};
/**
* Given a path to an array, apply an upgrade to each object in the array.
* @param arrayPath The path to the array to upgrade.
@@ -575,4 +589,7 @@ const UPGRADES = [
upgradeWithOverrides(CONF_MENU_BUTTONS_DOWNLOAD, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_FRIGATE_UI, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_FULLSCREEN, menuButtonBooleanToObject),
upgrade(CONF_LIVE_LAZY_UNLOAD, (val) =>
typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined,
),
];
+12 -1
View File
@@ -349,6 +349,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: 'auto', label: localize('config.view.dark_modes.auto') },
];
protected _lazyUnloadConditions: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'all', label: localize('config.live.lazy_unload_conditions.all') },
{ value: 'unselected', label: localize('config.live.lazy_unload_conditions.unselected') },
{ value: 'hidden', label: localize('config.live.lazy_unload_conditions.hidden') },
{ value: 'never', label: localize('config.live.lazy_unload_conditions.never') },
];
public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the configuration, so it may be
// partially or completely invalid. It's more useful to have a partially
@@ -990,7 +998,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderSwitch(CONF_LIVE_PRELOAD, defaults.live.preload)}
${this._renderSwitch(CONF_LIVE_DRAGGABLE, defaults.live.draggable)}
${this._renderSwitch(CONF_LIVE_LAZY_LOAD, defaults.live.lazy_load)}
${this._renderSwitch(CONF_LIVE_LAZY_UNLOAD, defaults.live.lazy_unload)}
${this._renderOptionSelector(
CONF_LIVE_LAZY_UNLOAD,
this._lazyUnloadConditions,
)}
${this._renderSwitch(CONF_LIVE_AUTO_UNMUTE, defaults.live.auto_unmute)}
${this._renderOptionSelector(
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
+6
View File
@@ -117,6 +117,12 @@
"draggable": "Live cameras view can be dragged/swiped",
"lazy_load": "Live cameras are lazily loaded",
"lazy_unload": "Live cameras are lazily unloaded",
"lazy_unload_conditions": {
"all": "All opportunities",
"unselected": "On unselection",
"hidden": "On browser/tab hiding",
"never": "Never"
},
"auto_unmute": "Automatically unmute live cameras",
"transition_effect": "Live camera transition effect",
"controls": {
+5 -2
View File
@@ -64,6 +64,9 @@ export const FRIGATE_MENU_PRIORITY_MAX = 100;
const LIVE_PROVIDERS = ['auto', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
export type LiveProvider = typeof LIVE_PROVIDERS[number];
const LAZY_UNLOAD_CONDITIONS = ['all', 'unselected', 'hidden', 'never'] as const;
export type LazyUnloadCondition = typeof LAZY_UNLOAD_CONDITIONS[number];
export class FrigateCardError extends Error {}
/**
@@ -560,7 +563,7 @@ const liveConfigDefault = {
auto_unmute: false,
preload: false,
lazy_load: true,
lazy_unload: false,
lazy_unload: 'never' as const,
draggable: true,
transition_effect: 'slide' as const,
controls: {
@@ -665,7 +668,7 @@ const liveConfigSchema = liveOverridableConfigSchema
auto_unmute: z.boolean().default(liveConfigDefault.auto_unmute),
preload: z.boolean().default(liveConfigDefault.preload),
lazy_load: z.boolean().default(liveConfigDefault.lazy_load),
lazy_unload: z.boolean().default(liveConfigDefault.lazy_unload),
lazy_unload: z.enum(LAZY_UNLOAD_CONDITIONS).default(liveConfigDefault.lazy_unload),
draggable: z.boolean().default(liveConfigDefault.draggable),
transition_effect: transitionEffectConfigSchema.default(
liveConfigDefault.transition_effect,