Support live JSMPEG view.
This commit is contained in:
+19
-18
@@ -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}
|
||||
|
||||
@@ -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() : ''}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
},
|
||||
"live_provider": {
|
||||
"frigate": "Frigate",
|
||||
"frigate-jsmpeg": "Frigate JSMpeg",
|
||||
"webrtc": "WebRTC"
|
||||
},
|
||||
"webrtc": {
|
||||
|
||||
+7
-2
@@ -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
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user