+ return html`
`;
}
@@ -947,10 +954,76 @@ export class FrigateCard extends LitElement {
return null;
}
+ protected async _getJSMPEGURL(): Promise
{
+ 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 {
+ 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((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 {
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`
${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index 2ed04b20..10035590 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -68,6 +68,7 @@
},
"live_provider": {
"frigate": "Frigate",
+ "frigate-jsmpeg": "Frigate JSMpeg",
"webrtc": "WebRTC"
},
"webrtc": {
diff --git a/src/scss/card.scss b/src/scss/card.scss
index c9a42ec0..2055ea1b 100644
--- a/src/scss/card.scss
+++ b/src/scss/card.scss
@@ -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;
diff --git a/src/types.ts b/src/types.ts
index 56bf9e1b..9d139e92 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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;
-}
\ No newline at end of file
+}
+
+export const signedPathSchema = z.object({
+ path: z.string(),
+});
+export type SignedPath = z.infer