Skip to content

Commit 978ef60

Browse files
authored
feat(WebrtcCamerastreamer): logging and recovery (#1731)
Signed-off-by: Pedro Lamas <[email protected]>
1 parent 069c3a2 commit 978ef60

File tree

1 file changed

+106
-48
lines changed

1 file changed

+106
-48
lines changed

src/components/widgets/camera/services/WebrtcCamerastreamerCamera.vue

Lines changed: 106 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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({})
2634
export 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

Comments
 (0)