James Kuszmaul | 4a42b18 | 2021-01-17 11:32:46 -0800 | [diff] [blame^] | 1 | 'use strict'; |
| 2 | |
| 3 | class 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 | |
| 300 | class 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 | |
| 347 | class 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 | } |