From 35dcb4670351fef99be515552f8fdc48706134ec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 5 Nov 2024 15:33:34 +0100 Subject: [PATCH] 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 --- src/components/ha-camera-stream.ts | 212 ++++++++++++++++++----------- 1 file changed, 133 insertions(+), 79 deletions(-) diff --git a/src/components/ha-camera-stream.ts b/src/components/ha-camera-stream.ts index 29bf3a7860..4fe9b9d302 100644 --- a/src/components/ha-camera-stream.ts +++ b/src/components/ha-camera-stream.ts @@ -7,6 +7,8 @@ import { type PropertyValues, } from "lit"; 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 { supportsFeature } from "../common/entity/supports-feature"; import { @@ -24,6 +26,13 @@ import type { HomeAssistant } from "../types"; import "./ha-hls-player"; import "./ha-web-rtc-player"; +const MJPEG_STREAM = "mjpeg"; + +type Stream = { + type: StreamType | typeof MJPEG_STREAM; + visible: boolean; +}; + @customElement("ha-camera-stream") export class HaCameraStream extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; @@ -46,8 +55,6 @@ export class HaCameraStream extends LitElement { @state() private _capabilities?: CameraCapabilities; - @state() private _streamType?: StreamType; - @state() private _hlsStreams?: { 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 { if ( changedProps.has("stateObj") && - !this._shouldRenderMJPEG && this.stateObj && (changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !== this.stateObj.entity_id @@ -79,52 +85,63 @@ export class HaCameraStream extends LitElement { if (!this.stateObj) { 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`${`Preview`; } - return html`${this._streamType === STREAM_TYPE_HLS || - (!this._streamType && - this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_HLS)) - ? html`` - : nothing} - ${this._streamType === STREAM_TYPE_WEB_RTC || - (!this._streamType && - this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_WEB_RTC)) - ? html`` - : nothing}`; + + if (stream.type === STREAM_TYPE_HLS) { + return html``; + } + + if (stream.type === STREAM_TYPE_WEB_RTC) { + return html``; + } + + return nothing; } private async _getCapabilities() { @@ -132,35 +149,13 @@ export class HaCameraStream extends LitElement { this._hlsStreams = undefined; this._webRtcStreams = undefined; if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) { + this._capabilities = { frontend_stream_types: [] }; return; } this._capabilities = await fetchCameraCapabilities( this.hass!, 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 { @@ -179,28 +174,87 @@ export class HaCameraStream extends LitElement { private _handleHlsStreams(ev: CustomEvent) { this._hlsStreams = ev.detail; - this._pickStreamType(); } private _handleWebRtcStreams(ev: CustomEvent) { this._webRtcStreams = ev.detail; - this._pickStreamType(); } - private _pickStreamType() { - if (!this._hlsStreams || !this._webRtcStreams) { - return; + private _streams = memoizeOne( + ( + 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 ( + (supportedTypes[0] === STREAM_TYPE_HLS && + hlsStreams?.hasVideo === false) || + (supportedTypes[0] === STREAM_TYPE_WEB_RTC && + webRtcStreams?.hasVideo === false) + ) { + // stream failed to load, fallback to mjpeg + return [{ type: MJPEG_STREAM, visible: true }]; + } + 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 }, + ]; } - if ( - this._hlsStreams.hasVideo && - this._hlsStreams.hasAudio && - !this._webRtcStreams.hasAudio - ) { - this._streamType = STREAM_TYPE_HLS; - } else if (this._webRtcStreams.hasVideo) { - this._streamType = STREAM_TYPE_WEB_RTC; - } - } + ); static get styles(): CSSResultGroup { return css`