blob: bad70e04abd7899a83acd7a0364bfd0b13234ff0 [file] [log] [blame]
'use strict';
class Peer {
constructor() {
this.pc = null;
this.localMid = null;
this.localCandidates = [];
this.localParameters = null;
this.localDescription = null;
this.remoteParameters = null;
this.remoteDescription = null;
var _waitGatheringComplete = {};
_waitGatheringComplete.promise = new Promise((resolve, reject) => {
_waitGatheringComplete.resolve = resolve;
_waitGatheringComplete.reject = reject;
});
this._waitGatheringComplete = _waitGatheringComplete;
this.dc = {}
}
createPeerConnection() {
if (this.pc) {
console.warn('RTCPeerConnection already created');
return this.pc;
}
var self = this;
// Create peer connection
var pc = new RTCPeerConnection({
iceServers: [{
urls: 'stun:stun.l.google.com:19302'
}]
});
// Bind peer connection events
pc.onnegotiationneeded = function(event) {
console.log('Negotiation needed')
};
pc.onicecandidate = function(event) {
if (event.candidate) {
console.log('Gathered candidate:', event.candidate);
self.localCandidates.push(event.candidate);
} else {
console.log('Gathering complete');
self._waitGatheringComplete.resolve();
}
};
pc.onicecandidateerror = function(event) {
console.error('ICE candidate error:', event.errorText);
};
pc.onsignalingstatechange = function(event) {
console.log('Signaling state changed to:', pc.signalingState);
};
pc.oniceconnectionstatechange = function(event) {
console.log('ICE connection state changed to:', pc.iceConnectionState);
};
pc.onicegatheringstatechange = function(event) {
console.log('ICE gathering state changed to:', pc.iceGatheringState);
};
pc.onconnectionstatechange = function(event) {
console.log('Connection state changed to:', pc.connectionState);
};
pc.ondatachannel = function(event) {
self.createDataChannel(event.channel);
};
this.pc = pc;
return pc;
}
createDataChannel(dc) {
// Create data channel
dc = (typeof dc !== 'undefined') ? dc : this.pc.createDataChannel('example-channel', {
ordered: true
});
// Bind data channel events
dc.onopen = function(event) {
console.log('Data channel', dc.label, '(', dc.id, ')', 'open');
// Send 'hello'
dc.send('Hello from WebRTC on', navigator.userAgent);
};
dc.onbufferedamountlow = function(event) {
console.log('Data channel', dc.label, '(', dc.id, ')', 'buffered amount low');
};
dc.onerror = function(event) {
console.error('Data channel', dc.label, '(', dc.id, ')', 'error:', event);
};
dc.onclose = function(event) {
console.log('Data channel', dc.label, '(', dc.id, ')', 'closed');
};
dc.onmessage = function(event) {
var length = event.data.size || event.data.byteLength || event.data.length;
console.info('Data channel', dc.label, '(', dc.id, ')', 'message size:', length);
};
// Store channel
this.dc[dc.label] = dc;
return dc;
}
getLocalParameters() {
return new Promise((resolve, reject) => {
var error;
var self = this;
if (!this.localDescription) {
error = 'Must create offer/answer';
console.error(error);
reject(error);
return;
}
// Initialise parameters
var parameters = {
iceParameters: null,
iceCandidates: [],
dtlsParameters: null,
sctpParameters: null,
};
// Split sections
var sections = SDPUtils.splitSections(this.localDescription.sdp);
var session = sections.shift();
// Go through media sections
sections.forEach(function(mediaSection, sdpMLineIndex) {
// TODO: Ignore anything else but data transports
// Get mid
// TODO: This breaks with multiple transceivers
if (!self.localMid) {
var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:');
if (mid.length > 0) {
self.localMid = mid[0].substr(6);
}
}
// Get ICE parameters
if (!parameters.iceParameters) {
parameters.iceParameters = SDPUtils.getIceParameters(mediaSection, session);
}
// Get DTLS parameters
if (!parameters.dtlsParameters) {
parameters.dtlsParameters = SDPUtils.getDtlsParameters(mediaSection, session);
}
// Get SCTP parameters
if (!parameters.sctpParameters) {
parameters.sctpParameters = SDPUtils.getSctpCapabilities(mediaSection, session);
parameters.sctpParameters.port = SDPUtils.getSctpPort(mediaSection, session);
}
});
// ICE lite parameter
if (!parameters.iceParameters
|| !parameters.dtlsParameters
|| !parameters.sctpParameters) {
error = 'Could not retrieve required parameters from local description';
console.error(error);
reject(error);
return;
}
parameters.iceParameters.iceLite =
SDPUtils.matchPrefix(session, 'a=ice-lite').length > 0;
// Get ICE candidates
this._waitGatheringComplete.promise.then(() => {
// Add ICE candidates
for (var sdpCandidate of self.localCandidates) {
var candidate = SDPUtils.parseCandidate(sdpCandidate.candidate);
parameters.iceCandidates.push(candidate);
}
// Add ICE candidate complete sentinel
// parameters.iceCandidates.push({complete: true}); // TODO
// Done
resolve(parameters);
});
});
}
setRemoteParameters(parameters, type, localMid = null) {
return new Promise((resolve, reject) => {
if (this.remoteDescription) {
resolve(this.remoteDescription);
return;
}
if (!this.pc) {
console.error('Must create RTCPeerConnection instance');
return;
}
if (!localMid) {
localMid = this.localMid;
}
this.remoteParameters = parameters;
// Translate DTLS role
// TODO: This somehow didn't make it into SDPUtils
var setupType;
switch (parameters.dtlsParameters.role) {
case 'client':
setupType = 'active';
break;
case 'server':
setupType = 'passive';
break;
default:
// We map 'offer' to 'controlling' and 'answer' to 'controlled',
// so rawrtc will take 'server' if offering and 'client' if answering
// as specified by the ORTC spec
switch (type) {
case 'offer':
// WebRTC requires actpass in offer
setupType = 'actpass';
break;
case 'answer':
setupType = 'active';
break;
}
break;
}
// Write session section
var sdp = SDPUtils.writeSessionBoilerplate();
sdp += 'a=group:BUNDLE ' + localMid + '\r\n';
sdp += 'a=ice-options:trickle\r\n';
if (parameters.iceParameters.iceLite) {
sdp += 'a=ice-lite\r\n';
}
// Write media section
// TODO: Replace
// sdp += 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n'; // (03)
sdp += 'm=application 9 DTLS/SCTP ' + parameters.sctpParameters.port + '\r\n'; // (01)
sdp += 'c=IN IP4 0.0.0.0\r\n';
sdp += 'a=mid:' + localMid + '\r\n';
sdp += 'a=sendrecv\r\n';
// SCTP part
sdp += SDPUtils.writeSctpCapabilities(parameters.sctpParameters);
sdp += SDPUtils.writeSctpPort(parameters.sctpParameters.port);
sdp += 'a=sctpmap:' + parameters.sctpParameters.port + ' webrtc-datachannel 65535\r\n'; // (01)
// DTLS part
sdp += SDPUtils.writeDtlsParameters(parameters.dtlsParameters, setupType);
// ICE part
sdp += 'a=connection:new\r\n'; // (03)
sdp += SDPUtils.writeIceParameters(parameters.iceParameters);
// Done
console.log('Remote description:\n' + sdp);
// Set remote description
this.pc.setRemoteDescription({type: type, sdp: sdp})
.then(() => {
console.log('Remote description:\n' + this.pc.remoteDescription.sdp);
this.remoteDescription = this.pc.remoteDescription;
// Add ICE candidates
for (var iceCandidate of parameters.iceCandidates) {
// Add component which ORTC doesn't have
// Note: We choose RTP as it doesn't actually matter for us
iceCandidate.component = 1; // RTP
// Create
var candidate = new RTCIceCandidate({
candidate: SDPUtils.writeCandidate(iceCandidate),
sdpMLineIndex: 0, // TODO: Fix
sdpMid: localMid // TODO: Fix
});
// Add
console.log(candidate.candidate);
this.pc.addIceCandidate(candidate)
.then(() => {
console.log('Added remote candidate', candidate);
});
}
// It's trickle ICE, no need to wait for candidates to be added
resolve();
})
.catch((error) => {
reject(error);
});
});
}
start() {}
}
class ControllingPeer extends Peer {
getLocalParameters() {
return new Promise((resolve, reject) => {
if (!this.pc) {
var error = 'Must create RTCPeerConnection instance';
console.error(error);
reject(error);
return;
}
var getLocalParameters = () => {
// Return parameters
super.getLocalParameters()
.then((parameters) => {
this.localParameters = parameters;
resolve(parameters);
})
.catch((error) => {
reject(error);
});
};
// Create offer
if (!this.localDescription) {
this.pc.createOffer()
.then((description) => {
return this.pc.setLocalDescription(description);
})
.then(() => {
console.log('Local description:\n' + this.pc.localDescription.sdp);
this.localDescription = this.pc.localDescription;
getLocalParameters();
})
.catch((error) => {
reject(error);
});
} else {
getLocalParameters();
}
});
}
setRemoteParameters(parameters, localMid = null) {
return super.setRemoteParameters(parameters, 'answer', localMid);
}
}
class ControlledPeer extends Peer {
getLocalParameters() {
return new Promise((resolve, reject) => {
var error;
if (!this.pc) {
error = 'Must create RTCPeerConnection instance';
console.error(error);
reject(error);
return;
}
if (!this.remoteDescription) {
error = 'Must have remote description';
console.error(error);
reject(error);
return;
}
var getLocalParameters = () => {
// Return parameters
super.getLocalParameters()
.then((parameters) => {
resolve(parameters);
})
.catch((error) => {
reject(error);
});
};
// Create answer
if (!this.localDescription) {
this.pc.createAnswer()
.then((description) => {
return this.pc.setLocalDescription(description);
})
.then(() => {
console.log('Local description:\n' + this.pc.localDescription.sdp);
this.localDescription = this.pc.localDescription;
getLocalParameters();
});
} else {
getLocalParameters();
}
});
}
setRemoteParameters(parameters, localMid = null) {
return super.setRemoteParameters(parameters, 'offer', localMid);
}
}