mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-26 18:56:39 +00:00
Add Stream Element (#3086)
* initial commit for stream element * lit elements are apparently not self closing * add disconnectedCallback to teardown on unload * refactor stream element to UpdatingElement and bundle MJPEG handling with it * attach video element for HLS native * update hui-image to optionally show a live camera view (video or mjpeg) * fix playing inline video on iOS * implement review feedback * Fix update bugs * Tweaks * Fix stateObj changed
This commit is contained in:
parent
6ed2d288e6
commit
a1a2a78531
211
src/components/ha-camera-stream.ts
Normal file
211
src/components/ha-camera-stream.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import {
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
CSSResult,
|
||||||
|
css,
|
||||||
|
customElement,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import computeStateName from "../common/entity/compute_state_name";
|
||||||
|
import { HomeAssistant, CameraEntity } from "../types";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
CAMERA_SUPPORT_STREAM,
|
||||||
|
fetchStreamUrl,
|
||||||
|
computeMJPEGStreamUrl,
|
||||||
|
} from "../data/camera";
|
||||||
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
|
|
||||||
|
type HLSModule = typeof import("hls.js");
|
||||||
|
|
||||||
|
@customElement("ha-camera-stream")
|
||||||
|
class HaCameraStream extends LitElement {
|
||||||
|
@property() public hass?: HomeAssistant;
|
||||||
|
@property() public stateObj?: CameraEntity;
|
||||||
|
@property({ type: Boolean }) public showControls = false;
|
||||||
|
@property() private _attached = false;
|
||||||
|
// We keep track if we should force MJPEG with a string
|
||||||
|
// that way it automatically resets if we change entity.
|
||||||
|
@property() private _forceMJPEG: string | undefined = undefined;
|
||||||
|
private _hlsPolyfillInstance?: Hls;
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._attached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.stateObj || !this._attached) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this._shouldRenderMJPEG
|
||||||
|
? html`
|
||||||
|
<img
|
||||||
|
@load=${this._elementResized}
|
||||||
|
.src=${__DEMO__
|
||||||
|
? "/demo/webcamp.jpg"
|
||||||
|
: computeMJPEGStreamUrl(this.stateObj)}
|
||||||
|
.alt=${computeStateName(this.stateObj)}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<video
|
||||||
|
autoplay
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
?controls=${this.showControls}
|
||||||
|
@loadeddata=${this._elementResized}
|
||||||
|
></video>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
const stateObjChanged = changedProps.has("stateObj");
|
||||||
|
const attachedChanged = changedProps.has("_attached");
|
||||||
|
|
||||||
|
const oldState = changedProps.get("stateObj") as this["stateObj"];
|
||||||
|
const oldEntityId = oldState ? oldState.entity_id : undefined;
|
||||||
|
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!stateObjChanged && !attachedChanged) ||
|
||||||
|
(stateObjChanged && oldEntityId === curEntityId)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are no longer attached, destroy polyfill.
|
||||||
|
if (attachedChanged && !this._attached) {
|
||||||
|
this._destroyPolyfill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing to do if we are render MJPEG.
|
||||||
|
if (this._shouldRenderMJPEG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tear down existing polyfill, if available
|
||||||
|
this._destroyPolyfill();
|
||||||
|
|
||||||
|
if (curEntityId) {
|
||||||
|
this._startHls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _shouldRenderMJPEG() {
|
||||||
|
return (
|
||||||
|
this._forceMJPEG === this.stateObj!.entity_id ||
|
||||||
|
!this.hass!.config.components.includes("stream") ||
|
||||||
|
!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _videoEl(): HTMLVideoElement {
|
||||||
|
return this.shadowRoot!.querySelector("video")!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _startHls(): Promise<void> {
|
||||||
|
// tslint:disable-next-line
|
||||||
|
const Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
||||||
|
.default as HLSModule;
|
||||||
|
let hlsSupported = Hls.isSupported();
|
||||||
|
const videoEl = this._videoEl;
|
||||||
|
|
||||||
|
if (!hlsSupported) {
|
||||||
|
hlsSupported =
|
||||||
|
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hlsSupported) {
|
||||||
|
this._forceMJPEG = this.stateObj!.entity_id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { url } = await fetchStreamUrl(
|
||||||
|
this.hass!,
|
||||||
|
this.stateObj!.entity_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
this._renderHLSPolyfill(videoEl, Hls, url);
|
||||||
|
} else {
|
||||||
|
this._renderHLSNative(videoEl, url);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
// Fails if we were unable to get a stream
|
||||||
|
// tslint:disable-next-line
|
||||||
|
console.error(err);
|
||||||
|
this._forceMJPEG = this.stateObj!.entity_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||||
|
videoEl.src = url;
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
videoEl.addEventListener("loadedmetadata", resolve)
|
||||||
|
);
|
||||||
|
videoEl.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _renderHLSPolyfill(
|
||||||
|
videoEl: HTMLVideoElement,
|
||||||
|
// tslint:disable-next-line
|
||||||
|
Hls: HLSModule,
|
||||||
|
url: string
|
||||||
|
) {
|
||||||
|
const hls = new Hls();
|
||||||
|
this._hlsPolyfillInstance = hls;
|
||||||
|
hls.attachMedia(videoEl);
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||||
|
hls.loadSource(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _elementResized() {
|
||||||
|
fireEvent(this, "iron-resize");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _destroyPolyfill(): void {
|
||||||
|
if (this._hlsPolyfillInstance) {
|
||||||
|
this._hlsPolyfillInstance.destroy();
|
||||||
|
this._hlsPolyfillInstance = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
:host,
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-camera-stream": HaCameraStream;
|
||||||
|
}
|
||||||
|
}
|
@ -8,39 +8,36 @@ import {
|
|||||||
css,
|
css,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
|
||||||
import computeStateName from "../../../common/entity/compute_state_name";
|
|
||||||
import { HomeAssistant, CameraEntity } from "../../../types";
|
import { HomeAssistant, CameraEntity } from "../../../types";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
|
||||||
import {
|
import {
|
||||||
fetchStreamUrl,
|
|
||||||
computeMJPEGStreamUrl,
|
|
||||||
CAMERA_SUPPORT_STREAM,
|
CAMERA_SUPPORT_STREAM,
|
||||||
CameraPreferences,
|
CameraPreferences,
|
||||||
fetchCameraPrefs,
|
fetchCameraPrefs,
|
||||||
updateCameraPrefs,
|
updateCameraPrefs,
|
||||||
} from "../../../data/camera";
|
} from "../../../data/camera";
|
||||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
|
import "../../../components/ha-camera-stream";
|
||||||
import "@polymer/paper-checkbox/paper-checkbox";
|
import "@polymer/paper-checkbox/paper-checkbox";
|
||||||
// Not duplicate import, it's for typing
|
// Not duplicate import, it's for typing
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||||
|
|
||||||
type HLSModule = typeof import("hls.js");
|
|
||||||
|
|
||||||
class MoreInfoCamera extends LitElement {
|
class MoreInfoCamera extends LitElement {
|
||||||
@property() public hass?: HomeAssistant;
|
@property() public hass?: HomeAssistant;
|
||||||
@property() public stateObj?: CameraEntity;
|
@property() public stateObj?: CameraEntity;
|
||||||
@property() private _cameraPrefs?: CameraPreferences;
|
@property() private _cameraPrefs?: CameraPreferences;
|
||||||
private _hlsPolyfillInstance?: Hls;
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._teardownPlayback();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.hass || !this.stateObj) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div id="root"></div>
|
<ha-camera-stream
|
||||||
|
.hass="${this.hass}"
|
||||||
|
.stateObj="${this.stateObj}"
|
||||||
|
showcontrols
|
||||||
|
></ha-camera-stream>
|
||||||
${this._cameraPrefs
|
${this._cameraPrefs
|
||||||
? html`
|
? html`
|
||||||
<paper-checkbox
|
<paper-checkbox
|
||||||
@ -68,122 +65,14 @@ class MoreInfoCamera extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tear down if we have something and we need to build it up
|
|
||||||
if (oldEntityId) {
|
|
||||||
this._teardownPlayback();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (curEntityId) {
|
|
||||||
this._startPlayback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _startPlayback(): Promise<void> {
|
|
||||||
if (!this.stateObj) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.hass!.config.components.includes("stream") ||
|
curEntityId &&
|
||||||
!supportsFeature(this.stateObj, CAMERA_SUPPORT_STREAM)
|
this.hass!.config.components.includes("stream") &&
|
||||||
|
supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)
|
||||||
) {
|
) {
|
||||||
this._renderMJPEG();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoEl = document.createElement("video");
|
|
||||||
videoEl.style.width = "100%";
|
|
||||||
videoEl.autoplay = true;
|
|
||||||
videoEl.controls = true;
|
|
||||||
videoEl.muted = true;
|
|
||||||
|
|
||||||
// tslint:disable-next-line
|
|
||||||
const Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
|
||||||
.default as HLSModule;
|
|
||||||
let hlsSupported = Hls.isSupported();
|
|
||||||
|
|
||||||
if (!hlsSupported) {
|
|
||||||
hlsSupported =
|
|
||||||
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hlsSupported) {
|
|
||||||
try {
|
|
||||||
const { url } = await fetchStreamUrl(
|
|
||||||
this.hass!,
|
|
||||||
this.stateObj.entity_id
|
|
||||||
);
|
|
||||||
// Fetch in background while we set up the video.
|
// Fetch in background while we set up the video.
|
||||||
this._fetchCameraPrefs();
|
this._fetchCameraPrefs();
|
||||||
|
|
||||||
if (Hls.isSupported()) {
|
|
||||||
this._renderHLSPolyfill(videoEl, Hls, url);
|
|
||||||
} else {
|
|
||||||
this._renderHLSNative(videoEl, url);
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
// When an error happens, we will do nothing so we render mjpeg.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._renderMJPEG();
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _videoRoot(): HTMLDivElement {
|
|
||||||
return this.shadowRoot!.getElementById("root")! as HTMLDivElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
|
||||||
videoEl.src = url;
|
|
||||||
this._videoRoot.appendChild(videoEl);
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
videoEl.addEventListener("loadedmetadata", resolve)
|
|
||||||
);
|
|
||||||
videoEl.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _renderHLSPolyfill(
|
|
||||||
videoEl: HTMLVideoElement,
|
|
||||||
// tslint:disable-next-line
|
|
||||||
Hls: HLSModule,
|
|
||||||
url: string
|
|
||||||
) {
|
|
||||||
const hls = new Hls();
|
|
||||||
this._hlsPolyfillInstance = hls;
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
hls.on(Hls.Events.MEDIA_ATTACHED, resolve);
|
|
||||||
hls.attachMedia(videoEl);
|
|
||||||
});
|
|
||||||
hls.loadSource(url);
|
|
||||||
this._videoRoot.appendChild(videoEl);
|
|
||||||
videoEl.addEventListener("loadeddata", () =>
|
|
||||||
fireEvent(this, "iron-resize")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderMJPEG() {
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.style.width = "100%";
|
|
||||||
img.addEventListener("load", () => fireEvent(this, "iron-resize"));
|
|
||||||
img.src = __DEMO__
|
|
||||||
? "/demo/webcamp.jpg"
|
|
||||||
: computeMJPEGStreamUrl(this.stateObj!);
|
|
||||||
img.alt = computeStateName(this.stateObj!);
|
|
||||||
this._videoRoot.appendChild(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _teardownPlayback(): any {
|
|
||||||
if (this._hlsPolyfillInstance) {
|
|
||||||
this._hlsPolyfillInstance.destroy();
|
|
||||||
this._hlsPolyfillInstance = undefined;
|
|
||||||
}
|
|
||||||
const root = this._videoRoot;
|
|
||||||
while (root.lastChild) {
|
|
||||||
root.removeChild(root.lastChild);
|
|
||||||
}
|
|
||||||
this.stateObj = undefined;
|
|
||||||
this._cameraPrefs = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchCameraPrefs() {
|
private async _fetchCameraPrefs() {
|
||||||
|
@ -60,6 +60,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
|||||||
.image="${this._config.image}"
|
.image="${this._config.image}"
|
||||||
.stateImage="${this._config.state_image}"
|
.stateImage="${this._config.state_image}"
|
||||||
.cameraImage="${this._config.camera_image}"
|
.cameraImage="${this._config.camera_image}"
|
||||||
|
.cameraView="${this._config.camera_view}"
|
||||||
.entity="${this._config.entity}"
|
.entity="${this._config.entity}"
|
||||||
.aspectRatio="${this._config.aspect_ratio}"
|
.aspectRatio="${this._config.aspect_ratio}"
|
||||||
></hui-image>
|
></hui-image>
|
||||||
|
@ -108,6 +108,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
|||||||
.cameraImage="${computeDomain(this._config.entity) === "camera"
|
.cameraImage="${computeDomain(this._config.entity) === "camera"
|
||||||
? this._config.entity
|
? this._config.entity
|
||||||
: this._config.camera_image}"
|
: this._config.camera_image}"
|
||||||
|
.cameraView="${this._config.camera_view}"
|
||||||
.entity="${this._config.entity}"
|
.entity="${this._config.entity}"
|
||||||
.aspectRatio="${this._config.aspect_ratio}"
|
.aspectRatio="${this._config.aspect_ratio}"
|
||||||
@ha-click="${this._handleTap}"
|
@ha-click="${this._handleTap}"
|
||||||
|
@ -131,6 +131,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
|||||||
.image="${this._config.image}"
|
.image="${this._config.image}"
|
||||||
.stateImage="${this._config.state_image}"
|
.stateImage="${this._config.state_image}"
|
||||||
.cameraImage="${this._config.camera_image}"
|
.cameraImage="${this._config.camera_image}"
|
||||||
|
.cameraView="${this._config.camera_view}"
|
||||||
.entity="${this._config.entity}"
|
.entity="${this._config.entity}"
|
||||||
.aspectRatio="${this._config.aspect_ratio}"
|
.aspectRatio="${this._config.aspect_ratio}"
|
||||||
></hui-image>
|
></hui-image>
|
||||||
|
@ -2,6 +2,7 @@ import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
|
|||||||
import { Condition } from "../common/validate-condition";
|
import { Condition } from "../common/validate-condition";
|
||||||
import { EntityConfig } from "../entity-rows/types";
|
import { EntityConfig } from "../entity-rows/types";
|
||||||
import { LovelaceElementConfig } from "../elements/types";
|
import { LovelaceElementConfig } from "../elements/types";
|
||||||
|
import { HuiImage } from "../components/hui-image";
|
||||||
|
|
||||||
export interface AlarmPanelCardConfig extends LovelaceCardConfig {
|
export interface AlarmPanelCardConfig extends LovelaceCardConfig {
|
||||||
entity: string;
|
entity: string;
|
||||||
@ -129,6 +130,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
|||||||
title?: string;
|
title?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
camera_image?: string;
|
camera_image?: string;
|
||||||
|
camera_view?: HuiImage["cameraView"];
|
||||||
state_image?: {};
|
state_image?: {};
|
||||||
aspect_ratio?: string;
|
aspect_ratio?: string;
|
||||||
entity?: string;
|
entity?: string;
|
||||||
@ -140,6 +142,7 @@ export interface PictureEntityCardConfig extends LovelaceCardConfig {
|
|||||||
name?: string;
|
name?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
camera_image?: string;
|
camera_image?: string;
|
||||||
|
camera_view?: HuiImage["cameraView"];
|
||||||
state_image?: {};
|
state_image?: {};
|
||||||
aspect_ratio?: string;
|
aspect_ratio?: string;
|
||||||
tap_action?: ActionConfig;
|
tap_action?: ActionConfig;
|
||||||
@ -153,6 +156,7 @@ export interface PictureGlanceCardConfig extends LovelaceCardConfig {
|
|||||||
title?: string;
|
title?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
camera_image?: string;
|
camera_image?: string;
|
||||||
|
camera_view?: HuiImage["cameraView"];
|
||||||
state_image?: {};
|
state_image?: {};
|
||||||
aspect_ratio?: string;
|
aspect_ratio?: string;
|
||||||
entity?: string;
|
entity?: string;
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
query,
|
query,
|
||||||
customElement,
|
customElement,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant, CameraEntity } from "../../../types";
|
||||||
import { styleMap } from "lit-html/directives/style-map";
|
import { styleMap } from "lit-html/directives/style-map";
|
||||||
import { classMap } from "lit-html/directives/class-map";
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
import { b64toBlob } from "../../../common/file/b64-to-blob";
|
import { b64toBlob } from "../../../common/file/b64-to-blob";
|
||||||
@ -28,7 +28,7 @@ export interface StateSpecificConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-image")
|
@customElement("hui-image")
|
||||||
class HuiImage extends LitElement {
|
export class HuiImage extends LitElement {
|
||||||
@property() public hass?: HomeAssistant;
|
@property() public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property() public entity?: string;
|
@property() public entity?: string;
|
||||||
@ -39,6 +39,8 @@ class HuiImage extends LitElement {
|
|||||||
|
|
||||||
@property() public cameraImage?: string;
|
@property() public cameraImage?: string;
|
||||||
|
|
||||||
|
@property() public cameraView?: "live" | "auto";
|
||||||
|
|
||||||
@property() public aspectRatio?: string;
|
@property() public aspectRatio?: string;
|
||||||
|
|
||||||
@property() public filter?: string;
|
@property() public filter?: string;
|
||||||
@ -60,8 +62,10 @@ class HuiImage extends LitElement {
|
|||||||
public connectedCallback(): void {
|
public connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._attached = true;
|
this._attached = true;
|
||||||
|
if (this.cameraImage && this.cameraView !== "live") {
|
||||||
this._startUpdateCameraInterval();
|
this._startUpdateCameraInterval();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public disconnectedCallback(): void {
|
public disconnectedCallback(): void {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
@ -77,11 +81,17 @@ class HuiImage extends LitElement {
|
|||||||
|
|
||||||
// Figure out image source to use
|
// Figure out image source to use
|
||||||
let imageSrc: string | undefined;
|
let imageSrc: string | undefined;
|
||||||
|
let cameraObj: CameraEntity | undefined;
|
||||||
// Track if we are we using a fallback image, used for filter.
|
// Track if we are we using a fallback image, used for filter.
|
||||||
let imageFallback = !this.stateImage;
|
let imageFallback = !this.stateImage;
|
||||||
|
|
||||||
if (this.cameraImage) {
|
if (this.cameraImage) {
|
||||||
|
if (this.cameraView === "live") {
|
||||||
|
cameraObj =
|
||||||
|
this.hass && (this.hass.states[this.cameraImage] as CameraEntity);
|
||||||
|
} else {
|
||||||
imageSrc = this._cameraImageSrc;
|
imageSrc = this._cameraImageSrc;
|
||||||
|
}
|
||||||
} else if (this.stateImage) {
|
} else if (this.stateImage) {
|
||||||
const stateImage = this.stateImage[state];
|
const stateImage = this.stateImage[state];
|
||||||
|
|
||||||
@ -119,6 +129,14 @@ class HuiImage extends LitElement {
|
|||||||
ratio: Boolean(ratio && ratio.w > 0 && ratio.h > 0),
|
ratio: Boolean(ratio && ratio.w > 0 && ratio.h > 0),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
${this.cameraImage && this.cameraView === "live"
|
||||||
|
? html`
|
||||||
|
<ha-camera-stream
|
||||||
|
.hass="${this.hass}"
|
||||||
|
.stateObj="${cameraObj}"
|
||||||
|
></ha-camera-stream>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
<img
|
<img
|
||||||
id="image"
|
id="image"
|
||||||
src=${imageSrc}
|
src=${imageSrc}
|
||||||
@ -129,6 +147,7 @@ class HuiImage extends LitElement {
|
|||||||
display: this._loadError ? "none" : "block",
|
display: this._loadError ? "none" : "block",
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
`}
|
||||||
<div
|
<div
|
||||||
id="brokenImage"
|
id="brokenImage"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
@ -141,7 +160,7 @@ class HuiImage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has("cameraImage")) {
|
if (changedProps.has("cameraImage") && this.cameraView !== "live") {
|
||||||
this._updateCameraImageSrc();
|
this._updateCameraImageSrc();
|
||||||
this._startUpdateCameraInterval();
|
this._startUpdateCameraInterval();
|
||||||
return;
|
return;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user