Split async/sync parts of viewer.

This commit is contained in:
Dermot Duffy
2021-10-25 00:48:28 -07:00
parent 54a9b4fca9
commit c45c2ec973
4 changed files with 181 additions and 78 deletions
View File
+5
View File
@@ -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}
+117 -76
View File
@@ -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', protected async _render(): Promise<TemplateResult | void> {
media_content_id: mediaSource.media_content_id, if (!this.hass || !this.view || !this.browseMediaQueryParameters) {
}; return html``;
return homeAssistantWSRequest(this.hass, resolvedMediaSchema, request);
} }
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> { const mediaToRender = this.view.media;
let autoplay = true; const resolvedMedia = mediaToRender ? this.resolvedMediaCache.get(
mediaToRender.media_content_id) : undefined;
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);
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>`
+57
View File
@@ -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;
}
}