@@ -22,6 +22,14 @@ type RTCConfigurationWithSdpSemantics = RTCConfiguration & {
2222 sdpSemantics: ' unified-plan'
2323}
2424
25+ const iceServers = [
26+ {
27+ urls: [
28+ ' stun:stun.l.google.com:19302'
29+ ]
30+ }
31+ ]
32+
2533@Component ({})
2634export default class WebrtcCamerastreamerCamera extends Mixins (CameraMixin ) {
2735 @Ref (' streamingElement' )
@@ -31,6 +39,7 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) {
3139 remoteId: string | null = null
3240 playbackAbortController: AbortController | null = null
3341 sleepAbortController: AbortController | null = null
42+ sendIceServers = true
3443
3544 // adapted from https://github.com/ayufan/camera-streamer/blob/2d3a4884378f384346680a55196bf9244b99b6b6/html/webrtc.html
3645
@@ -50,26 +59,7 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) {
5059 this .updateRawCameraUrl (url .toString ())
5160
5261 try {
53- const response = await fetch (url , {
54- body: JSON .stringify ({
55- type: ' request' ,
56- iceServers: [
57- {
58- urls: [
59- ' stun:stun.l.google.com:19302'
60- ]
61- }
62- ],
63- keepAlive: true
64- }),
65- headers: {
66- ' Content-Type' : ' application/json'
67- },
68- method: ' POST' ,
69- signal: abortControllerSignal
70- })
71-
72- const rtcSessionDescriptionInit = await response .json () as RTCSessionDescriptionInit
62+ const rtcSessionDescriptionInit = await this .sendInitialRequest (url , abortControllerSignal )
7363
7464 this .remoteId = (' id' in rtcSessionDescriptionInit && typeof rtcSessionDescriptionInit .id === ' string' )
7565 ? rtcSessionDescriptionInit .id
@@ -109,18 +99,7 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) {
10999 pc .onicecandidate = async (event : RTCPeerConnectionIceEvent ) => {
110100 if (event .candidate ) {
111101 try {
112- await fetch (url , {
113- body: JSON .stringify ({
114- type: ' remote_candidate' ,
115- id: this .remoteId ,
116- candidates: [event .candidate ]
117- }),
118- headers: {
119- ' Content-Type' : ' application/json'
120- },
121- method: ' POST' ,
122- signal: abortControllerSignal
123- })
102+ await this .sendRemoteCandidatesRequest (url , [event .candidate ], abortControllerSignal )
124103 } catch (e ) {
125104 consola .error (' [WebrtcCamerastreamerCamera] onicecandidate' , e )
126105 }
@@ -134,22 +113,9 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) {
134113
135114 await pc .setLocalDescription (rtcLocalSessionDescriptionInit )
136115
137- const offer = pc .localDescription
138-
139- const response2 = await fetch (url , {
140- body: JSON .stringify ({
141- type: offer ?.type ,
142- id: this .remoteId ,
143- sdp: offer ?.sdp
144- }),
145- headers: {
146- ' Content-Type' : ' application/json'
147- },
148- method: ' POST' ,
149- signal: abortControllerSignal
150- })
151-
152- await response2 .json ()
116+ if (pc .localDescription ) {
117+ await this .sendOfferRequest (url , pc .localDescription , abortControllerSignal )
118+ }
153119 } catch (e ) {
154120 consola .error (` [WebrtcCamerastreamerCamera] failed to start playback "${this .camera .name }" ` , e )
155121
@@ -190,6 +156,98 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) {
190156 await this .loadStream ()
191157 }
192158
159+ async sendInitialRequest (url : string | URL | Request , abortControllerSignal : AbortSignal ): Promise <RTCSessionDescriptionInit > {
160+ try {
161+ const response = await fetch (url , {
162+ body: JSON .stringify ({
163+ type: ' request' ,
164+ ... this .sendIceServers
165+ ? { iceServers }
166+ : undefined ,
167+ keepAlive: true
168+ }),
169+ headers: {
170+ ' Content-Type' : ' application/json'
171+ },
172+ method: ' POST' ,
173+ signal: abortControllerSignal
174+ })
175+
176+ await this .ensureResponseOk (response , ' application/json' )
177+
178+ const data = await response .json () as RTCSessionDescriptionInit
179+
180+ return data
181+ } catch (e ) {
182+ const aborted = (
183+ abortControllerSignal .aborted ||
184+ (
185+ e instanceof Error &&
186+ e .name === ' AbortError'
187+ )
188+ )
189+
190+ if (! aborted ) {
191+ // Switch whether to send iceServers next time
192+ this .sendIceServers = ! this .sendIceServers
193+ }
194+
195+ throw e
196+ }
197+ }
198+
199+ async sendRemoteCandidatesRequest (url : string | URL | Request , candidates : RTCIceCandidateInit [], abortControllerSignal : AbortSignal ): Promise <void > {
200+ const response = await fetch (url , {
201+ body: JSON .stringify ({
202+ type: ' remote_candidate' ,
203+ id: this .remoteId ,
204+ candidates
205+ }),
206+ headers: {
207+ ' Content-Type' : ' application/json'
208+ },
209+ method: ' POST' ,
210+ signal: abortControllerSignal
211+ })
212+
213+ await this .ensureResponseOk (response )
214+ }
215+
216+ async sendOfferRequest (url : string | URL | Request , offer : RTCSessionDescriptionInit , abortControllerSignal : AbortSignal ): Promise <void > {
217+ const response = await fetch (url , {
218+ body: JSON .stringify ({
219+ type: offer .type ,
220+ id: this .remoteId ,
221+ sdp: offer .sdp
222+ }),
223+ headers: {
224+ ' Content-Type' : ' application/json'
225+ },
226+ method: ' POST' ,
227+ signal: abortControllerSignal
228+ })
229+
230+ await this .ensureResponseOk (response )
231+ }
232+
233+ async ensureResponseOk (response : Response , expectedContentType ? : string ): Promise <void > {
234+ const contentType = response .headers .get (' Content-Type' )
235+
236+ const responseOk = (
237+ response .ok &&
238+ (
239+ ! expectedContentType ||
240+ contentType ?.includes (expectedContentType )
241+ )
242+ )
243+
244+ if (! responseOk ) {
245+ const body = await response .text ()
246+
247+ throw new Error (` Invalid response! Status: ${response .status }, Content-Type: ${contentType }, Body: ${body } ` )
248+ }
249+ }
250+
193251 stopPlayback () {
194252 this .updateStatus (' disconnected' )
195253 this .playbackAbortController ?.abort ()
0 commit comments