Support live JSMPEG view.

This commit is contained in:
Dermot Duffy
2021-09-12 15:36:41 -07:00
parent 4da2a5b82a
commit f9ea435586
7 changed files with 133 additions and 28 deletions
+19 -18
View File
@@ -126,6 +126,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
const liveProvider = {
'': '',
frigate: localize('live_provider.frigate'),
'frigate-jsmpeg': localize('live_provider.frigate-jsmpeg'),
webrtc: localize('live_provider.webrtc'),
};
@@ -182,6 +183,24 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
.configValue=${'frigate_camera_name'}
@value-changed=${this._valueChanged}
></paper-input>
<paper-dropdown-menu
.label=${localize('editor.live_provider')}
@value-changed=${this._valueChanged}
.configValue=${'live_provider'}
>
<paper-listbox
slot="dropdown-content"
.selected=${Object.keys(liveProvider).indexOf(
this._config?.live_provider || '',
)}
>
${Object.keys(liveProvider).map((key) => {
return html`
<paper-item .label="${key}">${liveProvider[key]} </paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
label=${localize('editor.default_view')}
@value-changed=${this._valueChanged}
@@ -368,24 +387,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div>
${options.webrtc.show
? html` <div class="values">
<paper-dropdown-menu
.label=${localize('editor.live_provider')}
@value-changed=${this._valueChanged}
.configValue=${'live_provider'}
>
<paper-listbox
slot="dropdown-content"
.selected=${Object.keys(liveProvider).indexOf(
this._config?.live_provider || '',
)}
>
${Object.keys(liveProvider).map((key) => {
return html`
<paper-item .label="${key}">${liveProvider[key]} </paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
.label=${localize('webrtc.entity')}
@value-changed=${this._valueChanged}
+83 -5
View File
@@ -29,10 +29,12 @@ import {
browseMediaSourceSchema,
frigateCardConfigSchema,
resolvedMediaSchema,
signedPathSchema,
} from './types';
import type {
BrowseMediaNeighbors,
BrowseMediaSource,
ExtendedHomeAssistant,
FrigateCardConfig,
FrigateCardView,
FrigateMenuMode,
@@ -46,6 +48,8 @@ import dayjs_custom_parse_format from 'dayjs/plugin/customParseFormat';
import { ZodSchema, z } from 'zod';
import { MessageBase } from 'home-assistant-js-websocket';
import JSMpeg from '@cycjimmy/jsmpeg-player';
const URL_TROUBLESHOOTING =
'https://github.com/dermotduffy/frigate-hass-card#troubleshooting';
@@ -257,7 +261,7 @@ export class FrigateCard extends LitElement {
public static getStubConfig(): Record<string, string> {
return {};
}
set hass(hass: HomeAssistant) {
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
if (this._webrtcElement) {
this._webrtcElement.hass = hass;
}
@@ -266,12 +270,14 @@ export class FrigateCard extends LitElement {
}
@property({ attribute: false })
protected _hass: HomeAssistant | null = null;
protected _hass: (HomeAssistant & ExtendedHomeAssistant) | null = null;
@state()
public config!: FrigateCardConfig;
protected _interactionTimerID: number | null = null;
protected _jsmpegCanvasElement: any | null = null;
protected _jsmpegPlayer: any | null = null;
protected _webrtcElement: any | null = null;
@property({ attribute: false })
@@ -406,6 +412,7 @@ export class FrigateCard extends LitElement {
} else {
this._view = view;
}
this._resetJSMPEGIfNecessary();
}
// Determine whether the card should be updated.
@@ -620,7 +627,7 @@ export class FrigateCard extends LitElement {
// Render a progress spinner while content loads.
protected _renderProgressIndicator(): TemplateResult {
return html` <div class="frigate-card-attention">
return html` <div class="attention">
<ha-circular-progress active="true" size="large"></ha-circular-progress>
</div>`;
}
@@ -947,10 +954,76 @@ export class FrigateCard extends LitElement {
return null;
}
protected async _getJSMPEGURL(): Promise<string | null> {
if (!this._hass) {
return null;
}
const request = {
type: 'auth/sign_path',
path:
`/api/frigate/${this.config.frigate_client_id}` +
`/jsmpeg/${this.config.frigate_camera_name}`,
};
// Sign the path so it includes an authSig parameter.
let response;
try {
response = await this._makeWSRequest(signedPathSchema, request);
} catch (err) {
console.warn(err);
return null;
}
const url = this._hass.hassUrl(response.path);
return url.replace(/^http/i, 'ws');
}
protected _resetJSMPEGIfNecessary(): void {
if (!this._view.is('live') || this.config.live_provider != 'frigate-jsmpeg') {
if (this._jsmpegPlayer) {
this._jsmpegPlayer.destroy();
this._jsmpegPlayer = null;
}
this._jsmpegCanvasElement = null;
}
}
// Cleanup and/or start the JSMPEG player.
protected async _renderJSMPEG(): Promise<TemplateResult> {
if (!this._jsmpegCanvasElement) {
this._jsmpegCanvasElement = document.createElement('canvas');
this._jsmpegCanvasElement.className = 'media';
}
if (!this._jsmpegPlayer) {
const jsmpeg_url = await this._getJSMPEGURL();
if (!jsmpeg_url) {
return this._renderError('Could not retrieve or sign JSMPEG websocket path');
}
return new Promise<TemplateResult>((resolve) => {
this._jsmpegPlayer = new JSMpeg.VideoElement(
this,
jsmpeg_url,
{
canvas: this._jsmpegCanvasElement,
hooks: {
play: () => {
resolve(html`${this._jsmpegCanvasElement}`);
},
},
},
{ protocols: [], videoBufferSize: 1024 * 1024 * 4 },
);
});
}
return html`${this._jsmpegCanvasElement}`;
}
// Render the live viewer.
// Note: The live viewer is the main element used to size the overall card. It
// is always rendered (but sometimes hidden).
protected _renderLiveViewer(): TemplateResult {
protected async _renderLiveViewer(): Promise<TemplateResult> {
if (!this._hass || !(this.config.camera_entity in this._hass.states)) {
return this._renderAttentionIcon(
'mdi:camera-off',
@@ -960,6 +1033,9 @@ export class FrigateCard extends LitElement {
if (this._webrtcElement) {
return html`${this._webrtcElement}`;
}
if (this.config.live_provider == 'frigate-jsmpeg') {
return await this._renderJSMPEG();
}
return html` <ha-camera-stream
.hass=${this._hass}
.stateObj=${this._hass.states[this.config.camera_entity]}
@@ -1016,7 +1092,9 @@ export class FrigateCard extends LitElement {
${this._view.is('clip') || this._view.is('snapshot')
? until(this._renderViewer(), this._renderProgressIndicator())
: ``}
${this._view.is('live') ? this._renderLiveViewer() : ``}
${this._view.is('live')
? until(this._renderLiveViewer(), this._renderProgressIndicator())
: ``}
</div>
</div>
${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
+1
View File
@@ -68,6 +68,7 @@
},
"live_provider": {
"frigate": "Frigate",
"frigate-jsmpeg": "Frigate JSMpeg",
"webrtc": "WebRTC"
},
"webrtc": {
+7 -2
View File
@@ -34,7 +34,7 @@
opacity: 1.0;
}
.frigate-card-contents img.media,video.media {
.frigate-card-contents img.media,video.media,canvas.media {
width: 100%;
}
@@ -49,7 +49,7 @@
justify-content: center;
align-items: center;
box-sizing: border-box;
padding: 5%;
padding: 10%;
}
.frigate-card-image-list {
@@ -83,11 +83,16 @@ ha-card {
width: 100%;
height: 100%;
position: relative;
color: var(--secondary-text-color, white);
background-color: var(--secondary-background-color, black);
transform-style: preserve-3d; /* Safari brings video elements forward without this */
}
ha-card a {
color: var(--primary-text-color, white);
}
/* Don't drop shadow or have radius for nested webrtc card */
webrtc-camera ha-card {
box-shadow: none;
+11 -2
View File
@@ -60,7 +60,7 @@ export const frigateCardConfigSchema = z.object({
.transform((val) => Number(val)),
)
.optional().default(180),
live_provider: z.enum(['frigate', 'webrtc']).default('frigate'),
live_provider: z.enum(['frigate', 'frigate-jsmpeg', 'webrtc']).default('frigate'),
webrtc: z.object({}).passthrough().optional(),
label: z.string().optional(),
zone: z.string().optional(),
@@ -96,6 +96,10 @@ export interface MenuButton {
emphasize?: boolean;
}
export interface ExtendedHomeAssistant {
hassUrl(path?): string;
}
/**
* Media Browser API types.
*/
@@ -143,4 +147,9 @@ export interface BrowseMediaNeighbors {
next: BrowseMediaSource | null;
nextIndex: number | null;
}
}
export const signedPathSchema = z.object({
path: z.string(),
});
export type SignedPath = z.infer<typeof signedPathSchema>;