Add a performance option to control media chunk size.

This commit is contained in:
Dermot Duffy
2023-02-13 21:27:39 -08:00
parent 5910c09383
commit 03cea41675
20 changed files with 311 additions and 263 deletions
+2
View File
@@ -736,6 +736,7 @@ performance:
| Option | Default | Overridable | Description |
| - | - | - | - |
| `animated_progress_indicator` | `true` | :heavy_multiplication_x: | Will show the animated progress indicator 'spinner' when `true` or a simple loading icon when `false`.|
| `media_chunk_size` | `50` | :heavy_multiplication_x: | How many media items to fetch and render at a time (e.g. thumbnails under a live view, or number of snapshots to load in the media viewer). This may only make partial sense in some contexts (e.g. the 'infinite gallery' is still infinite, just loads thumbnails this many items at a time) or not at all (e.g. the timeline will show the number of events dictated by the time span the user navigates to).|
#### Style Options
@@ -2241,6 +2242,7 @@ performance:
profile: high
features:
animated_progress_indicator: true
media_chunk_size: 50
style:
border_radius: true
box_shadow: true
+2 -1
View File
@@ -2,7 +2,6 @@ import { HomeAssistant } from 'custom-card-helpers';
import add from 'date-fns/add';
import endOfHour from 'date-fns/endOfHour';
import startOfHour from 'date-fns/startOfHour';
import { CAMERA_BIRDSEYE } from '../../const';
import { CameraConfig, CardWideConfig } from '../../types';
import { ViewMedia } from '../../view/media';
import { RequestCache, RecordingSegmentsCache } from '../cache';
@@ -76,6 +75,8 @@ import { GenericCameraManagerEngine } from '../generic/engine-generic';
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
const CAMERA_BIRDSEYE = 'birdseye' as const;
class FrigateQueryResultsClassifier {
public static isFrigateEventQueryResults(
results: QueryResults,
+5 -1
View File
@@ -44,6 +44,7 @@ import { localize } from '../localize/localize.js';
import { CameraInitializationError } from './error.js';
import { CameraManagerStore } from './store.js';
import { cloneDeep } from 'lodash-es';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
class QueryClassifier {
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
@@ -356,7 +357,6 @@ export class CameraManager {
queries: T[],
results: ViewMedia[],
direction: 'earlier' | 'later',
chunkSize: number,
): Promise<ExtendedMediaQueryResult<T> | null> {
const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => {
let output: Date | null = null;
@@ -374,6 +374,10 @@ export class CameraManager {
return output;
};
const chunkSize =
this._cardWideConfig?.performance?.features.media_chunk_size ??
MEDIA_CHUNK_SIZE_DEFAULT;
// The queries associated with the chunk to fetch.
const newChunkQueries: T[] = [];
+1
View File
@@ -1916,6 +1916,7 @@ class FrigateCard extends LitElement {
.view=${this._view}
.timelineConfig=${this._getConfig().timeline}
.cameraManager=${this._cameraManager}
.cardWideConfig=${this._cardWideConfig}
>
</frigate-card-timeline>`
: ``}
+48 -51
View File
@@ -33,12 +33,10 @@ import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/types'
import { MediaQueriesResults } from '../view/media-queries-results';
import { errorToConsole } from '../utils/basic';
import './media-filter';
import "./surround-basic";
import './surround-basic';
import { ViewMedia } from '../view/media';
import { localize } from '../localize/localize';
const GALLERY_MEDIA_CHUNK_SIZE = 100;
const GALLERY_MEDIA_FILTER_MENU_ICONS = {
closed: 'mdi:filter-cog-outline',
open: 'mdi:filter-cog',
@@ -70,7 +68,8 @@ export class FrigateCardGallery extends LitElement {
!this.hass ||
!this.view ||
!this.view.isGalleryView() ||
!this.cameraManager
!this.cameraManager ||
!this.cardWideConfig
) {
return;
}
@@ -81,18 +80,20 @@ export class FrigateCardGallery extends LitElement {
this,
this.hass,
this.cameraManager,
this.cardWideConfig,
this.view,
);
} else {
const mediaType = this.view.is('snapshots')
? 'snapshots'
: this.view.is('clips')
? 'clips'
: null;
? 'clips'
: null;
changeViewToRecentEventsForCameraAndDependents(
this,
this.hass,
this.cameraManager,
this.cardWideConfig,
this.view,
{
...(mediaType && { mediaType: mediaType }),
@@ -105,22 +106,22 @@ export class FrigateCardGallery extends LitElement {
return html`
<frigate-card-surround-basic
.drawerIcons=${{
...(this.galleryConfig &&
this.galleryConfig.controls.filter.mode !== 'none' && {
[this.galleryConfig.controls.filter.mode]: GALLERY_MEDIA_FILTER_MENU_ICONS,
}),
}}
...(this.galleryConfig &&
this.galleryConfig.controls.filter.mode !== 'none' && {
[this.galleryConfig.controls.filter.mode]: GALLERY_MEDIA_FILTER_MENU_ICONS,
}),
}}
>
${this.galleryConfig && this.galleryConfig.controls.filter.mode !== 'none'
? html` <frigate-card-media-filter
? html` <frigate-card-media-filter
.hass=${this.hass}
.cameraManager=${this.cameraManager}
.view=${this.view}
.mediaLimit=${GALLERY_MEDIA_CHUNK_SIZE}
.cardWideConfig=${this.cardWideConfig}
slot=${this.galleryConfig.controls.filter.mode}
>
</frigate-card-media-filter>`
: ''}
: ''}
<frigate-card-gallery-core
.hass=${this.hass}
.view=${this.view}
@@ -212,10 +213,10 @@ export class FrigateCardGalleryCore extends LitElement {
const columns = this.galleryConfig?.controls.thumbnails.show_details
? Math.max(1, Math.floor(this.clientWidth / THUMBNAIL_DETAILS_WIDTH_MIN))
: Math.max(
1,
Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
Math.ceil(this.clientWidth / thumbnailSize),
);
1,
Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
Math.ceil(this.clientWidth / thumbnailSize),
);
this.style.setProperty('--frigate-card-gallery-columns', String(columns));
}
@@ -253,7 +254,6 @@ export class FrigateCardGalleryCore extends LitElement {
rawQueries,
existingMedia,
'earlier',
GALLERY_MEDIA_CHUNK_SIZE,
);
} catch (e) {
errorToConsole(e as Error);
@@ -264,8 +264,8 @@ export class FrigateCardGalleryCore extends LitElement {
const newMediaQueries = MediaQueriesClassifier.areEventQueries(query)
? new EventMediaQueries(extension.queries as EventQuery[])
: MediaQueriesClassifier.areRecordingQueries(query)
? new RecordingMediaQueries(extension.queries as RecordingQuery[])
: null;
? new RecordingMediaQueries(extension.queries as RecordingQuery[])
: null;
if (newMediaQueries) {
this.view
@@ -301,11 +301,13 @@ export class FrigateCardGalleryCore extends LitElement {
this._showExtensionLoader = true;
const oldView: View | undefined = changedProps.get('view');
if (oldView?.queryResults?.getResults() !== this.view?.queryResults?.getResults()) {
if (
oldView?.queryResults?.getResults() !== this.view?.queryResults?.getResults()
) {
// Gallery places the most recent media at the top (the query results place
// the most recent media at the end for use in the viewer). This is copied
// to a new array to avoid reversing the query results in place.
this._media = [...this.view?.queryResults?.getResults() ?? []].reverse();
this._media = [...(this.view?.queryResults?.getResults() ?? [])].reverse();
}
}
}
@@ -315,12 +317,7 @@ export class FrigateCardGalleryCore extends LitElement {
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
if (
!this._media ||
!this.hass ||
!this.view ||
!this.view.isGalleryView()
) {
if (!this._media || !this.hass || !this.view || !this.view.isGalleryView()) {
return html``;
}
@@ -332,40 +329,40 @@ export class FrigateCardGalleryCore extends LitElement {
return html`
${this._media.map(
(media, index) =>
html`<frigate-card-thumbnail
(media, index) =>
html`<frigate-card-thumbnail
.hass=${this.hass}
.cameraManager=${this.cameraManager}
.media=${media}
.view=${this.view}
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
.show_favorite_control}
.show_favorite_control}
?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
.show_timeline_control}
.show_timeline_control}
@click=${(ev: Event) => {
if (this.view && this._media) {
this.view
.evolve({
view: 'media',
queryResults: this.view.queryResults?.clone().selectResult(
// Media in the gallery is reversed vs the queryResults (see
// note above).
this._media.length - index - 1
),
})
.dispatchChangeEvent(this);
}
stopEventFromActivatingCardWideActions(ev);
}}
if (this.view && this._media) {
this.view
.evolve({
view: 'media',
queryResults: this.view.queryResults?.clone().selectResult(
// Media in the gallery is reversed vs the queryResults (see
// note above).
this._media.length - index - 1,
),
})
.dispatchChangeEvent(this);
}
stopEventFromActivatingCardWideActions(ev);
}}
>
</frigate-card-thumbnail>`,
)}
)}
${this._showExtensionLoader
? html`${renderProgressIndicator({
cardWideConfig: this.cardWideConfig,
componentRef: this._refLoader,
})}`
cardWideConfig: this.cardWideConfig,
componentRef: this._refLoader,
})}`
: ''}
`;
}
+1
View File
@@ -228,6 +228,7 @@ export class FrigateCardLive extends LitElement {
.timelineConfig=${config.controls.timeline}
.cameraManager=${this.cameraManager}
.inBackground=${this._inBackground}
.cardWideConfig=${this.cardWideConfig}
@frigate-card:message=${(ev: CustomEvent<Message>) => {
this._renderKey++;
this._messageReceivedPostRender = true;
+40 -26
View File
@@ -13,7 +13,7 @@ import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { DateRange } from '../camera-manager/range';
import { localize } from '../localize/localize';
import mediaFilterStyle from '../scss/media-filter.scss';
import { createViewForEvents, createViewForRecordings } from '../utils/media-to-view.js';
import { executeMediaQueryForView } from '../utils/media-to-view.js';
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
import './select';
@@ -32,10 +32,8 @@ import { View } from '../view/view';
import { CameraManager } from '../camera-manager/manager';
import { HomeAssistant } from 'custom-card-helpers';
import {
EventQuery,
MediaMetadata,
QueryType,
RecordingQuery,
} from '../camera-manager/types';
import format from 'date-fns/format';
import endOfMonth from 'date-fns/endOfMonth';
@@ -43,6 +41,7 @@ import isEqual from 'lodash-es/isEqual';
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
import './select.js';
import orderBy from 'lodash-es/orderBy';
import { CardWideConfig } from '../types';
interface MediaFilterCoreDefaults {
mediaType?: MediaFilterMediaType;
@@ -83,7 +82,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
public view?: View;
@property({ attribute: false })
public mediaLimit?: number;
public cardWideConfig?: CardWideConfig;
static elementDefinitions = {
'frigate-card-select': FrigateCardSelect,
@@ -202,6 +201,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
// - Similarly, if the user chooses clips or snapshots, set the actual view
// to 'clips' or 'snapshots' in order to ensure the right icon is shown as
// selected in the menu.
const limit = this.cardWideConfig?.performance?.features.media_chunk_size;
if (
mediaType === MediaFilterMediaType.Clips ||
mediaType === MediaFilterMediaType.Snapshots
@@ -209,7 +210,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
const where = getArrayValueAsSet(this._refWhere.value?.value);
const what = getArrayValueAsSet(this._refWhat.value?.value);
const queries: EventQuery[] = [
const queries = new EventMediaQueries([
{
type: QueryType.Event,
cameraIDs: cameraIDs,
@@ -217,38 +218,51 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
...(where && { where: where }),
...(favorite !== null && { favorite: favorite }),
...(when && { start: when.start, end: when.end }),
...(this.mediaLimit && { limit: this.mediaLimit }),
...(limit && { limit: limit }),
...(mediaType === MediaFilterMediaType.Clips && { hasClip: true }),
...(mediaType === MediaFilterMediaType.Snapshots && {
hasSnapshot: true,
}),
},
];
]);
(
await createViewForEvents(this, this.hass, this.cameraManager, this.view, {
query: new EventMediaQueries(queries),
// See 'A note on views' above for these two arguments.
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
})
await executeMediaQueryForView(
this,
this.hass,
this.cameraManager,
this.view,
queries,
{
// See 'A note on views' above for these two arguments.
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
},
)
)?.dispatchChangeEvent(this);
} else if (mediaType === MediaFilterMediaType.Recordings) {
const query: RecordingQuery = {
type: QueryType.Recording,
cameraIDs: cameraIDs,
...(when && { start: when.start, end: when.end }),
};
const queries = new RecordingMediaQueries([
{
type: QueryType.Recording,
cameraIDs: cameraIDs,
...(limit && { limit: limit }),
...(when && { start: when.start, end: when.end }),
},
]);
(
await createViewForRecordings(this, this.hass, this.cameraManager, this.view, {
query: new RecordingMediaQueries([query]),
// See 'A note on views' above for these two arguments.
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
targetView: 'recordings',
})
await executeMediaQueryForView(
this,
this.hass,
this.cameraManager,
this.view,
queries,
{
// See 'A note on views' above for these two arguments.
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
targetView: 'recordings',
},
)
)?.dispatchChangeEvent(this);
}
}
+8 -2
View File
@@ -9,6 +9,7 @@ import {
import { customElement, property } from 'lit/decorators.js';
import surroundStyle from '../scss/surround.scss';
import {
CardWideConfig,
ClipsOrSnapshotsOrAll,
ExtendedHomeAssistant,
MiniTimelineControlConfig,
@@ -57,6 +58,9 @@ export class FrigateCardSurround extends LitElement {
@property({ attribute: false })
public cameraManager?: CameraManager;
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
protected _cameraIDsForTimeline?: Set<string>;
/**
@@ -68,6 +72,7 @@ export class FrigateCardSurround extends LitElement {
protected async _fetchMedia(): Promise<void> {
if (
!this.cameraManager ||
!this.cardWideConfig ||
!this.fetchMedia ||
this.inBackground ||
!this.hass ||
@@ -83,6 +88,7 @@ export class FrigateCardSurround extends LitElement {
this,
this.hass,
this.cameraManager,
this.cardWideConfig,
this.view,
{
targetView: this.view.view,
@@ -218,9 +224,9 @@ export class FrigateCardSurround extends LitElement {
.cameraIDs=${this._cameraIDsForTimeline}
.mini=${true}
.timelineConfig=${this.timelineConfig}
.thumbnailDetails=${this.thumbnailConfig?.show_details}
.thumbnailSize=${this.thumbnailConfig?.size}
.thumbnailConfig=${this.thumbnailConfig}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
>
</frigate-card-timeline-core>`
: ''}
+56 -38
View File
@@ -30,9 +30,11 @@ import { localize } from '../localize/localize';
import timelineCoreStyle from '../scss/timeline-core.scss';
import {
CameraConfig,
CardWideConfig,
ExtendedHomeAssistant,
frigateCardConfigDefaults,
FrigateCardView,
ThumbnailsControlConfig,
TimelineCoreConfig,
} from '../types';
import { stopEventFromActivatingCardWideActions } from '../utils/action';
@@ -41,10 +43,9 @@ import {
dispatchFrigateCardEvent,
isHoverableDevice,
} from '../utils/basic';
import {
createViewForEvents,
createViewForRecordings,
createQueriesForRecordingsView,
executeMediaQueryForView,
findClosestMediaIndex,
} from '../utils/media-to-view';
import { CameraManager } from '../camera-manager/manager';
@@ -168,10 +169,7 @@ export class FrigateCardTimelineCore extends LitElement {
public timelineConfig?: TimelineCoreConfig;
@property({ attribute: true, type: Boolean })
public thumbnailDetails? = false;
@property({ attribute: false })
public thumbnailSize?: number;
public thumbnailConfig?: ThumbnailsControlConfig;
// Whether or not this is a mini-timeline (in mini-mode the component takes a
// supportive role for other views).
@@ -186,6 +184,9 @@ export class FrigateCardTimelineCore extends LitElement {
@property({ attribute: false })
public cameraManager?: CameraManager;
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
@state()
protected _locked = false;
@@ -228,7 +229,7 @@ export class FrigateCardTimelineCore extends LitElement {
return `
<frigate-card-timeline-thumbnail
item='${item.id}'
${this.thumbnailDetails ? 'details' : ''}
${this.thumbnailConfig?.show_details ? 'details' : ''}
>
</frigate-card-timeline-thumbnail>`;
}
@@ -392,7 +393,6 @@ export class FrigateCardTimelineCore extends LitElement {
): Promise<void> {
const results = this.view?.queryResults;
const media = results?.getResults();
const cameraIDs = this._getTimelineCameraIDs();
if (
!media ||
!results ||
@@ -400,7 +400,6 @@ export class FrigateCardTimelineCore extends LitElement {
!this.view ||
!this.hass ||
!this.cameraManager ||
!cameraIDs ||
// Skip range changes that do not have hammerjs pan directions associated
// with them, as these outliers cause media matching issues below.
!properties.event.additionalEvent
@@ -418,7 +417,6 @@ export class FrigateCardTimelineCore extends LitElement {
findClosestMediaIndex(
media,
targetTime,
cameraIDs,
properties.event.additionalEvent === 'panright' ? 'end' : 'start',
),
);
@@ -467,6 +465,7 @@ export class FrigateCardTimelineCore extends LitElement {
!this._timeline ||
!this.view ||
!this.cameraManager ||
!this.cardWideConfig ||
!timelineCameraIDs ||
!properties.what
) {
@@ -479,34 +478,50 @@ export class FrigateCardTimelineCore extends LitElement {
this.timelineConfig?.show_recordings &&
['background', 'group-label'].includes(properties.what)
) {
view = await createViewForRecordings(
this,
this.hass,
const query = createQueriesForRecordingsView(
this.cameraManager,
this.view,
{
targetTime:
properties.what === 'background'
? properties.time
: this._timeline.getWindow().end,
...(properties.group && {
cameraIDs: new Set([String(properties.group)]),
}),
},
this.cardWideConfig,
new Set([String(properties.group)]),
);
if (query) {
view = await executeMediaQueryForView(
this,
this.hass,
this.cameraManager,
this.view,
query,
{
targetView: 'recording',
targetTime:
properties.what === 'background'
? properties.time
: this._timeline.getWindow().end,
},
);
}
} else if (this.timelineConfig?.show_recordings && properties.what === 'axis') {
view = await createViewForRecordings(
this,
this.hass,
const query = createQueriesForRecordingsView(
this.cameraManager,
this.view,
this.cardWideConfig,
timelineCameraIDs,
{
cameraIDs: timelineCameraIDs,
start: startOfHour(properties.time),
end: endOfHour(properties.time),
targetTime: properties.time,
},
);
if (query) {
view = await executeMediaQueryForView(
this,
this.hass,
this.cameraManager,
this.view,
query,
{
targetView: 'recording',
targetTime: properties.time,
},
);
}
} else if (properties.item && properties.what === 'item') {
const newResults = this.view.queryResults
?.clone()
@@ -618,7 +633,7 @@ export class FrigateCardTimelineCore extends LitElement {
protected _createEventMediaQuerys(options?: {
window?: TimelineWindow;
}): EventMediaQueries | null {
if (!this._timeline || !this._timelineSource) {
if (!this._timeline || !this._timelineSource || !this.cardWideConfig) {
return null;
}
@@ -644,15 +659,14 @@ export class FrigateCardTimelineCore extends LitElement {
if (!this.hass || !this.cameraManager || !this.view || !query) {
return null;
}
const view = await createViewForEvents(
const view = await executeMediaQueryForView(
this,
this.hass,
this.cameraManager,
this.view,
query,
{
query: query,
targetView: options?.targetView,
mediaType: this.timelineConfig?.media,
},
);
if (!view) {
@@ -1002,11 +1016,11 @@ export class FrigateCardTimelineCore extends LitElement {
* @param changedProps The changed properties
*/
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('thumbnailSize')) {
if (this.thumbnailSize !== undefined) {
if (changedProps.has('thumbnailConfig')) {
if (this.thumbnailConfig) {
this.style.setProperty(
'--frigate-card-thumbnail-size',
`${this.thumbnailSize}px`,
`${this.thumbnailConfig.size}px`,
);
} else {
this.style.removeProperty('--frigate-card-thumbnail-size');
@@ -1028,7 +1042,11 @@ export class FrigateCardTimelineCore extends LitElement {
changedProps.has('cameraIDs')
) {
const cameraIDs = this._getTimelineCameraIDs();
if (cameraIDs && this.cameraManager && this.timelineConfig) {
if (
cameraIDs &&
this.cameraManager &&
this.timelineConfig
) {
this._timelineSource = new TimelineDataSource(
this.cameraManager,
cameraIDs,
+6 -3
View File
@@ -1,7 +1,7 @@
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import timelineStyle from '../scss/timeline.scss';
import { ExtendedHomeAssistant, TimelineConfig } from '../types';
import { CardWideConfig, ExtendedHomeAssistant, TimelineConfig } from '../types';
import { CameraManager } from '../camera-manager/manager';
import { View } from '../view/view';
import './surround.js';
@@ -26,6 +26,9 @@ export class FrigateCardTimeline extends LitElement {
@property({ attribute: false })
public cameraManager?: CameraManager;
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
/**
* Master render method.
* @returns A rendered template.
@@ -45,9 +48,9 @@ export class FrigateCardTimeline extends LitElement {
.hass=${this.hass}
.view=${this.view}
.timelineConfig=${this.timelineConfig}
.thumbnailDetails=${this.timelineConfig.controls.thumbnails.show_details}
.thumbnailSize=${this.timelineConfig.controls.thumbnails.size}
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
>
</frigate-card-timeline-core>
</frigate-card-surround>`;
+5 -1
View File
@@ -95,7 +95,8 @@ export class FrigateCardViewer extends LitElement {
!this.hass ||
!this.view ||
!this.viewerConfig ||
!this.cameraManager
!this.cameraManager ||
!this.cardWideConfig
) {
return;
}
@@ -115,6 +116,7 @@ export class FrigateCardViewer extends LitElement {
this,
this.hass,
this.cameraManager,
this.cardWideConfig,
this.view,
{
targetView: 'recording',
@@ -125,6 +127,7 @@ export class FrigateCardViewer extends LitElement {
this,
this.hass,
this.cameraManager,
this.cardWideConfig,
this.view,
{
targetView: 'media',
@@ -141,6 +144,7 @@ export class FrigateCardViewer extends LitElement {
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
.timelineConfig=${this.viewerConfig.controls.timeline}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
>
<frigate-card-viewer-carousel
.hass=${this.hass}
+7 -1
View File
@@ -1,4 +1,3 @@
export const CAMERA_BIRDSEYE = 'birdseye' as const;
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card' as const;
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting` as const;
@@ -213,9 +212,16 @@ export const CONF_OVERRIDES = 'overrides' as const;
const CONF_PERFORMANCE = 'performance' as const;
export const CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR = `${CONF_PERFORMANCE}.features.animated_progress_indicator`;
export const CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE = `${CONF_PERFORMANCE}.features.media_chunk_size`;
export const CONF_PERFORMANCE_PROFILE = `${CONF_PERFORMANCE}.profile`;
export const CONF_PERFORMANCE_STYLE_BOX_SHADOW = `${CONF_PERFORMANCE}.style.box_shadow`;
export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.border_radius`;
// Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93
export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072;
// The number of media items to fetch at a time (for clips/snapshot views, and
// gallery chunks). Smaller values will cause more frequent smaller fetches, but
// improved rendering performance.
export const MEDIA_CHUNK_SIZE_DEFAULT = 50;
export const MEDIA_CHUNK_SIZE_MAX = 1000;
+5
View File
@@ -127,6 +127,8 @@ import {
CONF_VIEW_UPDATE_CYCLE_CAMERA,
CONF_VIEW_UPDATE_FORCE,
CONF_VIEW_UPDATE_SECONDS,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
MEDIA_CHUNK_SIZE_MAX,
} from './const.js';
import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss';
@@ -1853,6 +1855,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
this._defaults.performance.features.animated_progress_indicator,
)}
${this._renderNumberInput(CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE, {
max: MEDIA_CHUNK_SIZE_MAX,
})}
`,
)}
${this._putInSubmenu(
+2 -1
View File
@@ -255,7 +255,8 @@
"warning": "This card is in low profile mode so defaults have changed to optimize performance",
"features": {
"editor_label": "Feature Options",
"animated_progress_indicator": "Animated Progress Indicator"
"animated_progress_indicator": "Animated Progress Indicator",
"media_chunk_size": "Media chunk size"
},
"profile": "Performance profile",
"profiles": {
+18
View File
@@ -243,6 +243,24 @@
"overrides": {
"info": "Questa configurazione della scheda ha specificato manualmente le sostituzioni configurate che possono sostituire i valori mostrati nell'editor visivo, consultare l'editor di codice per visualizzare/modificare queste sostituzioni"
},
"performance": {
"warning": "",
"features": {
"editor_label": "",
"animated_progress_indicator": "",
"media_chunk_size": ""
},
"profile": "",
"profiles": {
"low": "",
"high": ""
},
"style": {
"editor_label": "",
"box_shadow": "",
"border_radius": ""
}
},
"view": {
"camera_select": "Visualizza per le telecamere appena selezionate",
"dark_mode": "Tema scuro",
+18
View File
@@ -243,6 +243,24 @@
"overrides": {
"info": "Esta configuração do cartão especificou manualmente as substituições configuradas que podem substituir os valores mostrados no editor visual, consulte o editor de código para visualizar/modificar essas substituições"
},
"performance": {
"warning": "",
"features": {
"editor_label": "",
"animated_progress_indicator": "",
"media_chunk_size": ""
},
"profile": "",
"profiles": {
"low": "",
"high": ""
},
"style": {
"editor_label": "",
"box_shadow": "",
"border_radius": ""
}
},
"view": {
"camera_select": "Visualização de câmeras recém-selecionadas",
"dark_mode": "Modo escuro",
+8 -5
View File
@@ -40,6 +40,7 @@ import {
CONF_MENU_BUTTONS_TIMELINE,
CONF_MENU_STYLE,
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
@@ -108,11 +109,6 @@ const LOW_PROFILE_DEFAULTS = {
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
// Refresh the live camera image every 10 seconds (same as stock Home
// Assistant Picture Glance).
[CONF_LIVE_IMAGE_REFRESH_SECONDS]: 10,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
@@ -120,9 +116,16 @@ const LOW_PROFILE_DEFAULTS = {
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
// Refresh the live camera image every 10 seconds (same as stock Home
// Assistant Picture Glance).
[CONF_LIVE_IMAGE_REFRESH_SECONDS]: 10,
// Disable all optional performance related features.
[CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR]: false,
// Load fewer media items by default.
[CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE]: 10,
// Disable all expensive CSS features.
[CONF_PERFORMANCE_STYLE_BORDER_RADIUS]: false,
[CONF_PERFORMANCE_STYLE_BOX_SHADOW]: false,
+16 -13
View File
@@ -13,6 +13,7 @@ import {
} from 'custom-card-helpers';
import { StyleInfo } from 'lit/directives/style-map.js';
import { z } from 'zod';
import { MEDIA_CHUNK_SIZE_DEFAULT, MEDIA_CHUNK_SIZE_MAX } from './const.js';
import { deepRemoveDefaults } from './utils/zod.js';
// The min allowed size of buttons.
@@ -48,7 +49,7 @@ const FRIGATE_CARD_VIEWS = [
'media',
] as const;
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
export type FrigateCardView = (typeof FRIGATE_CARD_VIEWS)[number];
export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const;
const FRIGATE_MENU_STYLES = [
@@ -66,7 +67,7 @@ const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
export const FRIGATE_MENU_PRIORITY_MAX = 100;
const LIVE_PROVIDERS = ['auto', 'image', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
export type LiveProvider = typeof LIVE_PROVIDERS[number];
export type LiveProvider = (typeof LIVE_PROVIDERS)[number];
const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
'all',
@@ -74,9 +75,9 @@ const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
'hidden',
'never',
] as const;
export type LazyUnloadCondition = typeof MEDIA_ACTION_NEGATIVE_CONDITIONS[number];
export type AutoMuteCondition = typeof MEDIA_ACTION_NEGATIVE_CONDITIONS[number];
export type AutoPauseCondition = typeof MEDIA_ACTION_NEGATIVE_CONDITIONS[number];
export type LazyUnloadCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
export type AutoMuteCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
export type AutoPauseCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
const MEDIA_ACTION_POSITIVE_CONDITIONS = [
'all',
@@ -84,14 +85,10 @@ const MEDIA_ACTION_POSITIVE_CONDITIONS = [
'visible',
'never',
] as const;
export type AutoUnmuteCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
export type AutoPlayCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
export type AutoUnmuteCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
export type AutoPlayCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
const ENGINES = [
'auto',
'frigate',
'generic',
] as const;
const ENGINES = ['auto', 'frigate', 'generic'] as const;
export class FrigateCardError extends Error {
context?: unknown;
@@ -225,7 +222,7 @@ const FRIGATE_CARD_ACTIONS = [
'camera_select',
'media_player',
] as const;
export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number];
export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number];
const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
@@ -1196,6 +1193,7 @@ const performanceConfigDefault = {
profile: 'high' as const,
features: {
animated_progress_indicator: true,
media_chunk_size: MEDIA_CHUNK_SIZE_DEFAULT,
},
style: {
border_radius: true,
@@ -1211,6 +1209,11 @@ const performanceConfigSchema = z
animated_progress_indicator: z
.boolean()
.default(performanceConfigDefault.features.animated_progress_indicator),
media_chunk_size: z
.number()
.min(0)
.max(MEDIA_CHUNK_SIZE_MAX)
.default(performanceConfigDefault.features.media_chunk_size),
})
.default(performanceConfigDefault.features),
style: z
+61 -118
View File
@@ -1,7 +1,5 @@
import add from 'date-fns/add';
import sub from 'date-fns/sub';
import { ViewContext } from 'view';
import { ClipsOrSnapshotsOrAll, FrigateCardView } from '../types';
import { CardWideConfig, ClipsOrSnapshotsOrAll, FrigateCardView } from '../types';
import { View } from '../view/view';
import {
EventMediaQueries,
@@ -16,71 +14,54 @@ import { dispatchFrigateCardErrorEvent } from '../components/message';
import { MediaQueriesResults } from '../view/media-queries-results';
import { errorToConsole } from './basic';
import { MediaQuery } from '../camera-manager/types';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const';
export const changeViewToRecentEventsForCameraAndDependents = async (
element: HTMLElement,
hass: HomeAssistant,
cameraManager: CameraManager,
cardWideConfig: CardWideConfig,
view: View,
options?: {
mediaType?: ClipsOrSnapshotsOrAll;
targetView?: FrigateCardView;
},
): Promise<void> => {
const cameras = cameraManager.getCameras();
if (!cameras) {
return;
}
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
const queries = createQueriesForEventsView(cameraManager, cardWideConfig, cameraIDs, {
mediaType: options?.mediaType,
});
if (!queries) {
return;
}
(
await createViewForEvents(element, hass, cameraManager, view, {
...options,
limit: 50, // Capture the 50 most recent events.
await executeMediaQueryForView(element, hass, cameraManager, view, queries, {
targetView: options?.targetView,
})
)?.dispatchChangeEvent(element);
};
export const createViewForEvents = async (
element: HTMLElement,
hass: HomeAssistant,
const createQueriesForEventsView = (
cameraManager: CameraManager,
view: View,
cardWideConfig: CardWideConfig,
cameraIDs: Set<string>,
options?: {
query?: EventMediaQueries;
cameraIDs?: Set<string>;
mediaType?: ClipsOrSnapshotsOrAll;
targetCameraID?: string;
targetView?: FrigateCardView;
limit?: number;
},
): Promise<View | null> => {
const cameras = cameraManager.getCameras();
if (!cameras) {
return null;
}
let query: EventMediaQueries;
const cameraIDs: Set<string> = options?.cameraIDs
? options.cameraIDs
: new Set(getAllDependentCameras(cameras, view.camera));
if (options?.query) {
query = options.query;
} else {
const eventQueries = cameraManager.generateDefaultEventQueries(cameraIDs, {
...(options?.limit && { limit: options.limit }),
...(options?.mediaType === 'clips' && { hasClip: true }),
...(options?.mediaType === 'snapshots' && { hasSnapshot: true }),
});
if (!eventQueries) {
return null;
}
query = new EventMediaQueries(eventQueries);
}
if (!query) {
return null;
}
return executeMediaQueryForView(element, hass, cameraManager, view, query, {
cameraIDs: cameraIDs,
targetView: options?.targetView,
targetCameraID: options?.targetCameraID,
): EventMediaQueries | null => {
const limit =
cardWideConfig.performance?.features.media_chunk_size ?? MEDIA_CHUNK_SIZE_DEFAULT;
const eventQueries = cameraManager.generateDefaultEventQueries(cameraIDs, {
limit: limit,
...(options?.mediaType === 'clips' && { hasClip: true }),
...(options?.mediaType === 'snapshots' && { hasSnapshot: true }),
});
return eventQueries ? new EventMediaQueries(eventQueries) : null;
};
/**
@@ -96,89 +77,61 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
element: HTMLElement,
hass: HomeAssistant,
cameraManager: CameraManager,
cardWideConfig: CardWideConfig,
view: View,
options?: {
targetView?: 'recording' | 'recordings';
},
): Promise<void> => {
const now = new Date();
const cameras = cameraManager.getCameras();
if (!cameras) {
return;
}
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
const queries = createQueriesForRecordingsView(
cameraManager,
cardWideConfig,
cameraIDs,
);
if (!queries) {
return;
}
(
await createViewForRecordings(element, hass, cameraManager, view, {
...options,
// Fetch 7 days worth of recordings (including recordings that are for the
// current hour).
start: sub(now, { days: 7 }),
end: add(now, { hours: 1 }),
await executeMediaQueryForView(element, hass, cameraManager, view, queries, {
targetView: options?.targetView,
})
)?.dispatchChangeEvent(element);
};
/**
* Create a view for recordings.
* @param element The element to dispatch the view change from.
* @param hass The Home Assistant object.
* @param cameraManager The datamanager to use for data access.
* @param cameras The camera configurations.
* @param view The current view.
* @param options A specific window (start and end) to fetch recordings for, a
* targetTime to seek to, a targetView to dispatch to and a set of cameraIDs to
* restrict to.
*/
export const createViewForRecordings = async (
element: HTMLElement,
hass: HomeAssistant,
export const createQueriesForRecordingsView = (
cameraManager: CameraManager,
view: View,
cardWideConfig: CardWideConfig,
cameraIDs: Set<string>,
options?: {
query?: RecordingMediaQueries;
cameraIDs?: Set<string>;
targetCameraID?: string;
targetView?: 'recording' | 'recordings';
targetTime?: Date;
start?: Date;
end?: Date;
},
): Promise<View | null> => {
const cameras = cameraManager.getCameras();
if (!cameras) {
return null;
}
const cameraIDs: Set<string> = options?.cameraIDs
? options.cameraIDs
: new Set(getAllDependentCameras(cameras, view.camera));
let query: RecordingMediaQueries;
if (options?.query) {
query = options.query;
} else {
const recordingQueries = cameraManager.generateDefaultRecordingQueries(cameraIDs, {
...(options?.start && { start: options.start }),
...(options?.end && { end: options.end }),
});
if (!recordingQueries) {
return null;
}
query = new RecordingMediaQueries(recordingQueries);
}
return executeMediaQueryForView(element, hass, cameraManager, view, query, {
cameraIDs: cameraIDs,
targetView: options?.targetView,
targetCameraID: options?.targetCameraID,
targetTime: options?.targetTime,
): RecordingMediaQueries | null => {
const limit =
cardWideConfig.performance?.features.media_chunk_size ?? MEDIA_CHUNK_SIZE_DEFAULT;
const recordingQueries = cameraManager.generateDefaultRecordingQueries(cameraIDs, {
limit: limit,
...(options?.start && { start: options.start }),
...(options?.end && { end: options.end }),
});
return recordingQueries ? new RecordingMediaQueries(recordingQueries) : null;
};
const executeMediaQueryForView = async (
export const executeMediaQueryForView = async (
element: HTMLElement,
hass: HomeAssistant,
cameraManager: CameraManager,
view: View,
query: MediaQueries,
options?: {
cameraIDs?: Set<string>;
targetCameraID?: string;
targetView?: FrigateCardView;
targetTime?: Date;
@@ -207,9 +160,9 @@ const executeMediaQueryForView = async (
const queryResults = new MediaQueriesResults(mediaArray, selectedIndex);
let viewerContext: ViewContext | undefined = {};
if (options?.targetTime && options.cameraIDs) {
if (options?.targetTime) {
queryResults.selectBestResult((media) =>
findClosestMediaIndex(media, options.targetTime as Date, options.cameraIDs),
findClosestMediaIndex(media, options.targetTime as Date),
);
viewerContext = {
mediaViewer: {
@@ -234,7 +187,6 @@ const executeMediaQueryForView = async (
* Find the closest matching media object.
* @param mediaArray The media. Must be sorted most recent first.
* @param targetTime The target time used to find the relevant child.
* @param cameraIDs The camera IDs to search for.
* @param refPoint Whether to find based on the start or end of the
* event/recording. If not specified, the first match is returned rather than
* the best match.
@@ -243,7 +195,6 @@ const executeMediaQueryForView = async (
export const findClosestMediaIndex = (
mediaArray: ViewMedia[],
targetTime: Date,
cameraIDs?: Set<string>,
refPoint?: 'start' | 'end',
): number | null => {
let bestMatch:
@@ -253,15 +204,7 @@ export const findClosestMediaIndex = (
}
| undefined;
if (!cameraIDs) {
return null;
}
for (const [i, media] of mediaArray.entries()) {
if (!cameraIDs.has(media.getCameraID())) {
continue;
}
if (media.includesTime(targetTime)) {
const start = media.getStartTime();
const end = media.getEndTime();
+1 -1
View File
@@ -132,7 +132,7 @@ export class TimelineDataSource {
}
const mediaArray = await this._cameraManager.executeMediaQueries(hass, eventQueries);
const data: FrigateCardTimelineItem[] = []
const data: FrigateCardTimelineItem[] = [];
for (const media of mediaArray ?? []) {
const endTime = media.getEndTime();
const startTime = media.getStartTime();