Set timeline to now not to most recent event.

This commit is contained in:
Dermot Duffy
2024-04-07 20:21:09 -07:00
parent 14a4f89726
commit f67620397f
22 changed files with 828 additions and 336 deletions
+224
View File
@@ -0,0 +1,224 @@
import sub from 'date-fns/sub';
import { LitElement, ReactiveController } from 'lit';
import { ViewContext } from 'view';
import { CameraManager } from '../../camera-manager/manager.js';
import { FrigateCardMessageEventTarget } from '../../components/message.js';
import { CardWideConfig, LiveConfig } from '../../config/types.js';
import { MediaLoadedInfo, Message } from '../../types.js';
import {
FrigateCardMediaLoadedEventTarget,
dispatchExistingMediaLoadedInfoAsEvent,
} from '../../utils/media-info.js';
import {
changeViewToRecentEventsForCameraAndDependents,
changeViewToRecentRecordingForCameraAndDependents,
} from '../../utils/media-to-view.js';
import { FrigateCardViewChangeEventTarget, View } from '../../view/view.js';
interface LiveViewContext {
// A cameraID override (used for dependencies/substreams to force a different
// camera to be live rather than the camera selected in the view).
overrides?: Map<string, string>;
ptzVisible?: boolean;
fetchThumbnails?: boolean;
}
declare module 'view' {
interface ViewContext {
live?: LiveViewContext;
}
}
interface LastMediaLoadedInfo {
mediaLoadedInfo: MediaLoadedInfo;
source: EventTarget;
}
type LiveControllerHost = LitElement &
FrigateCardMediaLoadedEventTarget &
FrigateCardMessageEventTarget &
FrigateCardViewChangeEventTarget;
export class LiveController implements ReactiveController {
protected _host: LiveControllerHost;
// Whether or not the live view is currently in the background (i.e. preloaded
// but not visible).
protected _inBackground = false;
// Intersection handler is used to detect when the live view flips between
// foreground and background (in preload mode).
protected _intersectionObserver: IntersectionObserver;
// Whether or not to allow updates.
protected _messageReceived = false;
// MediaLoadedInfo object and target from the underlying live media. In the
// case of pre-loading these may be propagated later (from the original
// source).
protected _lastMediaLoadedInfo: LastMediaLoadedInfo | null = null;
protected _renderEpoch = 0;
constructor(host: LiveControllerHost) {
this._host = host;
this._intersectionObserver = new IntersectionObserver(
this._intersectionHandler.bind(this),
);
}
public shouldUpdate(): boolean {
// Don't process updates if it's in the background and a message was
// received (otherwise an error message thrown by the background live
// component may continually be re-spammed hitting performance).
return !this._inBackground || !this._messageReceived;
}
public hostConnected(): void {
this._intersectionObserver.observe(this._host);
this._host.addEventListener('frigate-card:media:loaded', this._handleMediaLoaded);
this._host.addEventListener('frigate-card:message', this._handleMessage);
this._host.addEventListener('frigate-card:view:change', this._handleViewChange);
}
public hostDisconnected(): void {
this._intersectionObserver.disconnect();
this._host.removeEventListener('frigate-card:media:loaded', this._handleMediaLoaded);
this._host.removeEventListener('frigate-card:message', this._handleMessage);
this._host.removeEventListener('frigate-card:view:change', this._handleViewChange);
}
public clearMessageReceived(): void {
this._messageReceived = false;
}
public isInBackground(): boolean {
return this._inBackground;
}
public getRenderEpoch(): number {
return this._renderEpoch;
}
protected _handleMessage = (ev: CustomEvent<Message>): void => {
this._messageReceived = true;
if (this._inBackground) {
ev.stopPropagation();
// Force the whole DOM to re-render next time.
this._renderEpoch++;
}
};
protected _handleMediaLoaded = (ev: CustomEvent<MediaLoadedInfo>): void => {
this._lastMediaLoadedInfo = {
source: ev.composedPath()[0],
mediaLoadedInfo: ev.detail,
};
if (this._inBackground) {
ev.stopPropagation();
}
};
protected _handleViewChange = (ev: CustomEvent<View>): void => {
if (this._inBackground) {
ev.stopPropagation();
}
};
protected _intersectionHandler(entries: IntersectionObserverEntry[]): void {
const wasInBackground = this._inBackground;
this._inBackground = !entries.some((entry) => entry.isIntersecting);
if (!this._inBackground && !this._messageReceived && this._lastMediaLoadedInfo) {
// If this isn't being rendered in the background, the last render did not
// generate a message and there's a saved MediaInfo, dispatch it upwards.
dispatchExistingMediaLoadedInfoAsEvent(
// Specifically dispatch the event "where it came from", as otherwise
// the intermediate layers (e.g. media-carousel which controls the title
// popups) will not re-receive the events.
this._lastMediaLoadedInfo.source,
this._lastMediaLoadedInfo.mediaLoadedInfo,
);
}
if (wasInBackground !== this._inBackground) {
this._host.requestUpdate();
}
}
/**
* Fetch thumbnail media when a target is not already specified in the view
* (e.g. first time live is visited).
*/
public async fetchMediaInBackgroundIfNecessary(
view: View,
cameraManager: CameraManager,
cardWideConfig: CardWideConfig,
overriddenLiveConfig: LiveConfig,
): Promise<void> {
if (
this._inBackground ||
// Only fetch media if there isn't any already.
view.query ||
overriddenLiveConfig.controls.thumbnails.mode === 'none' ||
view.context?.live?.fetchThumbnails === false
) {
return;
}
const mediaType = overriddenLiveConfig.controls.thumbnails.media_type;
const now = new Date();
const viewContext: ViewContext = {
// Force the window to start at the most recent time, not
// necessarily when the most recent event/recording was:
// https://github.com/dermotduffy/frigate-hass-card/issues/1301
timeline: {
window: {
start: sub(now, {
seconds: overriddenLiveConfig.controls.timeline.window_seconds,
}),
end: now,
},
},
};
/* istanbul ignore else: the else path cannot be reached -- @preserve */
if (mediaType === 'events') {
await changeViewToRecentEventsForCameraAndDependents(
this._host,
cameraManager,
cardWideConfig,
view,
{
allCameras: view.isGrid(),
targetView: view.view,
eventsMediaType: overriddenLiveConfig.controls.thumbnails.events_media_type,
select: 'latest',
// Force the window to start at the most recent time, not
// necessarily when the most recent event was:
// https://github.com/dermotduffy/frigate-hass-card/issues/1301
viewContext: viewContext,
},
);
} else if (mediaType === 'recordings') {
await changeViewToRecentRecordingForCameraAndDependents(
this._host,
cameraManager,
cardWideConfig,
view,
{
allCameras: view.isGrid(),
targetView: view.view,
select: 'latest',
viewContext: viewContext,
},
);
}
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ import {
setOrRemoveAttribute,
} from '../utils/basic';
import {
FrigateMediaLoadedEventTarget,
FrigateCardMediaLoadedEventTarget,
dispatchExistingMediaLoadedInfoAsEvent,
dispatchMediaUnloadedEvent,
} from '../utils/media-info';
@@ -23,7 +23,7 @@ const MEDIA_GRID_DEFAULT_IDEAL_CELL_WIDTH = 600;
const MEDIA_GRID_DEFAULT_SELECTED_WIDTH_FACTOR = 2.0;
type GridID = string;
type MediaGridChild = HTMLElement & FrigateMediaLoadedEventTarget;
type MediaGridChild = HTMLElement & FrigateCardMediaLoadedEventTarget;
type MediaGridContents = Map<GridID, MediaGridChild>;
export interface MediaGridSelected {