Add support for both snapshots/clips in timeline.

This commit is contained in:
Dermot Duffy
2022-04-04 21:25:12 -07:00
parent f98d749d07
commit 9c372815f2
10 changed files with 212 additions and 157 deletions
+29 -26
View File
@@ -38,17 +38,18 @@ export class FrigateCardGallery extends LitElement {
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
if (!this.hass || !this.view || !this.cameraConfig) {
if (!this.hass || !this.view || !this.cameraConfig || !this.view.isGalleryView()) {
return;
}
if (!this.view.target) {
const browseMediaQueryParameters =
BrowseMediaUtil.getBrowseMediaQueryParametersOrDispatchError(
const browseMediaQueryParameters = BrowseMediaUtil.setMediaTypeFromView(
BrowseMediaUtil.getBrowseMediaQueryParametersBaseOrDispatchError(
this,
this.view,
this.cameraConfig,
);
),
this.view,
);
if (!browseMediaQueryParameters) {
return;
}
@@ -203,27 +204,29 @@ export class FrigateCardGalleryCore extends LitElement {
</div>`
: child.thumbnail
? html`<img
aria-label="${child.title}"
class="mdc-image-list__image"
src="${child.thumbnail}"
title="${child.title}"
@click=${(ev) => {
if (this.view) {
this.view
.evolve({
view: this.view.is('clips') ? 'clip' : 'snapshot',
childIndex: index,
previous: this.view,
})
.dispatchChangeEvent(this);
}
stopEventFromActivatingCardWideActions(ev);
}}
/>${child.frigate?.event?.retain_indefinitely ? html`<ha-icon
class="favorite"
icon="mdi:star"
title=${localize('thumbnail.retain_indefinitely')}
/>` : ``}`
aria-label="${child.title}"
class="mdc-image-list__image"
src="${child.thumbnail}"
title="${child.title}"
@click=${(ev: Event) => {
if (this.view) {
this.view
.evolve({
view: this.view.is('clips') ? 'clip' : 'snapshot',
childIndex: index,
previous: this.view,
})
.dispatchChangeEvent(this);
}
stopEventFromActivatingCardWideActions(ev);
}}
/>${child.frigate?.event?.retain_indefinitely
? html`<ha-icon
class="favorite"
icon="mdi:star"
title=${localize('thumbnail.retain_indefinitely')}
/>`
: ``}`
: ``}
</div>
</li>`,
+8 -14
View File
@@ -7,7 +7,6 @@ import {
PropertyValues,
} from 'lit';
import {
FrigateBrowseMediaSource,
ExtendedHomeAssistant,
CameraConfig,
JSMPEGConfig,
@@ -20,25 +19,21 @@ import {
LiveProvider,
TransitionEffect,
frigateCardConfigDefaults,
BrowseMediaQueryParameters,
} from '../types.js';
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
import { HomeAssistant } from 'custom-card-helpers';
import JSMpeg from '@cycjimmy/jsmpeg-player';
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { Task } from '@lit-labs/task';
import { customElement, property, query, state } from 'lit/decorators.js';
import { customElement, property, state } from 'lit/decorators.js';
import { until } from 'lit/directives/until.js';
import { styleMap } from 'lit/directives/style-map.js';
import { AutoMediaPlugin, AutoMediaPluginType } from './embla-plugins/automedia.js';
import { BrowseMediaUtil } from '../browse-media-util.js';
import { ConditionState, getOverriddenConfig } from '../card-condition.js';
import { FrigateCardMediaCarousel } from './media-carousel.js';
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
import {
FrigateCardThumbnailCarousel,
ThumbnailCarouselTap,
} from './thumbnail-carousel.js';
import { Lazyload } from './embla-plugins/lazyload.js';
import { View } from '../view.js';
import { localize } from '../localize/localize.js';
@@ -104,9 +99,6 @@ export class FrigateCardLive extends LitElement {
// pre-loading it may be propagated upwards later.
protected _savedMediaShowInfo?: MediaShowInfo;
@query('frigate-card-thumbnail-carousel')
protected _thumbnailCarousel?: FrigateCardThumbnailCarousel;
/**
* Handler for media show events that special cases preloaded live views.
* @param e The media show event.
@@ -135,13 +127,16 @@ export class FrigateCardLive extends LitElement {
this.conditionState,
) as LiveConfig;
const browseMediaParams = BrowseMediaUtil.getBrowseMediaQueryParameters(
config.controls.thumbnails.media,
const browseMediaParamsBase = BrowseMediaUtil.getBrowseMediaQueryParametersBase(
this.cameras.get(this.view.camera),
);
if (!browseMediaParams) {
if (!browseMediaParamsBase) {
return;
}
const browseMediaParams: BrowseMediaQueryParameters = {
...browseMediaParamsBase,
mediaType: config.controls.thumbnails.media,
}
// Note use of liveConfig and not config below -- the carousel will
// independently override the liveconfig to reflect the camera in the
@@ -150,7 +145,6 @@ export class FrigateCardLive extends LitElement {
.hass=${this.hass}
.view=${this.view}
.config=${config.controls.thumbnails}
.targetView=${config.controls.thumbnails.media == 'clips' ? 'clip' : 'snapshot'}
.browseMediaParams=${browseMediaParams}
>
<frigate-card-live-carousel
+4 -3
View File
@@ -1,7 +1,7 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { HomeAssistant } from 'custom-card-helpers';
import { Task } from '@lit-labs/task';
import { customElement, property, state } from 'lit/decorators.js';
import { customElement, property } from 'lit/decorators.js';
import { BrowseMediaUtil } from '../browse-media-util.js';
import {
@@ -45,7 +45,8 @@ export class FrigateCardSurround extends LitElement {
]);
/**
* Fetch thumbnail media.
* Fetch thumbnail media when a target is not specified in the view (e.g. for
* the live view).
* @param param Task parameters.
* @returns
*/
@@ -121,7 +122,7 @@ export class FrigateCardSurround extends LitElement {
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
this.view
?.evolve({
...(this.targetView && { view: this.targetView }),
view: this.targetView || 'event',
target: ev.detail.target,
childIndex: ev.detail.childIndex,
})
+92 -54
View File
@@ -1,5 +1,4 @@
// TODO: Clips vs snapshots: Should be able to navigate from snapshots view and it should just work.
// TODO: Remove HACK in view.ts on clips
// TODO: Hover over an event should show something useful.
// TODO: Periodically refetch events.
// TODO: Search for TODOs and logging statements.
@@ -16,7 +15,6 @@ import {
import { DataSet } from 'vis-data/esnext';
import {
DataGroupCollectionType,
IdType,
Timeline,
TimelineItem,
TimelineOptions,
@@ -36,8 +34,7 @@ import {
ExtendedHomeAssistant,
FrigateBrowseMediaSource,
MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_VIDEO,
MEDIA_CLASS_VIDEO,
MEDIA_TYPE_PLAYLIST,
TimelineConfig,
FrigateEvent,
} from '../types';
@@ -58,7 +55,9 @@ interface FrigateCardGroupData {
content: string;
}
interface FrigateCardTimelineItem extends TimelineItem {
source: FrigateBrowseMediaSource;
event: FrigateEvent;
clip?: FrigateBrowseMediaSource;
snapshot?: FrigateBrowseMediaSource;
}
interface TimelineViewContext extends ViewContext {
@@ -114,20 +113,29 @@ class TimelineEventManager {
protected _addMediaSource(camera: string, target: FrigateBrowseMediaSource): void {
const items: FrigateCardTimelineItem[] = [];
target.children?.forEach((child) => {
if (child.frigate) {
const item = {
id: child.media_content_id,
group: camera,
content: this._contentCallback?.(child) ?? '',
start: child.frigate.event.start_time * 1000,
source: child,
};
if (child.frigate.event.end_time) {
item['end'] = child.frigate.event.end_time * 1000;
const event = child.frigate?.event;
if (event && ['video', 'image'].includes(child.media_content_type)) {
let item = this._dataset.get(event.id);
if (!item) {
item = {
id: event.id,
group: camera,
content: this._contentCallback?.(child) ?? '',
start: event.start_time * 1000,
event: event,
};
}
if (event.end_time) {
item['end'] = event.end_time * 1000;
item['type'] = 'range';
} else {
item['type'] = 'point';
}
if (child.media_content_type === 'video') {
item['clip'] = child;
} else if (child.media_content_type === 'image') {
item['snapshot'] = child;
}
items.push(item);
}
});
@@ -161,13 +169,14 @@ class TimelineEventManager {
element: HTMLElement,
hass: HomeAssistant & ExtendedHomeAssistant,
cameras: Map<string, CameraConfig>,
media: 'all' | 'clips' | 'snapshots',
start: Date,
end: Date,
): Promise<boolean> {
if (this.hasCoverage(start, end)) {
return false;
}
await this._fetchEvents(element, hass, cameras, start, end);
await this._fetchEvents(element, hass, cameras, media, start, end);
return true;
}
@@ -183,6 +192,7 @@ class TimelineEventManager {
element: HTMLElement,
hass: HomeAssistant & ExtendedHomeAssistant,
cameras: Map<string, CameraConfig>,
media: 'all' | 'clips' | 'snapshots',
start: Date,
end: Date,
): Promise<void> {
@@ -193,24 +203,25 @@ class TimelineEventManager {
this._dateEnd = end;
}
const fetchCameraEvents = async (camera: string): Promise<void> => {
const fetchCameraEvents = async (
camera: string,
mediaType: 'clips' | 'snapshots',
): Promise<void> => {
const cameraConfig = cameras.get(camera);
if (!cameraConfig || !this._dateStart || !this._dateEnd) {
return;
}
const browseMediaQueryParameters = BrowseMediaUtil.getBrowseMediaQueryParameters(
'clips',
const browseMediaQueryParametersBase = BrowseMediaUtil.getBrowseMediaQueryParametersBase(
cameraConfig,
);
if (!browseMediaQueryParameters) {
if (!browseMediaQueryParametersBase) {
return;
}
try {
this._addMediaSource(
camera,
await BrowseMediaUtil.browseMediaQuery(hass, {
...browseMediaQueryParameters,
...browseMediaQueryParametersBase,
// Events are always fetched for the maximum extent of the managed
// range. This is because events may change at any point in time
@@ -218,6 +229,7 @@ class TimelineEventManager {
before: this._dateEnd.getTime() / 1000,
after: this._dateStart.getTime() / 1000,
unlimited: true,
mediaType: mediaType,
}),
);
} catch (e) {
@@ -225,7 +237,15 @@ class TimelineEventManager {
}
};
await Promise.all(Array.from(cameras.keys()).map(fetchCameraEvents.bind(this)));
const promises: Promise<void>[] = [];
(media === 'all' ? ['clips', 'snapshots'] : [media]).forEach((mediaType) =>
promises.push(
...Array.from(cameras.keys()).map((camera) =>
fetchCameraEvents(camera, mediaType as 'clips' | 'snapshots'),
),
),
);
await Promise.all(promises);
}
}
@@ -256,7 +276,6 @@ export class FrigateCardTimeline extends LitElement {
.hass=${this.hass}
.view=${this.view}
.config=${this.timelineConfig.controls.thumbnails}
.targetView=${'clip'}
>
<frigate-card-timeline-core
.hass=${this.hass}
@@ -330,12 +349,13 @@ export class FrigateCardTimelineCore extends LitElement {
console.info(
`Range changed: ${properties.start} -> ${properties.end} [${this._events.dataset.length}]`,
);
if (this.hass && this.cameras && this._timeline) {
if (this.hass && this.cameras && this._timeline && this.timelineConfig) {
this._events
.fetchEventsIfNecessary(
this,
this.hass,
this.cameras,
this.timelineConfig.media,
properties.start,
properties.end,
)
@@ -367,7 +387,9 @@ export class FrigateCardTimelineCore extends LitElement {
if (!this._thumbnails || !this._thumbnails.children || data.items.length <= 0) {
return;
}
const childIndex = this._findThumbnailIndex(data.items[0]);
const childIndex = this._thumbnails.children.findIndex(
(child) => child.frigate?.event.id === data.items[0],
);
if (childIndex >= 0) {
this.view
?.evolve({
@@ -379,19 +401,6 @@ export class FrigateCardTimelineCore extends LitElement {
}
}
/**
* Find the index of the given item in the thumbnails.
* @param id
* @returns The index of the item, or -1 if not found.
*/
public _findThumbnailIndex(id: IdType | IdType[]): number {
if (!this._thumbnails || !this._thumbnails.children) {
return -1;
}
id = Array.isArray(id) ? id[0] : id;
return this._thumbnails.children.findIndex((child) => child.media_content_id === id);
}
/**
* Regenerate the thumbnails from the timeline events.
* @returns
@@ -401,10 +410,33 @@ export class FrigateCardTimelineCore extends LitElement {
return;
}
const children: FrigateBrowseMediaSource[] = this._events.dataset
.get()
.filter((item) => BrowseMediaUtil.isTrueMedia(item.source))
.map((item) => item.source);
const selected = this._timeline.getSelection();
let childIndex = -1;
const children: FrigateBrowseMediaSource[] = [];
this._events.dataset.get().forEach((item) => {
if (this.timelineConfig) {
let added = false;
if (
item.clip &&
['all', 'clips'].includes(this.timelineConfig.media) &&
BrowseMediaUtil.isTrueMedia(item.clip)
) {
added = true;
children.push(item.clip);
} else if (
item.snapshot &&
['all', 'snapshots'].includes(this.timelineConfig.media) &&
BrowseMediaUtil.isTrueMedia(item.snapshot)
) {
added = true
children.push(item.snapshot);
}
if (added && selected.includes(item.event.id)) {
childIndex = children.length-1;
}
}
});
if (!children.length) {
return;
}
@@ -412,17 +444,16 @@ export class FrigateCardTimelineCore extends LitElement {
const target = {
title: `Timeline events`,
media_class: MEDIA_CLASS_PLAYLIST,
media_content_type: MEDIA_TYPE_VIDEO,
media_content_type: MEDIA_TYPE_PLAYLIST,
media_content_id: '',
can_play: false,
can_expand: true,
children_media_class: MEDIA_CLASS_VIDEO,
children_media_class: MEDIA_CLASS_PLAYLIST,
thumbnail: null,
children: children,
};
this._thumbnails = target;
const childIndex = this._findThumbnailIndex(this._timeline.getSelection());
// Update the thumbnail carousel with the regenerated thumbnails.
this.view
@@ -523,11 +554,11 @@ export class FrigateCardTimelineCore extends LitElement {
// different object types together (e.g. person and car).
return (
!!first.id &&
first.id !== this.view?.media?.media_content_id &&
first.id !== this.view?.media?.frigate?.event?.id &&
!!second.id &&
second.id != this.view?.media?.media_content_id &&
(<FrigateCardTimelineItem>first).source.frigate?.event.label ===
(<FrigateCardTimelineItem>second).source.frigate?.event.label
second.id != this.view?.media?.frigate?.event?.id &&
(<FrigateCardTimelineItem>first).event.label ===
(<FrigateCardTimelineItem>second).event.label
);
},
}
@@ -573,9 +604,15 @@ export class FrigateCardTimelineCore extends LitElement {
*/
protected async _updateTimelineFromView(): Promise<void> {
const event = this.view?.media?.frigate?.event;
const id = this.view?.media?.media_content_id;
if (!this.hass || !this.cameras || !this.view || !event || !id || !this._timeline) {
if (
!this.hass ||
!this.cameras ||
!this.view ||
!event ||
!this._timeline ||
!this.timelineConfig
) {
return;
}
@@ -585,6 +622,7 @@ export class FrigateCardTimelineCore extends LitElement {
this,
this.hass,
this.cameras,
this.timelineConfig.media,
eventWindowStart,
eventWindowEnd,
)
@@ -595,7 +633,7 @@ export class FrigateCardTimelineCore extends LitElement {
const eventStart = new Date(event.start_time * 1000);
const eventEnd = event.end_time ? new Date(event.end_time * 1000) : 0;
this._timeline.setSelection([id], {
this._timeline.setSelection([event.id], {
focus: false,
animation: {
animation: false,
@@ -628,7 +666,7 @@ export class FrigateCardTimelineCore extends LitElement {
// Hack: Clustering may not update unless the dataset changes, artifically
// update the dataset to ensure the newly selected item cannot be included
// in a cluster.
const item = this._events.dataset.get(id);
const item = this._events.dataset.get(event.id);
if (item) {
this._events.dataset.updateOnly(item);
}
+24 -27
View File
@@ -10,14 +10,14 @@ import { BrowseMediaUtil } from '../browse-media-util.js';
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
import { HomeAssistant } from 'custom-card-helpers';
import { Task } from '@lit-labs/task';
import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { customElement, property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { ref } from 'lit/directives/ref.js';
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
import type {
BrowseMediaNeighbors,
BrowseMediaQueryParameters,
BrowseMediaQueryParametersBase,
FrigateBrowseMediaSource,
CameraConfig,
ExtendedHomeAssistant,
@@ -25,7 +25,6 @@ import type {
TransitionEffect,
ViewerConfig,
} from '../types.js';
import { CarouselSelect } from './carousel.js';
import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js';
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js';
@@ -35,7 +34,6 @@ import {
contentsChanged,
createMediaShowInfo,
dispatchErrorMessageEvent,
dispatchFrigateCardEvent,
stopEventFromActivatingCardWideActions,
} from '../common.js';
import { renderProgressIndicator } from '../components/message.js';
@@ -71,17 +69,21 @@ export class FrigateCardViewer extends LitElement {
return;
}
const browseMediaQueryParameters =
BrowseMediaUtil.getBrowseMediaQueryParametersOrDispatchError(
const browseMediaQueryParametersBase =
BrowseMediaUtil.getBrowseMediaQueryParametersBaseOrDispatchError(
this,
this.view,
this.cameraConfig,
);
if (!browseMediaQueryParameters) {
return;
}
if (!this.view.target) {
const browseMediaQueryParameters = BrowseMediaUtil.setMediaTypeFromView(
browseMediaQueryParametersBase,
this.view,
);
if (!browseMediaQueryParameters) {
return;
}
BrowseMediaUtil.fetchLatestMediaAndDispatchViewChange(
this,
this.hass,
@@ -95,13 +97,12 @@ export class FrigateCardViewer extends LitElement {
.hass=${this.hass}
.view=${this.view}
.config=${this.viewerConfig.controls.thumbnails}
.browseMediaParams=${browseMediaQueryParameters}
>
<frigate-card-viewer-carousel
.hass=${this.hass}
.view=${this.view}
.viewerConfig=${this.viewerConfig}
.browseMediaQueryParameters=${browseMediaQueryParameters}
.browseMediaQueryParametersBase=${browseMediaQueryParametersBase}
.resolvedMediaCache=${this.resolvedMediaCache}
>
</frigate-card-viewer-carousel>
@@ -133,7 +134,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
protected viewerConfig?: ViewerConfig;
@property({ attribute: false })
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
protected browseMediaQueryParametersBase?: BrowseMediaQueryParametersBase;
@property({ attribute: false })
protected resolvedMediaCache?: ResolvedMediaCache;
@@ -267,16 +268,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
? this._lazyloadSlide.bind(this)
: undefined,
}),
// Don't need autoplay/pause for snapshots.
...(this.view?.is('clip')
? [
AutoMediaPlugin({
playerSelector: 'frigate-card-ha-hls-player',
autoPlayWhenVisible: !!this.viewerConfig?.auto_play,
autoUnmuteWhenVisible: !!this.viewerConfig?.auto_unmute,
}),
]
: []),
AutoMediaPlugin({
playerSelector: 'frigate-card-ha-hls-player',
autoPlayWhenVisible: !!this.viewerConfig?.auto_play,
autoUnmuteWhenVisible: !!this.viewerConfig?.auto_unmute,
}),
];
}
@@ -338,7 +334,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
!this.view.target ||
!this.view.target.children ||
!this.view.target.children.length ||
!this.browseMediaQueryParameters
!this.browseMediaQueryParametersBase
) {
return null;
}
@@ -381,7 +377,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
try {
clips = await BrowseMediaUtil.browseMediaQuery(this.hass, {
...this.browseMediaQueryParameters,
...this.browseMediaQueryParametersBase,
mediaType: 'clips',
before: latest,
after: earliest,
@@ -628,7 +624,8 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
if (
!this.view ||
!this.viewerConfig ||
!BrowseMediaUtil.isTrueMedia(mediaToRender)
!BrowseMediaUtil.isTrueMedia(mediaToRender) ||
!['video', 'image'].includes(mediaToRender.media_content_type)
) {
return;
}
@@ -641,7 +638,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
return html`
<div class="embla__slide">
${this.view.isClipRelatedView()
${mediaToRender.media_content_type === 'video'
? html`<frigate-card-ha-hls-player
allow-exoplayer
aria-label="${mediaToRender.title}"