Stream HLS (#2913)

* Stream HLS

* Lint
This commit is contained in:
Paulus Schoutsen 2019-03-11 22:40:41 -07:00 committed by GitHub
parent eeaaecd5b7
commit 19804a713d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 169 additions and 87 deletions

View File

@ -73,6 +73,7 @@
"deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0",
"fecha": "^3.0.0",
"hls.js": "^0.12.3",
"home-assistant-js-websocket": "^3.3.0",
"intl-messageformat": "^2.2.0",
"jquery": "^3.3.1",
@ -107,6 +108,7 @@
"@gfx/zopfli": "^1.0.9",
"@types/chai": "^4.1.7",
"@types/codemirror": "^0.0.71",
"@types/hls.js": "^0.12.2",
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "^4.1.0",
"@types/mocha": "^5.2.5",

View File

@ -1,12 +1,37 @@
import { HomeAssistant } from "../types";
import { HomeAssistant, CameraEntity } from "../types";
export interface CameraThumbnail {
content_type: string;
content: string;
}
export interface Stream {
url: string;
}
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
`/api/camera_proxy_stream/${entity.entity_id}?token=${
entity.attributes.access_token
}`;
export const fetchThumbnail = (hass: HomeAssistant, entityId: string) =>
hass.callWS<CameraThumbnail>({
type: "camera_thumbnail",
entity_id: entityId,
});
export const fetchStreamUrl = (
hass: HomeAssistant,
entityId: string,
format?: "hls"
) => {
const data = {
type: "camera/stream",
entity_id: entityId,
};
if (format) {
// @ts-ignore
data.format = format;
}
return hass.callWS<Stream>(data);
};

View File

@ -1,85 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import computeStateName from "../../../common/entity/compute_state_name";
import emptyImageBase64 from "../../../common/empty_image_base64";
import EventsMixin from "../../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
*/
class MoreInfoCamera extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
:host {
max-width: 640px;
}
.camera-image {
width: 100%;
}
</style>
<img
class="camera-image"
src="[[computeCameraImageUrl(hass, stateObj, isVisible)]]"
on-load="imageLoaded"
alt="[[_computeStateName(stateObj)]]"
/>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
stateObj: {
type: Object,
},
isVisible: {
type: Boolean,
value: true,
},
};
}
connectedCallback() {
super.connectedCallback();
this.isVisible = true;
}
disconnectedCallback() {
this.isVisible = false;
super.disconnectedCallback();
}
imageLoaded() {
this.fire("iron-resize");
}
_computeStateName(stateObj) {
return computeStateName(stateObj);
}
computeCameraImageUrl(hass, stateObj, isVisible) {
if (hass.demo) {
return "/demo/webcam.jpg";
}
if (stateObj && isVisible) {
return (
"/api/camera_proxy_stream/" +
stateObj.entity_id +
"?token=" +
stateObj.attributes.access_token
);
}
// Return an empty image if no stateObj (= dialog not open) or in cleanup mode.
return emptyImageBase64;
}
}
customElements.define("more-info-camera", MoreInfoCamera);

View File

@ -0,0 +1,113 @@
import { property, UpdatingElement, PropertyValues } 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";
type HLSModule = typeof import("hls.js");
class MoreInfoCamera extends UpdatingElement {
@property() public hass?: HomeAssistant;
@property() public stateObj?: CameraEntity;
private _mode: "loading" | "hls" | "mjpeg" = "loading";
public disconnectedCallback() {
super.disconnectedCallback();
this._teardownPlayback();
}
protected updated(changedProps: PropertyValues) {
if (!changedProps.has("stateObj")) {
return;
}
const oldState = changedProps.get("stateObj") as this["stateObj"];
const oldEntityId = oldState ? oldState.entity_id : undefined;
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
// Same entity, ignore.
if (curEntityId === oldEntityId) {
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;
}
// tslint:disable-next-line
const Hls = ((await import("hls.js")) as any).default as HLSModule;
if (Hls.isSupported()) {
try {
const { url } = await fetchStreamUrl(
this.hass!,
this.stateObj.entity_id
);
this._renderHLS(Hls, url);
return;
} catch (err) {
// Fails if entity doesn't support it. In that case we go
// for mjpeg.
}
}
this._renderMJPEG();
}
private async _renderHLS(
// tslint:disable-next-line
Hls: HLSModule,
url: string
) {
const videoEl = document.createElement("video");
videoEl.style.width = "100%";
videoEl.autoplay = true;
videoEl.controls = true;
videoEl.muted = true;
const hls = new Hls();
await new Promise((resolve) => {
hls.on(Hls.Events.MEDIA_ATTACHED, resolve);
hls.attachMedia(videoEl);
});
hls.loadSource(url);
this.appendChild(videoEl);
videoEl.addEventListener("loadeddata", () =>
fireEvent(this, "iron-resize")
);
}
private _renderMJPEG() {
this._mode = "mjpeg";
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.appendChild(img);
}
private _teardownPlayback(): any {
if (this._mode === "hls") {
// do something
}
this._mode = "loading";
while (this.lastChild) {
this.removeChild(this.lastChild);
}
}
}
customElements.define("more-info-camera", MoreInfoCamera);

View File

@ -200,6 +200,15 @@ export type GroupEntity = HassEntityBase & {
};
};
export type CameraEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
model_name: string;
access_token: string;
brand: string;
motion_detection: boolean;
};
};
export interface PanelInfo<T = unknown> {
component_name: string;
icon?: string;

View File

@ -1690,6 +1690,11 @@
"@types/node" "*"
"@types/vinyl" "*"
"@types/hls.js@^0.12.2":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@types/hls.js/-/hls.js-0.12.2.tgz#e16293a2b1cf4e975fad55a65621764a539f9657"
integrity sha512-VXLfVlZYlKWfsdsU2lo7rzwrKBTJj+DFdPIUyF84uTePbYXd30Gx0K6g62kzOPvojgYF/bMLkk1VDXHDb1X+VA==
"@types/html-minifier@^3.5.1":
version "3.5.2"
resolved "https://registry.yarnpkg.com/@types/html-minifier/-/html-minifier-3.5.2.tgz#f897a13d847a774e9b6fd91497e9b0e0ead71c35"
@ -5706,7 +5711,7 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
eventemitter3@^3.0.0:
eventemitter3@3.1.0, eventemitter3@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==
@ -7203,6 +7208,14 @@ he@1.2.x:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hls.js@^0.12.3:
version "0.12.3"
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-0.12.3.tgz#6743456fa443ed6050ab2888083e4b75c39b396f"
integrity sha512-tNvH/LIQzjLIXSI1AaAFYDLKxJKKKnE/rqCcFr76Ez6fVpMczWe65pI7qlYxFQC+urVhz9JokJYmeZod6d+5JA==
dependencies:
eventemitter3 "3.1.0"
url-toolkit "^2.1.6"
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -13634,6 +13647,11 @@ url-to-options@^1.0.1:
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=
url-toolkit@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.1.6.tgz#6d03246499e519aad224c44044a4ae20544154f2"
integrity sha512-UaZ2+50am4HwrV2crR/JAf63Q4VvPYphe63WGeoJxeu8gmOm0qxPt+KsukfakPNrX9aymGNEkkaoICwn+OuvBw==
url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"