337 lines
16 KiB
JavaScript
337 lines
16 KiB
JavaScript
/* jshint -W117 */
|
|
|
|
var JingleSession = require("./JingleSession");
|
|
|
|
module.exports = function(XMPP)
|
|
{
|
|
function CallIncomingJingle(sid, connection) {
|
|
var sess = connection.jingle.sessions[sid];
|
|
|
|
// TODO: do we check activecall == null?
|
|
connection.jingle.activecall = sess;
|
|
|
|
statistics.onConferenceCreated(sess);
|
|
RTC.onConferenceCreated(sess);
|
|
|
|
// TODO: check affiliation and/or role
|
|
console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
|
|
sess.usedrip = true; // not-so-naive trickle ice
|
|
sess.sendAnswer();
|
|
sess.accept();
|
|
|
|
};
|
|
|
|
Strophe.addConnectionPlugin('jingle', {
|
|
connection: null,
|
|
sessions: {},
|
|
jid2session: {},
|
|
ice_config: {iceServers: []},
|
|
pc_constraints: {},
|
|
activecall: null,
|
|
media_constraints: {
|
|
mandatory: {
|
|
'OfferToReceiveAudio': true,
|
|
'OfferToReceiveVideo': true
|
|
}
|
|
// MozDontOfferDataChannel: true when this is firefox
|
|
},
|
|
init: function (conn) {
|
|
this.connection = conn;
|
|
if (this.connection.disco) {
|
|
// http://xmpp.org/extensions/xep-0167.html#support
|
|
// http://xmpp.org/extensions/xep-0176.html#support
|
|
this.connection.disco.addFeature('urn:xmpp:jingle:1');
|
|
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
|
|
this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
|
|
this.connection.disco.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1');
|
|
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
|
|
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
|
|
|
|
|
|
// this is dealt with by SDP O/A so we don't need to annouce this
|
|
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
|
|
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
|
|
if (config.useRtcpMux) {
|
|
this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
|
|
}
|
|
if (config.useBundle) {
|
|
this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
|
|
}
|
|
//this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
|
|
}
|
|
this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
|
|
},
|
|
onJingle: function (iq) {
|
|
var sid = $(iq).find('jingle').attr('sid');
|
|
var action = $(iq).find('jingle').attr('action');
|
|
var fromJid = iq.getAttribute('from');
|
|
// send ack first
|
|
var ack = $iq({type: 'result',
|
|
to: fromJid,
|
|
id: iq.getAttribute('id')
|
|
});
|
|
console.log('on jingle ' + action + ' from ' + fromJid, iq);
|
|
var sess = this.sessions[sid];
|
|
if ('session-initiate' != action) {
|
|
if (sess === null) {
|
|
ack.type = 'error';
|
|
ack.c('error', {type: 'cancel'})
|
|
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
|
|
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
|
|
this.connection.send(ack);
|
|
return true;
|
|
}
|
|
// compare from to sess.peerjid (bare jid comparison for later compat with message-mode)
|
|
// local jid is not checked
|
|
if (Strophe.getBareJidFromJid(fromJid) != Strophe.getBareJidFromJid(sess.peerjid)) {
|
|
console.warn('jid mismatch for session id', sid, fromJid, sess.peerjid);
|
|
ack.type = 'error';
|
|
ack.c('error', {type: 'cancel'})
|
|
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
|
|
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
|
|
this.connection.send(ack);
|
|
return true;
|
|
}
|
|
} else if (sess !== undefined) {
|
|
// existing session with same session id
|
|
// this might be out-of-order if the sess.peerjid is the same as from
|
|
ack.type = 'error';
|
|
ack.c('error', {type: 'cancel'})
|
|
.c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
|
|
console.warn('duplicate session id', sid);
|
|
this.connection.send(ack);
|
|
return true;
|
|
}
|
|
// FIXME: check for a defined action
|
|
this.connection.send(ack);
|
|
// see http://xmpp.org/extensions/xep-0166.html#concepts-session
|
|
switch (action) {
|
|
case 'session-initiate':
|
|
sess = new JingleSession(
|
|
$(iq).attr('to'), $(iq).find('jingle').attr('sid'),
|
|
this.connection, XMPP);
|
|
// configure session
|
|
|
|
sess.media_constraints = this.media_constraints;
|
|
sess.pc_constraints = this.pc_constraints;
|
|
sess.ice_config = this.ice_config;
|
|
|
|
sess.initiate(fromJid, false);
|
|
// FIXME: setRemoteDescription should only be done when this call is to be accepted
|
|
sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
|
|
|
|
this.sessions[sess.sid] = sess;
|
|
this.jid2session[sess.peerjid] = sess;
|
|
|
|
// the callback should either
|
|
// .sendAnswer and .accept
|
|
// or .sendTerminate -- not necessarily synchronus
|
|
CallIncomingJingle(sess.sid, this.connection);
|
|
break;
|
|
case 'session-accept':
|
|
sess.setRemoteDescription($(iq).find('>jingle'), 'answer');
|
|
sess.accept();
|
|
$(document).trigger('callaccepted.jingle', [sess.sid]);
|
|
break;
|
|
case 'session-terminate':
|
|
// If this is not the focus sending the terminate, we have
|
|
// nothing more to do here.
|
|
if (Object.keys(this.sessions).length < 1
|
|
|| !(this.sessions[Object.keys(this.sessions)[0]]
|
|
instanceof JingleSession))
|
|
{
|
|
break;
|
|
}
|
|
console.log('terminating...', sess.sid);
|
|
sess.terminate();
|
|
this.terminate(sess.sid);
|
|
if ($(iq).find('>jingle>reason').length) {
|
|
$(document).trigger('callterminated.jingle', [
|
|
sess.sid,
|
|
sess.peerjid,
|
|
$(iq).find('>jingle>reason>:first')[0].tagName,
|
|
$(iq).find('>jingle>reason>text').text()
|
|
]);
|
|
} else {
|
|
$(document).trigger('callterminated.jingle',
|
|
[sess.sid, sess.peerjid]);
|
|
}
|
|
break;
|
|
case 'transport-info':
|
|
sess.addIceCandidate($(iq).find('>jingle>content'));
|
|
break;
|
|
case 'session-info':
|
|
var affected;
|
|
if ($(iq).find('>jingle>ringing[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
|
$(document).trigger('ringing.jingle', [sess.sid]);
|
|
} else if ($(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
|
affected = $(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
|
|
$(document).trigger('mute.jingle', [sess.sid, affected]);
|
|
} else if ($(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
|
affected = $(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
|
|
$(document).trigger('unmute.jingle', [sess.sid, affected]);
|
|
}
|
|
break;
|
|
case 'addsource': // FIXME: proprietary, un-jingleish
|
|
case 'source-add': // FIXME: proprietary
|
|
sess.addSource($(iq).find('>jingle>content'), fromJid);
|
|
break;
|
|
case 'removesource': // FIXME: proprietary, un-jingleish
|
|
case 'source-remove': // FIXME: proprietary
|
|
sess.removeSource($(iq).find('>jingle>content'), fromJid);
|
|
break;
|
|
default:
|
|
console.warn('jingle action not implemented', action);
|
|
break;
|
|
}
|
|
return true;
|
|
},
|
|
initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
|
|
var sess = new JingleSession(myjid || this.connection.jid,
|
|
Math.random().toString(36).substr(2, 12), // random string
|
|
this.connection, XMPP);
|
|
// configure session
|
|
|
|
sess.media_constraints = this.media_constraints;
|
|
sess.pc_constraints = this.pc_constraints;
|
|
sess.ice_config = this.ice_config;
|
|
|
|
sess.initiate(peerjid, true);
|
|
this.sessions[sess.sid] = sess;
|
|
this.jid2session[sess.peerjid] = sess;
|
|
sess.sendOffer();
|
|
return sess;
|
|
},
|
|
terminate: function (sid, reason, text) { // terminate by sessionid (or all sessions)
|
|
if (sid === null || sid === undefined) {
|
|
for (sid in this.sessions) {
|
|
if (this.sessions[sid].state != 'ended') {
|
|
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
|
|
this.sessions[sid].terminate();
|
|
}
|
|
delete this.jid2session[this.sessions[sid].peerjid];
|
|
delete this.sessions[sid];
|
|
}
|
|
} else if (this.sessions.hasOwnProperty(sid)) {
|
|
if (this.sessions[sid].state != 'ended') {
|
|
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
|
|
this.sessions[sid].terminate();
|
|
}
|
|
delete this.jid2session[this.sessions[sid].peerjid];
|
|
delete this.sessions[sid];
|
|
}
|
|
},
|
|
// Used to terminate a session when an unavailable presence is received.
|
|
terminateByJid: function (jid) {
|
|
if (this.jid2session.hasOwnProperty(jid)) {
|
|
var sess = this.jid2session[jid];
|
|
if (sess) {
|
|
sess.terminate();
|
|
console.log('peer went away silently', jid);
|
|
delete this.sessions[sess.sid];
|
|
delete this.jid2session[jid];
|
|
$(document).trigger('callterminated.jingle',
|
|
[sess.sid, jid], 'gone');
|
|
}
|
|
}
|
|
},
|
|
terminateRemoteByJid: function (jid, reason) {
|
|
if (this.jid2session.hasOwnProperty(jid)) {
|
|
var sess = this.jid2session[jid];
|
|
if (sess) {
|
|
sess.sendTerminate(reason || (!sess.active()) ? 'kick' : null);
|
|
sess.terminate();
|
|
console.log('terminate peer with jid', sess.sid, jid);
|
|
delete this.sessions[sess.sid];
|
|
delete this.jid2session[jid];
|
|
$(document).trigger('callterminated.jingle',
|
|
[sess.sid, jid, 'kicked']);
|
|
}
|
|
}
|
|
},
|
|
getStunAndTurnCredentials: function () {
|
|
// get stun and turn configuration from server via xep-0215
|
|
// uses time-limited credentials as described in
|
|
// http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
|
//
|
|
// see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
|
|
// for a prosody module which implements this
|
|
//
|
|
// currently, this doesn't work with updateIce and therefore credentials with a long
|
|
// validity have to be fetched before creating the peerconnection
|
|
// TODO: implement refresh via updateIce as described in
|
|
// https://code.google.com/p/webrtc/issues/detail?id=1650
|
|
var self = this;
|
|
this.connection.sendIQ(
|
|
$iq({type: 'get', to: this.connection.domain})
|
|
.c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
|
|
function (res) {
|
|
var iceservers = [];
|
|
$(res).find('>services>service').each(function (idx, el) {
|
|
el = $(el);
|
|
var dict = {};
|
|
var type = el.attr('type');
|
|
switch (type) {
|
|
case 'stun':
|
|
dict.url = 'stun:' + el.attr('host');
|
|
if (el.attr('port')) {
|
|
dict.url += ':' + el.attr('port');
|
|
}
|
|
iceservers.push(dict);
|
|
break;
|
|
case 'turn':
|
|
case 'turns':
|
|
dict.url = type + ':';
|
|
if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
|
|
if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
|
|
dict.url += el.attr('username') + '@';
|
|
} else {
|
|
dict.username = el.attr('username'); // only works in M28
|
|
}
|
|
}
|
|
dict.url += el.attr('host');
|
|
if (el.attr('port') && el.attr('port') != '3478') {
|
|
dict.url += ':' + el.attr('port');
|
|
}
|
|
if (el.attr('transport') && el.attr('transport') != 'udp') {
|
|
dict.url += '?transport=' + el.attr('transport');
|
|
}
|
|
if (el.attr('password')) {
|
|
dict.credential = el.attr('password');
|
|
}
|
|
iceservers.push(dict);
|
|
break;
|
|
}
|
|
});
|
|
self.ice_config.iceServers = iceservers;
|
|
},
|
|
function (err) {
|
|
console.warn('getting turn credentials failed', err);
|
|
console.warn('is mod_turncredentials or similar installed?');
|
|
}
|
|
);
|
|
// implement push?
|
|
},
|
|
|
|
/**
|
|
* Populates the log data
|
|
*/
|
|
populateData: function () {
|
|
var data = {};
|
|
Object.keys(this.sessions).forEach(function (sid) {
|
|
var session = this.sessions[sid];
|
|
if (session.peerconnection && session.peerconnection.updateLog) {
|
|
// FIXME: should probably be a .dump call
|
|
data["jingle_" + session.sid] = {
|
|
updateLog: session.peerconnection.updateLog,
|
|
stats: session.peerconnection.stats,
|
|
url: window.location.href
|
|
};
|
|
}
|
|
});
|
|
return data;
|
|
}
|
|
});
|
|
};
|
|
|