rtcpeerconnection
Version:
A tiny browser module that normalizes and simplifies the API for WebRTC peer connections.
946 lines (865 loc) • 35.3 kB
JavaScript
var util = require('util');
var SJJ = require('sdp-jingle-json');
var WildEmitter = require('wildemitter');
var cloneDeep = require('lodash.clonedeep');
function PeerConnection(config, constraints) {
var self = this;
var item;
WildEmitter.call(this);
config = config || {};
config.iceServers = config.iceServers || [];
// make sure this only gets enabled in Google Chrome
// EXPERIMENTAL FLAG, might get removed without notice
this.enableChromeNativeSimulcast = false;
if (constraints && constraints.optional && window.chrome &&
navigator.appVersion.match(/Chromium\//) === null) {
constraints.optional.forEach(function (constraint) {
if (constraint.enableChromeNativeSimulcast) {
self.enableChromeNativeSimulcast = true;
}
});
}
// EXPERIMENTAL FLAG, might get removed without notice
this.enableMultiStreamHacks = false;
if (constraints && constraints.optional && window.chrome) {
constraints.optional.forEach(function (constraint) {
if (constraint.enableMultiStreamHacks) {
self.enableMultiStreamHacks = true;
}
});
}
// EXPERIMENTAL FLAG, might get removed without notice
this.restrictBandwidth = 0;
if (constraints && constraints.optional) {
constraints.optional.forEach(function (constraint) {
if (constraint.andyetRestrictBandwidth) {
self.restrictBandwidth = constraint.andyetRestrictBandwidth;
}
});
}
// EXPERIMENTAL FLAG, might get removed without notice
// bundle up ice candidates, only works for jingle mode
// number > 0 is the delay to wait for additional candidates
// ~20ms seems good
this.batchIceCandidates = 0;
if (constraints && constraints.optional) {
constraints.optional.forEach(function (constraint) {
if (constraint.andyetBatchIce) {
self.batchIceCandidates = constraint.andyetBatchIce;
}
});
}
this.batchedIceCandidates = [];
// EXPERIMENTAL FLAG, might get removed without notice
// this attemps to strip out candidates with an already known foundation
// and type -- i.e. those which are gathered via the same TURN server
// but different transports (TURN udp, tcp and tls respectively)
if (constraints && constraints.optional && window.chrome) {
constraints.optional.forEach(function (constraint) {
if (constraint.andyetFasterICE) {
self.eliminateDuplicateCandidates = constraint.andyetFasterICE;
}
});
}
// EXPERIMENTAL FLAG, might get removed without notice
// when using a server such as the jitsi videobridge we don't need to signal
// our candidates
if (constraints && constraints.optional) {
constraints.optional.forEach(function (constraint) {
if (constraint.andyetDontSignalCandidates) {
self.dontSignalCandidates = constraint.andyetDontSignalCandidates;
}
});
}
// EXPERIMENTAL FLAG, might get removed without notice
this.assumeSetLocalSuccess = false;
if (constraints && constraints.optional) {
constraints.optional.forEach(function (constraint) {
if (constraint.andyetAssumeSetLocalSuccess) {
self.assumeSetLocalSuccess = constraint.andyetAssumeSetLocalSuccess;
}
});
}
// EXPERIMENTAL FLAG, might get removed without notice
// working around https://bugzilla.mozilla.org/show_bug.cgi?id=1087551
// pass in a timeout for this
if (window.navigator.mozGetUserMedia) {
if (constraints && constraints.optional) {
this.wtFirefox = 0;
constraints.optional.forEach(function (constraint) {
if (constraint.andyetFirefoxMakesMeSad) {
self.wtFirefox = constraint.andyetFirefoxMakesMeSad;
if (self.wtFirefox > 0) {
self.firefoxcandidatebuffer = [];
}
}
});
}
}
this.pc = new RTCPeerConnection(config, constraints);
if (typeof this.pc.getLocalStreams === 'function') {
this.getLocalStreams = this.pc.getLocalStreams.bind(this.pc);
} else {
this.getLocalStreams = function () {
return [];
};
}
if (typeof this.pc.getSenders === 'function') {
this.getSenders = this.pc.getSenders.bind(this.pc);
} else {
this.getSenders = function () {
return [];
};
}
if (typeof this.pc.getRemoteStreams === 'function') {
this.getRemoteStreams = this.pc.getRemoteStreams.bind(this.pc);
} else {
this.getRemoteStreams = function () {
return [];
};
}
if (typeof this.pc.getReceivers === 'function') {
this.getReceivers = this.pc.getReceivers.bind(this.pc);
} else {
this.getReceivers = function () {
return [];
};
}
this.addStream = this.pc.addStream.bind(this.pc);
this.removeStream = function (stream) {
if (typeof self.pc.removeStream === 'function') {
self.pc.removeStream.apply(self.pc, arguments);
} else if (typeof self.pc.removeTrack === 'function') {
stream.getTracks().forEach(function (track) {
self.pc.removeTrack(track);
});
}
};
if (typeof this.pc.removeTrack === 'function') {
this.removeTrack = this.pc.removeTrack.bind(this.pc);
}
// proxy some events directly
this.pc.onremovestream = this.emit.bind(this, 'removeStream');
this.pc.onremovetrack = this.emit.bind(this, 'removeTrack');
this.pc.onaddstream = this.emit.bind(this, 'addStream');
this.pc.ontrack = this.emit.bind(this, 'addTrack');
this.pc.onnegotiationneeded = this.emit.bind(this, 'negotiationNeeded');
this.pc.oniceconnectionstatechange = this.emit.bind(this, 'iceConnectionStateChange');
this.pc.onsignalingstatechange = this.emit.bind(this, 'signalingStateChange');
// handle ice candidate and data channel events
this.pc.onicecandidate = this._onIce.bind(this);
this.pc.ondatachannel = this._onDataChannel.bind(this);
this.localDescription = {
contents: []
};
this.remoteDescription = {
contents: []
};
this.config = {
debug: false,
sid: '',
isInitiator: true,
sdpSessionID: Date.now(),
useJingle: false
};
this.iceCredentials = {
local: {},
remote: {}
};
// apply our config
for (item in config) {
this.config[item] = config[item];
}
if (this.config.debug) {
this.on('*', function () {
var logger = config.logger || console;
logger.log('PeerConnection event:', arguments);
});
}
this.hadLocalStunCandidate = false;
this.hadRemoteStunCandidate = false;
this.hadLocalRelayCandidate = false;
this.hadRemoteRelayCandidate = false;
this.hadLocalIPv6Candidate = false;
this.hadRemoteIPv6Candidate = false;
// keeping references for all our data channels
// so they dont get garbage collected
// can be removed once the following bugs have been fixed
// https://crbug.com/405545
// https://bugzilla.mozilla.org/show_bug.cgi?id=964092
// to be filed for opera
this._remoteDataChannels = [];
this._localDataChannels = [];
this._candidateBuffer = [];
}
util.inherits(PeerConnection, WildEmitter);
Object.defineProperty(PeerConnection.prototype, 'signalingState', {
get: function () {
return this.pc.signalingState;
}
});
Object.defineProperty(PeerConnection.prototype, 'iceConnectionState', {
get: function () {
return this.pc.iceConnectionState;
}
});
PeerConnection.prototype._role = function () {
return this.isInitiator ? 'initiator' : 'responder';
};
// Add a stream to the peer connection object
PeerConnection.prototype.addStream = function (stream) {
this.localStream = stream;
stream.getTracks().forEach(
function(track) {
this.pc.addTrack(
track,
stream
);
}
);
};
// helper function to check if a remote candidate is a stun/relay
// candidate or an ipv6 candidate
PeerConnection.prototype._checkLocalCandidate = function (candidate) {
var cand = SJJ.toCandidateJSON(candidate);
if (cand.type == 'srflx') {
this.hadLocalStunCandidate = true;
} else if (cand.type == 'relay') {
this.hadLocalRelayCandidate = true;
}
if (cand.ip.indexOf(':') != -1) {
this.hadLocalIPv6Candidate = true;
}
};
// helper function to check if a remote candidate is a stun/relay
// candidate or an ipv6 candidate
PeerConnection.prototype._checkRemoteCandidate = function (candidate) {
var cand = SJJ.toCandidateJSON(candidate);
if (cand.type == 'srflx') {
this.hadRemoteStunCandidate = true;
} else if (cand.type == 'relay') {
this.hadRemoteRelayCandidate = true;
}
if (cand.ip.indexOf(':') != -1) {
this.hadRemoteIPv6Candidate = true;
}
};
// Init and add ice candidate object with correct constructor
PeerConnection.prototype.processIce = function (update, cb) {
cb = cb || function () {};
var self = this;
// ignore any added ice candidates to avoid errors. why does the
// spec not do this?
if (this.pc.signalingState === 'closed') return cb();
if (update.contents || (update.jingle && update.jingle.contents)) {
var contentNames = this.remoteDescription.contents.map(function (c) { return c.name; });
var contents = update.contents || update.jingle.contents;
contents.forEach(function (content) {
var transport = content.transport || {};
var candidates = transport.candidates || [];
var mline = contentNames.indexOf(content.name);
var mid = content.name;
var remoteContent = self.remoteDescription.contents.find(function (c) {
return c.name === content.name;
});
// process candidates as a callback, in case we need to
// update ufrag and pwd with offer/answer
var processCandidates = function () {
candidates.forEach(
function (candidate) {
var iceCandidate = SJJ.toCandidateSDP(candidate);
self.pc.addIceCandidate(
new RTCIceCandidate({
candidate: iceCandidate,
sdpMLineIndex: mline,
sdpMid: mid
})
).then(
function () {
// well, this success callback is pretty meaningless
},
function (err) {
self.emit('error', err);
}
);
self._checkRemoteCandidate(iceCandidate);
});
cb();
};
if (self.iceCredentials.remote[content.name] && transport.ufrag &&
self.iceCredentials.remote[content.name].ufrag !== transport.ufrag) {
if (remoteContent) {
remoteContent.transport.ufrag = transport.ufrag;
remoteContent.transport.pwd = transport.pwd;
var offer = {
type: 'offer',
jingle: self.remoteDescription
};
offer.sdp = SJJ.toSessionSDP(offer.jingle, {
sid: self.config.sdpSessionID,
role: self._role(),
direction: 'incoming'
});
self.pc.setRemoteDescription(
new RTCSessionDescription(offer)
).then(
function () {
processCandidates();
},
function (err) {
self.emit('error', err);
}
);
} else {
self.emit('error', 'ice restart failed to find matching content');
}
} else {
processCandidates();
}
});
} else {
// working around https://code.google.com/p/webrtc/issues/detail?id=3669
if (update.candidate && update.candidate.candidate.indexOf('a=') !== 0) {
update.candidate.candidate = 'a=' + update.candidate.candidate;
}
if (this.wtFirefox && this.firefoxcandidatebuffer !== null) {
// we cant add this yet due to https://bugzilla.mozilla.org/show_bug.cgi?id=1087551
if (this.pc.localDescription && this.pc.localDescription.type === 'offer') {
this.firefoxcandidatebuffer.push(update.candidate);
return cb();
}
}
self.pc.addIceCandidate(
new RTCIceCandidate(update.candidate)
).then(
function () { },
function (err) {
self.emit('error', err);
}
);
self._checkRemoteCandidate(update.candidate.candidate);
cb();
}
};
// Generate and emit an offer with the given constraints
PeerConnection.prototype.offer = function (constraints, cb) {
var self = this;
var hasConstraints = arguments.length === 2;
var mediaConstraints = hasConstraints && constraints ? constraints : {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
};
cb = hasConstraints ? cb : constraints;
cb = cb || function () {};
if (this.pc.signalingState === 'closed') return cb('Already closed');
// Actually generate the offer
this.pc.createOffer(
mediaConstraints
).then(
function (offer) {
// does not work for jingle, but jingle.js doesn't need
// this hack...
var expandedOffer = {
type: 'offer',
sdp: offer.sdp
};
if (self.assumeSetLocalSuccess) {
self.emit('offer', expandedOffer);
cb(null, expandedOffer);
}
self._candidateBuffer = [];
self.pc.setLocalDescription(offer).then(
function () {
var jingle;
if (self.config.useJingle) {
jingle = SJJ.toSessionJSON(offer.sdp, {
role: self._role(),
direction: 'outgoing'
});
jingle.sid = self.config.sid;
self.localDescription = jingle;
// Save ICE credentials
jingle.contents.forEach(function (content) {
var transport = content.transport || {};
if (transport.ufrag) {
self.iceCredentials.local[content.name] = {
ufrag: transport.ufrag,
pwd: transport.pwd
};
}
});
expandedOffer.jingle = jingle;
}
expandedOffer.sdp.split(/\r?\n/).forEach(function (line) {
if (line.indexOf('a=candidate:') === 0) {
self._checkLocalCandidate(line);
}
});
if (!self.assumeSetLocalSuccess) {
self.emit('offer', expandedOffer);
cb(null, expandedOffer);
}
},
function (err) {
self.emit('error', err);
cb(err);
}
);
},
function (err) {
self.emit('error', err);
cb(err);
}
);
};
// Process an incoming offer so that ICE may proceed before deciding
// to answer the request.
PeerConnection.prototype.handleOffer = function (offer, cb) {
cb = cb || function () {};
var self = this;
offer.type = 'offer';
if (offer.jingle) {
if (this.enableChromeNativeSimulcast) {
offer.jingle.contents.forEach(function (content) {
if (content.name === 'video') {
content.application.googConferenceFlag = true;
}
});
}
if (this.enableMultiStreamHacks) {
// add a mixed video stream as first stream
offer.jingle.contents.forEach(function (content) {
if (content.name === 'video') {
var sources = content.application.sources || [];
if (sources.length === 0 || sources[0].ssrc !== "3735928559") {
sources.unshift({
ssrc: "3735928559", // 0xdeadbeef
parameters: [
{
key: "cname",
value: "deadbeef"
},
{
key: "msid",
value: "mixyourfecintothis please"
}
]
});
content.application.sources = sources;
}
}
});
}
if (self.restrictBandwidth > 0) {
if (offer.jingle.contents.length >= 2 && offer.jingle.contents[1].name === 'video') {
var content = offer.jingle.contents[1];
var hasBw = content.application && content.application.bandwidth && content.application.bandwidth.bandwidth;
if (!hasBw) {
offer.jingle.contents[1].application.bandwidth = { type: 'AS', bandwidth: self.restrictBandwidth.toString() };
offer.sdp = SJJ.toSessionSDP(offer.jingle, {
sid: self.config.sdpSessionID,
role: self._role(),
direction: 'outgoing'
});
}
}
}
// Save ICE credentials
offer.jingle.contents.forEach(function (content) {
var transport = content.transport || {};
if (transport.ufrag) {
self.iceCredentials.remote[content.name] = {
ufrag: transport.ufrag,
pwd: transport.pwd
};
}
});
offer.sdp = SJJ.toSessionSDP(offer.jingle, {
sid: self.config.sdpSessionID,
role: self._role(),
direction: 'incoming'
});
self.remoteDescription = offer.jingle;
}
offer.sdp.split(/\r?\n/).forEach(function (line) {
if (line.indexOf('a=candidate:') === 0) {
self._checkRemoteCandidate(line);
}
});
self.pc.setRemoteDescription(
new RTCSessionDescription(offer)
).then(
function () {
cb();
},
cb
);
};
// Answer an offer with audio only
PeerConnection.prototype.answerAudioOnly = function (cb) {
var mediaConstraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: false
}
};
this._answer(mediaConstraints, cb);
};
// Answer an offer without offering to recieve
PeerConnection.prototype.answerBroadcastOnly = function (cb) {
var mediaConstraints = {
mandatory: {
OfferToReceiveAudio: false,
OfferToReceiveVideo: false
}
};
this._answer(mediaConstraints, cb);
};
// Answer an offer with given constraints default is audio/video
PeerConnection.prototype.answer = function (constraints, cb) {
var hasConstraints = arguments.length === 2;
var callback = hasConstraints ? cb : constraints;
var mediaConstraints = hasConstraints && constraints ? constraints : {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
this._answer(mediaConstraints, callback);
};
// Process an answer
PeerConnection.prototype.handleAnswer = function (answer, cb) {
cb = cb || function () {};
var self = this;
if (answer.jingle) {
answer.sdp = SJJ.toSessionSDP(answer.jingle, {
sid: self.config.sdpSessionID,
role: self._role(),
direction: 'incoming'
});
self.remoteDescription = answer.jingle;
// Save ICE credentials
answer.jingle.contents.forEach(function (content) {
var transport = content.transport || {};
if (transport.ufrag) {
self.iceCredentials.remote[content.name] = {
ufrag: transport.ufrag,
pwd: transport.pwd
};
}
});
}
answer.sdp.split(/\r?\n/).forEach(function (line) {
if (line.indexOf('a=candidate:') === 0) {
self._checkRemoteCandidate(line);
}
});
self.pc.setRemoteDescription(
new RTCSessionDescription(answer)
).then(
function () {
if (self.wtFirefox) {
window.setTimeout(function () {
self.firefoxcandidatebuffer.forEach(function (candidate) {
// add candidates later
self.pc.addIceCandidate(
new RTCIceCandidate(candidate)
).then(
function () { },
function (err) {
self.emit('error', err);
}
);
self._checkRemoteCandidate(candidate.candidate);
});
self.firefoxcandidatebuffer = null;
}, self.wtFirefox);
}
cb(null);
},
cb
);
};
// Close the peer connection
PeerConnection.prototype.close = function () {
this.pc.close();
this._localDataChannels = [];
this._remoteDataChannels = [];
this.emit('close');
};
// Internal code sharing for various types of answer methods
PeerConnection.prototype._answer = function (constraints, cb) {
cb = cb || function () {};
var self = this;
if (!this.pc.remoteDescription) {
// the old API is used, call handleOffer
throw new Error('remoteDescription not set');
}
if (this.pc.signalingState === 'closed') return cb('Already closed');
self.pc.createAnswer(
constraints
).then(
function (answer) {
var sim = [];
if (self.enableChromeNativeSimulcast) {
// native simulcast part 1: add another SSRC
answer.jingle = SJJ.toSessionJSON(answer.sdp, {
role: self._role(),
direction: 'outgoing'
});
if (answer.jingle.contents.length >= 2 && answer.jingle.contents[1].name === 'video') {
var groups = answer.jingle.contents[1].application.sourceGroups || [];
var hasSim = false;
groups.forEach(function (group) {
if (group.semantics == 'SIM') hasSim = true;
});
if (!hasSim &&
answer.jingle.contents[1].application.sources.length) {
var newssrc = JSON.parse(JSON.stringify(answer.jingle.contents[1].application.sources[0]));
newssrc.ssrc = '' + Math.floor(Math.random() * 0xffffffff); // FIXME: look for conflicts
answer.jingle.contents[1].application.sources.push(newssrc);
sim.push(answer.jingle.contents[1].application.sources[0].ssrc);
sim.push(newssrc.ssrc);
groups.push({
semantics: 'SIM',
sources: sim
});
// also create an RTX one for the SIM one
var rtxssrc = JSON.parse(JSON.stringify(newssrc));
rtxssrc.ssrc = '' + Math.floor(Math.random() * 0xffffffff); // FIXME: look for conflicts
answer.jingle.contents[1].application.sources.push(rtxssrc);
groups.push({
semantics: 'FID',
sources: [newssrc.ssrc, rtxssrc.ssrc]
});
answer.jingle.contents[1].application.sourceGroups = groups;
answer.sdp = SJJ.toSessionSDP(answer.jingle, {
sid: self.config.sdpSessionID,
role: self._role(),
direction: 'outgoing'
});
}
}
}
var expandedAnswer = {
type: 'answer',
sdp: answer.sdp
};
if (self.assumeSetLocalSuccess) {
// not safe to do when doing simulcast mangling
var copy = cloneDeep(expandedAnswer);
self.emit('answer', copy);
cb(null, copy);
}
self._candidateBuffer = [];
self.pc.setLocalDescription(answer).then(
function () {
if (self.config.useJingle) {
var jingle = SJJ.toSessionJSON(answer.sdp, {
role: self._role(),
direction: 'outgoing'
});
jingle.sid = self.config.sid;
self.localDescription = jingle;
expandedAnswer.jingle = jingle;
}
if (self.enableChromeNativeSimulcast) {
// native simulcast part 2:
// signal multiple tracks to the receiver
// for anything in the SIM group
if (!expandedAnswer.jingle) {
expandedAnswer.jingle = SJJ.toSessionJSON(answer.sdp, {
role: self._role(),
direction: 'outgoing'
});
}
expandedAnswer.jingle.contents[1].application.sources.forEach(function (source, idx) {
// the floor idx/2 is a hack that relies on a particular order
// of groups, alternating between sim and rtx
source.parameters = source.parameters.map(function (parameter) {
if (parameter.key === 'msid') {
parameter.value += '-' + Math.floor(idx / 2);
}
return parameter;
});
});
expandedAnswer.sdp = SJJ.toSessionSDP(expandedAnswer.jingle, {
sid: self.sdpSessionID,
role: self._role(),
direction: 'outgoing'
});
}
expandedAnswer.sdp.split(/\r?\n/).forEach(function (line) {
if (line.indexOf('a=candidate:') === 0) {
self._checkLocalCandidate(line);
}
});
if (!self.assumeSetLocalSuccess) {
var copy = cloneDeep(expandedAnswer);
self.emit('answer', copy);
cb(null, copy);
}
},
function (err) {
self.emit('error', err);
cb(err);
}
);
},
function (err) {
self.emit('error', err);
cb(err);
}
);
};
// Internal method for emitting ice candidates on our peer object
PeerConnection.prototype._onIce = function (event) {
var self = this;
if (event.candidate) {
if (this.dontSignalCandidates) return;
var ice = event.candidate;
var expandedCandidate = {
candidate: {
candidate: ice.candidate,
sdpMid: ice.sdpMid,
sdpMLineIndex: ice.sdpMLineIndex
}
};
this._checkLocalCandidate(ice.candidate);
var cand = SJJ.toCandidateJSON(ice.candidate);
var already;
var idx;
if (this.eliminateDuplicateCandidates && cand.type === 'relay') {
// drop candidates with same foundation, component
// take local type pref into account so we don't ignore udp
// ones when we know about a TCP one. unlikely but...
already = this._candidateBuffer.filter(
function (c) {
return c.type === 'relay';
}).map(function (c) {
return c.foundation + ':' + c.component;
}
);
idx = already.indexOf(cand.foundation + ':' + cand.component);
// remember: local type pref of udp is 0, tcp 1, tls 2
if (idx > -1 && ((cand.priority >> 24) >= (already[idx].priority >> 24))) {
// drop it, same foundation with higher (worse) type pref
return;
}
}
if (this.config.bundlePolicy === 'max-bundle') {
// drop candidates which are duplicate for audio/video/data
// duplicate means same host/port but different sdpMid
already = this._candidateBuffer.filter(
function (c) {
return cand.type === c.type;
}).map(function (cand) {
return cand.address + ':' + cand.port;
}
);
idx = already.indexOf(cand.address + ':' + cand.port);
if (idx > -1) return;
}
// also drop rtcp candidates since we know the peer supports RTCP-MUX
// this is a workaround until browsers implement this natively
if (this.config.rtcpMuxPolicy === 'require' && cand.component === '2') {
return;
}
this._candidateBuffer.push(cand);
if (self.config.useJingle) {
if (!ice.sdpMid) { // firefox doesn't set this
if (self.pc.remoteDescription && self.pc.remoteDescription.type === 'offer') {
// preserve name from remote
ice.sdpMid = self.remoteDescription.contents[ice.sdpMLineIndex].name;
} else {
ice.sdpMid = self.localDescription.contents[ice.sdpMLineIndex].name;
}
}
if (!self.iceCredentials.local[ice.sdpMid]) {
var jingle = SJJ.toSessionJSON(self.pc.localDescription.sdp, {
role: self._role(),
direction: 'outgoing'
});
jingle.contents.forEach(function (content) {
var transport = content.transport || {};
if (transport.ufrag) {
self.iceCredentials.local[content.name] = {
ufrag: transport.ufrag,
pwd: transport.pwd
};
}
});
}
expandedCandidate.jingle = {
contents: [{
name: ice.sdpMid,
creator: self._role(),
transport: {
transportType: 'iceUdp',
ufrag: self.iceCredentials.local[ice.sdpMid].ufrag,
pwd: self.iceCredentials.local[ice.sdpMid].pwd,
candidates: [
cand
]
}
}]
};
if (self.batchIceCandidates > 0) {
if (self.batchedIceCandidates.length === 0) {
window.setTimeout(function () {
var contents = {};
self.batchedIceCandidates.forEach(function (content) {
content = content.contents[0];
if (!contents[content.name]) contents[content.name] = content;
contents[content.name].transport.candidates.push(content.transport.candidates[0]);
});
var newCand = {
jingle: {
contents: []
}
};
Object.keys(contents).forEach(function (name) {
newCand.jingle.contents.push(contents[name]);
});
self.batchedIceCandidates = [];
self.emit('ice', newCand);
}, self.batchIceCandidates);
}
self.batchedIceCandidates.push(expandedCandidate.jingle);
return;
}
}
this.emit('ice', expandedCandidate);
} else {
this.emit('endOfCandidates');
}
};
// Internal method for processing a new data channel being added by the
// other peer.
PeerConnection.prototype._onDataChannel = function (event) {
// make sure we keep a reference so this doesn't get garbage collected
var channel = event.channel;
this._remoteDataChannels.push(channel);
this.emit('addChannel', channel);
};
// Create a data channel spec reference:
// http://dev.w3.org/2011/webrtc/editor/webrtc.html#idl-def-RTCDataChannelInit
PeerConnection.prototype.createDataChannel = function (name, opts) {
var channel = this.pc.createDataChannel(name, opts);
// make sure we keep a reference so this doesn't get garbage collected
this._localDataChannels.push(channel);
return channel;
};
PeerConnection.prototype.getStats = function () {
if (typeof arguments[0] === 'function') {
var cb = arguments[0];
this.pc.getStats().then(function (res) {
cb(null, res);
}, function (err) {
cb(err);
});
} else {
return this.pc.getStats.apply(this.pc, arguments);
}
};
module.exports = PeerConnection;