ha-frontend-cdce8p/src/components/ha-camera-stream.ts
Paulus Schoutsen 3becefaf8b
Add a couple of labels (#3310)
* Add a couple of labels

* Add some more labels
2019-06-27 17:47:19 -07:00

214 lines
5.2 KiB
TypeScript

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`
${__DEMO__ || this._shouldRenderMJPEG
? html`
<img
@load=${this._elementResized}
.src=${__DEMO__
? `/api/camera_proxy_stream/${this.stateObj.entity_id}`
: computeMJPEGStreamUrl(this.stateObj)}
.alt=${`Preview of the ${computeStateName(
this.stateObj
)} camera.`}
/>
`
: 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;
}
}