2013-12-16 11:22:23 +00:00
|
|
|
var Base64=(function(){var keyStr="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var obj={encode:function(input){var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;do{chr1=input.charCodeAt(i++);chr2=input.charCodeAt(i++);chr3=input.charCodeAt(i++);enc1=chr1>>2;enc2=((chr1&3)<<4)|(chr2>>4);enc3=((chr2&15)<<2)|(chr3>>6);enc4=chr3&63;if(isNaN(chr2)){enc3=enc4=64}else{if(isNaN(chr3)){enc4=64}}output=output+keyStr.charAt(enc1)+keyStr.charAt(enc2)+keyStr.charAt(enc3)+keyStr.charAt(enc4)}while(i<input.length);return output},decode:function(input){var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{enc1=keyStr.indexOf(input.charAt(i++));enc2=keyStr.indexOf(input.charAt(i++));enc3=keyStr.indexOf(input.charAt(i++));enc4=keyStr.indexOf(input.charAt(i++));chr1=(enc1<<2)|(enc2>>4);chr2=((enc2&15)<<4)|(enc3>>2);chr3=((enc3&3)<<6)|enc4;output=output+String.fromCharCode(chr1);if(enc3!=64){output=output+String.fromCharCode(chr2)}if(enc4!=64){output=output+String.fromCharCode(chr3)}}while(i<input.length);return output}};return obj})();var hexcase=0;var b64pad="=";var chrsz=8;function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length*chrsz))}function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length*chrsz))}function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length*chrsz))}function hex_hmac_sha1(key,data){return binb2hex(core_hmac_sha1(key,data))}function b64_hmac_sha1(key,data){return binb2b64(core_hmac_sha1(key,data))}function str_hmac_sha1(key,data){return binb2str(core_hmac_sha1(key,data))}function sha1_vm_test(){return hex_sha1("abc")=="a9993e364706816aba3e25717850c26c9cd0d89d"}function core_sha1(x,len){x[len>>5]|=128<<(24-len%32);x[((len+64>>9)<<4)+15]=len;var w=new Array(80);var a=1732584193;var b=-271733879;var c=-1732584194;var d=271733878;var e=-1009589776;var i,j,t,olda,oldb,oldc,oldd,olde;for(i=0;i<x.length;i+=16){olda=a;oldb=b;oldc=c;oldd=d;olde=e;for(j=0;j<80;j++){if(j<16){w[j]=x[i+j]}else{w[j]=rol(w[j-3]^w[j-8]^w[j-14]^w[j-16],1)}t=safe_add(safe_add(rol(a,5),sha1_ft(j,b,c,d)),safe_add(safe_add(e,w[j]),sha1_kt(j)));e=d;d=c;c=rol(b,30);b=a;a=t}a=safe_add(a,olda);b=safe_add(b,oldb);c=safe_add(c,oldc);d=safe_add(d,oldd);e=safe_add(e,olde)}return[a,b,c,d,e]}function sha1_ft(t,b,c,d){if(t<20){return(b&c)|((~b)&d)}if(t<40){return b^c^d}if(t<60){return(b&c)|(b&d)|(c&d)}return b^c^d}function sha1_kt(t){return(t<20)?1518500249:(t<40)?1859775393:(t<60)?-1894007588:-899497514}function core_hmac_sha1(key,data){var bkey=str2binb(key);if(bkey.length>16){bkey=core_sha1(bkey,key.length*chrsz)}var ipad=new Array(16),opad=new Array(16);for(var i=0;i<16;i++){ipad[i]=bkey[i]^909522486;opad[i]=bkey[i]^1549556828}var hash=core_sha1(ipad.concat(str2binb(data)),512+data.length*chrsz);return core_sha1(opad.concat(hash),512+160)}function safe_add(x,y){var lsw=(x&65535)+(y&65535);var msw=(x>>16)+(y>>16)+(lsw>>16);return(msw<<16)|(lsw&65535)}function rol(num,cnt){return(num<<cnt)|(num>>>(32-cnt))}function str2binb(str){var bin=[];var mask=(1<<chrsz)-1;for(var i=0;i<str.length*chrsz;i+=chrsz){bin[i>>5]|=(str.charCodeAt(i/chrsz)&mask)<<(32-chrsz-i%32)}return bin}function binb2str(bin){var str="";var mask=(1<<chrsz)-1;for(var i=0;i<bin.length*32;i+=chrsz){str+=String.fromCharCode((bin[i>>5]>>>(32-chrsz-i%32))&mask)}return str}function binb2hex(binarray){var hex_tab=hexcase?"0123456789ABCDEF":"0123456789abcdef";var str="";for(var i=0;i<binarray.length*4;i++){str+=hex_tab.charAt((binarray[i>>2]>>((3-i%4)*8+4))&15)+hex_tab.charAt((binarray[i>>2]>>((3-i%4)*8))&15)}return str}function binb2b64(binarray){var tab="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var str="";var triplet,j;for(var i=0;i<binarray.length*4;i+=3){triplet=(((binarray[i>>2]>>8*(3-i%4))&255)<<16)|(((binarray[i+1>>2]>>8*(3-(i+1)%4))&255)<<8)|((binarray[i+2>>2]>>8*(3-(i+2)%4))&255);for(j=0;j<4;j++){if(i*8+j*6>binarray.length*32){str+=b64pad}else{str+=tab.charAt((triplet>>6*(3-j))&63)}}}return str}var MD5=(function(){var hexcase=0;var b64pad="";var chr
|
2013-12-22 18:39:41 +00:00
|
|
|
function TraceablePeerConnection(ice_config, constraints) {
|
|
|
|
var self = this;
|
2013-12-26 13:26:54 +00:00
|
|
|
var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
|
2013-12-22 18:39:41 +00:00
|
|
|
this.peerconnection = new RTCPeerconnection(ice_config, constraints);
|
|
|
|
this.updateLog = [];
|
|
|
|
|
|
|
|
// override as desired
|
|
|
|
this.trace = function(what, info) {
|
|
|
|
//console.warn('WTRACE', what, info);
|
|
|
|
self.updateLog.push({
|
|
|
|
time: new Date(),
|
|
|
|
type: what,
|
|
|
|
value: info
|
|
|
|
});
|
|
|
|
};
|
|
|
|
this.onicecandidate = null;
|
|
|
|
this.peerconnection.onicecandidate = function (event) {
|
2013-12-30 14:34:12 +00:00
|
|
|
self.trace('onicecandidate', event.candidate);
|
2013-12-22 18:39:41 +00:00
|
|
|
if (self.onicecandidate !== null) {
|
|
|
|
self.onicecandidate(event);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.onaddstream = null;
|
|
|
|
this.peerconnection.onaddstream = function (event) {
|
2013-12-30 14:34:12 +00:00
|
|
|
self.trace('onaddstream', event.stream);
|
2013-12-22 18:39:41 +00:00
|
|
|
if (self.onaddstream !== null) {
|
|
|
|
self.onaddstream(event);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.onremovestream = null;
|
|
|
|
this.peerconnection.onremovestream = function (event) {
|
2013-12-30 14:34:12 +00:00
|
|
|
self.trace('onremovestream', event.stream);
|
2013-12-22 18:39:41 +00:00
|
|
|
if (self.onremovestream !== null) {
|
|
|
|
self.onremovestream(event);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.onsignalingstatechange = null;
|
|
|
|
this.peerconnection.onsignalingstatechange = function (event) {
|
2013-12-30 14:34:12 +00:00
|
|
|
self.trace('onsignalingstatechange', event);
|
2013-12-22 18:39:41 +00:00
|
|
|
if (self.onsignalingstatechange !== null) {
|
|
|
|
self.onsignalingstatechange(event);
|
|
|
|
}
|
|
|
|
};
|
2013-12-30 14:34:12 +00:00
|
|
|
this.oniceconnectionstatechange = null;
|
|
|
|
this.peerconnection.oniceconnectionstatechange = function (event) {
|
|
|
|
self.trace('oniceconnectionstatechange', event);
|
|
|
|
if (self.oniceconnectionstatechange !== null) {
|
|
|
|
self.oniceconnectionstatechange(event);
|
|
|
|
}
|
|
|
|
}
|
2013-12-22 18:39:41 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
|
|
|
|
TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
|
|
|
|
TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() { return this.peerconnection.localDescription; });
|
|
|
|
TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() { return this.peerconnection.remoteDescription; });
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.addStream = function (stream) {
|
|
|
|
this.trace('addStream', stream);
|
|
|
|
this.peerconnection.addStream(stream);
|
|
|
|
};
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.removeStream = function (stream) {
|
|
|
|
this.trace('removeStream', stream);
|
|
|
|
this.peerconnection.removeStream(stream);
|
|
|
|
};
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
|
|
|
|
var self = this;
|
|
|
|
this.trace('setLocalDescription', description);
|
|
|
|
this.peerconnection.setLocalDescription(description,
|
|
|
|
function () {
|
|
|
|
self.trace('setLocalDescriptionOnSuccess');
|
|
|
|
successCallback();
|
|
|
|
},
|
|
|
|
function (err) {
|
|
|
|
self.trace('setLocalDescriptionOnFailure', err);
|
|
|
|
failureCallback(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
|
|
|
|
var self = this;
|
|
|
|
this.trace('setRemoteDescription', description);
|
|
|
|
this.peerconnection.setRemoteDescription(description,
|
|
|
|
function () {
|
|
|
|
self.trace('setRemoteDescriptionOnSuccess');
|
|
|
|
successCallback();
|
|
|
|
},
|
|
|
|
function (err) {
|
|
|
|
self.trace('setRemoteDescriptionOnFailure', err);
|
|
|
|
failureCallback(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.close = function () {
|
|
|
|
this.trace('stop');
|
|
|
|
this.peerconnection.close();
|
|
|
|
};
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
|
|
|
|
var self = this;
|
|
|
|
this.trace('createOffer', constraints);
|
|
|
|
this.peerconnection.createOffer(
|
|
|
|
function (sdp) {
|
|
|
|
self.trace('createOfferOnSuccess', sdp);
|
|
|
|
successCallback(sdp);
|
|
|
|
},
|
|
|
|
function(err) {
|
|
|
|
self.trace('createOfferOnFailure', err);
|
|
|
|
failureCallback(err);
|
|
|
|
},
|
|
|
|
constraints
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
|
|
|
|
var self = this;
|
|
|
|
this.trace('createAnswer', constraints);
|
|
|
|
this.peerconnection.createAnswer(
|
|
|
|
function (sdp) {
|
|
|
|
self.trace('createAnswerOnSuccess', sdp);
|
|
|
|
successCallback(sdp);
|
|
|
|
},
|
|
|
|
function(err) {
|
|
|
|
self.trace('createAnswerOnFailure', err);
|
|
|
|
failureCallback(err);
|
|
|
|
},
|
|
|
|
constraints
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
|
|
|
|
var self = this;
|
|
|
|
this.trace('addIceCandidate', candidate);
|
|
|
|
this.peerconnection.addIceCandidate(candidate);
|
|
|
|
/* maybe later
|
|
|
|
this.peerconnection.addIceCandidate(candidate,
|
|
|
|
function () {
|
|
|
|
self.trace('addIceCandidateOnSuccess');
|
|
|
|
successCallback();
|
|
|
|
},
|
|
|
|
function (err) {
|
|
|
|
self.trace('addIceCandidateOnFailure', err);
|
|
|
|
failureCallback(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
*/
|
|
|
|
};
|
|
|
|
|
|
|
|
TraceablePeerConnection.prototype.getStats = function(callback) {
|
|
|
|
this.peerconnection.getStats(callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-12-16 11:22:23 +00:00
|
|
|
// mozilla chrome compat layer -- very similar to adapter.js
|
|
|
|
function setupRTC() {
|
|
|
|
var RTC = null;
|
|
|
|
if (navigator.mozGetUserMedia) {
|
|
|
|
console.log('This appears to be Firefox');
|
|
|
|
var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
|
|
|
if (version >= 22) {
|
|
|
|
RTC = {
|
|
|
|
peerconnection: mozRTCPeerConnection,
|
|
|
|
browser: 'firefox',
|
|
|
|
getUserMedia: navigator.mozGetUserMedia.bind(navigator),
|
|
|
|
attachMediaStream: function (element, stream) {
|
|
|
|
element[0].mozSrcObject = stream;
|
|
|
|
element[0].play();
|
|
|
|
},
|
|
|
|
pc_constraints: {}
|
|
|
|
};
|
|
|
|
if (!MediaStream.prototype.getVideoTracks)
|
|
|
|
MediaStream.prototype.getVideoTracks = function () { return []; };
|
|
|
|
if (!MediaStream.prototype.getAudioTracks)
|
|
|
|
MediaStream.prototype.getAudioTracks = function () { return []; };
|
|
|
|
RTCSessionDescription = mozRTCSessionDescription;
|
|
|
|
RTCIceCandidate = mozRTCIceCandidate;
|
|
|
|
}
|
|
|
|
} else if (navigator.webkitGetUserMedia) {
|
|
|
|
console.log('This appears to be Chrome');
|
|
|
|
RTC = {
|
|
|
|
peerconnection: webkitRTCPeerConnection,
|
|
|
|
browser: 'chrome',
|
|
|
|
getUserMedia: navigator.webkitGetUserMedia.bind(navigator),
|
|
|
|
attachMediaStream: function (element, stream) {
|
|
|
|
element.attr('src', webkitURL.createObjectURL(stream));
|
|
|
|
},
|
2013-12-30 14:34:12 +00:00
|
|
|
// DTLS should now be enabled by default but..
|
|
|
|
pc_constraints: {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]}
|
2013-12-16 11:22:23 +00:00
|
|
|
};
|
|
|
|
if (navigator.userAgent.indexOf('Android') != -1) {
|
|
|
|
RTC.pc_constraints = {}; // disable DTLS on Android
|
|
|
|
}
|
|
|
|
if (!webkitMediaStream.prototype.getVideoTracks) {
|
|
|
|
webkitMediaStream.prototype.getVideoTracks = function () {
|
|
|
|
return this.videoTracks;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (!webkitMediaStream.prototype.getAudioTracks) {
|
|
|
|
webkitMediaStream.prototype.getAudioTracks = function () {
|
|
|
|
return this.audioTracks;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (RTC === null) {
|
|
|
|
try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { }
|
|
|
|
}
|
|
|
|
return RTC;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getUserMediaWithConstraints(um, resolution, bandwidth, fps) {
|
|
|
|
var constraints = {audio: false, video: false};
|
|
|
|
|
|
|
|
if (um.indexOf('video') >= 0) {
|
|
|
|
constraints.video = {mandatory: {}};// same behaviour as true
|
|
|
|
}
|
|
|
|
if (um.indexOf('audio') >= 0) {
|
|
|
|
constraints.audio = {};// same behaviour as true
|
|
|
|
}
|
|
|
|
if (um.indexOf('screen') >= 0) {
|
|
|
|
constraints.video = {
|
|
|
|
"mandatory": {
|
|
|
|
"chromeMediaSource": "screen"
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (resolution && !constraints.video) {
|
|
|
|
constraints.video = {mandatory: {}};// same behaviour as true
|
|
|
|
}
|
|
|
|
// see https://code.google.com/p/chromium/issues/detail?id=143631#c9 for list of supported resolutions
|
|
|
|
switch (resolution) {
|
|
|
|
// 16:9 first
|
|
|
|
case '1080':
|
|
|
|
case 'fullhd':
|
|
|
|
constraints.video.mandatory.minWidth = 1920;
|
|
|
|
constraints.video.mandatory.minHeight = 1080;
|
|
|
|
constraints.video.mandatory.minAspectRatio = 1.77;
|
|
|
|
break;
|
|
|
|
case '720':
|
|
|
|
case 'hd':
|
|
|
|
constraints.video.mandatory.minWidth = 1280;
|
|
|
|
constraints.video.mandatory.minHeight = 720;
|
|
|
|
constraints.video.mandatory.minAspectRatio = 1.77;
|
|
|
|
break;
|
|
|
|
case '360':
|
|
|
|
constraints.video.mandatory.minWidth = 640;
|
|
|
|
constraints.video.mandatory.minHeight = 360;
|
|
|
|
constraints.video.mandatory.minAspectRatio = 1.77;
|
|
|
|
break;
|
|
|
|
case '180':
|
|
|
|
constraints.video.mandatory.minWidth = 320;
|
|
|
|
constraints.video.mandatory.minHeight = 180;
|
|
|
|
constraints.video.mandatory.minAspectRatio = 1.77;
|
|
|
|
break;
|
|
|
|
// 4:3
|
|
|
|
case '960':
|
|
|
|
constraints.video.mandatory.minWidth = 960;
|
|
|
|
constraints.video.mandatory.minHeight = 720;
|
|
|
|
break;
|
|
|
|
case '640':
|
|
|
|
case 'vga':
|
|
|
|
constraints.video.mandatory.minWidth = 640;
|
|
|
|
constraints.video.mandatory.minHeight = 480;
|
|
|
|
break;
|
|
|
|
case '320':
|
|
|
|
constraints.video.mandatory.minWidth = 320;
|
|
|
|
constraints.video.mandatory.minHeight = 240;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
if (navigator.userAgent.indexOf('Android') != -1) {
|
|
|
|
constraints.video.mandatory.minWidth = 320;
|
|
|
|
constraints.video.mandatory.minHeight = 240;
|
|
|
|
constraints.video.mandatory.maxFrameRate = 15;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bandwidth) { // doesn't work currently, see webrtc issue 1846
|
|
|
|
if (!constraints.video) constraints.video = {mandatory: {}};//same behaviour as true
|
|
|
|
constraints.video.optional = [{bandwidth: bandwidth}];
|
|
|
|
}
|
|
|
|
if (fps) { // for some cameras it might be necessary to request 30fps
|
|
|
|
// so they choose 30fps mjpg over 10fps yuy2
|
|
|
|
if (!constraints.video) constraints.video = {mandatory: {}};// same behaviour as tru;
|
|
|
|
constraints.video.mandatory.minFrameRate = fps;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
RTC.getUserMedia(constraints,
|
|
|
|
function (stream) {
|
|
|
|
console.log('onUserMediaSuccess');
|
|
|
|
$(document).trigger('mediaready.jingle', [stream]);
|
|
|
|
},
|
|
|
|
function (error) {
|
|
|
|
console.warn('Failed to get access to local media. Error ', error);
|
|
|
|
$(document).trigger('mediafailure.jingle');
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error('GUM failed: ', e);
|
|
|
|
$(document).trigger('mediafailure.jingle');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* jshint -W117 */
|
|
|
|
Strophe.addConnectionPlugin('jingle', {
|
|
|
|
connection: null,
|
|
|
|
sessions: {},
|
|
|
|
jid2session: {},
|
|
|
|
ice_config: {iceServers: []},
|
|
|
|
pc_constraints: {},
|
|
|
|
media_constraints: {
|
|
|
|
mandatory: {
|
|
|
|
'OfferToReceiveAudio': true,
|
|
|
|
'OfferToReceiveVideo': true
|
|
|
|
}
|
|
|
|
// MozDontOfferDataChannel: true when this is firefox
|
|
|
|
},
|
|
|
|
localStream: null,
|
|
|
|
|
|
|
|
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: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
|
|
|
|
this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
|
|
|
|
//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');
|
|
|
|
// send ack first
|
|
|
|
var ack = $iq({type: 'result',
|
|
|
|
to: iq.getAttribute('from'),
|
|
|
|
id: iq.getAttribute('id')
|
|
|
|
});
|
|
|
|
console.log('on jingle ' + action);
|
|
|
|
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(iq.getAttribute('from')) != Strophe.getBareJidFromJid(sess.peerjid)) {
|
|
|
|
console.warn('jid mismatch for session id', sid, iq.getAttribute('from'), 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);
|
|
|
|
// configure session
|
|
|
|
if (this.localStream) {
|
|
|
|
sess.localStreams.push(this.localStream);
|
|
|
|
}
|
|
|
|
sess.media_constraints = this.media_constraints;
|
|
|
|
sess.pc_constraints = this.pc_constraints;
|
|
|
|
sess.ice_config = this.ice_config;
|
|
|
|
|
|
|
|
sess.initiate($(iq).attr('from'), 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
|
|
|
|
$(document).trigger('callincoming.jingle', [sess.sid]);
|
|
|
|
break;
|
|
|
|
case 'session-accept':
|
|
|
|
sess.setRemoteDescription($(iq).find('>jingle'), 'answer');
|
|
|
|
sess.accept();
|
|
|
|
break;
|
|
|
|
case 'session-terminate':
|
|
|
|
console.log('terminating...');
|
|
|
|
sess.terminate();
|
|
|
|
this.terminate(sess.sid);
|
|
|
|
if ($(iq).find('>jingle>reason').length) {
|
|
|
|
$(document).trigger('callterminated.jingle', [
|
|
|
|
sess.sid,
|
|
|
|
$(iq).find('>jingle>reason>:first')[0].tagName,
|
|
|
|
$(iq).find('>jingle>reason>text').text()
|
|
|
|
]);
|
|
|
|
} else {
|
|
|
|
$(document).trigger('callterminated.jingle', [sess.sid]);
|
|
|
|
}
|
|
|
|
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
|
|
|
|
sess.addSource($(iq).find('>jingle>content'));
|
|
|
|
break;
|
|
|
|
case 'removesource': // FIXME: proprietary
|
|
|
|
sess.removeSource($(iq).find('>jingle>content'));
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
console.warn('jingle action not implemented', action);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
|
2013-12-30 14:34:12 +00:00
|
|
|
var sess = new JingleSession(myjid || this.connection.jid,
|
2013-12-16 11:22:23 +00:00
|
|
|
Math.random().toString(36).substr(2, 12), // random string
|
|
|
|
this.connection);
|
|
|
|
// configure session
|
|
|
|
if (this.localStream) {
|
|
|
|
sess.localStreams.push(this.localStream);
|
|
|
|
}
|
|
|
|
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];
|
|
|
|
}
|
|
|
|
},
|
|
|
|
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, 'gone']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
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
|
|
|
|
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 = {};
|
|
|
|
switch (el.attr('type')) {
|
|
|
|
case 'stun':
|
|
|
|
dict.url = 'stun:' + el.attr('host');
|
|
|
|
if (el.attr('port')) {
|
|
|
|
dict.url += ':' + el.attr('port');
|
|
|
|
}
|
|
|
|
iceservers.push(dict);
|
|
|
|
break;
|
|
|
|
case 'turn':
|
|
|
|
dict.url = 'turn:';
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.ice_config.iceServers = iceservers;
|
|
|
|
},
|
|
|
|
function (err) {
|
|
|
|
console.warn('getting turn credentials failed', err);
|
|
|
|
console.warn('is mod_turncredentials or similar installed?');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
// implement push?
|
|
|
|
}
|
|
|
|
});
|
|
|
|
/* jshint -W117 */
|
|
|
|
// SDP STUFF
|
|
|
|
function SDP(sdp) {
|
|
|
|
this.media = sdp.split('\r\nm=');
|
|
|
|
for (var i = 1; i < this.media.length; i++) {
|
|
|
|
this.media[i] = 'm=' + this.media[i];
|
|
|
|
if (i != this.media.length - 1) {
|
|
|
|
this.media[i] += '\r\n';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.session = this.media.shift() + '\r\n';
|
|
|
|
this.raw = this.session + this.media.join('');
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove iSAC and CN from SDP
|
|
|
|
SDP.prototype.mangle = function () {
|
|
|
|
var i, j, mline, lines, rtpmap, newdesc;
|
|
|
|
for (i = 0; i < this.media.length; i++) {
|
|
|
|
lines = this.media[i].split('\r\n');
|
|
|
|
lines.pop(); // remove empty last element
|
|
|
|
mline = SDPUtil.parse_mline(lines.shift());
|
|
|
|
if (mline.media != 'audio')
|
|
|
|
continue;
|
|
|
|
newdesc = '';
|
|
|
|
mline.fmt.length = 0;
|
|
|
|
for (j = 0; j < lines.length; j++) {
|
|
|
|
if (lines[j].substr(0, 9) == 'a=rtpmap:') {
|
|
|
|
rtpmap = SDPUtil.parse_rtpmap(lines[j]);
|
|
|
|
if (rtpmap.name == 'CN' || rtpmap.name == 'ISAC')
|
|
|
|
continue;
|
|
|
|
mline.fmt.push(rtpmap.id);
|
|
|
|
newdesc += lines[j] + '\r\n';
|
|
|
|
} else {
|
|
|
|
newdesc += lines[j] + '\r\n';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.media[i] = SDPUtil.build_mline(mline) + '\r\n';
|
|
|
|
this.media[i] += newdesc;
|
|
|
|
}
|
|
|
|
this.raw = this.session + this.media.join('');
|
|
|
|
};
|
|
|
|
|
|
|
|
// remove lines matching prefix from session section
|
|
|
|
SDP.prototype.removeSessionLines = function(prefix) {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
var lines = SDPUtil.find_lines(this.session, prefix);
|
|
|
|
lines.forEach(function(line) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.session = self.session.replace(line + '\r\n', '');
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
this.raw = this.session + this.media.join('');
|
|
|
|
return lines;
|
|
|
|
}
|
|
|
|
// remove lines matching prefix from a media section specified by mediaindex
|
|
|
|
// TODO: non-numeric mediaindex could match mid
|
|
|
|
SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
|
|
|
|
lines.forEach(function(line) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
this.raw = this.session + this.media.join('');
|
|
|
|
return lines;
|
|
|
|
}
|
|
|
|
|
|
|
|
// add content's to a jingle element
|
|
|
|
SDP.prototype.toJingle = function (elem, thecreator) {
|
|
|
|
var i, j, k, mline, ssrc, rtpmap, tmp, line, lines;
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
// new bundle plan
|
|
|
|
if (SDPUtil.find_line(this.session, 'a=group:')) {
|
|
|
|
lines = SDPUtil.find_lines(this.session, 'a=group:');
|
|
|
|
for (i = 0; i < lines.length; i++) {
|
|
|
|
tmp = lines[i].split(' ');
|
|
|
|
var semantics = tmp.shift().substr(8);
|
|
|
|
// new plan
|
|
|
|
elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', type: semantics, semantics:semantics});
|
|
|
|
for (j = 0; j < tmp.length; j++) {
|
|
|
|
elem.c('content', {name: tmp[j]}).up();
|
|
|
|
}
|
|
|
|
elem.up();
|
|
|
|
|
|
|
|
// temporary plan, to be removed
|
|
|
|
elem.c('group', {xmlns: 'urn:ietf:rfc:5888', type: semantics});
|
|
|
|
for (j = 0; j < tmp.length; j++) {
|
|
|
|
elem.c('content', {name: tmp[j]}).up();
|
|
|
|
}
|
|
|
|
elem.up();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// old bundle plan, to be removed
|
|
|
|
var bundle = [];
|
|
|
|
if (SDPUtil.find_line(this.session, 'a=group:BUNDLE')) {
|
|
|
|
bundle = SDPUtil.find_line(this.session, 'a=group:BUNDLE ').split(' ');
|
|
|
|
bundle.shift();
|
|
|
|
}
|
|
|
|
for (i = 0; i < this.media.length; i++) {
|
|
|
|
mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
|
|
|
|
if (!(mline.media == 'audio' || mline.media == 'video')) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
|
|
|
|
ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first
|
|
|
|
} else {
|
|
|
|
ssrc = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
elem.c('content', {creator: thecreator, name: mline.media});
|
|
|
|
if (SDPUtil.find_line(this.media[i], 'a=mid:')) {
|
|
|
|
// prefer identifier from a=mid if present
|
|
|
|
var mid = SDPUtil.parse_mid(SDPUtil.find_line(this.media[i], 'a=mid:'));
|
|
|
|
elem.attrs({ name: mid });
|
|
|
|
|
|
|
|
// old BUNDLE plan, to be removed
|
|
|
|
if (bundle.indexOf(mid) != -1) {
|
|
|
|
elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
|
|
|
|
bundle.splice(bundle.indexOf(mid), 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length) {
|
|
|
|
elem.c('description',
|
|
|
|
{xmlns: 'urn:xmpp:jingle:apps:rtp:1',
|
|
|
|
media: mline.media });
|
|
|
|
if (ssrc) {
|
|
|
|
elem.attrs({ssrc: ssrc});
|
|
|
|
}
|
|
|
|
for (j = 0; j < mline.fmt.length; j++) {
|
|
|
|
rtpmap = SDPUtil.find_line(this.media[i], 'a=rtpmap:' + mline.fmt[j]);
|
|
|
|
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
|
|
|
|
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
|
|
|
|
if (SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j])) {
|
|
|
|
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j]));
|
|
|
|
for (k = 0; k < tmp.length; k++) {
|
|
|
|
elem.c('parameter', tmp[k]).up();
|
|
|
|
}
|
|
|
|
}
|
2013-12-29 17:11:33 +00:00
|
|
|
this.RtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
|
2013-12-16 11:22:23 +00:00
|
|
|
|
|
|
|
elem.up();
|
|
|
|
}
|
|
|
|
if (SDPUtil.find_line(this.media[i], 'a=crypto:', this.session)) {
|
|
|
|
elem.c('encryption', {required: 1});
|
|
|
|
var crypto = SDPUtil.find_lines(this.media[i], 'a=crypto:', this.session);
|
|
|
|
crypto.forEach(function(line) {
|
|
|
|
elem.c('crypto', SDPUtil.parse_crypto(line)).up();
|
|
|
|
});
|
|
|
|
elem.up(); // end of encryption
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ssrc) {
|
|
|
|
// new style mapping
|
|
|
|
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
|
|
// FIXME: group by ssrc and support multiple different ssrcs
|
|
|
|
var ssrclines = SDPUtil.find_lines(this.media[i], 'a=ssrc:');
|
|
|
|
ssrclines.forEach(function(line) {
|
|
|
|
idx = line.indexOf(' ');
|
|
|
|
var linessrc = line.substr(0, idx).substr(7);
|
|
|
|
if (linessrc != ssrc) {
|
|
|
|
elem.up();
|
|
|
|
ssrc = linessrc;
|
|
|
|
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
|
|
}
|
|
|
|
var kv = line.substr(idx + 1);
|
|
|
|
elem.c('parameter');
|
|
|
|
if (kv.indexOf(':') == -1) {
|
|
|
|
elem.attrs({ name: kv });
|
|
|
|
} else {
|
|
|
|
elem.attrs({ name: kv.split(':', 2)[0] });
|
|
|
|
elem.attrs({ value: kv.split(':', 2)[1] });
|
|
|
|
}
|
|
|
|
elem.up();
|
|
|
|
});
|
|
|
|
elem.up();
|
|
|
|
|
|
|
|
// old proprietary mapping, to be removed at some point
|
|
|
|
tmp = SDPUtil.parse_ssrc(this.media[i]);
|
|
|
|
tmp.xmlns = 'http://estos.de/ns/ssrc';
|
|
|
|
tmp.ssrc = ssrc;
|
|
|
|
elem.c('ssrc', tmp).up(); // ssrc is part of description
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
|
|
|
|
elem.c('rtcp-mux').up();
|
|
|
|
}
|
|
|
|
|
|
|
|
// XEP-0293 -- map a=rtcp-fb:*
|
2013-12-29 17:11:33 +00:00
|
|
|
this.RtcpFbToJingle(i, elem, '*');
|
2013-12-16 11:22:23 +00:00
|
|
|
|
|
|
|
// XEP-0294
|
|
|
|
if (SDPUtil.find_line(this.media[i], 'a=extmap:')) {
|
|
|
|
lines = SDPUtil.find_lines(this.media[i], 'a=extmap:');
|
|
|
|
for (j = 0; j < lines.length; j++) {
|
|
|
|
tmp = SDPUtil.parse_extmap(lines[j]);
|
|
|
|
elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
|
|
|
|
uri: tmp.uri,
|
|
|
|
id: tmp.value });
|
|
|
|
if (tmp.hasOwnProperty('direction')) {
|
|
|
|
switch (tmp.direction) {
|
|
|
|
case 'sendonly':
|
|
|
|
elem.attrs({senders: 'responder'});
|
|
|
|
break;
|
|
|
|
case 'recvonly':
|
|
|
|
elem.attrs({senders: 'initiator'});
|
|
|
|
break;
|
|
|
|
case 'sendrecv':
|
|
|
|
elem.attrs({senders: 'both'});
|
|
|
|
break;
|
|
|
|
case 'inactive':
|
|
|
|
elem.attrs({senders: 'none'});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// TODO: handle params
|
|
|
|
elem.up();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elem.up(); // end of description
|
|
|
|
}
|
|
|
|
|
2013-12-29 17:11:33 +00:00
|
|
|
// map ice-ufrag/pwd, dtls fingerprint, candidates
|
|
|
|
this.TransportToJingle(i, elem);
|
2013-12-16 11:22:23 +00:00
|
|
|
|
|
|
|
if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) {
|
|
|
|
elem.attrs({senders: 'both'});
|
|
|
|
} else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) {
|
|
|
|
elem.attrs({senders: 'initiator'});
|
|
|
|
} else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) {
|
|
|
|
elem.attrs({senders: 'responder'});
|
|
|
|
} else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) {
|
|
|
|
elem.attrs({senders: 'none'});
|
|
|
|
}
|
|
|
|
if (mline.port == '0') {
|
|
|
|
// estos hack to reject an m-line
|
|
|
|
elem.attrs({senders: 'rejected'});
|
|
|
|
}
|
|
|
|
elem.up(); // end of content
|
|
|
|
}
|
|
|
|
elem.up();
|
|
|
|
return elem;
|
|
|
|
};
|
|
|
|
|
2013-12-29 17:11:33 +00:00
|
|
|
SDP.prototype.TransportToJingle = function (mediaindex, elem) {
|
|
|
|
var i = mediaindex;
|
|
|
|
var tmp;
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-29 17:11:33 +00:00
|
|
|
elem.c('transport');
|
|
|
|
|
|
|
|
// XEP-0320
|
|
|
|
var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
|
|
|
|
fingerprints.forEach(function(line) {
|
|
|
|
tmp = SDPUtil.parse_fingerprint(line);
|
|
|
|
tmp.xmlns = 'urn:xmpp:tmp:jingle:apps:dtls:0';
|
|
|
|
// tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0'; -- FIXME: update receivers first
|
|
|
|
elem.c('fingerprint').t(tmp.fingerprint);
|
|
|
|
delete tmp.fingerprint;
|
2014-01-03 16:41:50 +00:00
|
|
|
line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
|
2013-12-29 17:11:33 +00:00
|
|
|
if (line) {
|
|
|
|
tmp.setup = line.substr(8);
|
|
|
|
}
|
|
|
|
elem.attrs(tmp);
|
|
|
|
elem.up(); // end of fingerprint
|
|
|
|
});
|
|
|
|
tmp = SDPUtil.iceparams(this.media[mediaindex], this.session);
|
|
|
|
if (tmp) {
|
|
|
|
tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
|
|
|
|
elem.attrs(tmp);
|
|
|
|
// XEP-0176
|
|
|
|
if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines
|
|
|
|
var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
|
|
|
|
lines.forEach(function (line) {
|
|
|
|
elem.c('candidate', SDPUtil.candidateToJingle(line)).up();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elem.up(); // end of transport
|
|
|
|
}
|
|
|
|
|
|
|
|
SDP.prototype.RtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
|
|
|
|
var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
|
|
|
|
lines.forEach(function (line) {
|
|
|
|
var tmp = SDPUtil.parse_rtcpfb(line);
|
2013-12-16 11:22:23 +00:00
|
|
|
if (tmp.type == 'trr-int') {
|
|
|
|
elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]});
|
|
|
|
elem.up();
|
|
|
|
} else {
|
|
|
|
elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type});
|
|
|
|
if (tmp.params.length > 0) {
|
|
|
|
elem.attrs({'subtype': tmp.params[0]});
|
|
|
|
}
|
|
|
|
elem.up();
|
|
|
|
}
|
2013-12-29 17:11:33 +00:00
|
|
|
});
|
2013-12-16 11:22:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
SDP.prototype.RtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293
|
|
|
|
var media = '';
|
|
|
|
var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
|
|
|
|
if (tmp.length) {
|
|
|
|
media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' ';
|
|
|
|
if (tmp.attr('value')) {
|
|
|
|
media += tmp.attr('value');
|
|
|
|
} else {
|
|
|
|
media += '0';
|
|
|
|
}
|
|
|
|
media += '\r\n';
|
|
|
|
}
|
|
|
|
tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
|
|
|
|
tmp.each(function () {
|
|
|
|
media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type');
|
|
|
|
if ($(this).attr('subtype')) {
|
|
|
|
media += ' ' + $(this).attr('subtype');
|
|
|
|
}
|
|
|
|
media += '\r\n';
|
|
|
|
});
|
|
|
|
return media;
|
|
|
|
};
|
|
|
|
|
|
|
|
// construct an SDP from a jingle stanza
|
|
|
|
SDP.prototype.fromJingle = function (jingle) {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
this.raw = 'v=0\r\n' +
|
|
|
|
'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
|
|
|
|
's=-\r\n' +
|
|
|
|
't=0 0\r\n';
|
|
|
|
// http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8
|
|
|
|
if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) {
|
|
|
|
$(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) {
|
|
|
|
var contents = $(group).find('>content').map(function (idx, content) {
|
2013-12-26 13:26:54 +00:00
|
|
|
return content.getAttribute('name');
|
2013-12-16 11:22:23 +00:00
|
|
|
}).get();
|
|
|
|
if (contents.length > 0) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
|
2013-12-16 11:22:23 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
} else if ($(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').length) {
|
|
|
|
// temporary namespace, not to be used. to be removed soon.
|
|
|
|
$(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').each(function (idx, group) {
|
|
|
|
var contents = $(group).find('>content').map(function (idx, content) {
|
2013-12-26 13:26:54 +00:00
|
|
|
return content.getAttribute('name');
|
2013-12-16 11:22:23 +00:00
|
|
|
}).get();
|
2013-12-26 13:26:54 +00:00
|
|
|
if (group.getAttribute('type') !== null && contents.length > 0) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.raw += 'a=group:' + group.getAttribute('type') + ' ' + contents.join(' ') + '\r\n';
|
2013-12-16 11:22:23 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// for backward compability, to be removed soon
|
|
|
|
// assume all contents are in the same bundle group, can be improved upon later
|
|
|
|
var bundle = $(jingle).find('>content').filter(function (idx, content) {
|
|
|
|
//elem.c('bundle', {xmlns:'http://estos.de/ns/bundle'});
|
|
|
|
return $(content).find('>bundle').length > 0;
|
|
|
|
}).map(function (idx, content) {
|
2013-12-26 13:26:54 +00:00
|
|
|
return content.getAttribute('name');
|
2013-12-16 11:22:23 +00:00
|
|
|
}).get();
|
|
|
|
if (bundle.length) {
|
|
|
|
this.raw += 'a=group:BUNDLE ' + bundle.join(' ') + '\r\n';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.session = this.raw;
|
|
|
|
jingle.find('>content').each(function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
var m = self.jingle2media($(this));
|
|
|
|
self.media.push(m);
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// reconstruct msid-semantic -- apparently not necessary
|
|
|
|
/*
|
|
|
|
var msid = SDPUtil.parse_ssrc(this.raw);
|
|
|
|
if (msid.hasOwnProperty('mslabel')) {
|
|
|
|
this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
this.raw = this.session + this.media.join('');
|
|
|
|
};
|
|
|
|
|
|
|
|
// translate a jingle content element into an an SDP media part
|
|
|
|
SDP.prototype.jingle2media = function (content) {
|
|
|
|
var media = '',
|
|
|
|
desc = content.find('description'),
|
|
|
|
ssrc = desc.attr('ssrc'),
|
|
|
|
self = this,
|
|
|
|
tmp;
|
|
|
|
|
|
|
|
tmp = { media: desc.attr('media') };
|
|
|
|
tmp.port = '1';
|
|
|
|
if (content.attr('senders') == 'rejected') {
|
|
|
|
// estos hack to reject an m-line.
|
|
|
|
tmp.port = '0';
|
|
|
|
}
|
|
|
|
if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
|
|
|
|
tmp.proto = 'RTP/SAVPF';
|
|
|
|
} else {
|
|
|
|
tmp.proto = 'RTP/AVPF';
|
|
|
|
}
|
2013-12-26 13:26:54 +00:00
|
|
|
tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get();
|
2013-12-16 11:22:23 +00:00
|
|
|
media += SDPUtil.build_mline(tmp) + '\r\n';
|
|
|
|
media += 'c=IN IP4 0.0.0.0\r\n';
|
|
|
|
media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
|
|
|
|
tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
|
|
|
|
if (tmp.length) {
|
|
|
|
if (tmp.attr('ufrag')) {
|
|
|
|
media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n';
|
|
|
|
}
|
|
|
|
if (tmp.attr('pwd')) {
|
|
|
|
media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n';
|
|
|
|
}
|
|
|
|
tmp.find('>fingerprint').each(function () {
|
|
|
|
// FIXME: check namespace at some point
|
2013-12-26 13:26:54 +00:00
|
|
|
media += 'a=fingerprint:' + this.getAttribute('hash');
|
2013-12-16 11:22:23 +00:00
|
|
|
media += ' ' + $(this).text();
|
|
|
|
media += '\r\n';
|
2013-12-26 13:26:54 +00:00
|
|
|
if (this.getAttribute('setup')) {
|
|
|
|
media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
|
2013-12-16 11:22:23 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
switch (content.attr('senders')) {
|
|
|
|
case 'initiator':
|
|
|
|
media += 'a=sendonly\r\n';
|
|
|
|
break;
|
|
|
|
case 'responder':
|
|
|
|
media += 'a=recvonly\r\n';
|
|
|
|
break;
|
|
|
|
case 'none':
|
|
|
|
media += 'a=inactive\r\n';
|
|
|
|
break;
|
|
|
|
case 'both':
|
|
|
|
media += 'a=sendrecv\r\n';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
media += 'a=mid:' + content.attr('name') + '\r\n';
|
|
|
|
|
|
|
|
// <description><rtcp-mux/></description>
|
|
|
|
// see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
|
|
|
|
// and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
|
|
|
|
if (desc.find('rtcp-mux').length) {
|
|
|
|
media += 'a=rtcp-mux\r\n';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (desc.find('encryption').length) {
|
|
|
|
desc.find('encryption>crypto').each(function () {
|
2013-12-26 13:26:54 +00:00
|
|
|
media += 'a=crypto:' + this.getAttribute('tag');
|
|
|
|
media += ' ' + this.getAttribute('crypto-suite');
|
|
|
|
media += ' ' + this.getAttribute('key-params');
|
|
|
|
if (this.getAttribute('session-params')) {
|
|
|
|
media += ' ' + this.getAttribute('session-params');
|
2013-12-16 11:22:23 +00:00
|
|
|
}
|
|
|
|
media += '\r\n';
|
|
|
|
});
|
|
|
|
}
|
|
|
|
desc.find('payload-type').each(function () {
|
|
|
|
media += SDPUtil.build_rtpmap(this) + '\r\n';
|
|
|
|
if ($(this).find('>parameter').length) {
|
2013-12-26 13:26:54 +00:00
|
|
|
media += 'a=fmtp:' + this.getAttribute('id') + ' ';
|
|
|
|
media += $(this).find('parameter').map(function () { return (this.getAttribute('name') ? (this.getAttribute('name') + '=') : '') + this.getAttribute('value'); }).get().join(';');
|
2013-12-16 11:22:23 +00:00
|
|
|
media += '\r\n';
|
|
|
|
}
|
|
|
|
// xep-0293
|
2013-12-26 13:26:54 +00:00
|
|
|
media += self.RtcpFbFromJingle($(this), this.getAttribute('id'));
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// xep-0293
|
|
|
|
media += self.RtcpFbFromJingle(desc, '*');
|
|
|
|
|
|
|
|
// xep-0294
|
|
|
|
tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]');
|
|
|
|
tmp.each(function () {
|
2013-12-26 13:26:54 +00:00
|
|
|
media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () {
|
|
|
|
media += SDPUtil.candidateFromJingle(this);
|
|
|
|
});
|
|
|
|
|
|
|
|
tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
|
|
|
tmp.each(function () {
|
2013-12-26 13:26:54 +00:00
|
|
|
var ssrc = this.getAttribute('ssrc');
|
2013-12-16 11:22:23 +00:00
|
|
|
$(this).find('>parameter').each(function () {
|
2013-12-26 13:26:54 +00:00
|
|
|
media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
|
|
|
|
if (this.getAttribute('value') && this.getAttribute('value').length)
|
|
|
|
media += ':' + this.getAttribute('value');
|
2013-12-16 11:22:23 +00:00
|
|
|
media += '\r\n';
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
if (tmp.length === 0) {
|
|
|
|
// fallback to proprietary mapping of a=ssrc lines
|
|
|
|
tmp = content.find('description>ssrc[xmlns="http://estos.de/ns/ssrc"]');
|
|
|
|
if (tmp.length) {
|
|
|
|
media += 'a=ssrc:' + ssrc + ' cname:' + tmp.attr('cname') + '\r\n';
|
|
|
|
media += 'a=ssrc:' + ssrc + ' msid:' + tmp.attr('msid') + '\r\n';
|
|
|
|
media += 'a=ssrc:' + ssrc + ' mslabel:' + tmp.attr('mslabel') + '\r\n';
|
|
|
|
media += 'a=ssrc:' + ssrc + ' label:' + tmp.attr('label') + '\r\n';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return media;
|
|
|
|
};
|
|
|
|
|
|
|
|
SDPUtil = {
|
|
|
|
iceparams: function (mediadesc, sessiondesc) {
|
|
|
|
var data = null;
|
|
|
|
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
|
|
|
|
SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
|
|
|
|
data = {
|
|
|
|
ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
|
|
|
|
pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
parse_iceufrag: function (line) {
|
|
|
|
return line.substring(12);
|
|
|
|
},
|
|
|
|
build_iceufrag: function (frag) {
|
|
|
|
return 'a=ice-ufrag:' + frag;
|
|
|
|
},
|
|
|
|
parse_icepwd: function (line) {
|
|
|
|
return line.substring(10);
|
|
|
|
},
|
|
|
|
build_icepwd: function (pwd) {
|
|
|
|
return 'a=ice-pwd:' + pwd;
|
|
|
|
},
|
|
|
|
parse_mid: function (line) {
|
|
|
|
return line.substring(6);
|
|
|
|
},
|
|
|
|
parse_mline: function (line) {
|
|
|
|
var parts = line.substring(2).split(' '),
|
|
|
|
data = {};
|
|
|
|
data.media = parts.shift();
|
|
|
|
data.port = parts.shift();
|
|
|
|
data.proto = parts.shift();
|
|
|
|
if (parts[parts.length - 1] === '') { // trailing whitespace
|
|
|
|
parts.pop();
|
|
|
|
}
|
|
|
|
data.fmt = parts;
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
build_mline: function (mline) {
|
|
|
|
return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
|
|
|
|
},
|
|
|
|
parse_rtpmap: function (line) {
|
|
|
|
var parts = line.substring(9).split(' '),
|
|
|
|
data = {};
|
|
|
|
data.id = parts.shift();
|
|
|
|
parts = parts[0].split('/');
|
|
|
|
data.name = parts.shift();
|
|
|
|
data.clockrate = parts.shift();
|
|
|
|
data.channels = parts.length ? parts.shift() : '1';
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
build_rtpmap: function (el) {
|
|
|
|
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
|
|
|
|
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
|
|
|
|
line += '/' + el.getAttribute('channels');
|
|
|
|
}
|
|
|
|
return line;
|
|
|
|
},
|
|
|
|
parse_crypto: function (line) {
|
|
|
|
var parts = line.substring(9).split(' '),
|
|
|
|
data = {};
|
|
|
|
data.tag = parts.shift();
|
|
|
|
data['crypto-suite'] = parts.shift();
|
|
|
|
data['key-params'] = parts.shift();
|
|
|
|
if (parts.length) {
|
|
|
|
data['session-params'] = parts.join(' ');
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
parse_fingerprint: function (line) { // RFC 4572
|
|
|
|
var parts = line.substring(14).split(' '),
|
|
|
|
data = {};
|
|
|
|
data.hash = parts.shift();
|
|
|
|
data.fingerprint = parts.shift();
|
|
|
|
// TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
parse_fmtp: function (line) {
|
|
|
|
var parts = line.split(' '),
|
|
|
|
i, key, value,
|
|
|
|
data = [];
|
|
|
|
parts.shift();
|
|
|
|
parts = parts.join(' ').split(';');
|
|
|
|
for (i = 0; i < parts.length; i++) {
|
|
|
|
key = parts[i].split('=')[0];
|
|
|
|
while (key.length && key[0] == ' ') {
|
|
|
|
key = key.substring(1);
|
|
|
|
}
|
|
|
|
value = parts[i].split('=')[1];
|
|
|
|
if (key && value) {
|
|
|
|
data.push({name: key, value: value});
|
|
|
|
} else if (key) {
|
|
|
|
// rfc 4733 (DTMF) style stuff
|
|
|
|
data.push({name: '', value: key});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
parse_icecandidate: function (line) {
|
|
|
|
var candidate = {},
|
|
|
|
elems = line.split(' ');
|
|
|
|
candidate.foundation = elems[0].substring(12);
|
|
|
|
candidate.component = elems[1];
|
|
|
|
candidate.protocol = elems[2].toLowerCase();
|
|
|
|
candidate.priority = elems[3];
|
|
|
|
candidate.ip = elems[4];
|
|
|
|
candidate.port = elems[5];
|
|
|
|
// elems[6] => "typ"
|
|
|
|
candidate.type = elems[7];
|
|
|
|
candidate.generation = 0; // default value, may be overwritten below
|
|
|
|
for (var i = 8; i < elems.length; i += 2) {
|
|
|
|
switch (elems[i]) {
|
|
|
|
case 'raddr':
|
|
|
|
candidate['rel-addr'] = elems[i + 1];
|
|
|
|
break;
|
|
|
|
case 'rport':
|
|
|
|
candidate['rel-port'] = elems[i + 1];
|
|
|
|
break;
|
|
|
|
case 'generation':
|
|
|
|
candidate.generation = elems[i + 1];
|
|
|
|
break;
|
|
|
|
default: // TODO
|
|
|
|
console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
candidate.network = '1';
|
|
|
|
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
|
|
|
return candidate;
|
|
|
|
},
|
|
|
|
build_icecandidate: function (cand) {
|
|
|
|
var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
|
|
|
|
line += ' ';
|
|
|
|
switch (cand.type) {
|
|
|
|
case 'srflx':
|
|
|
|
case 'prflx':
|
|
|
|
case 'relay':
|
|
|
|
if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
|
|
|
|
line += 'raddr';
|
|
|
|
line += ' ';
|
|
|
|
line += cand['rel-addr'];
|
|
|
|
line += ' ';
|
|
|
|
line += 'rport';
|
|
|
|
line += ' ';
|
|
|
|
line += cand['rel-port'];
|
|
|
|
line += ' ';
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
line += 'generation';
|
|
|
|
line += ' ';
|
|
|
|
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
|
|
|
|
return line;
|
|
|
|
},
|
|
|
|
parse_ssrc: function (desc) {
|
|
|
|
// proprietary mapping of a=ssrc lines
|
|
|
|
// TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
|
|
|
|
// and parse according to that
|
|
|
|
var lines = desc.split('\r\n'),
|
|
|
|
data = {};
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
|
|
if (lines[i].substring(0, 7) == 'a=ssrc:') {
|
|
|
|
var idx = lines[i].indexOf(' ');
|
|
|
|
data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
parse_rtcpfb: function (line) {
|
|
|
|
var parts = line.substr(10).split(' ');
|
|
|
|
var data = {};
|
|
|
|
data.pt = parts.shift();
|
|
|
|
data.type = parts.shift();
|
|
|
|
data.params = parts;
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
parse_extmap: function (line) {
|
|
|
|
var parts = line.substr(9).split(' ');
|
|
|
|
var data = {};
|
|
|
|
data.value = parts.shift();
|
|
|
|
if (data.value.indexOf('/') != -1) {
|
|
|
|
data.direction = data.value.substr(data.value.indexOf('/') + 1);
|
|
|
|
data.value = data.value.substr(0, data.value.indexOf('/'));
|
|
|
|
} else {
|
|
|
|
data.direction = 'both';
|
|
|
|
}
|
|
|
|
data.uri = parts.shift();
|
|
|
|
data.params = parts;
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
find_line: function (haystack, needle, sessionpart) {
|
|
|
|
var lines = haystack.split('\r\n');
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
|
|
if (lines[i].substring(0, needle.length) == needle) {
|
|
|
|
return lines[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!sessionpart) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// search session part
|
|
|
|
lines = sessionpart.split('\r\n');
|
|
|
|
for (var j = 0; j < lines.length; j++) {
|
|
|
|
if (lines[j].substring(0, needle.length) == needle) {
|
|
|
|
return lines[j];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
find_lines: function (haystack, needle, sessionpart) {
|
|
|
|
var lines = haystack.split('\r\n'),
|
|
|
|
needles = [];
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
|
|
if (lines[i].substring(0, needle.length) == needle)
|
|
|
|
needles.push(lines[i]);
|
|
|
|
}
|
|
|
|
if (needles.length || !sessionpart) {
|
|
|
|
return needles;
|
|
|
|
}
|
|
|
|
// search session part
|
|
|
|
lines = sessionpart.split('\r\n');
|
|
|
|
for (var j = 0; j < lines.length; j++) {
|
|
|
|
if (lines[j].substring(0, needle.length) == needle) {
|
|
|
|
needles.push(lines[j]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return needles;
|
|
|
|
},
|
|
|
|
candidateToJingle: function (line) {
|
|
|
|
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
|
|
|
|
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
|
|
|
|
if (line.substring(0, 12) != 'a=candidate:') {
|
|
|
|
console.log('parseCandidate called with a line that is not a candidate line');
|
|
|
|
console.log(line);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (line.substring(line.length - 2) == '\r\n') // chomp it
|
|
|
|
line = line.substring(0, line.length - 2);
|
|
|
|
var candidate = {},
|
|
|
|
elems = line.split(' '),
|
|
|
|
i;
|
|
|
|
if (elems[6] != 'typ') {
|
|
|
|
console.log('did not find typ in the right place');
|
|
|
|
console.log(line);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
candidate.foundation = elems[0].substring(12);
|
|
|
|
candidate.component = elems[1];
|
|
|
|
candidate.protocol = elems[2].toLowerCase();
|
|
|
|
candidate.priority = elems[3];
|
|
|
|
candidate.ip = elems[4];
|
|
|
|
candidate.port = elems[5];
|
|
|
|
// elems[6] => "typ"
|
|
|
|
candidate.type = elems[7];
|
|
|
|
for (i = 8; i < elems.length; i += 2) {
|
|
|
|
switch (elems[i]) {
|
|
|
|
case 'raddr':
|
|
|
|
candidate['rel-addr'] = elems[i + 1];
|
|
|
|
break;
|
|
|
|
case 'rport':
|
|
|
|
candidate['rel-port'] = elems[i + 1];
|
|
|
|
break;
|
|
|
|
case 'generation':
|
|
|
|
candidate.generation = elems[i + 1];
|
|
|
|
break;
|
|
|
|
default: // TODO
|
|
|
|
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
candidate.network = '1';
|
|
|
|
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
|
|
|
return candidate;
|
|
|
|
},
|
|
|
|
candidateFromJingle: function (cand) {
|
|
|
|
var line = 'a=candidate:';
|
|
|
|
line += cand.getAttribute('foundation');
|
|
|
|
line += ' ';
|
|
|
|
line += cand.getAttribute('component');
|
|
|
|
line += ' ';
|
|
|
|
line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
|
|
|
|
line += ' ';
|
|
|
|
line += cand.getAttribute('priority');
|
|
|
|
line += ' ';
|
|
|
|
line += cand.getAttribute('ip');
|
|
|
|
line += ' ';
|
|
|
|
line += cand.getAttribute('port');
|
|
|
|
line += ' ';
|
|
|
|
line += 'typ';
|
|
|
|
line += ' ' + cand.getAttribute('type');
|
|
|
|
line += ' ';
|
|
|
|
switch (cand.getAttribute('type')) {
|
|
|
|
case 'srflx':
|
|
|
|
case 'prflx':
|
|
|
|
case 'relay':
|
|
|
|
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
|
|
|
|
line += 'raddr';
|
|
|
|
line += ' ';
|
|
|
|
line += cand.getAttribute('rel-addr');
|
|
|
|
line += ' ';
|
|
|
|
line += 'rport';
|
|
|
|
line += ' ';
|
|
|
|
line += cand.getAttribute('rel-port');
|
|
|
|
line += ' ';
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
line += 'generation';
|
|
|
|
line += ' ';
|
|
|
|
line += cand.getAttribute('generation') || '0';
|
|
|
|
return line + '\r\n';
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/* jshint -W117 */
|
|
|
|
// Jingle stuff
|
|
|
|
function JingleSession(me, sid, connection) {
|
|
|
|
this.me = me;
|
|
|
|
this.sid = sid;
|
|
|
|
this.connection = connection;
|
|
|
|
this.initiator = null;
|
|
|
|
this.responder = null;
|
|
|
|
this.isInitiator = null;
|
|
|
|
this.peerjid = null;
|
|
|
|
this.state = null;
|
|
|
|
this.peerconnection = null;
|
|
|
|
this.remoteStream = null;
|
|
|
|
this.localSDP = null;
|
|
|
|
this.remoteSDP = null;
|
|
|
|
this.localStreams = [];
|
|
|
|
this.relayedStreams = [];
|
|
|
|
this.remoteStreams = [];
|
|
|
|
this.startTime = null;
|
|
|
|
this.stopTime = null;
|
|
|
|
this.media_constraints = null;
|
|
|
|
this.pc_constraints = null;
|
|
|
|
this.ice_config = {},
|
|
|
|
this.drip_container = [];
|
|
|
|
|
|
|
|
this.usetrickle = true;
|
|
|
|
this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718
|
|
|
|
this.usedrip = false; // dripping is sending trickle candidates not one-by-one
|
|
|
|
|
|
|
|
this.hadstuncandidate = false;
|
|
|
|
this.hadturncandidate = false;
|
|
|
|
this.lasticecandidate = false;
|
|
|
|
|
|
|
|
this.statsinterval = null;
|
|
|
|
|
|
|
|
this.reason = null;
|
|
|
|
|
|
|
|
this.addssrc = [];
|
|
|
|
this.removessrc = [];
|
|
|
|
|
|
|
|
this.wait = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
JingleSession.prototype.initiate = function (peerjid, isInitiator) {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
if (this.state !== null) {
|
|
|
|
console.error('attempt to initiate on session ' + this.sid +
|
|
|
|
'in state ' + this.state);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.isInitiator = isInitiator;
|
|
|
|
this.state = 'pending';
|
|
|
|
this.initiator = isInitiator ? this.me : peerjid;
|
|
|
|
this.responder = !isInitiator ? this.me : peerjid;
|
|
|
|
this.peerjid = peerjid;
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('create PeerConnection ' + JSON.stringify(this.ice_config));
|
2013-12-16 11:22:23 +00:00
|
|
|
try {
|
|
|
|
this.peerconnection = new RTCPeerconnection(this.ice_config,
|
|
|
|
this.pc_constraints);
|
|
|
|
} catch (e) {
|
|
|
|
console.error('Failed to create PeerConnection, exception: ',
|
|
|
|
e.message);
|
|
|
|
console.error(e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.hadstuncandidate = false;
|
|
|
|
this.hadturncandidate = false;
|
|
|
|
this.lasticecandidate = false;
|
|
|
|
this.peerconnection.onicecandidate = function (event) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.sendIceCandidate(event.candidate);
|
2013-12-16 11:22:23 +00:00
|
|
|
};
|
|
|
|
this.peerconnection.onaddstream = function (event) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.remoteStream = event.stream;
|
|
|
|
self.remoteStreams.push(event.stream);
|
|
|
|
$(document).trigger('remotestreamadded.jingle', [event, self.sid]);
|
2013-12-16 11:22:23 +00:00
|
|
|
};
|
|
|
|
this.peerconnection.onremovestream = function (event) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.remoteStream = null;
|
2013-12-16 11:22:23 +00:00
|
|
|
// FIXME: remove from this.remoteStreams
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
|
2013-12-16 11:22:23 +00:00
|
|
|
};
|
|
|
|
this.peerconnection.onsignalingstatechange = function (event) {
|
2014-01-03 16:41:50 +00:00
|
|
|
if (!(self && self.peerconnection)) return;
|
2013-12-16 11:22:23 +00:00
|
|
|
};
|
|
|
|
this.peerconnection.oniceconnectionstatechange = function (event) {
|
2014-01-03 16:41:50 +00:00
|
|
|
if (!(self && self.peerconnection)) return;
|
|
|
|
switch (self.peerconnection.iceConnectionState) {
|
2013-12-16 11:22:23 +00:00
|
|
|
case 'connected':
|
|
|
|
this.startTime = new Date();
|
|
|
|
break;
|
|
|
|
case 'disconnected':
|
|
|
|
this.stopTime = new Date();
|
|
|
|
break;
|
|
|
|
}
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
|
2013-12-16 11:22:23 +00:00
|
|
|
};
|
|
|
|
// add any local and relayed stream
|
|
|
|
this.localStreams.forEach(function(stream) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.peerconnection.addStream(stream);
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
this.relayedStreams.forEach(function(stream) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.peerconnection.addStream(stream);
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.accept = function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
this.state = 'active';
|
|
|
|
|
|
|
|
var pranswer = this.peerconnection.localDescription;
|
|
|
|
if (!pranswer || pranswer.type != 'pranswer') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.log('going from pranswer to answer');
|
|
|
|
if (this.usetrickle) {
|
|
|
|
// remove candidates already sent from session-accept
|
|
|
|
var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:');
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
|
|
pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', '');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) {
|
|
|
|
// FIXME: change any inactive to sendrecv or whatever they were originally
|
|
|
|
pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
|
|
|
|
}
|
|
|
|
var prsdp = new SDP(pranswer.sdp);
|
|
|
|
var accept = $iq({to: this.peerjid,
|
|
|
|
type: 'set'})
|
|
|
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
|
|
|
action: 'session-accept',
|
|
|
|
initiator: this.initiator,
|
|
|
|
responder: this.responder,
|
|
|
|
sid: this.sid });
|
|
|
|
prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
|
|
|
|
this.connection.sendIQ(accept,
|
|
|
|
function () {
|
|
|
|
var ack = {};
|
|
|
|
ack.source = 'answer';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('ack.jingle', [self.sid, ack]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (stanza) {
|
|
|
|
var error = ($(stanza).find('error').length) ? {
|
|
|
|
code: $(stanza).find('error').attr('code'),
|
|
|
|
reason: $(stanza).find('error :first')[0].tagName,
|
|
|
|
}:{};
|
|
|
|
error.source = 'answer';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('error.jingle', [self.sid, error]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
10000);
|
|
|
|
|
|
|
|
var sdp = this.peerconnection.localDescription.sdp;
|
|
|
|
while (SDPUtil.find_line(sdp, 'a=inactive')) {
|
|
|
|
// FIXME: change any inactive to sendrecv or whatever they were originally
|
|
|
|
sdp = sdp.replace('a=inactive', 'a=sendrecv');
|
|
|
|
}
|
|
|
|
this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}),
|
|
|
|
function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('setLocalDescription success');
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (e) {
|
|
|
|
console.error('setLocalDescription failed', e);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.terminate = function (reason) {
|
|
|
|
this.state = 'ended';
|
|
|
|
this.reason = reason;
|
|
|
|
this.peerconnection.close();
|
|
|
|
if (this.statsinterval !== null) {
|
|
|
|
window.clearInterval(this.statsinterval);
|
|
|
|
this.statsinterval = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.active = function () {
|
|
|
|
return this.state == 'active';
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.sendIceCandidate = function (candidate) {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
if (candidate && !this.lasticecandidate) {
|
2013-12-26 13:26:54 +00:00
|
|
|
var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
|
|
|
|
var jcand = SDPUtil.candidateToJingle(candidate.candidate);
|
2013-12-16 11:22:23 +00:00
|
|
|
if (!(ice && jcand)) {
|
|
|
|
console.error('failed to get ice && jcand');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
|
|
|
|
|
|
|
|
if (jcand.type === 'srflx') {
|
|
|
|
this.hadstuncandidate = true;
|
|
|
|
} else if (jcand.type === 'relay') {
|
|
|
|
this.hadturncandidate = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.usetrickle) {
|
|
|
|
if (this.usedrip) {
|
|
|
|
if (this.drip_container.length === 0) {
|
|
|
|
// start 10ms callout
|
|
|
|
window.setTimeout(function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
if (self.drip_container.length === 0) return;
|
|
|
|
var allcands = self.drip_container;
|
|
|
|
self.drip_container = [];
|
|
|
|
var cand = $iq({to: self.peerjid, type: 'set'})
|
2013-12-16 11:22:23 +00:00
|
|
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
|
|
|
action: 'transport-info',
|
2014-01-03 16:41:50 +00:00
|
|
|
initiator: self.initiator,
|
|
|
|
sid: self.sid});
|
|
|
|
for (var mid = 0; mid < self.localSDP.media.length; mid++) {
|
2013-12-16 11:22:23 +00:00
|
|
|
var cands = allcands.filter(function (el) { return el.sdpMLineIndex == mid; });
|
|
|
|
if (cands.length > 0) {
|
2014-01-03 16:41:50 +00:00
|
|
|
var ice = SDPUtil.iceparams(self.localSDP.media[mid], self.localSDP.session);
|
2013-12-16 11:22:23 +00:00
|
|
|
ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
|
2014-01-03 16:41:50 +00:00
|
|
|
cand.c('content', {creator: self.initiator == self.me ? 'initiator' : 'responder',
|
2013-12-16 11:22:23 +00:00
|
|
|
name: cands[0].sdpMid
|
|
|
|
}).c('transport', ice);
|
|
|
|
for (var i = 0; i < cands.length; i++) {
|
|
|
|
cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
|
|
|
|
}
|
|
|
|
// add fingerprint
|
2014-01-03 16:41:50 +00:00
|
|
|
if (SDPUtil.find_line(self.localSDP.media[mid], 'a=fingerprint:', self.localSDP.session)) {
|
|
|
|
var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(self.localSDP.media[mid], 'a=fingerprint:', self.localSDP.session));
|
2013-12-16 11:22:23 +00:00
|
|
|
tmp.required = true;
|
|
|
|
cand.c('fingerprint').t(tmp.fingerprint);
|
|
|
|
delete tmp.fingerprint;
|
|
|
|
cand.attrs(tmp);
|
|
|
|
cand.up();
|
|
|
|
}
|
|
|
|
cand.up(); // transport
|
|
|
|
cand.up(); // content
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('was this the last candidate', self.lasticecandidate);
|
|
|
|
self.connection.sendIQ(cand,
|
2013-12-16 11:22:23 +00:00
|
|
|
function () {
|
|
|
|
var ack = {};
|
|
|
|
ack.source = 'transportinfo';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('ack.jingle', [self.sid, ack]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (stanza) {
|
|
|
|
var error = ($(stanza).find('error').length) ? {
|
|
|
|
code: $(stanza).find('error').attr('code'),
|
|
|
|
reason: $(stanza).find('error :first')[0].tagName,
|
|
|
|
}:{};
|
|
|
|
error.source = 'transportinfo';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('error.jingle', [self.sid, error]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
10000);
|
|
|
|
}, 10);
|
|
|
|
}
|
|
|
|
this.drip_container.push(event.candidate);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// map to transport-info
|
|
|
|
var cand = $iq({to: this.peerjid, type: 'set'})
|
|
|
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
|
|
|
action: 'transport-info',
|
|
|
|
initiator: this.initiator,
|
|
|
|
sid: this.sid})
|
|
|
|
.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
|
|
|
|
name: candidate.sdpMid
|
|
|
|
})
|
|
|
|
.c('transport', ice)
|
|
|
|
.c('candidate', jcand);
|
|
|
|
cand.up();
|
|
|
|
// add fingerprint
|
|
|
|
if (SDPUtil.find_line(this.localSDP.media[candidate.sdpMLineIndex], 'a=fingerprint:', this.localSDP.session)) {
|
|
|
|
var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[candidate.sdpMLineIndex], 'a=fingerprint:', this.localSDP.session));
|
|
|
|
tmp.required = true;
|
|
|
|
cand.c('fingerprint').t(tmp.fingerprint);
|
|
|
|
delete tmp.fingerprint;
|
|
|
|
cand.attrs(tmp);
|
|
|
|
cand.up();
|
|
|
|
}
|
|
|
|
this.connection.sendIQ(cand,
|
|
|
|
function () {
|
|
|
|
var ack = {};
|
|
|
|
ack.source = 'transportinfo';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('ack.jingle', [self.sid, ack]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (stanza) {
|
|
|
|
console.error('transport info error');
|
|
|
|
var error = ($(stanza).find('error').length) ? {
|
|
|
|
code: $(stanza).find('error').attr('code'),
|
|
|
|
reason: $(stanza).find('error :first')[0].tagName,
|
|
|
|
}:{};
|
|
|
|
error.source = 'transportinfo';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('error.jingle', [self.sid, error]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
10000);
|
|
|
|
}
|
|
|
|
} else {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('sendIceCandidate: last candidate.');
|
2013-12-16 11:22:23 +00:00
|
|
|
if (!this.usetrickle) {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('should send full offer now...');
|
2013-12-16 11:22:23 +00:00
|
|
|
var init = $iq({to: this.peerjid,
|
|
|
|
type: 'set'})
|
|
|
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
|
|
|
action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept',
|
|
|
|
initiator: this.initiator,
|
|
|
|
sid: this.sid});
|
|
|
|
this.localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
|
|
|
this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder');
|
|
|
|
this.connection.sendIQ(init,
|
|
|
|
function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('session initiate ack');
|
2013-12-16 11:22:23 +00:00
|
|
|
var ack = {};
|
|
|
|
ack.source = 'offer';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('ack.jingle', [self.sid, ack]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (stanza) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.state = 'error';
|
|
|
|
self.peerconnection.close();
|
2013-12-16 11:22:23 +00:00
|
|
|
var error = ($(stanza).find('error').length) ? {
|
|
|
|
code: $(stanza).find('error').attr('code'),
|
|
|
|
reason: $(stanza).find('error :first')[0].tagName,
|
|
|
|
}:{};
|
|
|
|
error.source = 'offer';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('error.jingle', [self.sid, error]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
10000);
|
|
|
|
}
|
|
|
|
this.lasticecandidate = true;
|
|
|
|
console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate);
|
|
|
|
console.log('Have we encountered any relay candidates? ' + this.hadturncandidate);
|
|
|
|
|
|
|
|
if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') {
|
|
|
|
$(document).trigger('nostuncandidates.jingle', [this.sid]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.sendOffer = function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('sendOffer...');
|
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
this.peerconnection.createOffer(function (sdp) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.createdOffer(sdp);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (e) {
|
|
|
|
console.error('createOffer failed', e);
|
|
|
|
},
|
|
|
|
this.media_constraints
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.createdOffer = function (sdp) {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('createdOffer', sdp);
|
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
this.localSDP = new SDP(sdp.sdp);
|
|
|
|
//this.localSDP.mangle();
|
|
|
|
if (this.usetrickle) {
|
|
|
|
var init = $iq({to: this.peerjid,
|
|
|
|
type: 'set'})
|
|
|
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
|
|
|
action: 'session-initiate',
|
|
|
|
initiator: this.initiator,
|
|
|
|
sid: this.sid});
|
|
|
|
this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder');
|
|
|
|
this.connection.sendIQ(init,
|
|
|
|
function () {
|
|
|
|
var ack = {};
|
|
|
|
ack.source = 'offer';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('ack.jingle', [self.sid, ack]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (stanza) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.state = 'error';
|
|
|
|
self.peerconnection.close();
|
2013-12-16 11:22:23 +00:00
|
|
|
var error = ($(stanza).find('error').length) ? {
|
|
|
|
code: $(stanza).find('error').attr('code'),
|
|
|
|
reason: $(stanza).find('error :first')[0].tagName,
|
|
|
|
}:{};
|
|
|
|
error.source = 'offer';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('error.jingle', [self.sid, error]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
10000);
|
|
|
|
}
|
|
|
|
sdp.sdp = this.localSDP.raw;
|
2014-01-03 16:41:50 +00:00
|
|
|
this.peerconnection.setLocalDescription(sdp,
|
|
|
|
function () {
|
|
|
|
//console.log('setLocalDescription success');
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (e) {
|
|
|
|
console.error('setLocalDescription failed', e);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
|
|
|
|
for (var i = 0; i < cands.length; i++) {
|
|
|
|
var cand = SDPUtil.parse_icecandidate(cands[i]);
|
|
|
|
if (cand.type == 'srflx') {
|
|
|
|
this.hadstuncandidate = true;
|
|
|
|
} else if (cand.type == 'relay') {
|
|
|
|
this.hadturncandidate = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('setting remote description... ', desctype);
|
2013-12-16 11:22:23 +00:00
|
|
|
this.remoteSDP = new SDP('');
|
|
|
|
this.remoteSDP.fromJingle(elem);
|
|
|
|
if (this.peerconnection.remoteDescription !== null) {
|
|
|
|
console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription);
|
|
|
|
if (this.peerconnection.remoteDescription.type == 'pranswer') {
|
|
|
|
var pranswer = new SDP(this.peerconnection.remoteDescription.sdp);
|
|
|
|
for (var i = 0; i < pranswer.media.length; i++) {
|
|
|
|
// make sure we have ice ufrag and pwd
|
|
|
|
if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) {
|
|
|
|
if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) {
|
|
|
|
this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n';
|
|
|
|
} else {
|
|
|
|
console.warn('no ice ufrag?');
|
|
|
|
}
|
|
|
|
if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) {
|
|
|
|
this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n';
|
|
|
|
} else {
|
|
|
|
console.warn('no ice pwd?');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// copy over candidates
|
|
|
|
var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:');
|
|
|
|
for (var j = 0; j < lines.length; j++) {
|
|
|
|
this.remoteSDP.media[i] += lines[j] + '\r\n';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw});
|
|
|
|
|
|
|
|
this.peerconnection.setRemoteDescription(remotedesc,
|
|
|
|
function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('setRemoteDescription success');
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (e) {
|
|
|
|
console.error('setRemoteDescription error', e);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.addIceCandidate = function (elem) {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
if (this.peerconnection.signalingState == 'closed') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') {
|
|
|
|
console.log('trickle ice candidate arriving before session accept...');
|
|
|
|
// create a PRANSWER for setRemoteDescription
|
|
|
|
if (!this.remoteSDP) {
|
|
|
|
var cobbled = 'v=0\r\n' +
|
|
|
|
'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
|
|
|
|
's=-\r\n' +
|
|
|
|
't=0 0\r\n';
|
|
|
|
// first, take some things from the local description
|
|
|
|
for (var i = 0; i < this.localSDP.media.length; i++) {
|
|
|
|
cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n';
|
|
|
|
cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n';
|
|
|
|
if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) {
|
|
|
|
cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n';
|
|
|
|
}
|
|
|
|
cobbled += 'a=inactive\r\n';
|
|
|
|
}
|
|
|
|
this.remoteSDP = new SDP(cobbled);
|
|
|
|
}
|
|
|
|
// then add things like ice and dtls from remote candidate
|
|
|
|
elem.each(function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
for (var i = 0; i < self.remoteSDP.media.length; i++) {
|
|
|
|
if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
|
|
|
|
self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
|
|
|
|
if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) {
|
2013-12-16 11:22:23 +00:00
|
|
|
var tmp = $(this).find('transport');
|
2014-01-03 16:41:50 +00:00
|
|
|
self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
|
|
|
|
self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
|
2013-12-16 11:22:23 +00:00
|
|
|
tmp = $(this).find('transport>fingerprint');
|
|
|
|
if (tmp.length) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
|
2013-12-16 11:22:23 +00:00
|
|
|
} else {
|
|
|
|
console.log('no dtls fingerprint (webrtc issue #1718?)');
|
2014-01-03 16:41:50 +00:00
|
|
|
self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
|
2013-12-16 11:22:23 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
|
|
|
|
|
|
|
|
// we need a complete SDP with ice-ufrag/ice-pwd in all parts
|
|
|
|
// this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts
|
|
|
|
// but it could be in the session part as well. since the code above constructs this sdp this can't happen however
|
|
|
|
var iscomplete = this.remoteSDP.media.filter(function (mediapart) {
|
|
|
|
return SDPUtil.find_line(mediapart, 'a=ice-ufrag:');
|
|
|
|
}).length == this.remoteSDP.media.length;
|
|
|
|
|
|
|
|
if (iscomplete) {
|
|
|
|
console.log('setting pranswer');
|
|
|
|
try {
|
2014-01-03 16:41:50 +00:00
|
|
|
this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
|
|
|
|
function() {
|
|
|
|
},
|
|
|
|
function(e) {
|
|
|
|
console.log('setRemoteDescription pranswer failed', e.toString());
|
|
|
|
});
|
2013-12-16 11:22:23 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.error('setting pranswer failed', e);
|
|
|
|
}
|
|
|
|
} else {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('not yet setting pranswer');
|
2013-12-16 11:22:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// operate on each content element
|
|
|
|
elem.each(function () {
|
|
|
|
// would love to deactivate this, but firefox still requires it
|
|
|
|
var idx = -1;
|
|
|
|
var i;
|
2014-01-03 16:41:50 +00:00
|
|
|
for (i = 0; i < self.remoteSDP.media.length; i++) {
|
|
|
|
if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
|
|
|
|
self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
|
2013-12-16 11:22:23 +00:00
|
|
|
idx = i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (idx == -1) { // fall back to localdescription
|
2014-01-03 16:41:50 +00:00
|
|
|
for (i = 0; i < self.localSDP.media.length; i++) {
|
|
|
|
if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
|
|
|
|
self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
|
2013-12-16 11:22:23 +00:00
|
|
|
idx = i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var name = $(this).attr('name');
|
|
|
|
// TODO: check ice-pwd and ice-ufrag?
|
|
|
|
$(this).find('transport>candidate').each(function () {
|
|
|
|
var line, candidate;
|
|
|
|
line = SDPUtil.candidateFromJingle(this);
|
|
|
|
candidate = new RTCIceCandidate({sdpMLineIndex: idx,
|
|
|
|
sdpMid: name,
|
|
|
|
candidate: line});
|
|
|
|
try {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.peerconnection.addIceCandidate(candidate);
|
2013-12-16 11:22:23 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.error('addIceCandidate failed', e.toString(), line);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.sendAnswer = function (provisional) {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('createAnswer', provisional);
|
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
this.peerconnection.createAnswer(
|
|
|
|
function (sdp) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.createdAnswer(sdp, provisional);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (e) {
|
|
|
|
console.error('createAnswer failed', e);
|
|
|
|
},
|
|
|
|
this.media_constraints
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.createdAnswer = function (sdp, provisional) {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('createAnswer callback');
|
2013-12-16 11:22:23 +00:00
|
|
|
console.log(sdp);
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
this.localSDP = new SDP(sdp.sdp);
|
|
|
|
//this.localSDP.mangle();
|
|
|
|
this.usepranswer = provisional === true;
|
|
|
|
if (this.usetrickle) {
|
|
|
|
if (!this.usepranswer) {
|
|
|
|
var accept = $iq({to: this.peerjid,
|
|
|
|
type: 'set'})
|
|
|
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
|
|
|
action: 'session-accept',
|
|
|
|
initiator: this.initiator,
|
|
|
|
responder: this.responder,
|
|
|
|
sid: this.sid });
|
|
|
|
this.localSDP.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
|
|
|
|
this.connection.sendIQ(accept,
|
|
|
|
function () {
|
|
|
|
var ack = {};
|
|
|
|
ack.source = 'answer';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('ack.jingle', [self.sid, ack]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (stanza) {
|
|
|
|
var error = ($(stanza).find('error').length) ? {
|
|
|
|
code: $(stanza).find('error').attr('code'),
|
|
|
|
reason: $(stanza).find('error :first')[0].tagName,
|
|
|
|
}:{};
|
|
|
|
error.source = 'answer';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('error.jingle', [self.sid, error]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
10000);
|
|
|
|
} else {
|
|
|
|
sdp.type = 'pranswer';
|
|
|
|
for (var i = 0; i < this.localSDP.media.length; i++) {
|
|
|
|
this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n');
|
|
|
|
}
|
|
|
|
this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join('');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sdp.sdp = this.localSDP.raw;
|
|
|
|
this.peerconnection.setLocalDescription(sdp,
|
|
|
|
function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('setLocalDescription success');
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (e) {
|
|
|
|
console.error('setLocalDescription failed', e);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
|
|
|
|
for (var j = 0; j < cands.length; j++) {
|
|
|
|
var cand = SDPUtil.parse_icecandidate(cands[j]);
|
|
|
|
if (cand.type == 'srflx') {
|
|
|
|
this.hadstuncandidate = true;
|
|
|
|
} else if (cand.type == 'relay') {
|
|
|
|
this.hadturncandidate = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.sendTerminate = function (reason, text) {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this,
|
2013-12-16 11:22:23 +00:00
|
|
|
term = $iq({to: this.peerjid,
|
|
|
|
type: 'set'})
|
|
|
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
|
|
|
action: 'session-terminate',
|
|
|
|
initiator: this.initiator,
|
|
|
|
sid: this.sid})
|
|
|
|
.c('reason')
|
|
|
|
.c(reason || 'success');
|
|
|
|
|
|
|
|
if (text) {
|
|
|
|
term.up().c('text').t(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.connection.sendIQ(term,
|
|
|
|
function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.peerconnection.close();
|
|
|
|
self.peerconnection = null;
|
|
|
|
self.terminate();
|
2013-12-16 11:22:23 +00:00
|
|
|
var ack = {};
|
|
|
|
ack.source = 'terminate';
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('ack.jingle', [self.sid, ack]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function (stanza) {
|
|
|
|
var error = ($(stanza).find('error').length) ? {
|
|
|
|
code: $(stanza).find('error').attr('code'),
|
|
|
|
reason: $(stanza).find('error :first')[0].tagName,
|
|
|
|
}:{};
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('ack.jingle', [self.sid, error]);
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
10000);
|
|
|
|
if (this.statsinterval !== null) {
|
|
|
|
window.clearInterval(this.statsinterval);
|
|
|
|
this.statsinterval = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
JingleSession.prototype.addSource = function (elem) {
|
|
|
|
console.log('addssrc', new Date().getTime());
|
|
|
|
console.log('ice', this.peerconnection.iceConnectionState);
|
|
|
|
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
|
|
|
|
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
$(elem).each(function (idx, content) {
|
|
|
|
var name = $(content).attr('name');
|
|
|
|
var lines = '';
|
|
|
|
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
|
|
|
tmp.each(function () {
|
|
|
|
var ssrc = $(this).attr('ssrc');
|
|
|
|
$(this).find('>parameter').each(function () {
|
|
|
|
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
|
|
|
if ($(this).attr('value') && $(this).attr('value').length)
|
|
|
|
lines += ':' + $(this).attr('value');
|
|
|
|
lines += '\r\n';
|
|
|
|
});
|
|
|
|
});
|
|
|
|
sdp.media.forEach(function(media, idx) {
|
|
|
|
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
|
|
|
return;
|
|
|
|
sdp.media[idx] += lines;
|
2014-01-03 16:41:50 +00:00
|
|
|
if (!self.addssrc[idx]) self.addssrc[idx] = '';
|
|
|
|
self.addssrc[idx] += lines;
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
sdp.raw = sdp.session + sdp.media.join('');
|
|
|
|
});
|
|
|
|
this.modifySources();
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.removeSource = function (elem) {
|
|
|
|
console.log('removessrc', new Date().getTime());
|
|
|
|
console.log('ice', this.peerconnection.iceConnectionState);
|
|
|
|
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
|
|
|
|
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
$(elem).each(function (idx, content) {
|
|
|
|
var name = $(content).attr('name');
|
|
|
|
var lines = '';
|
|
|
|
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
|
|
|
tmp.each(function () {
|
|
|
|
var ssrc = $(this).attr('ssrc');
|
|
|
|
$(this).find('>parameter').each(function () {
|
|
|
|
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
|
|
|
if ($(this).attr('value') && $(this).attr('value').length)
|
|
|
|
lines += ':' + $(this).attr('value');
|
|
|
|
lines += '\r\n';
|
|
|
|
});
|
|
|
|
});
|
|
|
|
sdp.media.forEach(function(media, idx) {
|
|
|
|
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
|
|
|
return;
|
|
|
|
sdp.media[idx] += lines;
|
2014-01-03 16:41:50 +00:00
|
|
|
if (!self.addssrc[idx]) self.removessrc[idx] = '';
|
|
|
|
self.removessrc[idx] += lines;
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
sdp.raw = sdp.session + sdp.media.join('');
|
|
|
|
});
|
|
|
|
this.modifySources();
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.modifySources = function() {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
if (!(this.addssrc.length || this.removessrc.length)) return;
|
|
|
|
if (this.peerconnection.signalingState == 'closed') return;
|
|
|
|
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
|
|
|
|
console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
|
|
|
|
this.wait = true;
|
2014-01-03 16:41:50 +00:00
|
|
|
window.setTimeout(function() { self.modifySources(); }, 250);
|
2013-12-16 11:22:23 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.wait) {
|
2014-01-03 16:41:50 +00:00
|
|
|
window.setTimeout(function() { self.modifySources(); }, 2500);
|
2013-12-16 11:22:23 +00:00
|
|
|
this.wait = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
|
|
|
|
|
|
|
|
// add sources
|
|
|
|
this.addssrc.forEach(function(lines, idx) {
|
|
|
|
sdp.media[idx] += lines;
|
|
|
|
});
|
|
|
|
this.addssrc = [];
|
|
|
|
|
|
|
|
// remove sources
|
|
|
|
this.removessrc.forEach(function(lines, idx) {
|
|
|
|
lines = lines.split('\r\n');
|
|
|
|
lines.pop(); // remove empty last element;
|
|
|
|
lines.forEach(function(line) {
|
|
|
|
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.removessrc = [];
|
|
|
|
|
|
|
|
sdp.raw = sdp.session + sdp.media.join('');
|
|
|
|
this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
|
|
|
|
function() {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.peerconnection.createAnswer(
|
2013-12-16 11:22:23 +00:00
|
|
|
function(modifiedAnswer) {
|
2014-01-03 16:41:50 +00:00
|
|
|
self.peerconnection.setLocalDescription(modifiedAnswer,
|
2013-12-16 11:22:23 +00:00
|
|
|
function() {
|
2014-01-03 16:41:50 +00:00
|
|
|
//console.log('modified setLocalDescription ok');
|
2013-12-16 11:22:23 +00:00
|
|
|
},
|
|
|
|
function(error) {
|
|
|
|
console.log('modified setLocalDescription failed');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
function(error) {
|
|
|
|
console.log('modified answer failed');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
function(error) {
|
|
|
|
console.log('modify failed');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.sendMute = function (muted, content) {
|
|
|
|
var info = $iq({to: this.peerjid,
|
|
|
|
type: 'set'})
|
|
|
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
|
|
|
action: 'session-info',
|
|
|
|
initiator: this.initiator,
|
|
|
|
sid: this.sid });
|
|
|
|
info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
|
|
|
|
info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'});
|
|
|
|
if (content) {
|
|
|
|
info.attrs({'name': content});
|
|
|
|
}
|
|
|
|
this.connection.send(info);
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.sendRinging = function () {
|
|
|
|
var info = $iq({to: this.peerjid,
|
|
|
|
type: 'set'})
|
|
|
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
|
|
|
action: 'session-info',
|
|
|
|
initiator: this.initiator,
|
|
|
|
sid: this.sid });
|
|
|
|
info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
|
|
|
|
this.connection.send(info);
|
|
|
|
};
|
|
|
|
|
|
|
|
JingleSession.prototype.getStats = function (interval) {
|
2014-01-03 16:41:50 +00:00
|
|
|
var self = this;
|
2013-12-16 11:22:23 +00:00
|
|
|
var recv = {audio: 0, video: 0};
|
|
|
|
var lost = {audio: 0, video: 0};
|
|
|
|
var lastrecv = {audio: 0, video: 0};
|
|
|
|
var lastlost = {audio: 0, video: 0};
|
|
|
|
var loss = {audio: 0, video: 0};
|
|
|
|
var delta = {audio: 0, video: 0};
|
|
|
|
this.statsinterval = window.setInterval(function () {
|
2014-01-03 16:41:50 +00:00
|
|
|
if (self && self.peerconnection && self.peerconnection.getStats) {
|
|
|
|
self.peerconnection.getStats(function (stats) {
|
2013-12-16 11:22:23 +00:00
|
|
|
var results = stats.result();
|
|
|
|
// TODO: there are so much statistics you can get from this..
|
|
|
|
for (var i = 0; i < results.length; ++i) {
|
|
|
|
if (results[i].type == 'ssrc') {
|
|
|
|
var packetsrecv = results[i].stat('packetsReceived');
|
|
|
|
var packetslost = results[i].stat('packetsLost');
|
|
|
|
if (packetsrecv && packetslost) {
|
|
|
|
packetsrecv = parseInt(packetsrecv, 10);
|
|
|
|
packetslost = parseInt(packetslost, 10);
|
|
|
|
|
|
|
|
if (results[i].stat('googFrameRateReceived')) {
|
|
|
|
lastlost.video = lost.video;
|
|
|
|
lastrecv.video = recv.video;
|
|
|
|
recv.video = packetsrecv;
|
|
|
|
lost.video = packetslost;
|
|
|
|
} else {
|
|
|
|
lastlost.audio = lost.audio;
|
|
|
|
lastrecv.audio = recv.audio;
|
|
|
|
recv.audio = packetsrecv;
|
|
|
|
lost.audio = packetslost;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
delta.audio = recv.audio - lastrecv.audio;
|
|
|
|
delta.video = recv.video - lastrecv.video;
|
|
|
|
loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0;
|
|
|
|
loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0;
|
2014-01-03 16:41:50 +00:00
|
|
|
$(document).trigger('packetloss.jingle', [self.sid, loss]);
|
2013-12-16 11:22:23 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}, interval || 3000);
|
|
|
|
return this.statsinterval;
|
|
|
|
};
|
|
|
|
|