diff --git a/README.md b/README.md index 8799abd0..e233da0d 100644 --- a/README.md +++ b/README.md @@ -946,7 +946,7 @@ This card supports several different views: |`snapshot`|Shows a Media viewer for the most recent snapshot for this camera/zone/label. Can also be accessed by holding down the `snapshots` menu icon.| |`clips`|Shows an event gallery of clips for this camera/zone/label.| |`clip`|Shows a Media viewer for the most recent clip for this camera/zone/label. Can also be accessed by holding down the `clips` menu icon.| -|`recordings`|Shows a gallery of recordings for this camera.| +|`recordings`|Shows a gallery of recent (last day) recordings for this camera and its dependents.| |`recording`|Shows a Media viewer for the most recent recording for this camera. Can also be accessed by holding down the `recordings` menu icon.| |`image`|Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view.timeout_seconds`).| @@ -2693,7 +2693,7 @@ menu: ### Using a dependent camera -`dependencies.cameras` allows events for other cameras to be shown along with the currently selected camera. For example, this can be used to show events with the `birdseye` camera (since it will not have events of its own). +`dependencies.cameras` allows events/recordings for other cameras to be shown along with the currently selected camera. For example, this can be used to show events with the `birdseye` camera (since it will not have events of its own).
Expand: Using dependent cameras with birdseye diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index ebb1d37b..104a2af4 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -62,7 +62,7 @@ import { } from '../utils/media-to-view'; import { FrigateCardTimelineItem, - sortTimelineItemsYoungestToOldest, + sortYoungestToOldest, TimelineDataManager, } from '../utils/timeline-data-manager'; import { View } from '../view'; @@ -619,7 +619,7 @@ export class FrigateCardTimelineCore extends LitElement { this._dataview ?.get({ filter: (item) => item.type !== 'background', - order: sortTimelineItemsYoungestToOldest, + order: sortYoungestToOldest, }) .forEach((item) => { const cameraID = item.group ? String(item.group) : null; diff --git a/src/utils/camera.ts b/src/utils/camera.ts index 8ef70904..3dd747e6 100644 --- a/src/utils/camera.ts +++ b/src/utils/camera.ts @@ -107,3 +107,34 @@ export const getAllDependentCameras = ( } return cameraIDs; }; + +/** + * Return the cameraIDs of truly unique cameras (some configured cameras may be + * the same Frigate came but with different zone/labels). + * @param cameras The full set of cameras. + * @param cameraIDs The specific IDs to dedup. + */ +export const getTrueCameras = ( + cameras: Map, + cameraIDs: Set, +): Set => { + const getTrueCameraID = (cameraConfig: CameraConfig): string => { + return `${cameraConfig.frigate?.client_id ?? ''}/${ + cameraConfig.frigate.camera_name ?? '' + }`; + }; + + const output = new Set(); + const visitedTrueCameras = new Set(); + cameraIDs.forEach((cameraID: string) => { + const cameraConfig = cameras.get(cameraID) ?? null; + if (cameraConfig && cameraConfig.frigate.camera_name) { + const trueCameraID = getTrueCameraID(cameraConfig); + if (!visitedTrueCameras.has(trueCameraID)) { + output.add(cameraID); + visitedTrueCameras.add(trueCameraID); + } + } + }); + return output; +}; diff --git a/src/utils/ha/browse-media.ts b/src/utils/ha/browse-media.ts index 02cb574b..8fce396d 100644 --- a/src/utils/ha/browse-media.ts +++ b/src/utils/ha/browse-media.ts @@ -183,27 +183,7 @@ export const mergeFrigateBrowseMediaSources = async ( } } - const eventSort = ( - a: FrigateBrowseMediaSource, - b: FrigateBrowseMediaSource, - ): number => { - if ( - !a.frigate?.event || - (b.frigate?.event && b.frigate.event.start_time > a.frigate.event.start_time) - ) { - return 1; - } - - if ( - !b.frigate?.event || - (a.frigate?.event && b.frigate.event.start_time < a.frigate.event.start_time) - ) { - return -1; - } - return 0; - }; - - return createEventParentForChildren('Merged events', children.sort(eventSort)); + return createEventParentForChildren('Merged events', children.sort(sortYoungestToOldest)); }; /** @@ -445,3 +425,32 @@ export const createChild = ( } return result; }; + +/** + * Sort the timeline items most recent to least recent. + * @param a The first item. + * @param b The second item. + * @returns -1, 0, 1 (standard array sort function configuration). + */ +export const sortYoungestToOldest = ( + a: FrigateBrowseMediaSource, + b: FrigateBrowseMediaSource, +): number => { + const a_source = a.frigate?.event ?? a.frigate?.recording; + const b_source = b.frigate?.event ?? b.frigate?.recording; + + if ( + !a_source || + (b_source && b_source.start_time > a_source.start_time) + ) { + return 1; + } + + if ( + !b_source || + (a_source && b_source.start_time < a_source.start_time) + ) { + return -1; + } + return 0; +}; \ No newline at end of file diff --git a/src/utils/media-to-view.ts b/src/utils/media-to-view.ts index 3874872b..a248fb41 100644 --- a/src/utils/media-to-view.ts +++ b/src/utils/media-to-view.ts @@ -6,13 +6,13 @@ import { CameraConfig, ExtendedHomeAssistant, FrigateBrowseMediaSource } from '. import { View } from '../view'; import { formatDateAndTime, prettifyTitle } from './basic'; import { getRecordingMediaContentID } from './frigate'; -import { createChild, createEventParentForChildren } from './ha/browse-media'; +import { createChild, createEventParentForChildren, sortYoungestToOldest } from './ha/browse-media'; import { RecordingSegmentsItem, - sortSegmentsOldestToYoungest, + sortOldestToYoungest, TimelineDataManager, } from './timeline-data-manager'; -import { getAllDependentCameras } from './camera.js'; +import { getAllDependentCameras, getTrueCameras } from './camera.js'; /** * Change the view to a recent recording. @@ -132,8 +132,10 @@ const createRecordingChildren = ( ): FrigateBrowseMediaSource[] => { const children: FrigateBrowseMediaSource[] = []; - for (const cameraID of cameraIDs) { - const config = cameras.get(cameraID); + for (const cameraID of getTrueCameras( + cameras, cameraIDs + )) { + const config = cameras.get(cameraID) ?? null; const recordingSummary = dataManager.getRecordingSummaryForCamera(cameraID); if (!config?.frigate.camera_name || !recordingSummary) { continue; @@ -175,7 +177,9 @@ const createRecordingChildren = ( } } } - return children; + // Sort the events by time (to align recordings for different cameras at the + // same time). + return children.sort(sortYoungestToOldest); }; /** @@ -208,7 +212,7 @@ export const generateMediaViewerContextForChildren = ( segment.cameraID === child.frigate?.cameraID && segment.start >= start && segment.end <= end, - order: sortSegmentsOldestToYoungest, + order: sortOldestToYoungest, }); seekSeconds = getSeekTimeInSegments( // Recordings start from the top of the hour. diff --git a/src/utils/timeline-data-manager.ts b/src/utils/timeline-data-manager.ts index 680c351b..24a7016f 100644 --- a/src/utils/timeline-data-manager.ts +++ b/src/utils/timeline-data-manager.ts @@ -49,9 +49,9 @@ export interface RecordingSegmentsItem { * @param b The second item. * @returns -1, 0, 1 (standard array sort function configuration). */ -export const sortTimelineItemsYoungestToOldest = ( - a: FrigateCardTimelineItem, - b: FrigateCardTimelineItem, +export const sortYoungestToOldest = ( + a: RecordingSegmentsItem | FrigateCardTimelineItem, + b: RecordingSegmentsItem | FrigateCardTimelineItem, ): number => { if (a.start < b.start) { return 1; @@ -68,9 +68,9 @@ export const sortTimelineItemsYoungestToOldest = ( * @param b The second item. * @returns -1, 0, 1 (standard array sort function configuration). */ -export const sortSegmentsOldestToYoungest = ( - a: RecordingSegmentsItem, - b: RecordingSegmentsItem, +export const sortOldestToYoungest = ( + a: RecordingSegmentsItem | FrigateCardTimelineItem, + b: RecordingSegmentsItem | FrigateCardTimelineItem, ): number => { if (a.start < b.start) { return -1; @@ -438,7 +438,7 @@ export class TimelineDataManager { this._cameras.forEach((_, cameraID) => { const segments = this._recordingSegments.get({ filter: (item) => item.cameraID === cameraID, - order: sortSegmentsOldestToYoungest, + order: sortOldestToYoungest, }); let current: RecordingSegmentsItem | null = null; for (let i = 0; i < segments.length; ++i) {