diff --git a/README.md b/README.md index fd98663e..04dee783 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/camera-manager/frigate/engine-frigate.ts b/src/camera-manager/frigate/engine-frigate.ts index f84fd128..dc63dc76 100644 --- a/src/camera-manager/frigate/engine-frigate.ts +++ b/src/camera-manager/frigate/engine-frigate.ts @@ -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, diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts index 1ed8ac73..a39d64ac 100644 --- a/src/camera-manager/manager.ts +++ b/src/camera-manager/manager.ts @@ -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 | 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[] = []; diff --git a/src/card.ts b/src/card.ts index 28e706f6..3b2ce336 100644 --- a/src/card.ts +++ b/src/card.ts @@ -1916,6 +1916,7 @@ class FrigateCard extends LitElement { .view=${this._view} .timelineConfig=${this._getConfig().timeline} .cameraManager=${this._cameraManager} + .cardWideConfig=${this._cardWideConfig} > ` : ``} diff --git a/src/components/gallery.ts b/src/components/gallery.ts index 591ce222..877e9a02 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -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` ${this.galleryConfig && this.galleryConfig.controls.filter.mode !== 'none' - ? html` ` - : ''} + : ''} - html` + html` { - 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); + }} > `, - )} + )} ${this._showExtensionLoader ? html`${renderProgressIndicator({ - cardWideConfig: this.cardWideConfig, - componentRef: this._refLoader, - })}` + cardWideConfig: this.cardWideConfig, + componentRef: this._refLoader, + })}` : ''} `; } diff --git a/src/components/live/live.ts b/src/components/live/live.ts index 769fe5cd..575c9010 100644 --- a/src/components/live/live.ts +++ b/src/components/live/live.ts @@ -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) => { this._renderKey++; this._messageReceivedPostRender = true; diff --git a/src/components/media-filter.ts b/src/components/media-filter.ts index eb3dcdcf..ee9bed19 100644 --- a/src/components/media-filter.ts +++ b/src/components/media-filter.ts @@ -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); } } diff --git a/src/components/surround.ts b/src/components/surround.ts index 12ee3b60..10706196 100644 --- a/src/components/surround.ts +++ b/src/components/surround.ts @@ -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; /** @@ -68,6 +72,7 @@ export class FrigateCardSurround extends LitElement { protected async _fetchMedia(): Promise { 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} > ` : ''} diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index 7e68c7a7..36ac813e 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -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 ` `; } @@ -392,7 +393,6 @@ export class FrigateCardTimelineCore extends LitElement { ): Promise { 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, diff --git a/src/components/timeline.ts b/src/components/timeline.ts index 31aafbe1..2030a762 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -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} > `; diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 521882ee..390e5df4 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -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} > => { + 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, options?: { - query?: EventMediaQueries; - cameraIDs?: Set; mediaType?: ClipsOrSnapshotsOrAll; - targetCameraID?: string; - targetView?: FrigateCardView; - limit?: number; }, -): Promise => { - const cameras = cameraManager.getCameras(); - if (!cameras) { - return null; - } - let query: EventMediaQueries; - const cameraIDs: Set = 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 => { - 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, options?: { - query?: RecordingMediaQueries; - cameraIDs?: Set; - targetCameraID?: string; - targetView?: 'recording' | 'recordings'; - targetTime?: Date; start?: Date; end?: Date; }, -): Promise => { - const cameras = cameraManager.getCameras(); - if (!cameras) { - return null; - } - const cameraIDs: Set = 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; 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, 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(); diff --git a/src/utils/timeline-source.ts b/src/utils/timeline-source.ts index 79886099..67d2a2a8 100644 --- a/src/utils/timeline-source.ts +++ b/src/utils/timeline-source.ts @@ -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();