diff --git a/src/data/camera.ts b/src/data/camera.ts index 62f94096e4..62ff707130 100644 --- a/src/data/camera.ts +++ b/src/data/camera.ts @@ -1,6 +1,13 @@ import { HomeAssistant, CameraEntity } from "../types"; import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; +export const CAMERA_SUPPORT_ON_OFF = 1; +export const CAMERA_SUPPORT_STREAM = 2; + +export interface CameraPreferences { + preload_stream: boolean; +} + export interface CameraThumbnail { content_type: string; content: string; @@ -41,3 +48,22 @@ export const fetchStreamUrl = ( } return hass.callWS(data); }; + +export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) => + hass.callWS({ + type: "camera/get_prefs", + entity_id: entityId, + }); + +export const updateCameraPrefs = ( + hass: HomeAssistant, + entityId: string, + prefs: { + preload_stream?: boolean; + } +) => + hass.callWS({ + type: "camera/update_prefs", + entity_id: entityId, + ...prefs, + }); diff --git a/src/dialogs/more-info/controls/more-info-camera.ts b/src/dialogs/more-info/controls/more-info-camera.ts index 3fbc56a727..bb8ed1e393 100644 --- a/src/dialogs/more-info/controls/more-info-camera.ts +++ b/src/dialogs/more-info/controls/more-info-camera.ts @@ -1,15 +1,36 @@ -import { property, UpdatingElement, PropertyValues } from "lit-element"; +import { + property, + PropertyValues, + LitElement, + TemplateResult, + html, + CSSResult, + css, +} from "lit-element"; import computeStateName from "../../../common/entity/compute_state_name"; import { HomeAssistant, CameraEntity } from "../../../types"; import { fireEvent } from "../../../common/dom/fire_event"; -import { fetchStreamUrl, computeMJPEGStreamUrl } from "../../../data/camera"; +import { + fetchStreamUrl, + computeMJPEGStreamUrl, + CAMERA_SUPPORT_STREAM, + CameraPreferences, + fetchCameraPrefs, + updateCameraPrefs, +} from "../../../data/camera"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import "@polymer/paper-checkbox/paper-checkbox"; +// Not duplicate import, it's for typing +// tslint:disable-next-line +import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; type HLSModule = typeof import("hls.js"); -class MoreInfoCamera extends UpdatingElement { +class MoreInfoCamera extends LitElement { @property() public hass?: HomeAssistant; @property() public stateObj?: CameraEntity; + @property() private _cameraPrefs?: CameraPreferences; private _hlsPolyfillInstance?: Hls; public disconnectedCallback() { @@ -17,6 +38,22 @@ class MoreInfoCamera extends UpdatingElement { this._teardownPlayback(); } + protected render(): TemplateResult | void { + return html` +
+ ${this._cameraPrefs + ? html` + + Preload stream + + ` + : undefined} + `; + } + protected updated(changedProps: PropertyValues) { if (!changedProps.has("stateObj")) { return; @@ -46,7 +83,10 @@ class MoreInfoCamera extends UpdatingElement { return; } - if (!this.hass!.config.components.includes("stream")) { + if ( + !this.hass!.config.components.includes("stream") || + !supportsFeature(this.stateObj, CAMERA_SUPPORT_STREAM) + ) { this._renderMJPEG(); return; } @@ -73,6 +113,8 @@ class MoreInfoCamera extends UpdatingElement { this.hass!, this.stateObj.entity_id ); + // Fetch in background while we set up the video. + this._fetchCameraPrefs(); if (Hls.isSupported()) { this._renderHLSPolyfill(videoEl, Hls, url); @@ -81,16 +123,20 @@ class MoreInfoCamera extends UpdatingElement { } return; } catch (err) { - // Fails if entity doesn't support it. In that case we go - // for mjpeg. + // 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) ); @@ -110,7 +156,7 @@ class MoreInfoCamera extends UpdatingElement { hls.attachMedia(videoEl); }); hls.loadSource(url); - this.appendChild(videoEl); + this._videoRoot.appendChild(videoEl); videoEl.addEventListener("loadeddata", () => fireEvent(this, "iron-resize") ); @@ -124,7 +170,7 @@ class MoreInfoCamera extends UpdatingElement { ? "/demo/webcamp.jpg" : computeMJPEGStreamUrl(this.stateObj!); img.alt = computeStateName(this.stateObj!); - this.appendChild(img); + this._videoRoot.appendChild(img); } private _teardownPlayback(): any { @@ -132,11 +178,48 @@ class MoreInfoCamera extends UpdatingElement { this._hlsPolyfillInstance.destroy(); this._hlsPolyfillInstance = undefined; } - while (this.lastChild) { - this.removeChild(this.lastChild); + const root = this._videoRoot; + while (root.lastChild) { + root.removeChild(root.lastChild); } this.stateObj = undefined; } + + private async _fetchCameraPrefs() { + this._cameraPrefs = await fetchCameraPrefs( + this.hass!, + this.stateObj!.entity_id + ); + } + + private async _handleCheckboxChanged(ev) { + const checkbox = ev.currentTarget as PaperCheckboxElement; + try { + this._cameraPrefs = await updateCameraPrefs( + this.hass!, + this.stateObj!.entity_id, + { + preload_stream: checkbox.checked!, + } + ); + } catch (err) { + alert(err.message); + checkbox.checked = !checkbox.checked; + } + } + + static get styles(): CSSResult { + return css` + paper-checkbox { + position: absolute; + top: 0; + right: 0; + background-color: var(--secondary-background-color); + padding: 5px; + border-bottom-left-radius: 6px; + } + `; + } } customElements.define("more-info-camera", MoreInfoCamera); diff --git a/src/dialogs/more-info/controls/more-info-content.ts b/src/dialogs/more-info/controls/more-info-content.ts index e60c03b6e0..2d0f0e4cc7 100644 --- a/src/dialogs/more-info/controls/more-info-content.ts +++ b/src/dialogs/more-info/controls/more-info-content.ts @@ -43,6 +43,7 @@ class MoreInfoContent extends UpdatingElement { } protected firstUpdated(): void { + this.style.position = "relative"; this.style.display = "block"; }