jingle
Advanced tools
Comparing version 1.4.0 to 1.5.0
@@ -1,4 +0,3 @@ | ||
var _ = require('underscore'); | ||
var util = require('util'); | ||
var bows = require('bows'); | ||
var extend = require('extend-object'); | ||
var JingleSession = require('./genericSession'); | ||
@@ -9,5 +8,2 @@ var RTCPeerConnection = require('rtcpeerconnection'); | ||
var log = bows('JingleFile'); | ||
function FileSession(opts) { | ||
@@ -39,6 +35,6 @@ JingleSession.call(this, opts); | ||
this.sender.on('progress', function (sent, size) { | ||
log(self.sid + ': Send progress ' + sent + '/' + size); | ||
self._log('debug', 'Send progress ' + sent + '/' + size); | ||
}); | ||
this.sender.on('sentFile', function (metadata) { | ||
log(self.sid + ': Sent file ' + metadata.name); | ||
self._log('debug', 'Sent file ' + metadata.name); | ||
@@ -67,3 +63,3 @@ // send hash via description update | ||
this.receiver.on('progress', function (received, size) { | ||
log(self.sid + ': Receive progress ' + received + '/' + size); | ||
self._log('debug', 'Receive progress ' + received + '/' + size); | ||
}); | ||
@@ -78,3 +74,3 @@ } | ||
FileSession.prototype = _.extend(FileSession.prototype, { | ||
FileSession.prototype = extend(FileSession.prototype, { | ||
start: function (file) { | ||
@@ -119,3 +115,3 @@ var self = this; | ||
log(this.sid + ': Accepted incoming session'); | ||
this._log('debug', 'Accepted incoming session'); | ||
@@ -125,3 +121,3 @@ this.state = 'active'; | ||
if (err) { | ||
log(self.sid + ': Could not create WebRTC answer', err); | ||
self._log('error', 'Could not create WebRTC answer', err); | ||
return self.end('failed-application'); | ||
@@ -140,3 +136,3 @@ } | ||
log(self.sid + ': Initiating incoming session'); | ||
this._log('debug', 'Initiating incoming session'); | ||
@@ -161,3 +157,3 @@ this.state = 'pending'; | ||
if (err) { | ||
log(self.sid + ': Could not create WebRTC answer', err); | ||
self._log('error', 'Could not create WebRTC answer', err); | ||
return cb({condition: 'general-error'}); | ||
@@ -171,3 +167,3 @@ } | ||
log(this.sid + ': Activating accepted outbound session'); | ||
this._log('debug', 'Activating accepted outbound session'); | ||
@@ -180,3 +176,3 @@ this.state = 'active'; | ||
if (err) { | ||
log(self.sid + ': Could not process WebRTC answer', err); | ||
self._log('error', 'Could not process WebRTC answer', err); | ||
return cb({condition: 'general-error'}); | ||
@@ -190,3 +186,3 @@ } | ||
onSessionTerminate: function (changes, cb) { | ||
log(this.sid + ': Terminating session'); | ||
this._log('debug', 'Terminating session'); | ||
this.pc.close(); | ||
@@ -199,7 +195,7 @@ JingleSession.prototype.end.call(this, changes.reason, true); | ||
log(this.sid + ': Adding ICE candidate'); | ||
this._log('debug', 'Adding ICE candidate'); | ||
this.pc.processIce(changes, function (err) { | ||
if (err) { | ||
log(self.sid + ': Could not process ICE candidate', err); | ||
self._log('error', 'Could not process ICE candidate', err); | ||
} | ||
@@ -221,3 +217,3 @@ cb(); | ||
_onIceCandidate: function (candidateInfo) { | ||
log(this.sid + ': Discovered new ICE candidate', candidateInfo.jingle); | ||
this._log('debug', 'Discovered new ICE candidate', candidateInfo.jingle); | ||
candidateInfo.jingle.contents[0].name = 'data'; | ||
@@ -230,7 +226,7 @@ this.send('transport-info', candidateInfo.jingle); | ||
} else if (this.receiver.metadata.hash.value === this.receiver.metadata.actualhash) { | ||
log(this.sid + ': Hash matches'); | ||
this._log('debug', 'Hash matches'); | ||
this.parent.emit('receivedFile', this, this.receivedFile, this.receiver.metadata); | ||
this.end('success'); | ||
} else { | ||
log(this.sid + ': Hash mismatch, terminating'); | ||
this._log('error', 'Hash mismatch, terminating'); | ||
this.end('media-error'); | ||
@@ -237,0 +233,0 @@ } |
@@ -1,52 +0,67 @@ | ||
var bows = require('bows'); | ||
var util = require('util'); | ||
var async = require('async'); | ||
var extend = require('extend-object'); | ||
var WildEmitter = require('wildemitter'); | ||
var log = bows('JingleSession'); | ||
var ACTIONS = { | ||
'content-accept': 'onContentAccept', | ||
'content-add': 'onContentAdd', | ||
'content-modify': 'onConentModify', | ||
'content-reject': 'onContentReject', | ||
'content-remove': 'onContentRemove', | ||
'description-info': 'onDescriptionInfo', | ||
'security-info': 'onSecurityInfo', | ||
'session-accept': 'onSessionAccept', | ||
'session-info': 'onSessionInfo', | ||
'session-initiate': 'onSessionInitiate', | ||
'session-terminate': 'onSessionTerminate', | ||
'transport-accept': 'onTransportAccept', | ||
'transport-info': 'onTransportInfo', | ||
'transport-reject': 'onTransportReject', | ||
'transport-replace': 'onTransportReplace', | ||
// Unstandardized actions: might go away anytime without notice | ||
'source-add': 'onSourceAdd', | ||
'source-remove': 'onSourceRemove' | ||
}; | ||
function actionToMethod(action) { | ||
var words = action.split('-'); | ||
return 'on' + words[0][0].toUpperCase() + words[0].substr(1) + words[1][0].toUpperCase() + words[1].substr(1); | ||
} | ||
// actions defined in http://xmpp.org/extensions/xep-0166.html#def-action | ||
var actions = [ | ||
'content-accept', 'content-add', 'content-modify', | ||
'content-reject', 'content-remove', 'description-info', | ||
'session-accept', 'session-info', 'session-initiate', | ||
'session-terminate', | ||
'source-add', 'source-remove', // unspecified actions, might go away anytime without notice | ||
'transport-accept', 'transport-info', | ||
'transport-reject', 'transport-replace' | ||
]; | ||
function GenericSession(opts) { | ||
WildEmitter.call(this); | ||
var self = this; | ||
function JingleSession(opts) { | ||
var self = this; | ||
this.sid = opts.sid || Date.now().toString(); | ||
this.peer = opts.peer; | ||
this.peerID = opts.peerID || this.peer.full || this.peer; | ||
this.isInitiator = opts.initiator || false; | ||
this.parent = opts.parent; | ||
this.state = 'starting'; | ||
this.parent = opts.parent; | ||
this.connectionState = 'starting'; | ||
// We track the intial pending description types in case | ||
// of the need for a tie-breaker. | ||
this.pendingDescriptionTypes = opts.descriptionTypes || []; | ||
this.pendingAction = false; | ||
// Here is where we'll ensure that all actions are processed | ||
// in order, even if a particular action requires async handling. | ||
this.processingQueue = async.queue(function (task, next) { | ||
var action = task.action; | ||
var action = task.action; | ||
var changes = task.changes; | ||
var cb = task.cb; | ||
log(self.sid + ': ' + action); | ||
self._log('debug', action); | ||
if (actions.indexOf(action) === -1) { | ||
log(this.sid + ': Invalid action ' + action); | ||
if (!ACTIONS[action]) { | ||
self._log('error', 'Invalid action: ' + action); | ||
cb({condition: 'bad-request'}); | ||
next(); | ||
return; | ||
return next(); | ||
} | ||
var method = actionToMethod(action); | ||
self[method](changes, function (err) { | ||
cb(err); | ||
next(); | ||
self[ACTIONS[action]](changes, function (err, result) { | ||
cb(err, result); | ||
return next(); | ||
}); | ||
@@ -56,98 +71,262 @@ }); | ||
JingleSession.prototype = Object.create(WildEmitter.prototype, { | ||
constructor: { | ||
value: JingleSession | ||
util.inherits(GenericSession, WildEmitter); | ||
// We don't know how to handle any particular content types, | ||
// so no actions are supported. | ||
Object.keys(ACTIONS).forEach(function (action) { | ||
var method = ACTIONS[action]; | ||
GenericSession.prototype[method] = function (changes, cb) { | ||
this._log('error', 'Unsupported action: ' + action); | ||
cb(); | ||
}; | ||
}); | ||
// Provide some convenience properties for checking | ||
// the session's state. | ||
Object.defineProperties(GenericSession.prototype, { | ||
state: { | ||
get: function () { | ||
return this._sessionState; | ||
}, | ||
set: function (value) { | ||
if (value !== this._sessionState) { | ||
var prev = this._sessionState; | ||
this._log('info', 'Changing session state to: ' + value); | ||
this._sessionState = value; | ||
this.emit('change:sessionState', this, value); | ||
this.emit('change:' + value, this, true); | ||
if (prev) { | ||
this.emit('change:' + prev, this, false); | ||
} | ||
} | ||
} | ||
}, | ||
connectionState: { | ||
get: function () { | ||
return this._connectionState; | ||
}, | ||
set: function (value) { | ||
if (value !== this._connectionState) { | ||
var prev = this._connectionState; | ||
this._log('info', 'Changing connection state to: ' + value); | ||
this._connectionState = value; | ||
this.emit('change:connectionState', this, value); | ||
this.emit('change:' + value, this, true); | ||
if (prev) { | ||
this.emit('change:' + prev, this, false); | ||
} | ||
} | ||
} | ||
}, | ||
starting: { | ||
get: function () { | ||
return this._sessionState === 'starting'; | ||
} | ||
}, | ||
pending: { | ||
get: function () { | ||
return this._sessionState === 'pending'; | ||
} | ||
}, | ||
active: { | ||
get: function () { | ||
return this._sessionState === 'active'; | ||
} | ||
}, | ||
ended: { | ||
get: function () { | ||
return this._sessionState === 'ended'; | ||
} | ||
}, | ||
connected: { | ||
get: function () { | ||
return this._connectionState === 'connected'; | ||
} | ||
}, | ||
connecting: { | ||
get: function () { | ||
return this._connectionState === 'connecting'; | ||
} | ||
}, | ||
disconnected: { | ||
get: function () { | ||
return this._connectionState === 'disconnected'; | ||
} | ||
}, | ||
interrupted: { | ||
get: function () { | ||
return this._connectionState === 'interrupted'; | ||
} | ||
} | ||
}); | ||
GenericSession.prototype = extend(GenericSession.prototype, { | ||
_log: function (level, message) { | ||
message = this.sid + ': ' + message; | ||
this.emit('log:' + level, message); | ||
}, | ||
send: function (action, data) { | ||
data = data || {}; | ||
data.sid = this.sid; | ||
data.action = action; | ||
JingleSession.prototype.process = function (action, changes, cb) { | ||
this.processingQueue.push({ | ||
action: action, | ||
changes: changes, | ||
cb: cb | ||
}); | ||
}; | ||
var requirePending = { | ||
'session-inititate': true, | ||
'session-accept': true, | ||
'content-add': true, | ||
'content-remove': true, | ||
'content-reject': true, | ||
'content-accept': true, | ||
'content-modify': true, | ||
'transport-replace': true, | ||
'transport-reject': true, | ||
'transport-accept': true, | ||
'source-add': true, | ||
'source-remove': true | ||
}; | ||
JingleSession.prototype.send = function (type, data) { | ||
data = data || {}; | ||
data.sid = this.sid; | ||
data.action = type; | ||
this.parent.emit('send', { | ||
to: this.peer, | ||
type: 'set', | ||
jingle: data | ||
}); | ||
}; | ||
if (requirePending[action]) { | ||
this.pendingAction = action; | ||
} else { | ||
this.pendingAction = false; | ||
} | ||
Object.defineProperty(JingleSession.prototype, 'state', { | ||
get: function () { | ||
return this._state; | ||
this.emit('send', { | ||
to: this.peer, | ||
type: 'set', | ||
jingle: data | ||
}); | ||
}, | ||
set: function (value) { | ||
var validStates = { | ||
starting: true, | ||
pending: true, | ||
active: true, | ||
ended: true | ||
}; | ||
process: function (action, changes, cb) { | ||
this.processingQueue.push({ | ||
action: action, | ||
changes: changes, | ||
cb: cb | ||
}); | ||
}, | ||
start: function () { | ||
this._log('error', 'Can not start base sessions'); | ||
this.end('unsupported-applications', true); | ||
}, | ||
accept: function () { | ||
this._log('error', 'Can not accept base sessions'); | ||
this.end('unsupported-applications'); | ||
}, | ||
cancel: function () { | ||
this.end('cancel'); | ||
}, | ||
decline: function () { | ||
this.end('decline'); | ||
}, | ||
end: function (reason, silent) { | ||
this.state = 'ended'; | ||
if (!validStates[value]) { | ||
throw new Error('Invalid Jingle Session State: ' + value); | ||
if (!reason) { | ||
reason = 'success'; | ||
} | ||
if (this._state !== value) { | ||
this._state = value; | ||
log(this.sid + ': State changed to ' + value); | ||
if (typeof reason === 'string') { | ||
reason = { | ||
condition: reason | ||
}; | ||
} | ||
} | ||
}); | ||
Object.defineProperty(JingleSession.prototype, 'starting', { | ||
get: function () { | ||
return this._state === 'starting'; | ||
} | ||
}); | ||
Object.defineProperty(JingleSession.prototype, 'pending', { | ||
get: function () { | ||
return this._state === 'pending'; | ||
} | ||
}); | ||
Object.defineProperty(JingleSession.prototype, 'active', { | ||
get: function () { | ||
return this._state === 'active'; | ||
} | ||
}); | ||
Object.defineProperty(JingleSession.prototype, 'ended', { | ||
get: function () { | ||
return this._state === 'ended'; | ||
} | ||
}); | ||
if (!silent) { | ||
this.send('session-terminate', { | ||
reason: reason | ||
}); | ||
} | ||
this.emit('terminated', this, reason); | ||
}, | ||
JingleSession.prototype.start = function () { | ||
this.state = 'pending'; | ||
log(this.sid + ': Can not start generic session'); | ||
}; | ||
JingleSession.prototype.end = function (reason, silence) { | ||
this.parent.peers[this.peer].splice(this.parent.peers[this.peer].indexOf(this), 1); | ||
delete this.parent.sessions[this.sid]; | ||
// It is mandatory to reply to a session-info action with | ||
// an unsupported-info error if the info isn't recognized. | ||
// | ||
// However, a session-info action with no associated payload | ||
// is acceptable (works like a ping). | ||
onSessionInfo: function (changes, cb) { | ||
var okKeys = { | ||
sid: true, | ||
action: true, | ||
initiator: true, | ||
responder: true | ||
}; | ||
this.state = 'ended'; | ||
var unknownPayload = false; | ||
Object.keys(changes).forEach(function (key) { | ||
if (!okKeys[key]) { | ||
unknownPayload = true; | ||
} | ||
}); | ||
reason = reason || {}; | ||
if (unknownPayload) { | ||
cb({ | ||
type: 'modify', | ||
condition: 'feature-not-implemented', | ||
jingleCondition: 'unsupported-info' | ||
}); | ||
} else { | ||
cb(); | ||
} | ||
}, | ||
if (!silence) { | ||
this.send('session-terminate', {reason: reason}); | ||
} | ||
// It is mandatory to reply to a description-info action with | ||
// an unsupported-info error if the info isn't recognized. | ||
onDescriptionInfo: function (changes, cb) { | ||
cb({ | ||
type: 'modify', | ||
condition: 'feature-not-implemented', | ||
jingleCondition: 'unsupported-info' | ||
}); | ||
}, | ||
this.parent.emit('terminated', this, reason); | ||
}; | ||
// It is mandatory to reply to a transport-info action with | ||
// an unsupported-info error if the info isn't recognized. | ||
onTransportInfo: function (changes, cb) { | ||
cb({ | ||
type: 'modify', | ||
condition: 'feature-not-implemented', | ||
jingleCondition: 'unsupported-info' | ||
}); | ||
}, | ||
actions.forEach(function (action) { | ||
var method = actionToMethod(action); | ||
JingleSession.prototype[method] = function (changes, cb) { | ||
log(this.sid + ': Unsupported action ' + action); | ||
// It is mandatory to reply to a content-add action with either | ||
// a content-accept or content-reject. | ||
onContentAdd: function (changes, cb) { | ||
// Allow ack for the content-add to be sent. | ||
cb(); | ||
}; | ||
this.send('content-reject', { | ||
reason: { | ||
condition: 'failed-application', | ||
text: 'content-add is not supported' | ||
} | ||
}); | ||
}, | ||
// It is mandatory to reply to a transport-add action with either | ||
// a transport-accept or transport-reject. | ||
onTransportReplace: function (changes, cb) { | ||
// Allow ack for the transport-replace be sent. | ||
cb(); | ||
this.send('transport-reject', { | ||
reason: { | ||
condition: 'failed-application', | ||
text: 'transport-replace is not supported' | ||
} | ||
}); | ||
} | ||
}); | ||
module.exports = JingleSession; | ||
module.exports = GenericSession; |
@@ -1,10 +0,7 @@ | ||
var _ = require('underscore'); | ||
var util = require('util'); | ||
var bows = require('bows'); | ||
var extend = require('extend-object'); | ||
var JingleSession = require('./genericSession'); | ||
var RTCPeerConnection = require('rtcpeerconnection'); | ||
var log = bows('JingleMedia'); | ||
function MediaSession(opts) { | ||
@@ -46,3 +43,3 @@ JingleSession.call(this, opts); | ||
MediaSession.prototype = _.extend(MediaSession.prototype, { | ||
MediaSession.prototype = extend(MediaSession.prototype, { | ||
start: function (constraints) { | ||
@@ -76,3 +73,3 @@ var self = this; | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -85,3 +82,3 @@ self.send('session-initiate', sessDesc.jingle); | ||
this.pc.close(); | ||
_.each(this.streams, function (stream) { | ||
this.streams.forEach(function (stream) { | ||
self._onStreamRemoved({stream: stream}); | ||
@@ -94,3 +91,3 @@ }); | ||
log(this.sid + ': Accepted incoming session'); | ||
this._log('debug', 'Accepted incoming session'); | ||
@@ -100,3 +97,3 @@ this.state = 'active'; | ||
if (err) { | ||
log(self.sid + ': Could not create WebRTC answer', err); | ||
self._log('error', 'Could not create WebRTC answer', err); | ||
return self.end('failed-application'); | ||
@@ -111,3 +108,3 @@ } | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -118,19 +115,19 @@ self.send('session-accept', answer.jingle); | ||
ring: function () { | ||
log(this.sid + ': Ringing on incoming session'); | ||
this._log('debug', 'Ringing on incoming session'); | ||
this.send('session-info', {ringing: true}); | ||
}, | ||
mute: function (creator, name) { | ||
log(this.sid + ': Muting'); | ||
this._log('debug', 'Muting'); | ||
this.send('session-info', {mute: {creator: creator, name: name}}); | ||
}, | ||
unmute: function (creator, name) { | ||
log(this.sid + ': Unmuting'); | ||
this._log('debug', 'Unmuting'); | ||
this.send('session-info', {unmute: {creator: creator, name: name}}); | ||
}, | ||
hold: function () { | ||
log(this.sid + ': Placing on hold'); | ||
this._log('debug', 'Placing on hold'); | ||
this.send('session-info', {hold: true}); | ||
}, | ||
resume: function () { | ||
log(this.sid + ': Resuing from hold'); | ||
this._log('debug', 'Resuing from hold'); | ||
this.send('session-info', {active: true}); | ||
@@ -207,3 +204,3 @@ }, | ||
log(self.sid + ': Initiating incoming session'); | ||
this._log('debug', 'Initiating incoming session'); | ||
@@ -215,3 +212,3 @@ this.state = 'pending'; | ||
if (err) { | ||
log(self.sid + ': Could not create WebRTC answer', err); | ||
self._log('error', 'Could not create WebRTC answer', err); | ||
return cb({condition: 'general-error'}); | ||
@@ -225,3 +222,3 @@ } | ||
log(this.sid + ': Activating accepted outbound session'); | ||
this._log('debug', 'Activating accepted outbound session'); | ||
@@ -231,3 +228,3 @@ this.state = 'active'; | ||
if (err) { | ||
log(self.sid + ': Could not process WebRTC answer', err); | ||
self._log('error', 'Could not process WebRTC answer', err); | ||
return cb({condition: 'general-error'}); | ||
@@ -242,5 +239,5 @@ } | ||
var self = this; | ||
log(this.sid + ': Terminating session'); | ||
this._log('debug', 'Terminating session'); | ||
this.pc.close(); | ||
_.each(this.streams, function (stream) { | ||
this.streams.forEach(function (stream) { | ||
self._onStreamRemoved({stream: stream}); | ||
@@ -254,7 +251,7 @@ }); | ||
log(this.sid + ': Adding ICE candidate'); | ||
this._log('debug', 'Adding ICE candidate'); | ||
this.pc.processIce(changes, function (err) { | ||
if (err) { | ||
log(self.sid + ': Could not process ICE candidate', err); | ||
self._log('error', 'Could not process ICE candidate', err); | ||
} | ||
@@ -265,5 +262,5 @@ cb(); | ||
onSessionInfo: function (info, cb) { | ||
log(info); | ||
this._log('debug', 'Session info', info); | ||
if (info.ringing) { | ||
log(this.sid + ': Ringing on remote stream'); | ||
this._log('debug', 'Ringing on remote stream'); | ||
this.parent.emit('ringing', this); | ||
@@ -273,3 +270,3 @@ } | ||
if (info.hold) { | ||
log(this.sid + ': On hold'); | ||
this._log('debug', 'On hold'); | ||
this.parent.emit('hold', this); | ||
@@ -279,3 +276,3 @@ } | ||
if (info.active) { | ||
log(this.sid + ': Resumed from hold'); | ||
this._log('debug', 'Resumed from hold'); | ||
this.parent.emit('resumed', this); | ||
@@ -285,3 +282,3 @@ } | ||
if (info.mute) { | ||
log(this.sid + ': Muted', info.mute); | ||
this._log('debug', 'Muted', info.mute); | ||
this.parent.emit('mute', this, info.mute); | ||
@@ -291,3 +288,3 @@ } | ||
if (info.unmute) { | ||
log(this.sid + ': Unmuted', info.unmute); | ||
this._log('debug', 'Unmuted', info.unmute); | ||
this.parent.emit('unmute', this, info.unmute); | ||
@@ -299,3 +296,3 @@ } | ||
onSourceAdd: function (changes, cb) { | ||
// note that this method is highly experimental and may | ||
// note that this method is highly experimental and may | ||
// go away without notice | ||
@@ -323,5 +320,5 @@ var self = this; | ||
} | ||
log(this.sid + ': source-add'); | ||
this._log('debug', 'source-add'); | ||
var newDesc = this.pc.remoteDescription; | ||
@@ -350,3 +347,3 @@ this.pc.remoteDescription.contents.forEach(function (content, idx) { | ||
// FIXME: this block is pretty reusable, even though sometimes the | ||
// FIXME: this block is pretty reusable, even though sometimes the | ||
// order of setRemoteDescription/setLocalDescription should change | ||
@@ -356,3 +353,3 @@ this.pc.handleOffer({type: 'offer', jingle: newDesc}, function (err) { | ||
// handle error | ||
log(this.sid + ': source-add offer error'); | ||
self._log('error', 'source-add offer error'); | ||
return cb({condition: 'general-error'}); | ||
@@ -363,3 +360,3 @@ } | ||
if (err) { | ||
log(this.sid + ': source-add answer error'); | ||
self._log('error', 'source-add answer error'); | ||
return cb({condition: 'general-error'}); | ||
@@ -372,3 +369,3 @@ } | ||
onSourceRemove: function (changes, cb) { | ||
// note that this method is highly experimental and may | ||
// note that this method is highly experimental and may | ||
// go away without notice | ||
@@ -397,3 +394,3 @@ var self = this; | ||
log(this.sid + ': source-remove'); | ||
this._log('debug', 'source-remove'); | ||
@@ -461,3 +458,3 @@ var newDesc = this.pc.remoteDescription; | ||
}); | ||
// FIXME: this block is pretty reusable, even though sometimes the | ||
// FIXME: this block is pretty reusable, even though sometimes the | ||
// order of setRemoteDescription/setLocalDescription should change | ||
@@ -467,3 +464,3 @@ this.pc.handleOffer({type: 'offer', jingle: newDesc}, function (err) { | ||
// handle error | ||
log(this.sid + ': source-remove offer error'); | ||
self._log('error', 'source-remove offer error'); | ||
return cb({condition: 'general-error'}); | ||
@@ -474,3 +471,3 @@ } | ||
if (err) { | ||
log(this.sid + ': source-remove answer error'); | ||
self._log('error', 'source-remove answer error'); | ||
return cb({condition: 'general-error'}); | ||
@@ -522,7 +519,7 @@ } | ||
_onIceCandidate: function (candidateInfo) { | ||
log(this.sid + ': Discovered new ICE candidate', candidateInfo.jingle); | ||
this._log('debug', 'Discovered new ICE candidate', candidateInfo.jingle); | ||
this.send('transport-info', candidateInfo.jingle); | ||
}, | ||
_onStreamAdded: function (event) { | ||
log(this.sid + ': Remote media stream added'); | ||
this._log('debug', 'Remote media stream added'); | ||
@@ -539,3 +536,3 @@ // unfortunately, firefox does not support this yet | ||
_onStreamRemoved: function (event) { | ||
log(this.sid + ': Remote media stream removed'); | ||
this._log('debug', 'Remote media stream removed'); | ||
this.parent.emit('peerStreamRemoved', this, event.stream); | ||
@@ -542,0 +539,0 @@ } |
@@ -1,6 +0,5 @@ | ||
var _ = require('underscore'); | ||
var bows = require('bows'); | ||
var hark = require('hark'); | ||
var util = require('util'); | ||
var intersect = require('intersect'); | ||
var WildEmitter = require('wildemitter'); | ||
var webrtc = require('webrtcsupport'); | ||
var WildEmitter = require('wildemitter'); | ||
@@ -12,36 +11,24 @@ var GenericSession = require('./genericSession'); | ||
var log = bows('Jingle'); | ||
function SessionManager(conf) { | ||
WildEmitter.call(this); | ||
conf = conf || {}; | ||
function Jingle(opts) { | ||
opts = opts || {}; | ||
var config = this.config = { | ||
debug: false, | ||
peerConnectionConfig: { | ||
iceServers: [{'url': 'stun:stun.l.google.com:19302'}] | ||
}, | ||
peerConnectionConstraints: { | ||
optional: [ | ||
{DtlsSrtpKeyAgreement: true}, | ||
{RtpDataChannels: false} | ||
] | ||
}, | ||
autoAdjustMic: false, | ||
media: { | ||
audio: true, | ||
video: true | ||
} | ||
}; | ||
this.jid = conf.jid; | ||
this.selfID = conf.selfID || (this.jid && this.jid.full) || this.jid || ''; | ||
this.MediaSession = MediaSession; | ||
this.jid = opts.jid; | ||
this.sessions = {}; | ||
this.peers = {}; | ||
this.prepareSession = conf.prepareSession || function (opts) { | ||
if (opts.descriptionTypes.indexOf('rtp') >= 0) { | ||
return new MediaSession(opts); | ||
} | ||
if (opts.descriptionTypes.indexOf('filetransfer') >= 0) { | ||
return new FileSession(opts); | ||
} | ||
}; | ||
this.screenSharingSupport = webrtc.screenSharing; | ||
for (var item in opts) { | ||
config[item] = opts[item]; | ||
} | ||
this.capabilities = [ | ||
@@ -68,63 +55,163 @@ 'urn:xmpp:jingle:1' | ||
]; | ||
} else { | ||
log('WebRTC not supported'); | ||
} | ||
WildEmitter.call(this); | ||
this.config = { | ||
debug: false, | ||
peerConnectionConfig: { | ||
iceServers: conf.iceServers || [{'url': 'stun:stun.l.google.com:19302'}] | ||
}, | ||
peerConnectionConstraints: { | ||
optional: [ | ||
{DtlsSrtpKeyAgreement: true}, | ||
{RtpDataChannesl: false} | ||
] | ||
}, | ||
media: { | ||
audio: true, | ||
video: true | ||
} | ||
}; | ||
if (this.config.debug) { | ||
this.on('*', function (event, val1, val2) { | ||
log(event, val1, val2); | ||
}); | ||
for (var item in conf) { | ||
this.config[item] = conf[item]; | ||
} | ||
this.iceServers = this.config.peerConnectionConfig.iceServers; | ||
} | ||
Jingle.prototype = Object.create(WildEmitter.prototype, { | ||
constructor: { | ||
value: Jingle | ||
util.inherits(SessionManager, WildEmitter); | ||
SessionManager.prototype.addICEServer = function (server) { | ||
// server == { | ||
// url: '', | ||
// [username: '',] | ||
// [credential: ''] | ||
// } | ||
if (typeof server === 'string') { | ||
server = {url: server}; | ||
} | ||
}); | ||
Jingle.prototype.addICEServer = function (server) { | ||
this.config.peerConnectionConfig.iceServers.push(server); | ||
this.iceServers.push(server); | ||
}; | ||
Jingle.prototype.setupAudioMonitor = function (stream) { | ||
log('Setup audio'); | ||
var audio = hark(stream); | ||
SessionManager.prototype.addSession = function (session) { | ||
var self = this; | ||
var timeout; | ||
audio.on('speaking', function () { | ||
if (self.hardMuted) { | ||
return; | ||
var sid = session.sid; | ||
var peer = session.peerID; | ||
this.sessions[sid] = session; | ||
if (!this.peers[peer]) { | ||
this.peers[peer] = []; | ||
} | ||
this.peers[peer].push(session); | ||
// Automatically clean up tracked sessions | ||
session.on('terminated', function () { | ||
var peers = self.peers[peer] || []; | ||
if (peers.length) { | ||
peers.splice(peers.indexOf(session), 1); | ||
} | ||
self.setMicIfEnabled(1); | ||
self.emit('speaking'); | ||
delete self.sessions[sid]; | ||
}); | ||
audio.on('stopped_speaking', function () { | ||
if (self.hardMuted) { | ||
return; | ||
// Proxy session events | ||
session.on('*', function (name, data, extraData, extraData2) { | ||
// Listen for when we actually try to start a session to | ||
// trigger the outgoing event. | ||
if (name === 'send') { | ||
var action = data.jingle && data.jingle.action; | ||
if (session.isInitiator && action === 'session-initiate') { | ||
self.emit('outgoing', session); | ||
} | ||
} | ||
if (timeout) { | ||
clearTimeout(timeout); | ||
if (self.config.debug && (name === 'log:debug' || name === 'log:error')) { | ||
console.log('Jingle:', data, extraData, extraData2); | ||
} | ||
timeout = setTimeout(function () { | ||
self.setMicIfEnabled(0.5); | ||
self.emit('stoppedSpeaking'); | ||
}, 1000); | ||
// Don't proxy change:* events, since those don't apply to | ||
// the session manager itself. | ||
if (name.indexOf('change') === 0) { | ||
return; | ||
} | ||
self.emit(name, data, extraData, extraData2); | ||
}); | ||
this.emit('createdSession', session); | ||
return session; | ||
}; | ||
Jingle.prototype.setMicIfEnabled = function (volume) { | ||
if (!this.config.autoAdjustMic) { | ||
return; | ||
SessionManager.prototype.createMediaSession = function (peer, sid, stream) { | ||
var session = new MediaSession({ | ||
sid: sid, | ||
peer: peer, | ||
initiator: true, | ||
stream: stream, | ||
parent: this | ||
}); | ||
this.addSession(session); | ||
return session; | ||
}; | ||
SessionManager.prototype.createFileTransferSession = function (peer, sid) { | ||
var session = new FileSession({ | ||
sid: sid, | ||
peer: peer, | ||
initiator: true, | ||
parent: this | ||
}); | ||
this.addSession(session); | ||
return session; | ||
}; | ||
SessionManager.prototype.endPeerSessions = function (peer, reason, silent) { | ||
peer = peer.full || peer; | ||
var sessions = this.peers[peer] || []; | ||
delete this.peers[peer]; | ||
sessions.forEach(function (session) { | ||
session.end(reason || 'gone', silent); | ||
}); | ||
}; | ||
SessionManager.prototype.endAllSessions = function (reason, silent) { | ||
var self = this; | ||
Object.keys(this.peers).forEach(function (peer) { | ||
self.endPeerSessions(peer, reason, silent); | ||
}); | ||
}; | ||
SessionManager.prototype._createIncomingSession = function (meta, req) { | ||
var session; | ||
if (this.prepareSession) { | ||
session = this.prepareSession(meta, req); | ||
} | ||
this.gainController.setGain(volume); | ||
// Fallback to a generic session type, which can | ||
// only be used to end the session. | ||
if (!session) { | ||
session = new GenericSession(meta); | ||
} | ||
this.addSession(session); | ||
return session; | ||
}; | ||
Jingle.prototype.sendError = function (to, id, data) { | ||
data.type = 'cancel'; | ||
SessionManager.prototype._sendError = function (to, id, data) { | ||
if (!data.type) { | ||
data.type = 'cancel'; | ||
} | ||
this.emit('send', { | ||
@@ -138,25 +225,52 @@ to: to, | ||
Jingle.prototype.process = function (req) { | ||
SessionManager.prototype._log = function (level, message) { | ||
this.emit('log:' + level, message); | ||
}; | ||
SessionManager.prototype.process = function (req) { | ||
var self = this; | ||
// Extract the request metadata that we need to verify | ||
var sid = !!req.jingle ? req.jingle.sid : null; | ||
var session = this.sessions[sid] || null; | ||
var rid = req.id; | ||
var sender = req.from.full || req.from; | ||
if (req.type === 'error') { | ||
return this.emit('error', req); | ||
var isTieBreak = req.error && req.error.jingleCondition === 'tie-break'; | ||
if (session && session.pending && isTieBreak) { | ||
return session.end('alternative-session', true); | ||
} else { | ||
if (session) { | ||
session.pendingAction = false; | ||
} | ||
return this.emit('error', req); | ||
} | ||
} | ||
if (req.type === 'result') { | ||
if (session) { | ||
session.pendingAction = false; | ||
} | ||
return; | ||
} | ||
var sids, currsid, sess; | ||
var sid = req.jingle.sid; | ||
var action = req.jingle.action; | ||
var contents = req.jingle.contents || []; | ||
var contentTypes = _.map(contents, function (content) { | ||
return (content.description || {}).descType; | ||
var descriptionTypes = contents.map(function (content) { | ||
if (content.description) { | ||
return content.description.descType; | ||
} | ||
}); | ||
var transportTypes = contents.map(function (content) { | ||
if (content.transport) { | ||
return content.transport.transType; | ||
} | ||
}); | ||
var session = this.sessions[sid] || null; | ||
var sender = req.from.full || req.from; | ||
var reqid = req.id; | ||
// Now verify that we are allowed to actually process the | ||
// requested action | ||
@@ -166,4 +280,4 @@ if (action !== 'session-initiate') { | ||
if (!session) { | ||
log('Unknown session', sid); | ||
return this.sendError(sender, reqid, { | ||
this._log('error', 'Unknown session', sid); | ||
return this._sendError(sender, rid, { | ||
condition: 'item-not-found', | ||
@@ -175,5 +289,5 @@ jingleCondition: 'unknown-session' | ||
// Check if someone is trying to hijack a session. | ||
if (session.peer !== sender || session.ended) { | ||
log('Session has ended, or action has wrong sender'); | ||
return this.sendError(sender, reqid, { | ||
if (session.peerID !== sender || session.ended) { | ||
this._log('error', 'Session has ended, or action has wrong sender'); | ||
return this._sendError(sender, rid, { | ||
condition: 'item-not-found', | ||
@@ -186,4 +300,4 @@ jingleCondition: 'unknown-session' | ||
if (action === 'session-accept' && !session.pending) { | ||
log('Tried to accept session twice', sid); | ||
return this.sendError(sender, reqid, { | ||
this._log('error', 'Tried to accept session twice', sid); | ||
return this._sendError(sender, rid, { | ||
condition: 'unexpected-request', | ||
@@ -195,6 +309,6 @@ jingleCondition: 'out-of-order' | ||
// Can't process two requests at once, need to tie break | ||
if (action !== 'session-terminate' && session.pendingAction) { | ||
log('Tie break during pending request'); | ||
if (action !== 'session-terminate' && action === session.pendingAction) { | ||
this._log('error', 'Tie break during pending request'); | ||
if (session.isInitiator) { | ||
return this.sendError(sender, reqid, { | ||
return this._sendError(sender, rid, { | ||
condition: 'conflict', | ||
@@ -207,5 +321,5 @@ jingleCondition: 'tie-break' | ||
// Don't accept a new session if we already have one. | ||
if (session.peer !== sender) { | ||
log('Duplicate sid from new sender'); | ||
return this.sendError(sender, reqid, { | ||
if (session.peerID !== sender) { | ||
this._log('error', 'Duplicate sid from new sender'); | ||
return this._sendError(sender, rid, { | ||
condition: 'service-unavailable' | ||
@@ -218,5 +332,5 @@ }); | ||
if (session.pending) { | ||
if (this.jid > session.peer) { | ||
log('Tie break new session because of duplicate sids'); | ||
return this.sendError(sender, reqid, { | ||
if (this.selfID > session.peerID) { | ||
this._log('error', 'Tie break new session because of duplicate sids'); | ||
return this._sendError(sender, rid, { | ||
condition: 'conflict', | ||
@@ -226,24 +340,23 @@ jingleCondition: 'tie-break' | ||
} | ||
} else { | ||
// The other side is just doing it wrong. | ||
this._log('error', 'Someone is doing this wrong'); | ||
return this._sendError(sender, rid, { | ||
condition: 'unexpected-request', | ||
jingleCondition: 'out-of-order' | ||
}); | ||
} | ||
// The other side is just doing it wrong. | ||
log('Someone is doing this wrong'); | ||
return this.sendError(sender, reqid, { | ||
condition: 'unexpected-request', | ||
jingleCondition: 'out-of-order' | ||
}); | ||
} else if (Object.keys(this.peers[sender] || {}).length) { | ||
} else if (this.peers[sender] && this.peers[sender].length) { | ||
// Check if we need to have a tie breaker because we already have | ||
// a different session that is using the requested content types. | ||
sids = Object.keys(this.peers[sender]); | ||
for (var i = 0; i < sids.length; i++) { | ||
currsid = sids[i]; | ||
sess = this.sessions[currsid]; | ||
// a different session with this peer that is using the requested | ||
// content description types. | ||
for (var i = 0, len = this.peers[sender].length; i < len; i++) { | ||
var sess = this.peers[sender][i]; | ||
if (sess && sess.pending) { | ||
if (_.intersection(contentTypes, sess.contentTypes).length) { | ||
if (intersect(descriptionTypes, sess.pendingDescriptionTypes).length) { | ||
// We already have a pending session request for this content type. | ||
if (currsid > sid) { | ||
if (sess.sid > sid) { | ||
// We won the tie breaker | ||
log('Tie break'); | ||
return this.sendError(sender, reqid, { | ||
this._log('info', 'Tie break'); | ||
return this._sendError(sender, rid, { | ||
condition: 'conflict', | ||
@@ -258,23 +371,20 @@ jingleCondition: 'tie-break' | ||
// We've now weeded out invalid requests, so we can process the action now. | ||
if (action === 'session-initiate') { | ||
var opts = { | ||
if (!contents.length) { | ||
return self._sendError(sender, rid, { | ||
condition: 'bad-request' | ||
}); | ||
} | ||
session = this._createIncomingSession({ | ||
sid: sid, | ||
peer: sender, | ||
peer: req.from, | ||
peerID: sender, | ||
initiator: false, | ||
parent: this | ||
}; | ||
if (contentTypes.indexOf('rtp') >= 0) { | ||
session = new MediaSession(opts); | ||
} else if (contentTypes.indexOf('filetransfer') >= 0) { | ||
session = new FileSession(opts); | ||
} else { | ||
session = new GenericSession(opts); | ||
} | ||
this.sessions[sid] = session; | ||
if (!this.peers[sender]) { | ||
this.peers[sender] = []; | ||
} | ||
this.peers[sender].push(session); | ||
this.emit('createdSession', session); | ||
parent: this, | ||
descriptionTypes: descriptionTypes, | ||
transportTypes: transportTypes | ||
}, req); | ||
} | ||
@@ -284,11 +394,14 @@ | ||
if (err) { | ||
log('Could not process request', req, err); | ||
self.sendError(sender, reqid, err); | ||
self._log('error', 'Could not process request', req, err); | ||
self._sendError(sender, rid, err); | ||
} else { | ||
self.emit( | ||
'send', | ||
{ to: sender, id: reqid, type: 'result', action: action } | ||
); | ||
self.emit('send', { | ||
to: sender, | ||
id: rid, | ||
type: 'result', | ||
}); | ||
// Wait for the initial action to be processed before emitting | ||
// the session for the user to accept/reject. | ||
if (action === 'session-initiate') { | ||
log('Incoming session request from ', sender, session); | ||
self.emit('incoming', session); | ||
@@ -300,65 +413,3 @@ } | ||
Jingle.prototype.createMediaSession = function (peer, sid, stream) { | ||
var session = new MediaSession({ | ||
sid: sid, | ||
peer: peer, | ||
initiator: true, | ||
stream: stream, | ||
parent: this | ||
}); | ||
sid = session.sid; | ||
this.sessions[sid] = session; | ||
if (!this.peers[peer]) { | ||
this.peers[peer] = []; | ||
} | ||
this.peers[peer].push(session); | ||
this.emit('createdSession', session); | ||
log('Outgoing session', session.sid, session); | ||
this.emit('outgoing', session); | ||
return session; | ||
}; | ||
Jingle.prototype.createFileTransferSession = function (peer, sid) { | ||
var session = new FileSession({ | ||
sid: sid, | ||
peer: peer, | ||
initiator: true, | ||
parent: this | ||
}); | ||
sid = session.sid; | ||
this.sessions[sid] = session; | ||
if (!this.peers[peer]) { | ||
this.peers[peer] = []; | ||
} | ||
this.peers[peer].push(session); | ||
this.emit('createdSession', session); | ||
log('Outgoing session', session.sid, session); | ||
this.emit('outgoing', session); | ||
return session; | ||
}; | ||
Jingle.prototype.endPeerSessions = function (peer, silence) { | ||
log('Ending all sessions with', peer); | ||
var sessions = this.peers[peer] || []; | ||
sessions.forEach(function (session) { | ||
session.end('gone', silence); | ||
}); | ||
delete this.peers[peer]; | ||
}; | ||
Jingle.prototype.endAllPeerSessions = function () { | ||
log('Ending all peer sessions'); | ||
var self = this; | ||
Object.keys(this.peers).forEach(function (peer) { | ||
self.endPeerSessions(peer); | ||
}); | ||
}; | ||
module.exports = Jingle; | ||
module.exports = SessionManager; |
{ | ||
"name": "jingle", | ||
"description": "Generic Jingle via WebRTC session manager.", | ||
"version": "1.4.0", | ||
"version": "1.5.0", | ||
"author": "Lance Stout <lance@andyet.net>", | ||
@@ -13,9 +13,6 @@ "bugs": "https://github.com/otalk/jingle.js/issues", | ||
"async": "^0.9.0", | ||
"bows": "^0.3.0", | ||
"extend-object": "^1.0.0", | ||
"filetransfer": "^1.0.0", | ||
"hark": "^1.1.1", | ||
"mockconsole": "0.0.1", | ||
"rtcpeerconnection": "^2.6.6", | ||
"sdp-jingle-json": "^1.3.0", | ||
"underscore": "^1.6.0", | ||
"webrtcsupport": "^1.1.0", | ||
@@ -33,3 +30,2 @@ "wildemitter": "^1.0.1" | ||
"precommit-hook": "^1.0.0", | ||
"tape": "2.x", | ||
"uglify-js": "2.x" | ||
@@ -36,0 +32,0 @@ }, |
54222
7
9
1325
+ Addedextend-object@^1.0.0
+ Addedextend-object@1.0.0(transitive)
- Removedbows@^0.3.0
- Removedhark@^1.1.1
- Removedsdp-jingle-json@^1.3.0
- Removedunderscore@^1.6.0
- Removedandlog@1.0.2(transitive)
- Removedbows@0.3.0(transitive)
- Removedhark@1.2.3(transitive)
- Removedsdp-jingle-json@1.3.0(transitive)