-
-
Notifications
You must be signed in to change notification settings - Fork 588
feat(WebrtcCamerastreamer): logging and recovery #1731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,14 @@ type RTCConfigurationWithSdpSemantics = RTCConfiguration & { | |
| sdpSemantics: 'unified-plan' | ||
| } | ||
|
|
||
| const iceServers = [ | ||
| { | ||
| urls: [ | ||
| 'stun:stun.l.google.com:19302' | ||
| ] | ||
| } | ||
| ] | ||
|
|
||
| @Component({}) | ||
| export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) { | ||
| @Ref('streamingElement') | ||
|
|
@@ -31,6 +39,7 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) { | |
| remoteId: string | null = null | ||
| playbackAbortController: AbortController | null = null | ||
| sleepAbortController: AbortController | null = null | ||
| sendIceServers = true | ||
|
|
||
| // adapted from https://github.com/ayufan/camera-streamer/blob/2d3a4884378f384346680a55196bf9244b99b6b6/html/webrtc.html | ||
|
|
||
|
|
@@ -50,26 +59,7 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) { | |
| this.updateRawCameraUrl(url.toString()) | ||
|
|
||
| try { | ||
| const response = await fetch(url, { | ||
| body: JSON.stringify({ | ||
| type: 'request', | ||
| iceServers: [ | ||
| { | ||
| urls: [ | ||
| 'stun:stun.l.google.com:19302' | ||
| ] | ||
| } | ||
| ], | ||
| keepAlive: true | ||
| }), | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| method: 'POST', | ||
| signal: abortControllerSignal | ||
| }) | ||
|
|
||
| const rtcSessionDescriptionInit = await response.json() as RTCSessionDescriptionInit | ||
| const rtcSessionDescriptionInit = await this.sendInitialRequest(url, abortControllerSignal) | ||
|
|
||
| this.remoteId = ('id' in rtcSessionDescriptionInit && typeof rtcSessionDescriptionInit.id === 'string') | ||
| ? rtcSessionDescriptionInit.id | ||
|
|
@@ -109,18 +99,7 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) { | |
| pc.onicecandidate = async (event: RTCPeerConnectionIceEvent) => { | ||
| if (event.candidate) { | ||
| try { | ||
| await fetch(url, { | ||
| body: JSON.stringify({ | ||
| type: 'remote_candidate', | ||
| id: this.remoteId, | ||
| candidates: [event.candidate] | ||
| }), | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| method: 'POST', | ||
| signal: abortControllerSignal | ||
| }) | ||
| await this.sendRemoteCandidatesRequest(url, [event.candidate], abortControllerSignal) | ||
| } catch (e) { | ||
| consola.error('[WebrtcCamerastreamerCamera] onicecandidate', e) | ||
| } | ||
|
|
@@ -134,22 +113,9 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) { | |
|
|
||
| await pc.setLocalDescription(rtcLocalSessionDescriptionInit) | ||
|
|
||
| const offer = pc.localDescription | ||
|
|
||
| const response2 = await fetch(url, { | ||
| body: JSON.stringify({ | ||
| type: offer?.type, | ||
| id: this.remoteId, | ||
| sdp: offer?.sdp | ||
| }), | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| method: 'POST', | ||
| signal: abortControllerSignal | ||
| }) | ||
|
|
||
| await response2.json() | ||
| if (pc.localDescription) { | ||
| await this.sendOfferRequest(url, pc.localDescription, abortControllerSignal) | ||
| } | ||
| } catch (e) { | ||
| consola.error(`[WebrtcCamerastreamerCamera] failed to start playback "${this.camera.name}"`, e) | ||
|
|
||
|
|
@@ -190,6 +156,98 @@ export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) { | |
| await this.loadStream() | ||
| } | ||
|
|
||
| async sendInitialRequest (url: string | URL | Request, abortControllerSignal: AbortSignal): Promise<RTCSessionDescriptionInit> { | ||
| try { | ||
| const response = await fetch(url, { | ||
| body: JSON.stringify({ | ||
| type: 'request', | ||
| ...this.sendIceServers | ||
| ? { iceServers } | ||
| : undefined, | ||
|
pedrolamas marked this conversation as resolved.
|
||
| keepAlive: true | ||
| }), | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| method: 'POST', | ||
| signal: abortControllerSignal | ||
| }) | ||
|
|
||
| await this.ensureResponseOk(response, 'application/json') | ||
|
|
||
| const data = await response.json() as RTCSessionDescriptionInit | ||
|
|
||
| return data | ||
| } catch (e) { | ||
| const aborted = ( | ||
| abortControllerSignal.aborted || | ||
| ( | ||
| e instanceof Error && | ||
| e.name === 'AbortError' | ||
| ) | ||
| ) | ||
|
|
||
| if (!aborted) { | ||
| // Switch whether to send iceServers next time | ||
| this.sendIceServers = !this.sendIceServers | ||
| } | ||
|
pedrolamas marked this conversation as resolved.
|
||
|
|
||
| throw e | ||
| } | ||
| } | ||
|
pedrolamas marked this conversation as resolved.
|
||
|
|
||
| async sendRemoteCandidatesRequest (url: string | URL | Request, candidates: RTCIceCandidateInit[], abortControllerSignal: AbortSignal): Promise<void> { | ||
| const response = await fetch(url, { | ||
| body: JSON.stringify({ | ||
| type: 'remote_candidate', | ||
| id: this.remoteId, | ||
| candidates | ||
| }), | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| method: 'POST', | ||
| signal: abortControllerSignal | ||
| }) | ||
|
|
||
| await this.ensureResponseOk(response) | ||
| } | ||
|
Comment on lines
+213
to
+214
|
||
|
|
||
| async sendOfferRequest (url: string | URL | Request, offer: RTCSessionDescriptionInit, abortControllerSignal: AbortSignal): Promise<void> { | ||
| const response = await fetch(url, { | ||
| body: JSON.stringify({ | ||
| type: offer.type, | ||
| id: this.remoteId, | ||
| sdp: offer.sdp | ||
| }), | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| method: 'POST', | ||
| signal: abortControllerSignal | ||
| }) | ||
|
|
||
| await this.ensureResponseOk(response) | ||
| } | ||
|
Comment on lines
+230
to
+231
|
||
|
|
||
| async ensureResponseOk (response: Response, expectedContentType?: string): Promise<void> { | ||
| const contentType = response.headers.get('Content-Type') | ||
|
|
||
| const responseOk = ( | ||
| response.ok && | ||
| ( | ||
| !expectedContentType || | ||
| contentType?.includes(expectedContentType) | ||
| ) | ||
| ) | ||
|
|
||
| if (!responseOk) { | ||
| const body = await response.text() | ||
|
|
||
| throw new Error(`Invalid response! Status: ${response.status}, Content-Type: ${contentType}, Body: ${body}`) | ||
| } | ||
| } | ||
|
|
||
| stopPlayback () { | ||
| this.updateStatus('disconnected') | ||
| this.playbackAbortController?.abort() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.