mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 16:56:35 +00:00
Group stream picking logic (#22674)
* Group stream picking logic * MJPEG too * handle errors when 1 stream type * correct import * change to array * Update ha-camera-stream.ts * Update ha-camera-stream.ts * Update ha-camera-stream.ts * rename
This commit is contained in:
parent
17db85ebad
commit
35dcb46703
@ -7,6 +7,8 @@ import {
|
|||||||
type PropertyValues,
|
type PropertyValues,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
import { supportsFeature } from "../common/entity/supports-feature";
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
import {
|
import {
|
||||||
@ -24,6 +26,13 @@ import type { HomeAssistant } from "../types";
|
|||||||
import "./ha-hls-player";
|
import "./ha-hls-player";
|
||||||
import "./ha-web-rtc-player";
|
import "./ha-web-rtc-player";
|
||||||
|
|
||||||
|
const MJPEG_STREAM = "mjpeg";
|
||||||
|
|
||||||
|
type Stream = {
|
||||||
|
type: StreamType | typeof MJPEG_STREAM;
|
||||||
|
visible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-camera-stream")
|
@customElement("ha-camera-stream")
|
||||||
export class HaCameraStream extends LitElement {
|
export class HaCameraStream extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
@ -46,8 +55,6 @@ export class HaCameraStream extends LitElement {
|
|||||||
|
|
||||||
@state() private _capabilities?: CameraCapabilities;
|
@state() private _capabilities?: CameraCapabilities;
|
||||||
|
|
||||||
@state() private _streamType?: StreamType;
|
|
||||||
|
|
||||||
@state() private _hlsStreams?: { hasAudio: boolean; hasVideo: boolean };
|
@state() private _hlsStreams?: { hasAudio: boolean; hasVideo: boolean };
|
||||||
|
|
||||||
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
|
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
|
||||||
@ -55,7 +62,6 @@ export class HaCameraStream extends LitElement {
|
|||||||
public willUpdate(changedProps: PropertyValues): void {
|
public willUpdate(changedProps: PropertyValues): void {
|
||||||
if (
|
if (
|
||||||
changedProps.has("stateObj") &&
|
changedProps.has("stateObj") &&
|
||||||
!this._shouldRenderMJPEG &&
|
|
||||||
this.stateObj &&
|
this.stateObj &&
|
||||||
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
|
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
|
||||||
this.stateObj.entity_id
|
this.stateObj.entity_id
|
||||||
@ -79,20 +85,35 @@ export class HaCameraStream extends LitElement {
|
|||||||
if (!this.stateObj) {
|
if (!this.stateObj) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
if (__DEMO__ || this._shouldRenderMJPEG) {
|
const streams = this._streams(
|
||||||
|
this._capabilities?.frontend_stream_types,
|
||||||
|
this._hlsStreams,
|
||||||
|
this._webRtcStreams
|
||||||
|
);
|
||||||
|
return html`${repeat(
|
||||||
|
streams,
|
||||||
|
(stream) => stream.type + this.stateObj!.entity_id,
|
||||||
|
(stream) => this._renderStream(stream)
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderStream(stream: Stream) {
|
||||||
|
if (!this.stateObj) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
if (stream.type === MJPEG_STREAM) {
|
||||||
return html`<img
|
return html`<img
|
||||||
.src=${__DEMO__
|
.src=${__DEMO__
|
||||||
? this.stateObj.attributes.entity_picture!
|
? this.stateObj.attributes.entity_picture!
|
||||||
: this._connected
|
: this._connected
|
||||||
? computeMJPEGStreamUrl(this.stateObj)
|
? computeMJPEGStreamUrl(this.stateObj)
|
||||||
: ""}
|
: this._posterUrl || ""}
|
||||||
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
|
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
|
||||||
/>`;
|
/>`;
|
||||||
}
|
}
|
||||||
return html`${this._streamType === STREAM_TYPE_HLS ||
|
|
||||||
(!this._streamType &&
|
if (stream.type === STREAM_TYPE_HLS) {
|
||||||
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_HLS))
|
return html`<ha-hls-player
|
||||||
? html`<ha-hls-player
|
|
||||||
autoplay
|
autoplay
|
||||||
playsinline
|
playsinline
|
||||||
.allowExoPlayer=${this.allowExoPlayer}
|
.allowExoPlayer=${this.allowExoPlayer}
|
||||||
@ -102,15 +123,12 @@ export class HaCameraStream extends LitElement {
|
|||||||
.entityid=${this.stateObj.entity_id}
|
.entityid=${this.stateObj.entity_id}
|
||||||
.posterUrl=${this._posterUrl}
|
.posterUrl=${this._posterUrl}
|
||||||
@streams=${this._handleHlsStreams}
|
@streams=${this._handleHlsStreams}
|
||||||
class=${!this._streamType && this._webRtcStreams?.hasVideo
|
class=${stream.visible ? "" : "hidden"}
|
||||||
? "hidden"
|
></ha-hls-player>`;
|
||||||
: ""}
|
}
|
||||||
></ha-hls-player>`
|
|
||||||
: nothing}
|
if (stream.type === STREAM_TYPE_WEB_RTC) {
|
||||||
${this._streamType === STREAM_TYPE_WEB_RTC ||
|
return html`<ha-web-rtc-player
|
||||||
(!this._streamType &&
|
|
||||||
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_WEB_RTC))
|
|
||||||
? html`<ha-web-rtc-player
|
|
||||||
autoplay
|
autoplay
|
||||||
playsinline
|
playsinline
|
||||||
.muted=${this.muted}
|
.muted=${this.muted}
|
||||||
@ -119,12 +137,11 @@ export class HaCameraStream extends LitElement {
|
|||||||
.entityid=${this.stateObj.entity_id}
|
.entityid=${this.stateObj.entity_id}
|
||||||
.posterUrl=${this._posterUrl}
|
.posterUrl=${this._posterUrl}
|
||||||
@streams=${this._handleWebRtcStreams}
|
@streams=${this._handleWebRtcStreams}
|
||||||
class=${this._streamType !== STREAM_TYPE_WEB_RTC &&
|
class=${stream.visible ? "" : "hidden"}
|
||||||
!this._webRtcStreams
|
></ha-web-rtc-player>`;
|
||||||
? "hidden"
|
}
|
||||||
: ""}
|
|
||||||
></ha-web-rtc-player>`
|
return nothing;
|
||||||
: nothing}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getCapabilities() {
|
private async _getCapabilities() {
|
||||||
@ -132,35 +149,13 @@ export class HaCameraStream extends LitElement {
|
|||||||
this._hlsStreams = undefined;
|
this._hlsStreams = undefined;
|
||||||
this._webRtcStreams = undefined;
|
this._webRtcStreams = undefined;
|
||||||
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
|
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
|
||||||
|
this._capabilities = { frontend_stream_types: [] };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._capabilities = await fetchCameraCapabilities(
|
this._capabilities = await fetchCameraCapabilities(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
this.stateObj!.entity_id
|
this.stateObj!.entity_id
|
||||||
);
|
);
|
||||||
if (this._capabilities.frontend_stream_types.length === 1) {
|
|
||||||
this._streamType = this._capabilities.frontend_stream_types[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _shouldRenderMJPEG() {
|
|
||||||
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
|
|
||||||
// Steaming is not supported by the camera so fallback to MJPEG stream
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this._capabilities &&
|
|
||||||
(!this._capabilities.frontend_stream_types.includes(STREAM_TYPE_HLS) ||
|
|
||||||
this._hlsStreams?.hasVideo === false) &&
|
|
||||||
(!this._capabilities.frontend_stream_types.includes(
|
|
||||||
STREAM_TYPE_WEB_RTC
|
|
||||||
) ||
|
|
||||||
this._webRtcStreams?.hasVideo === false)
|
|
||||||
) {
|
|
||||||
// No video in HLS stream and no video in WebRTC stream
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getPosterUrl(): Promise<void> {
|
private async _getPosterUrl(): Promise<void> {
|
||||||
@ -179,29 +174,88 @@ export class HaCameraStream extends LitElement {
|
|||||||
|
|
||||||
private _handleHlsStreams(ev: CustomEvent) {
|
private _handleHlsStreams(ev: CustomEvent) {
|
||||||
this._hlsStreams = ev.detail;
|
this._hlsStreams = ev.detail;
|
||||||
this._pickStreamType();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleWebRtcStreams(ev: CustomEvent) {
|
private _handleWebRtcStreams(ev: CustomEvent) {
|
||||||
this._webRtcStreams = ev.detail;
|
this._webRtcStreams = ev.detail;
|
||||||
this._pickStreamType();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _pickStreamType() {
|
private _streams = memoizeOne(
|
||||||
if (!this._hlsStreams || !this._webRtcStreams) {
|
(
|
||||||
return;
|
supportedTypes?: StreamType[],
|
||||||
|
hlsStreams?: { hasAudio: boolean; hasVideo: boolean },
|
||||||
|
webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }
|
||||||
|
): Stream[] => {
|
||||||
|
if (__DEMO__) {
|
||||||
|
return [{ type: MJPEG_STREAM, visible: true }];
|
||||||
}
|
}
|
||||||
|
if (!supportedTypes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (supportedTypes.length === 0) {
|
||||||
|
// doesn't support any stream type, fallback to mjpeg
|
||||||
|
return [{ type: MJPEG_STREAM, visible: true }];
|
||||||
|
}
|
||||||
|
if (supportedTypes.length === 1) {
|
||||||
|
// only 1 stream type, no need to choose
|
||||||
if (
|
if (
|
||||||
this._hlsStreams.hasVideo &&
|
(supportedTypes[0] === STREAM_TYPE_HLS &&
|
||||||
this._hlsStreams.hasAudio &&
|
hlsStreams?.hasVideo === false) ||
|
||||||
!this._webRtcStreams.hasAudio
|
(supportedTypes[0] === STREAM_TYPE_WEB_RTC &&
|
||||||
|
webRtcStreams?.hasVideo === false)
|
||||||
) {
|
) {
|
||||||
this._streamType = STREAM_TYPE_HLS;
|
// stream failed to load, fallback to mjpeg
|
||||||
} else if (this._webRtcStreams.hasVideo) {
|
return [{ type: MJPEG_STREAM, visible: true }];
|
||||||
this._streamType = STREAM_TYPE_WEB_RTC;
|
}
|
||||||
|
return [{ type: supportedTypes[0], visible: true }];
|
||||||
|
}
|
||||||
|
if (hlsStreams && webRtcStreams) {
|
||||||
|
// fully loaded
|
||||||
|
if (
|
||||||
|
hlsStreams.hasVideo &&
|
||||||
|
hlsStreams.hasAudio &&
|
||||||
|
!webRtcStreams.hasAudio
|
||||||
|
) {
|
||||||
|
// webRTC stream is missing audio, use HLS
|
||||||
|
return [{ type: STREAM_TYPE_HLS, visible: true }];
|
||||||
|
}
|
||||||
|
if (webRtcStreams.hasVideo) {
|
||||||
|
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
|
||||||
|
}
|
||||||
|
// both streams failed to load, fallback to mjpeg
|
||||||
|
return [{ type: MJPEG_STREAM, visible: true }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hlsStreams?.hasVideo !== webRtcStreams?.hasVideo) {
|
||||||
|
// one of the two streams is loaded, or errored
|
||||||
|
// choose the one that has video or is still loading
|
||||||
|
if (hlsStreams?.hasVideo) {
|
||||||
|
return [
|
||||||
|
{ type: STREAM_TYPE_HLS, visible: true },
|
||||||
|
{ type: STREAM_TYPE_WEB_RTC, visible: false },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (hlsStreams?.hasVideo === false) {
|
||||||
|
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
|
||||||
|
}
|
||||||
|
if (webRtcStreams?.hasVideo) {
|
||||||
|
return [
|
||||||
|
{ type: STREAM_TYPE_WEB_RTC, visible: true },
|
||||||
|
{ type: STREAM_TYPE_HLS, visible: false },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (webRtcStreams?.hasVideo === false) {
|
||||||
|
return [{ type: STREAM_TYPE_HLS, visible: true }];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ type: STREAM_TYPE_HLS, visible: true },
|
||||||
|
{ type: STREAM_TYPE_WEB_RTC, visible: false },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host,
|
:host,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user