blob: bad70e04abd7899a83acd7a0364bfd0b13234ff0 [file] [log] [blame]
James Kuszmaul4a42b182021-01-17 11:32:46 -08001'use strict';
2
3class Peer {
4 constructor() {
5 this.pc = null;
6 this.localMid = null;
7 this.localCandidates = [];
8 this.localParameters = null;
9 this.localDescription = null;
10 this.remoteParameters = null;
11 this.remoteDescription = null;
12 var _waitGatheringComplete = {};
13 _waitGatheringComplete.promise = new Promise((resolve, reject) => {
14 _waitGatheringComplete.resolve = resolve;
15 _waitGatheringComplete.reject = reject;
16 });
17 this._waitGatheringComplete = _waitGatheringComplete;
18 this.dc = {}
19 }
20
21 createPeerConnection() {
22 if (this.pc) {
23 console.warn('RTCPeerConnection already created');
24 return this.pc;
25 }
26
27 var self = this;
28
29 // Create peer connection
30 var pc = new RTCPeerConnection({
31 iceServers: [{
32 urls: 'stun:stun.l.google.com:19302'
33 }]
34 });
35
36 // Bind peer connection events
37 pc.onnegotiationneeded = function(event) {
38 console.log('Negotiation needed')
39 };
40 pc.onicecandidate = function(event) {
41 if (event.candidate) {
42 console.log('Gathered candidate:', event.candidate);
43 self.localCandidates.push(event.candidate);
44 } else {
45 console.log('Gathering complete');
46 self._waitGatheringComplete.resolve();
47 }
48 };
49 pc.onicecandidateerror = function(event) {
50 console.error('ICE candidate error:', event.errorText);
51 };
52 pc.onsignalingstatechange = function(event) {
53 console.log('Signaling state changed to:', pc.signalingState);
54 };
55 pc.oniceconnectionstatechange = function(event) {
56 console.log('ICE connection state changed to:', pc.iceConnectionState);
57 };
58 pc.onicegatheringstatechange = function(event) {
59 console.log('ICE gathering state changed to:', pc.iceGatheringState);
60 };
61 pc.onconnectionstatechange = function(event) {
62 console.log('Connection state changed to:', pc.connectionState);
63 };
64 pc.ondatachannel = function(event) {
65 self.createDataChannel(event.channel);
66 };
67
68 this.pc = pc;
69 return pc;
70 }
71
72 createDataChannel(dc) {
73 // Create data channel
74 dc = (typeof dc !== 'undefined') ? dc : this.pc.createDataChannel('example-channel', {
75 ordered: true
76 });
77
78 // Bind data channel events
79 dc.onopen = function(event) {
80 console.log('Data channel', dc.label, '(', dc.id, ')', 'open');
81 // Send 'hello'
82 dc.send('Hello from WebRTC on', navigator.userAgent);
83 };
84 dc.onbufferedamountlow = function(event) {
85 console.log('Data channel', dc.label, '(', dc.id, ')', 'buffered amount low');
86 };
87 dc.onerror = function(event) {
88 console.error('Data channel', dc.label, '(', dc.id, ')', 'error:', event);
89 };
90 dc.onclose = function(event) {
91 console.log('Data channel', dc.label, '(', dc.id, ')', 'closed');
92 };
93 dc.onmessage = function(event) {
94 var length = event.data.size || event.data.byteLength || event.data.length;
95 console.info('Data channel', dc.label, '(', dc.id, ')', 'message size:', length);
96 };
97
98 // Store channel
99 this.dc[dc.label] = dc;
100
101 return dc;
102 }
103
104 getLocalParameters() {
105 return new Promise((resolve, reject) => {
106 var error;
107 var self = this;
108
109 if (!this.localDescription) {
110 error = 'Must create offer/answer';
111 console.error(error);
112 reject(error);
113 return;
114 }
115
116 // Initialise parameters
117 var parameters = {
118 iceParameters: null,
119 iceCandidates: [],
120 dtlsParameters: null,
121 sctpParameters: null,
122 };
123
124 // Split sections
125 var sections = SDPUtils.splitSections(this.localDescription.sdp);
126 var session = sections.shift();
127
128 // Go through media sections
129 sections.forEach(function(mediaSection, sdpMLineIndex) {
130 // TODO: Ignore anything else but data transports
131
132 // Get mid
133 // TODO: This breaks with multiple transceivers
134 if (!self.localMid) {
135 var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:');
136 if (mid.length > 0) {
137 self.localMid = mid[0].substr(6);
138 }
139 }
140
141 // Get ICE parameters
142 if (!parameters.iceParameters) {
143 parameters.iceParameters = SDPUtils.getIceParameters(mediaSection, session);
144 }
145
146 // Get DTLS parameters
147 if (!parameters.dtlsParameters) {
148 parameters.dtlsParameters = SDPUtils.getDtlsParameters(mediaSection, session);
149 }
150
151 // Get SCTP parameters
152 if (!parameters.sctpParameters) {
153 parameters.sctpParameters = SDPUtils.getSctpCapabilities(mediaSection, session);
154 parameters.sctpParameters.port = SDPUtils.getSctpPort(mediaSection, session);
155 }
156 });
157
158 // ICE lite parameter
159 if (!parameters.iceParameters
160 || !parameters.dtlsParameters
161 || !parameters.sctpParameters) {
162 error = 'Could not retrieve required parameters from local description';
163 console.error(error);
164 reject(error);
165 return;
166 }
167 parameters.iceParameters.iceLite =
168 SDPUtils.matchPrefix(session, 'a=ice-lite').length > 0;
169
170 // Get ICE candidates
171 this._waitGatheringComplete.promise.then(() => {
172 // Add ICE candidates
173 for (var sdpCandidate of self.localCandidates) {
174 var candidate = SDPUtils.parseCandidate(sdpCandidate.candidate);
175 parameters.iceCandidates.push(candidate);
176 }
177
178 // Add ICE candidate complete sentinel
179 // parameters.iceCandidates.push({complete: true}); // TODO
180
181 // Done
182 resolve(parameters);
183 });
184 });
185 }
186
187 setRemoteParameters(parameters, type, localMid = null) {
188 return new Promise((resolve, reject) => {
189 if (this.remoteDescription) {
190 resolve(this.remoteDescription);
191 return;
192 }
193
194 if (!this.pc) {
195 console.error('Must create RTCPeerConnection instance');
196 return;
197 }
198
199 if (!localMid) {
200 localMid = this.localMid;
201 }
202 this.remoteParameters = parameters;
203
204 // Translate DTLS role
205 // TODO: This somehow didn't make it into SDPUtils
206 var setupType;
207 switch (parameters.dtlsParameters.role) {
208 case 'client':
209 setupType = 'active';
210 break;
211 case 'server':
212 setupType = 'passive';
213 break;
214 default:
215 // We map 'offer' to 'controlling' and 'answer' to 'controlled',
216 // so rawrtc will take 'server' if offering and 'client' if answering
217 // as specified by the ORTC spec
218 switch (type) {
219 case 'offer':
220 // WebRTC requires actpass in offer
221 setupType = 'actpass';
222 break;
223 case 'answer':
224 setupType = 'active';
225 break;
226 }
227 break;
228 }
229
230 // Write session section
231 var sdp = SDPUtils.writeSessionBoilerplate();
232 sdp += 'a=group:BUNDLE ' + localMid + '\r\n';
233 sdp += 'a=ice-options:trickle\r\n';
234 if (parameters.iceParameters.iceLite) {
235 sdp += 'a=ice-lite\r\n';
236 }
237
238 // Write media section
239 // TODO: Replace
240 // sdp += 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n'; // (03)
241 sdp += 'm=application 9 DTLS/SCTP ' + parameters.sctpParameters.port + '\r\n'; // (01)
242 sdp += 'c=IN IP4 0.0.0.0\r\n';
243 sdp += 'a=mid:' + localMid + '\r\n';
244 sdp += 'a=sendrecv\r\n';
245
246 // SCTP part
247 sdp += SDPUtils.writeSctpCapabilities(parameters.sctpParameters);
248 sdp += SDPUtils.writeSctpPort(parameters.sctpParameters.port);
249 sdp += 'a=sctpmap:' + parameters.sctpParameters.port + ' webrtc-datachannel 65535\r\n'; // (01)
250
251 // DTLS part
252 sdp += SDPUtils.writeDtlsParameters(parameters.dtlsParameters, setupType);
253
254 // ICE part
255 sdp += 'a=connection:new\r\n'; // (03)
256 sdp += SDPUtils.writeIceParameters(parameters.iceParameters);
257
258 // Done
259 console.log('Remote description:\n' + sdp);
260
261 // Set remote description
262 this.pc.setRemoteDescription({type: type, sdp: sdp})
263 .then(() => {
264 console.log('Remote description:\n' + this.pc.remoteDescription.sdp);
265 this.remoteDescription = this.pc.remoteDescription;
266
267 // Add ICE candidates
268 for (var iceCandidate of parameters.iceCandidates) {
269 // Add component which ORTC doesn't have
270 // Note: We choose RTP as it doesn't actually matter for us
271 iceCandidate.component = 1; // RTP
272
273 // Create
274 var candidate = new RTCIceCandidate({
275 candidate: SDPUtils.writeCandidate(iceCandidate),
276 sdpMLineIndex: 0, // TODO: Fix
277 sdpMid: localMid // TODO: Fix
278 });
279
280 // Add
281 console.log(candidate.candidate);
282 this.pc.addIceCandidate(candidate)
283 .then(() => {
284 console.log('Added remote candidate', candidate);
285 });
286 }
287
288 // It's trickle ICE, no need to wait for candidates to be added
289 resolve();
290 })
291 .catch((error) => {
292 reject(error);
293 });
294 });
295 }
296
297 start() {}
298}
299
300class ControllingPeer extends Peer {
301 getLocalParameters() {
302 return new Promise((resolve, reject) => {
303 if (!this.pc) {
304 var error = 'Must create RTCPeerConnection instance';
305 console.error(error);
306 reject(error);
307 return;
308 }
309
310 var getLocalParameters = () => {
311 // Return parameters
312 super.getLocalParameters()
313 .then((parameters) => {
314 this.localParameters = parameters;
315 resolve(parameters);
316 })
317 .catch((error) => {
318 reject(error);
319 });
320 };
321
322 // Create offer
323 if (!this.localDescription) {
324 this.pc.createOffer()
325 .then((description) => {
326 return this.pc.setLocalDescription(description);
327 })
328 .then(() => {
329 console.log('Local description:\n' + this.pc.localDescription.sdp);
330 this.localDescription = this.pc.localDescription;
331 getLocalParameters();
332 })
333 .catch((error) => {
334 reject(error);
335 });
336 } else {
337 getLocalParameters();
338 }
339 });
340 }
341
342 setRemoteParameters(parameters, localMid = null) {
343 return super.setRemoteParameters(parameters, 'answer', localMid);
344 }
345}
346
347class ControlledPeer extends Peer {
348 getLocalParameters() {
349 return new Promise((resolve, reject) => {
350 var error;
351
352 if (!this.pc) {
353 error = 'Must create RTCPeerConnection instance';
354 console.error(error);
355 reject(error);
356 return;
357 }
358 if (!this.remoteDescription) {
359 error = 'Must have remote description';
360 console.error(error);
361 reject(error);
362 return;
363 }
364
365 var getLocalParameters = () => {
366 // Return parameters
367 super.getLocalParameters()
368 .then((parameters) => {
369 resolve(parameters);
370 })
371 .catch((error) => {
372 reject(error);
373 });
374 };
375
376 // Create answer
377 if (!this.localDescription) {
378 this.pc.createAnswer()
379 .then((description) => {
380 return this.pc.setLocalDescription(description);
381 })
382 .then(() => {
383 console.log('Local description:\n' + this.pc.localDescription.sdp);
384 this.localDescription = this.pc.localDescription;
385 getLocalParameters();
386 });
387 } else {
388 getLocalParameters();
389 }
390 });
391 }
392
393 setRemoteParameters(parameters, localMid = null) {
394 return super.setRemoteParameters(parameters, 'offer', localMid);
395 }
396}