7
7
type PropertyValues ,
8
8
} from "lit" ;
9
9
import { customElement , property , state } from "lit/decorators" ;
10
+ import { repeat } from "lit/directives/repeat" ;
11
+ import memoizeOne from "memoize-one" ;
10
12
import { computeStateName } from "../common/entity/compute_state_name" ;
11
13
import { supportsFeature } from "../common/entity/supports-feature" ;
12
14
import {
@@ -24,6 +26,13 @@ import type { HomeAssistant } from "../types";
24
26
import "./ha-hls-player" ;
25
27
import "./ha-web-rtc-player" ;
26
28
29
+ const MJPEG_STREAM = "mjpeg" ;
30
+
31
+ type Stream = {
32
+ type : StreamType | typeof MJPEG_STREAM ;
33
+ visible : boolean ;
34
+ } ;
35
+
27
36
@customElement ( "ha-camera-stream" )
28
37
export class HaCameraStream extends LitElement {
29
38
@property ( { attribute : false } ) public hass ?: HomeAssistant ;
@@ -46,16 +55,13 @@ export class HaCameraStream extends LitElement {
46
55
47
56
@state ( ) private _capabilities ?: CameraCapabilities ;
48
57
49
- @state ( ) private _streamType ?: StreamType ;
50
-
51
58
@state ( ) private _hlsStreams ?: { hasAudio : boolean ; hasVideo : boolean } ;
52
59
53
60
@state ( ) private _webRtcStreams ?: { hasAudio : boolean ; hasVideo : boolean } ;
54
61
55
62
public willUpdate ( changedProps : PropertyValues ) : void {
56
63
if (
57
64
changedProps . has ( "stateObj" ) &&
58
- ! this . _shouldRenderMJPEG &&
59
65
this . stateObj &&
60
66
( changedProps . get ( "stateObj" ) as CameraEntity | undefined ) ?. entity_id !==
61
67
this . stateObj . entity_id
@@ -79,88 +85,77 @@ export class HaCameraStream extends LitElement {
79
85
if ( ! this . stateObj ) {
80
86
return nothing ;
81
87
}
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 ) {
83
105
return html `<img
84
106
.src = ${ __DEMO__
85
107
? this . stateObj . attributes . entity_picture !
86
108
: this . _connected
87
109
? computeMJPEGStreamUrl ( this . stateObj )
88
- : "" }
110
+ : this . _posterUrl || "" }
89
111
alt= ${ `Preview of the ${ computeStateName ( this . stateObj ) } camera.` }
90
112
/ > ` ;
91
113
}
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
- auto play
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
- auto play
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
+ auto play
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
+ auto play
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 ;
128
145
}
129
146
130
147
private async _getCapabilities ( ) {
131
148
this . _capabilities = undefined ;
132
149
this . _hlsStreams = undefined ;
133
150
this . _webRtcStreams = undefined ;
134
151
if ( ! supportsFeature ( this . stateObj ! , CAMERA_SUPPORT_STREAM ) ) {
152
+ this . _capabilities = { frontend_stream_types : [ ] } ;
135
153
return ;
136
154
}
137
155
this . _capabilities = await fetchCameraCapabilities (
138
156
this . hass ! ,
139
157
this . stateObj ! . entity_id
140
158
) ;
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 ;
164
159
}
165
160
166
161
private async _getPosterUrl ( ) : Promise < void > {
@@ -179,28 +174,87 @@ export class HaCameraStream extends LitElement {
179
174
180
175
private _handleHlsStreams ( ev : CustomEvent ) {
181
176
this . _hlsStreams = ev . detail ;
182
- this . _pickStreamType ( ) ;
183
177
}
184
178
185
179
private _handleWebRtcStreams ( ev : CustomEvent ) {
186
180
this . _webRtcStreams = ev . detail ;
187
- this . _pickStreamType ( ) ;
188
181
}
189
182
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
+ ] ;
202
256
}
203
- }
257
+ ) ;
204
258
205
259
static get styles ( ) : CSSResultGroup {
206
260
return css `
0 commit comments