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 -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,
+7 -4
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';
@@ -10,7 +10,7 @@ import './timeline-core.js';
// This file is kept separate from timeline-core.ts to avoid a circular dependency:
// FrigateCardTimeline ->
// FrigateCardSurround ->
// FrigateCardTimelineCore
// FrigateCardTimelineCore
@customElement('frigate-card-timeline')
export class FrigateCardTimeline extends LitElement {
@@ -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();