259 lines
7.3 KiB
TypeScript
259 lines
7.3 KiB
TypeScript
import { ViewContext } from 'view';
|
|
import { CameraManager } from '../camera-manager/manager';
|
|
import { MediaQuery } from '../camera-manager/types';
|
|
import { dispatchFrigateCardErrorEvent } from '../components/message';
|
|
import { CardWideConfig, FrigateCardView } from '../config/types';
|
|
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const';
|
|
import { ClipsOrSnapshotsOrAll } from '../types';
|
|
import { ViewMedia } from '../view/media';
|
|
import {
|
|
EventMediaQueries,
|
|
MediaQueries,
|
|
RecordingMediaQueries,
|
|
} from '../view/media-queries';
|
|
import { MediaQueriesResults } from '../view/media-queries-results';
|
|
import { View } from '../view/view';
|
|
import { errorToConsole } from './basic';
|
|
|
|
type ResultSelectType = 'latest' | 'time' | 'none';
|
|
|
|
export const changeViewToRecentEventsForCameraAndDependents = async (
|
|
element: HTMLElement,
|
|
cameraManager: CameraManager,
|
|
cardWideConfig: CardWideConfig,
|
|
view: View,
|
|
options?: {
|
|
allCameras?: boolean;
|
|
mediaType?: ClipsOrSnapshotsOrAll;
|
|
targetView?: FrigateCardView;
|
|
select?: ResultSelectType;
|
|
},
|
|
): Promise<void> => {
|
|
const cameraIDs = options?.allCameras
|
|
? cameraManager.getStore().getVisibleCameraIDs()
|
|
: cameraManager.getStore().getAllDependentCameras(view.camera);
|
|
if (!cameraIDs.size) {
|
|
return;
|
|
}
|
|
|
|
const queries = createQueriesForEventsView(cameraManager, cardWideConfig, cameraIDs, {
|
|
mediaType: options?.mediaType,
|
|
});
|
|
if (!queries) {
|
|
return;
|
|
}
|
|
|
|
(
|
|
await executeMediaQueryForViewWithErrorDispatching(
|
|
element,
|
|
cameraManager,
|
|
view,
|
|
queries,
|
|
{
|
|
targetView: options?.targetView,
|
|
select: options?.select,
|
|
},
|
|
)
|
|
)?.dispatchChangeEvent(element);
|
|
};
|
|
|
|
const createQueriesForEventsView = (
|
|
cameraManager: CameraManager,
|
|
cardWideConfig: CardWideConfig,
|
|
cameraIDs: Set<string>,
|
|
options?: {
|
|
mediaType?: ClipsOrSnapshotsOrAll;
|
|
},
|
|
): 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;
|
|
};
|
|
|
|
/**
|
|
* Change the view to a recent recording.
|
|
* @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 set of cameraIDs to fetch recordings for, and a targetView to dispatch to.
|
|
*/
|
|
export const changeViewToRecentRecordingForCameraAndDependents = async (
|
|
element: HTMLElement,
|
|
cameraManager: CameraManager,
|
|
cardWideConfig: CardWideConfig,
|
|
view: View,
|
|
options?: {
|
|
allCameras?: boolean;
|
|
targetView?: 'recording' | 'recordings';
|
|
select?: ResultSelectType;
|
|
},
|
|
): Promise<void> => {
|
|
const cameraIDs = options?.allCameras
|
|
? cameraManager.getStore().getVisibleCameraIDs()
|
|
: cameraManager.getStore().getAllDependentCameras(view.camera);
|
|
if (!cameraIDs.size) {
|
|
return;
|
|
}
|
|
|
|
const queries = createQueriesForRecordingsView(
|
|
cameraManager,
|
|
cardWideConfig,
|
|
cameraIDs,
|
|
);
|
|
if (!queries) {
|
|
return;
|
|
}
|
|
|
|
(
|
|
await executeMediaQueryForViewWithErrorDispatching(
|
|
element,
|
|
cameraManager,
|
|
view,
|
|
queries,
|
|
{
|
|
targetView: options?.targetView,
|
|
select: options?.select,
|
|
},
|
|
)
|
|
)?.dispatchChangeEvent(element);
|
|
};
|
|
|
|
const createQueriesForRecordingsView = (
|
|
cameraManager: CameraManager,
|
|
cardWideConfig: CardWideConfig,
|
|
cameraIDs: Set<string>,
|
|
): RecordingMediaQueries | null => {
|
|
const limit =
|
|
cardWideConfig.performance?.features.media_chunk_size ?? MEDIA_CHUNK_SIZE_DEFAULT;
|
|
const recordingQueries = cameraManager.generateDefaultRecordingQueries(cameraIDs, {
|
|
limit: limit,
|
|
});
|
|
return recordingQueries ? new RecordingMediaQueries(recordingQueries) : null;
|
|
};
|
|
|
|
export const executeMediaQueryForView = async (
|
|
cameraManager: CameraManager,
|
|
view: View,
|
|
query: MediaQueries,
|
|
options?: {
|
|
targetCameraID?: string;
|
|
targetView?: FrigateCardView;
|
|
targetTime?: Date;
|
|
select?: ResultSelectType;
|
|
},
|
|
): Promise<View | null> => {
|
|
const queries = query.getQueries();
|
|
if (!queries) {
|
|
return null;
|
|
}
|
|
|
|
const mediaArray = await cameraManager.executeMediaQueries<MediaQuery>(queries);
|
|
if (!mediaArray) {
|
|
return null;
|
|
}
|
|
|
|
const queryResults = new MediaQueriesResults({ results: mediaArray });
|
|
let viewerContext: ViewContext | undefined = {};
|
|
const cameraID = options?.targetCameraID ?? view.camera;
|
|
|
|
if (options?.select === 'time' && options?.targetTime) {
|
|
queryResults.selectBestResult((media) =>
|
|
findBestMediaIndex(media, options.targetTime as Date, cameraID),
|
|
);
|
|
viewerContext = {
|
|
mediaViewer: {
|
|
seek: options.targetTime,
|
|
},
|
|
};
|
|
}
|
|
|
|
return view
|
|
.evolve({
|
|
query: query,
|
|
queryResults: queryResults,
|
|
view: options?.targetView,
|
|
camera: cameraID,
|
|
})
|
|
.mergeInContext(viewerContext);
|
|
};
|
|
|
|
export const executeMediaQueryForViewWithErrorDispatching = async (
|
|
element: HTMLElement,
|
|
cameraManager: CameraManager,
|
|
view: View,
|
|
query: MediaQueries,
|
|
options?: {
|
|
targetCameraID?: string;
|
|
targetView?: FrigateCardView;
|
|
targetTime?: Date;
|
|
select?: ResultSelectType;
|
|
},
|
|
): Promise<View | null> => {
|
|
try {
|
|
return await executeMediaQueryForView(cameraManager, view, query, {
|
|
targetCameraID: options?.targetCameraID,
|
|
targetView: options?.targetView,
|
|
targetTime: options?.targetTime,
|
|
select: options?.select,
|
|
});
|
|
} catch (e: unknown) {
|
|
errorToConsole(e as Error);
|
|
dispatchFrigateCardErrorEvent(element, e as Error);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Find the longest matching media object that contains a given targetTime.
|
|
* Longest is chosen to give the most stability to the media viewer.
|
|
* @param mediaArray The media.
|
|
* @param targetTime The target time used to find the relevant child.
|
|
* @returns The childindex or null if no matching child is found.
|
|
*/
|
|
export const findBestMediaIndex = (
|
|
mediaArray: ViewMedia[],
|
|
targetTime: Date,
|
|
favorCameraID?: string,
|
|
): number | null => {
|
|
let bestMatch:
|
|
| {
|
|
index: number;
|
|
duration: number;
|
|
cameraID: string;
|
|
}
|
|
| undefined;
|
|
|
|
for (const [i, media] of mediaArray.entries()) {
|
|
const start = media.getStartTime();
|
|
const end = media.getUsableEndTime();
|
|
|
|
if (media.includesTime(targetTime) && start && end) {
|
|
const duration = end.getTime() - start.getTime();
|
|
|
|
if (
|
|
// No best match so far ...
|
|
!bestMatch ||
|
|
// ... or there is a best-match, but it's from a non-favored camera (unlike this one) ...
|
|
(favorCameraID &&
|
|
bestMatch.cameraID !== favorCameraID &&
|
|
media.getCameraID() === favorCameraID) ||
|
|
// ... or this match is longer and either there's no favored camera or this is it.
|
|
(duration > bestMatch.duration &&
|
|
(!favorCameraID ||
|
|
bestMatch.cameraID !== favorCameraID ||
|
|
media.getCameraID() === favorCameraID))
|
|
) {
|
|
bestMatch = { index: i, duration: duration, cameraID: media.getCameraID() };
|
|
}
|
|
}
|
|
}
|
|
return bestMatch ? bestMatch.index : null;
|
|
};
|