Skip to content

Commit 35dcb46

Browse files
authored
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
1 parent 17db85e commit 35dcb46

File tree

1 file changed

+133
-79
lines changed

1 file changed

+133
-79
lines changed

src/components/ha-camera-stream.ts

Lines changed: 133 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
type PropertyValues,
88
} from "lit";
99
import { customElement, property, state } from "lit/decorators";
10+
import { repeat } from "lit/directives/repeat";
11+
import memoizeOne from "memoize-one";
1012
import { computeStateName } from "../common/entity/compute_state_name";
1113
import { supportsFeature } from "../common/entity/supports-feature";
1214
import {
@@ -24,6 +26,13 @@ import type { HomeAssistant } from "../types";
2426
import "./ha-hls-player";
2527
import "./ha-web-rtc-player";
2628

29+
const MJPEG_STREAM = "mjpeg";
30+
31+
type Stream = {
32+
type: StreamType | typeof MJPEG_STREAM;
33+
visible: boolean;
34+
};
35+
2736
@customElement("ha-camera-stream")
2837
export class HaCameraStream extends LitElement {
2938
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -46,16 +55,13 @@ export class HaCameraStream extends LitElement {
4655

4756
@state() private _capabilities?: CameraCapabilities;
4857

49-
@state() private _streamType?: StreamType;
50-
5158
@state() private _hlsStreams?: { hasAudio: boolean; hasVideo: boolean };
5259

5360
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
5461

5562
public willUpdate(changedProps: PropertyValues): void {
5663
if (
5764
changedProps.has("stateObj") &&
58-
!this._shouldRenderMJPEG &&
5965
this.stateObj &&
6066
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
6167
this.stateObj.entity_id
@@ -79,88 +85,77 @@ export class HaCameraStream extends LitElement {
7985
if (!this.stateObj) {
8086
return nothing;
8187
}
82-
if (__DEMO__ || this._shouldRenderMJPEG) {
88+
const streams = this._streams(
89+
this._capabilities?.frontend_stream_types,
90+
this._hlsStreams,
91+
this._webRtcStreams
92+
);
93+
return html`${repeat(
94+
streams,
95+
(stream) => stream.type + this.stateObj!.entity_id,
96+
(stream) => this._renderStream(stream)
97+
)}`;
98+
}
99+
100+
private _renderStream(stream: Stream) {
101+
if (!this.stateObj) {
102+
return nothing;
103+
}
104+
if (stream.type === MJPEG_STREAM) {
83105
return html`<img
84106
.src=${__DEMO__
85107
? this.stateObj.attributes.entity_picture!
86108
: this._connected
87109
? computeMJPEGStreamUrl(this.stateObj)
88-
: ""}
110+
: this._posterUrl || ""}
89111
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
90112
/>`;
91113
}
92-
return html`${this._streamType === STREAM_TYPE_HLS ||
93-
(!this._streamType &&
94-
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_HLS))
95-
? html`<ha-hls-player
96-
autoplay
97-
playsinline
98-
.allowExoPlayer=${this.allowExoPlayer}
99-
.muted=${this.muted}
100-
.controls=${this.controls}
101-
.hass=${this.hass}
102-
.entityid=${this.stateObj.entity_id}
103-
.posterUrl=${this._posterUrl}
104-
@streams=${this._handleHlsStreams}
105-
class=${!this._streamType && this._webRtcStreams?.hasVideo
106-
? "hidden"
107-
: ""}
108-
></ha-hls-player>`
109-
: nothing}
110-
${this._streamType === STREAM_TYPE_WEB_RTC ||
111-
(!this._streamType &&
112-
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_WEB_RTC))
113-
? html`<ha-web-rtc-player
114-
autoplay
115-
playsinline
116-
.muted=${this.muted}
117-
.controls=${this.controls}
118-
.hass=${this.hass}
119-
.entityid=${this.stateObj.entity_id}
120-
.posterUrl=${this._posterUrl}
121-
@streams=${this._handleWebRtcStreams}
122-
class=${this._streamType !== STREAM_TYPE_WEB_RTC &&
123-
!this._webRtcStreams
124-
? "hidden"
125-
: ""}
126-
></ha-web-rtc-player>`
127-
: nothing}`;
114+
115+
if (stream.type === STREAM_TYPE_HLS) {
116+
return html`<ha-hls-player
117+
autoplay
118+
playsinline
119+
.allowExoPlayer=${this.allowExoPlayer}
120+
.muted=${this.muted}
121+
.controls=${this.controls}
122+
.hass=${this.hass}
123+
.entityid=${this.stateObj.entity_id}
124+
.posterUrl=${this._posterUrl}
125+
@streams=${this._handleHlsStreams}
126+
class=${stream.visible ? "" : "hidden"}
127+
></ha-hls-player>`;
128+
}
129+
130+
if (stream.type === STREAM_TYPE_WEB_RTC) {
131+
return html`<ha-web-rtc-player
132+
autoplay
133+
playsinline
134+
.muted=${this.muted}
135+
.controls=${this.controls}
136+
.hass=${this.hass}
137+
.entityid=${this.stateObj.entity_id}
138+
.posterUrl=${this._posterUrl}
139+
@streams=${this._handleWebRtcStreams}
140+
class=${stream.visible ? "" : "hidden"}
141+
></ha-web-rtc-player>`;
142+
}
143+
144+
return nothing;
128145
}
129146

130147
private async _getCapabilities() {
131148
this._capabilities = undefined;
132149
this._hlsStreams = undefined;
133150
this._webRtcStreams = undefined;
134151
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
152+
this._capabilities = { frontend_stream_types: [] };
135153
return;
136154
}
137155
this._capabilities = await fetchCameraCapabilities(
138156
this.hass!,
139157
this.stateObj!.entity_id
140158
);
141-
if (this._capabilities.frontend_stream_types.length === 1) {
142-
this._streamType = this._capabilities.frontend_stream_types[0];
143-
}
144-
}
145-
146-
private get _shouldRenderMJPEG() {
147-
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
148-
// Steaming is not supported by the camera so fallback to MJPEG stream
149-
return true;
150-
}
151-
if (
152-
this._capabilities &&
153-
(!this._capabilities.frontend_stream_types.includes(STREAM_TYPE_HLS) ||
154-
this._hlsStreams?.hasVideo === false) &&
155-
(!this._capabilities.frontend_stream_types.includes(
156-
STREAM_TYPE_WEB_RTC
157-
) ||
158-
this._webRtcStreams?.hasVideo === false)
159-
) {
160-
// No video in HLS stream and no video in WebRTC stream
161-
return true;
162-
}
163-
return false;
164159
}
165160

166161
private async _getPosterUrl(): Promise<void> {
@@ -179,28 +174,87 @@ export class HaCameraStream extends LitElement {
179174

180175
private _handleHlsStreams(ev: CustomEvent) {
181176
this._hlsStreams = ev.detail;
182-
this._pickStreamType();
183177
}
184178

185179
private _handleWebRtcStreams(ev: CustomEvent) {
186180
this._webRtcStreams = ev.detail;
187-
this._pickStreamType();
188181
}
189182

190-
private _pickStreamType() {
191-
if (!this._hlsStreams || !this._webRtcStreams) {
192-
return;
193-
}
194-
if (
195-
this._hlsStreams.hasVideo &&
196-
this._hlsStreams.hasAudio &&
197-
!this._webRtcStreams.hasAudio
198-
) {
199-
this._streamType = STREAM_TYPE_HLS;
200-
} else if (this._webRtcStreams.hasVideo) {
201-
this._streamType = STREAM_TYPE_WEB_RTC;
183+
private _streams = memoizeOne(
184+
(
185+
supportedTypes?: StreamType[],
186+
hlsStreams?: { hasAudio: boolean; hasVideo: boolean },
187+
webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }
188+
): Stream[] => {
189+
if (__DEMO__) {
190+
return [{ type: MJPEG_STREAM, visible: true }];
191+
}
192+
if (!supportedTypes) {
193+
return [];
194+
}
195+
if (supportedTypes.length === 0) {
196+
// doesn't support any stream type, fallback to mjpeg
197+
return [{ type: MJPEG_STREAM, visible: true }];
198+
}
199+
if (supportedTypes.length === 1) {
200+
// only 1 stream type, no need to choose
201+
if (
202+
(supportedTypes[0] === STREAM_TYPE_HLS &&
203+
hlsStreams?.hasVideo === false) ||
204+
(supportedTypes[0] === STREAM_TYPE_WEB_RTC &&
205+
webRtcStreams?.hasVideo === false)
206+
) {
207+
// stream failed to load, fallback to mjpeg
208+
return [{ type: MJPEG_STREAM, visible: true }];
209+
}
210+
return [{ type: supportedTypes[0], visible: true }];
211+
}
212+
if (hlsStreams && webRtcStreams) {
213+
// fully loaded
214+
if (
215+
hlsStreams.hasVideo &&
216+
hlsStreams.hasAudio &&
217+
!webRtcStreams.hasAudio
218+
) {
219+
// webRTC stream is missing audio, use HLS
220+
return [{ type: STREAM_TYPE_HLS, visible: true }];
221+
}
222+
if (webRtcStreams.hasVideo) {
223+
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
224+
}
225+
// both streams failed to load, fallback to mjpeg
226+
return [{ type: MJPEG_STREAM, visible: true }];
227+
}
228+
229+
if (hlsStreams?.hasVideo !== webRtcStreams?.hasVideo) {
230+
// one of the two streams is loaded, or errored
231+
// choose the one that has video or is still loading
232+
if (hlsStreams?.hasVideo) {
233+
return [
234+
{ type: STREAM_TYPE_HLS, visible: true },
235+
{ type: STREAM_TYPE_WEB_RTC, visible: false },
236+
];
237+
}
238+
if (hlsStreams?.hasVideo === false) {
239+
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
240+
}
241+
if (webRtcStreams?.hasVideo) {
242+
return [
243+
{ type: STREAM_TYPE_WEB_RTC, visible: true },
244+
{ type: STREAM_TYPE_HLS, visible: false },
245+
];
246+
}
247+
if (webRtcStreams?.hasVideo === false) {
248+
return [{ type: STREAM_TYPE_HLS, visible: true }];
249+
}
250+
}
251+
252+
return [
253+
{ type: STREAM_TYPE_HLS, visible: true },
254+
{ type: STREAM_TYPE_WEB_RTC, visible: false },
255+
];
202256
}
203-
}
257+
);
204258

205259
static get styles(): CSSResultGroup {
206260
return css`

0 commit comments

Comments
 (0)