Split async/sync parts of viewer.
This commit is contained in:
@@ -51,6 +51,7 @@ import './patches/ha-camera-stream.js';
|
|||||||
import './patches/ha-hls-player.js';
|
import './patches/ha-hls-player.js';
|
||||||
|
|
||||||
import cardStyle from './scss/card.scss';
|
import cardStyle from './scss/card.scss';
|
||||||
|
import { ResolvedMediaCache } from './resolved-media.js';
|
||||||
|
|
||||||
const MEDIA_HEIGHT_CUTOFF = 50;
|
const MEDIA_HEIGHT_CUTOFF = 50;
|
||||||
const MEDIA_WIDTH_CUTOFF = MEDIA_HEIGHT_CUTOFF;
|
const MEDIA_WIDTH_CUTOFF = MEDIA_HEIGHT_CUTOFF;
|
||||||
@@ -133,6 +134,9 @@ export class FrigateCard extends LitElement {
|
|||||||
// Error/info message to render.
|
// Error/info message to render.
|
||||||
protected _message: Message | null = null;
|
protected _message: Message | null = null;
|
||||||
|
|
||||||
|
// A cache of resolved media URLs/mimetypes for use in the whole card.
|
||||||
|
protected _resolvedMediaCache = new ResolvedMediaCache();
|
||||||
|
|
||||||
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
|
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
|
||||||
this._hass = hass;
|
this._hass = hass;
|
||||||
|
|
||||||
@@ -724,6 +728,7 @@ export class FrigateCard extends LitElement {
|
|||||||
.browseMediaQueryParameters=${mediaQueryParameters}
|
.browseMediaQueryParameters=${mediaQueryParameters}
|
||||||
.nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
|
.nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
|
||||||
.autoplayClip=${this.config.autoplay_clip}
|
.autoplayClip=${this.config.autoplay_clip}
|
||||||
|
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||||
class="${classMap(viewerClasses)}"
|
class="${classMap(viewerClasses)}"
|
||||||
@frigate-card:change-view=${this._changeViewHandler}
|
@frigate-card:change-view=${this._changeViewHandler}
|
||||||
@frigate-card:media-load=${this._mediaLoadHandler}
|
@frigate-card:media-load=${this._mediaLoadHandler}
|
||||||
|
|||||||
+119
-78
@@ -6,14 +6,12 @@ import { HomeAssistant } from 'custom-card-helpers';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dayjs_custom_parse_format from 'dayjs/plugin/customParseFormat.js';
|
import dayjs_custom_parse_format from 'dayjs/plugin/customParseFormat.js';
|
||||||
|
|
||||||
import { resolvedMediaSchema } from '../types.js';
|
|
||||||
import type {
|
import type {
|
||||||
BrowseMediaNeighbors,
|
BrowseMediaNeighbors,
|
||||||
BrowseMediaQueryParameters,
|
BrowseMediaQueryParameters,
|
||||||
BrowseMediaSource,
|
BrowseMediaSource,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
NextPreviousControlStyle,
|
NextPreviousControlStyle,
|
||||||
ResolvedMedia,
|
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import {
|
import {
|
||||||
@@ -24,7 +22,6 @@ import {
|
|||||||
dispatchPauseEvent,
|
dispatchPauseEvent,
|
||||||
dispatchPlayEvent,
|
dispatchPlayEvent,
|
||||||
getFirstTrueMediaChildIndex,
|
getFirstTrueMediaChildIndex,
|
||||||
homeAssistantWSRequest,
|
|
||||||
} from '../common.js';
|
} from '../common.js';
|
||||||
|
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
@@ -35,6 +32,7 @@ import {
|
|||||||
import './next-prev-control.js';
|
import './next-prev-control.js';
|
||||||
|
|
||||||
import viewerStyle from '../scss/viewer.scss';
|
import viewerStyle from '../scss/viewer.scss';
|
||||||
|
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
|
||||||
|
|
||||||
// Load dayjs plugin(s).
|
// Load dayjs plugin(s).
|
||||||
dayjs.extend(dayjs_custom_parse_format);
|
dayjs.extend(dayjs_custom_parse_format);
|
||||||
@@ -42,33 +40,111 @@ dayjs.extend(dayjs_custom_parse_format);
|
|||||||
@customElement('frigate-card-viewer')
|
@customElement('frigate-card-viewer')
|
||||||
export class FrigateCardViewer extends LitElement {
|
export class FrigateCardViewer extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected hass!: HomeAssistant & ExtendedHomeAssistant;
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view!: View;
|
protected view?: View;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected browseMediaQueryParameters!: BrowseMediaQueryParameters;
|
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected nextPreviousControlStyle!: NextPreviousControlStyle;
|
protected nextPreviousControlStyle?: NextPreviousControlStyle;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected autoplayClip!: boolean;
|
protected autoplayClip?: boolean;
|
||||||
|
|
||||||
protected async _resolveMedia(
|
@property({ attribute: false })
|
||||||
mediaSource: BrowseMediaSource | null,
|
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||||
): Promise<ResolvedMedia | null> {
|
|
||||||
if (!mediaSource) {
|
protected render(): TemplateResult | void {
|
||||||
return null;
|
return html`${until(this._render(), renderProgressIndicator())}`;
|
||||||
}
|
|
||||||
const request = {
|
|
||||||
type: 'media_source/resolve_media',
|
|
||||||
media_content_id: mediaSource.media_content_id,
|
|
||||||
};
|
|
||||||
return homeAssistantWSRequest(this.hass, resolvedMediaSchema, request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async _render(): Promise<TemplateResult | void> {
|
||||||
|
if (!this.hass || !this.view || !this.browseMediaQueryParameters) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
let autoplay = true;
|
||||||
|
let view = this.view;
|
||||||
|
|
||||||
|
if (!view.target) {
|
||||||
|
let parent: BrowseMediaSource | null = null;
|
||||||
|
try {
|
||||||
|
parent = await browseMediaQuery(this.hass, this.browseMediaQueryParameters);
|
||||||
|
} catch (e) {
|
||||||
|
return dispatchErrorMessageEvent(this, (e as Error).message);
|
||||||
|
}
|
||||||
|
const childIndex = getFirstTrueMediaChildIndex(parent);
|
||||||
|
if (!parent || !parent.children || childIndex == null) {
|
||||||
|
return dispatchMessageEvent(
|
||||||
|
this,
|
||||||
|
this.view.is('clip')
|
||||||
|
? localize('common.no_clip')
|
||||||
|
: localize('common.no_snapshot'),
|
||||||
|
this.view.is('clip') ? 'mdi:filmstrip-off' : 'mdi:camera-off',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
view = new View({
|
||||||
|
view: this.view.view,
|
||||||
|
target: parent,
|
||||||
|
childIndex: childIndex,
|
||||||
|
})
|
||||||
|
|
||||||
|
// In this block, no clip has been manually selected, so this is loading
|
||||||
|
// the most recent clip on card load. In this mode, autoplay of the clip
|
||||||
|
// may be disabled by configuration. If does not make sense to disable
|
||||||
|
// autoplay when the user has explicitly picked an event to play in the
|
||||||
|
// gallery.
|
||||||
|
autoplay = this.autoplayClip ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedMedia = await ResolvedMediaUtil.resolveMedia(
|
||||||
|
this.hass, view.media, this.resolvedMediaCache);
|
||||||
|
if (!resolvedMedia) {
|
||||||
|
// Home Assistant could not resolve media item.
|
||||||
|
return dispatchErrorMessageEvent(this, localize('error.could_not_resolve'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<frigate-card-viewer-core
|
||||||
|
.view=${view}
|
||||||
|
.nextPreviousControlStyle=${this.nextPreviousControlStyle}
|
||||||
|
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||||
|
.autoplayClip=${autoplay}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.browseMediaQueryParameters=${this.browseMediaQueryParameters}
|
||||||
|
>
|
||||||
|
</frigate-card-viewer-core>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(viewerStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@customElement('frigate-card-viewer-core')
|
||||||
|
export class FrigateCardViewerCore extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected view?: View;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected nextPreviousControlStyle?: NextPreviousControlStyle;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected autoplayClip?: boolean;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
|
||||||
|
|
||||||
protected _extractEventStartTimeFromBrowseMedia(
|
protected _extractEventStartTimeFromBrowseMedia(
|
||||||
browseMedia: BrowseMediaSource,
|
browseMedia: BrowseMediaSource,
|
||||||
): number | null {
|
): number | null {
|
||||||
@@ -87,18 +163,18 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the previous and next real media items, given the index
|
// Get the previous and next real media items, given the index
|
||||||
protected _getMediaNeighbors(
|
protected _getMediaNeighbors() : BrowseMediaNeighbors | null {
|
||||||
parent: BrowseMediaSource,
|
if (!this.view ||
|
||||||
index: number | null,
|
!this.view.target ||
|
||||||
): BrowseMediaNeighbors | null {
|
!this.view.target.children ||
|
||||||
if (index == null || !parent.children) {
|
this.view.childIndex === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Work backwards from the index to get the previous real media.
|
// Work backwards from the index to get the previous real media.
|
||||||
let prevIndex: number | null = null;
|
let prevIndex: number | null = null;
|
||||||
for (let i = index - 1; i >= 0; i--) {
|
for (let i = this.view.childIndex - 1; i >= 0; i--) {
|
||||||
const media = parent.children[i];
|
const media = this.view.target.children[i];
|
||||||
if (media && !media.can_expand) {
|
if (media && !media.can_expand) {
|
||||||
prevIndex = i;
|
prevIndex = i;
|
||||||
break;
|
break;
|
||||||
@@ -107,8 +183,8 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
|
|
||||||
// Work forwards from the index to get the next real media.
|
// Work forwards from the index to get the next real media.
|
||||||
let nextIndex: number | null = null;
|
let nextIndex: number | null = null;
|
||||||
for (let i = index + 1; i < parent.children.length; i++) {
|
for (let i = this.view.childIndex + 1; i < this.view.target.children.length; i++) {
|
||||||
const media = parent.children[i];
|
const media = this.view.target.children[i];
|
||||||
if (media && !media.can_expand) {
|
if (media && !media.can_expand) {
|
||||||
nextIndex = i;
|
nextIndex = i;
|
||||||
break;
|
break;
|
||||||
@@ -117,9 +193,9 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
previousIndex: prevIndex,
|
previousIndex: prevIndex,
|
||||||
previous: prevIndex != null ? parent.children[prevIndex] : null,
|
previous: prevIndex != null ? this.view.target.children[prevIndex] : null,
|
||||||
nextIndex: nextIndex,
|
nextIndex: nextIndex,
|
||||||
next: nextIndex != null ? parent.children[nextIndex] : null,
|
next: nextIndex != null ? this.view.target.children[nextIndex] : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +203,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
protected async _findRelatedClips(
|
protected async _findRelatedClips(
|
||||||
snapshot: BrowseMediaSource | null,
|
snapshot: BrowseMediaSource | null,
|
||||||
): Promise<BrowseMediaSource | null> {
|
): Promise<BrowseMediaSource | null> {
|
||||||
if (!snapshot) {
|
if (!snapshot || !this.hass || !this.browseMediaQueryParameters) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,59 +231,24 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return html`${until(this._render(), renderProgressIndicator())}`;
|
if (!this.view || !this.resolvedMediaCache) {
|
||||||
}
|
return html``;
|
||||||
|
|
||||||
protected async _render(): Promise<TemplateResult | void> {
|
|
||||||
let autoplay = true;
|
|
||||||
|
|
||||||
let parent: BrowseMediaSource | null = null;
|
|
||||||
let childIndex: number | null = null;
|
|
||||||
let mediaToRender: BrowseMediaSource | null = null;
|
|
||||||
|
|
||||||
if (this.view.target) {
|
|
||||||
parent = this.view.target;
|
|
||||||
childIndex = this.view.childIndex ?? null;
|
|
||||||
mediaToRender = this.view.media ?? null;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
parent = await browseMediaQuery(this.hass, this.browseMediaQueryParameters);
|
|
||||||
} catch (e) {
|
|
||||||
return dispatchErrorMessageEvent(this, (e as Error).message);
|
|
||||||
}
|
|
||||||
childIndex = getFirstTrueMediaChildIndex(parent);
|
|
||||||
if (!parent || !parent.children || childIndex == null) {
|
|
||||||
return dispatchMessageEvent(
|
|
||||||
this,
|
|
||||||
this.view.is('clip')
|
|
||||||
? localize('common.no_clip')
|
|
||||||
: localize('common.no_snapshot'),
|
|
||||||
this.view.is('clip') ? 'mdi:filmstrip-off' : 'mdi:camera-off',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
mediaToRender = parent.children[childIndex];
|
|
||||||
|
|
||||||
// In this block, no clip has been manually selected, so this is loading
|
|
||||||
// the most recent clip on card load. In this mode, autoplay of the clip
|
|
||||||
// may be disabled by configuration. If does not make sense to disable
|
|
||||||
// autoplay when the user has explicitly picked an event to play in the
|
|
||||||
// gallery.
|
|
||||||
autoplay = this.autoplayClip;
|
|
||||||
}
|
}
|
||||||
const resolvedMedia = await this._resolveMedia(mediaToRender);
|
|
||||||
|
const mediaToRender = this.view.media;
|
||||||
|
const resolvedMedia = mediaToRender ? this.resolvedMediaCache.get(
|
||||||
|
mediaToRender.media_content_id) : undefined;
|
||||||
if (!mediaToRender || !resolvedMedia) {
|
if (!mediaToRender || !resolvedMedia) {
|
||||||
// Home Assistant could not resolve media item.
|
return html``;
|
||||||
return dispatchErrorMessageEvent(this, localize('error.could_not_resolve'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const neighbors = this._getMediaNeighbors(parent, childIndex);
|
const neighbors = this._getMediaNeighbors();
|
||||||
|
|
||||||
return html` <div>
|
return html` <div>
|
||||||
${neighbors?.previousIndex != null
|
${neighbors?.previousIndex != null
|
||||||
? html`<frigate-card-next-previous-control
|
? html`<frigate-card-next-previous-control
|
||||||
.control=${'previous'}
|
.control=${'previous'}
|
||||||
.controlStyle=${this.nextPreviousControlStyle}
|
.controlStyle=${this.nextPreviousControlStyle}
|
||||||
.parent=${parent}
|
.parent=${this.view.target}
|
||||||
.childIndex=${neighbors.previousIndex}
|
.childIndex=${neighbors.previousIndex}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
></frigate-card-next-previous-control>`
|
></frigate-card-next-previous-control>`
|
||||||
@@ -222,7 +263,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
controls
|
controls
|
||||||
playsinline
|
playsinline
|
||||||
allow-exoplayer
|
allow-exoplayer
|
||||||
?autoplay="${autoplay}"
|
?autoplay="${this.autoplayClip}"
|
||||||
>
|
>
|
||||||
</frigate-card-ha-hls-player>`
|
</frigate-card-ha-hls-player>`
|
||||||
: html`<video
|
: html`<video
|
||||||
@@ -230,7 +271,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
muted
|
muted
|
||||||
controls
|
controls
|
||||||
playsinline
|
playsinline
|
||||||
?autoplay="${autoplay}"
|
?autoplay="${this.autoplayClip}"
|
||||||
@loadedmetadata=${(e) => dispatchMediaLoadEvent(this, e)}a
|
@loadedmetadata=${(e) => dispatchMediaLoadEvent(this, e)}a
|
||||||
@play=${() => dispatchPlayEvent(this)}
|
@play=${() => dispatchPlayEvent(this)}
|
||||||
@pause=${() => dispatchPauseEvent(this)}
|
@pause=${() => dispatchPauseEvent(this)}
|
||||||
@@ -259,7 +300,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
? html`<frigate-card-next-previous-control
|
? html`<frigate-card-next-previous-control
|
||||||
.control=${'next'}
|
.control=${'next'}
|
||||||
.controlStyle=${this.nextPreviousControlStyle}
|
.controlStyle=${this.nextPreviousControlStyle}
|
||||||
.parent=${parent}
|
.parent=${this.view.target}
|
||||||
.childIndex=${neighbors.nextIndex}
|
.childIndex=${neighbors.nextIndex}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
></frigate-card-next-previous-control>`
|
></frigate-card-next-previous-control>`
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { homeAssistantWSRequest } from './common.js';
|
||||||
|
import {
|
||||||
|
BrowseMediaSource,
|
||||||
|
ExtendedHomeAssistant,
|
||||||
|
ResolvedMedia,
|
||||||
|
resolvedMediaSchema,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
export class ResolvedMediaCache {
|
||||||
|
protected _cache: Record<string, ResolvedMedia>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._cache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public has(id: string): boolean {
|
||||||
|
return id in this._cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(id: string): ResolvedMedia | undefined {
|
||||||
|
return this._cache[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(id: string, resolvedMedia: ResolvedMedia): void {
|
||||||
|
this._cache[id] = resolvedMedia;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResolvedMediaUtil {
|
||||||
|
static async resolveMedia(
|
||||||
|
hass: HomeAssistant & ExtendedHomeAssistant,
|
||||||
|
mediaSource?: BrowseMediaSource,
|
||||||
|
cache?: ResolvedMediaCache,
|
||||||
|
): Promise<ResolvedMedia | null> {
|
||||||
|
if (!mediaSource) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cachedValue = cache ? cache.get(mediaSource.media_content_id) : undefined;
|
||||||
|
if (cachedValue) {
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
const request = {
|
||||||
|
type: 'media_source/resolve_media',
|
||||||
|
media_content_id: mediaSource.media_content_id,
|
||||||
|
};
|
||||||
|
const resolvedMedia = await homeAssistantWSRequest(
|
||||||
|
hass,
|
||||||
|
resolvedMediaSchema,
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
if (cache && resolvedMedia) {
|
||||||
|
cache.set(mediaSource.media_content_id, resolvedMedia);
|
||||||
|
}
|
||||||
|
return resolvedMedia;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user