diff --git a/index.html b/index.html
index 7e77a7bec..291f7517c 100644
--- a/index.html
+++ b/index.html
@@ -2,8 +2,13 @@
WebRTC, meet the Jitsi Videobridge
-
-
+
+
+
+
+
+
+
diff --git a/libs/colibri.js b/libs/colibri/colibri.focus.js
similarity index 92%
rename from libs/colibri.js
rename to libs/colibri/colibri.focus.js
index 6242ea182..0b549016b 100644
--- a/libs/colibri.js
+++ b/libs/colibri/colibri.focus.js
@@ -1,4 +1,4 @@
-/* colibri.js -- a COLIBRI focus
+/* colibri.js -- a COLIBRI focus
* The colibri spec has been submitted to the XMPP Standards Foundation
* for publications as a XMPP extensions:
* http://xmpp.org/extensions/inbox/colibri.html
@@ -7,32 +7,32 @@
* in the conference. The conference itself can be ad-hoc, through a
* MUC, through PubSub, etc.
*
- * colibri.js relies heavily on the strophe.jingle library available
+ * colibri.js relies heavily on the strophe.jingle library available
* from https://github.com/ESTOS/strophe.jingle
* and interoperates with the Jitsi videobridge available from
* https://jitsi.org/Projects/JitsiVideobridge
*/
/*
-Copyright (c) 2013 ESTOS GmbH
+ Copyright (c) 2013 ESTOS GmbH
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-*/
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
/* jshint -W117 */
function ColibriFocus(connection, bridgejid) {
this.connection = connection;
@@ -86,20 +86,20 @@ ColibriFocus.prototype.makeConference = function (peers) {
this.peerconnection.oniceconnectionstatechange = function (event) {
console.warn('ice connection state changed to', self.peerconnection.iceConnectionState);
/*
- if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
- console.log('adding new remote SSRCs from iceconnectionstatechange');
- window.setTimeout(function() { self.modifySources(); }, 1000);
- }
- */
+ if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
+ console.log('adding new remote SSRCs from iceconnectionstatechange');
+ window.setTimeout(function() { self.modifySources(); }, 1000);
+ }
+ */
};
this.peerconnection.onsignalingstatechange = function (event) {
console.warn(self.peerconnection.signalingState);
/*
- if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
- console.log('adding new remote SSRCs from signalingstatechange');
- window.setTimeout(function() { self.modifySources(); }, 1000);
- }
- */
+ if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
+ console.log('adding new remote SSRCs from signalingstatechange');
+ window.setTimeout(function() { self.modifySources(); }, 1000);
+ }
+ */
};
this.peerconnection.onaddstream = function (event) {
self.remoteStream = event.stream;
@@ -163,7 +163,7 @@ ColibriFocus.prototype._makeConference = function () {
/*
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) {
- var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
+ var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
elem.c('content', {name: name});
elem.c('channel', {initiator: 'false', expire: '15'});
@@ -303,10 +303,10 @@ ColibriFocus.prototype.createdConference = function (result) {
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
var localSDP = new SDP(self.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) {
- var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
+ var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
elem.c('content', {name: name});
elem.c('channel', {
- initiator: 'true',
+ initiator: 'true',
expire: '15',
id: self.mychannel[channel].attr('id')
});
@@ -495,7 +495,7 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) {
- var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
+ var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
elem.c('content', {name: name});
elem.c('channel', {initiator: 'true', expire:'15'});
elem.up(); // end of channel
@@ -531,7 +531,7 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
// TODO: too much copy-paste
var rtpmap = SDPUtil.parse_rtpmap(val);
change.c('payload-type', rtpmap);
- //
+ //
// put any 'a=fmtp:' + mline.fmt[j] lines into
/*
if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
@@ -582,7 +582,7 @@ ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) {
sid: peersess.sid
}
);
- // FIXME: only announce video ssrcs since we mix audio and dont need
+ // FIXME: only announce video ssrcs since we mix audio and dont need
// the audio ssrcs therefore
var modified = false;
for (channel = 0; channel < sdp.media.length; channel++) {
@@ -867,7 +867,7 @@ ColibriFocus.prototype.modifySources = function () {
self.pendingop = null;
}
- // FIXME: pushing down an answer while ice connection state
+ // FIXME: pushing down an answer while ice connection state
// is still checking is bad...
//console.log(self.peerconnection.iceConnectionState);
@@ -931,55 +931,3 @@ ColibriFocus.prototype.hardMuteVideo = function (muted) {
track.enabled = !muted;
});
};
-
-// A colibri session is similar to a jingle session, it just implements some things differently
-// FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
-function ColibriSession(me, sid, connection) {
- this.me = me;
- this.sid = sid;
- this.connection = connection;
- //this.peerconnection = null;
- //this.mychannel = null;
- //this.channels = null;
- this.peerjid = null;
-
- this.colibri = null;
-}
-
-// implementation of JingleSession interface
-ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
- this.peerjid = peerjid;
-};
-
-ColibriSession.prototype.sendOffer = function (offer) {
- console.log('ColibriSession.sendOffer');
-};
-
-
-ColibriSession.prototype.accept = function () {
- console.log('ColibriSession.accept');
-};
-
-ColibriSession.prototype.terminate = function (reason) {
- this.colibri.terminate(this, reason);
-};
-
-ColibriSession.prototype.active = function () {
- console.log('ColibriSession.active');
-};
-
-ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
- this.colibri.setRemoteDescription(this, elem, desctype);
-};
-
-ColibriSession.prototype.addIceCandidate = function (elem) {
- this.colibri.addIceCandidate(this, elem);
-};
-
-ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
- console.log('ColibriSession.sendAnswer');
-};
-
-ColibriSession.prototype.sendTerminate = function (reason, text) {
- console.log('ColibriSession.sendTerminate');
-};
diff --git a/libs/colibri/colibri.session.js b/libs/colibri/colibri.session.js
new file mode 100644
index 000000000..1adbd5e46
--- /dev/null
+++ b/libs/colibri/colibri.session.js
@@ -0,0 +1,86 @@
+/* colibri.js -- a COLIBRI focus
+ * The colibri spec has been submitted to the XMPP Standards Foundation
+ * for publications as a XMPP extensions:
+ * http://xmpp.org/extensions/inbox/colibri.html
+ *
+ * colibri.js is a participating focus, i.e. the focus participates
+ * in the conference. The conference itself can be ad-hoc, through a
+ * MUC, through PubSub, etc.
+ *
+ * colibri.js relies heavily on the strophe.jingle library available
+ * from https://github.com/ESTOS/strophe.jingle
+ * and interoperates with the Jitsi videobridge available from
+ * https://jitsi.org/Projects/JitsiVideobridge
+ */
+/*
+Copyright (c) 2013 ESTOS GmbH
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+// A colibri session is similar to a jingle session, it just implements some things differently
+// FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
+function ColibriSession(me, sid, connection) {
+ this.me = me;
+ this.sid = sid;
+ this.connection = connection;
+ //this.peerconnection = null;
+ //this.mychannel = null;
+ //this.channels = null;
+ this.peerjid = null;
+
+ this.colibri = null;
+}
+
+// implementation of JingleSession interface
+ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
+ this.peerjid = peerjid;
+};
+
+ColibriSession.prototype.sendOffer = function (offer) {
+ console.log('ColibriSession.sendOffer');
+};
+
+
+ColibriSession.prototype.accept = function () {
+ console.log('ColibriSession.accept');
+};
+
+ColibriSession.prototype.terminate = function (reason) {
+ this.colibri.terminate(this, reason);
+};
+
+ColibriSession.prototype.active = function () {
+ console.log('ColibriSession.active');
+};
+
+ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
+ this.colibri.setRemoteDescription(this, elem, desctype);
+};
+
+ColibriSession.prototype.addIceCandidate = function (elem) {
+ this.colibri.addIceCandidate(this, elem);
+};
+
+ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
+ console.log('ColibriSession.sendAnswer');
+};
+
+ColibriSession.prototype.sendTerminate = function (reason, text) {
+ console.log('ColibriSession.sendTerminate');
+};
diff --git a/libs/strophe/strophe.jingle.adapter.js b/libs/strophe/strophe.jingle.adapter.js
new file mode 100644
index 000000000..113df4f08
--- /dev/null
+++ b/libs/strophe/strophe.jingle.adapter.js
@@ -0,0 +1,381 @@
+function TraceablePeerConnection(ice_config, constraints) {
+ var self = this;
+ var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
+ this.peerconnection = new RTCPeerconnection(ice_config, constraints);
+ this.updateLog = [];
+ this.stats = {};
+ this.statsinterval = null;
+ this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
+
+ // 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) {
+ self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
+ if (self.onicecandidate !== null) {
+ self.onicecandidate(event);
+ }
+ };
+ this.onaddstream = null;
+ this.peerconnection.onaddstream = function (event) {
+ self.trace('onaddstream', event.stream.id);
+ if (self.onaddstream !== null) {
+ self.onaddstream(event);
+ }
+ };
+ this.onremovestream = null;
+ this.peerconnection.onremovestream = function (event) {
+ self.trace('onremovestream', event.stream.id);
+ if (self.onremovestream !== null) {
+ self.onremovestream(event);
+ }
+ };
+ this.onsignalingstatechange = null;
+ this.peerconnection.onsignalingstatechange = function (event) {
+ self.trace('onsignalingstatechange', event.srcElement.signalingState);
+ if (self.onsignalingstatechange !== null) {
+ self.onsignalingstatechange(event);
+ }
+ };
+ this.oniceconnectionstatechange = null;
+ this.peerconnection.oniceconnectionstatechange = function (event) {
+ self.trace('oniceconnectionstatechange', event.srcElement.iceConnectionState);
+ if (self.oniceconnectionstatechange !== null) {
+ self.oniceconnectionstatechange(event);
+ }
+ };
+ this.onnegotiationneeded = null;
+ this.peerconnection.onnegotiationneeded = function (event) {
+ self.trace('onnegotiationneeded');
+ if (self.onnegotiationneeded !== null) {
+ self.onnegotiationneeded(event);
+ }
+ };
+ self.ondatachannel = null;
+ this.peerconnection.ondatachannel = function (event) {
+ self.trace('ondatachannel', event);
+ if (self.ondatachannel !== null) {
+ self.ondatachannel(event);
+ }
+ }
+ if (!navigator.mozGetUserMedia) {
+ this.statsinterval = window.setInterval(function() {
+ self.peerconnection.getStats(function(stats) {
+ var results = stats.result();
+ for (var i = 0; i < results.length; ++i) {
+ //console.log(results[i].type, results[i].id, results[i].names())
+ var now = new Date();
+ results[i].names().forEach(function (name) {
+ var id = results[i].id + '-' + name;
+ if (!self.stats[id]) {
+ self.stats[id] = {
+ startTime: now,
+ endTime: now,
+ values: [],
+ times: []
+ };
+ }
+ self.stats[id].values.push(results[i].stat(name));
+ self.stats[id].times.push(now.getTime());
+ if (self.stats[id].values.length > self.maxstats) {
+ self.stats[id].values.shift();
+ self.stats[id].times.shift();
+ }
+ self.stats[id].endTime = now;
+ });
+ }
+ });
+
+ }, 1000);
+ }
+};
+
+dumpSDP = function(description) {
+ return 'type: ' + description.type + '\r\n' + description.sdp;
+}
+
+if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
+ 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.id);
+ this.peerconnection.addStream(stream);
+};
+
+TraceablePeerConnection.prototype.removeStream = function (stream) {
+ this.trace('removeStream', stream.id);
+ this.peerconnection.removeStream(stream);
+};
+
+TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
+ this.trace('createDataChannel', label, opts);
+ this.peerconnection.createDataChannel(label, opts);
+}
+
+TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
+ var self = this;
+ this.trace('setLocalDescription', dumpSDP(description));
+ this.peerconnection.setLocalDescription(description,
+ function () {
+ self.trace('setLocalDescriptionOnSuccess');
+ successCallback();
+ },
+ function (err) {
+ self.trace('setLocalDescriptionOnFailure', err);
+ failureCallback(err);
+ }
+ );
+ /*
+ if (this.statsinterval === null && this.maxstats > 0) {
+ // start gathering stats
+ }
+ */
+};
+
+TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
+ var self = this;
+ this.trace('setRemoteDescription', dumpSDP(description));
+ this.peerconnection.setRemoteDescription(description,
+ function () {
+ self.trace('setRemoteDescriptionOnSuccess');
+ successCallback();
+ },
+ function (err) {
+ self.trace('setRemoteDescriptionOnFailure', err);
+ failureCallback(err);
+ }
+ );
+ /*
+ if (this.statsinterval === null && this.maxstats > 0) {
+ // start gathering stats
+ }
+ */
+};
+
+TraceablePeerConnection.prototype.close = function () {
+ this.trace('stop');
+ if (this.statsinterval !== null) {
+ window.clearInterval(this.statsinterval);
+ this.statsinterval = null;
+ }
+ this.peerconnection.close();
+};
+
+TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
+ var self = this;
+ this.trace('createOffer', JSON.stringify(constraints, null, ' '));
+ this.peerconnection.createOffer(
+ function (offer) {
+ self.trace('createOfferOnSuccess', dumpSDP(offer));
+ successCallback(offer);
+ },
+ function(err) {
+ self.trace('createOfferOnFailure', err);
+ failureCallback(err);
+ },
+ constraints
+ );
+};
+
+TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
+ var self = this;
+ this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
+ this.peerconnection.createAnswer(
+ function (answer) {
+ self.trace('createAnswerOnSuccess', dumpSDP(answer));
+ successCallback(answer);
+ },
+ function(err) {
+ self.trace('createAnswerOnFailure', err);
+ failureCallback(err);
+ },
+ constraints
+ );
+};
+
+TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
+ var self = this;
+ this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
+ 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, errback) {
+ if (navigator.mozGetUserMedia) {
+ // ignore for now...
+ } else {
+ this.peerconnection.getStats(callback);
+ }
+};
+
+// 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));
+ },
+ // DTLS should now be enabled by default but..
+ pc_constraints: {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]}
+ };
+ 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');
+ }
+}
\ No newline at end of file
diff --git a/libs/strophe/strophe.jingle.bundle.js b/libs/strophe/strophe.jingle.bundle.js
new file mode 100644
index 000000000..94a06c8ad
--- /dev/null
+++ b/libs/strophe/strophe.jingle.bundle.js
@@ -0,0 +1,4 @@
+/*! strophe.js v1.1.3 - built on 20-01-2014 */
+function b64_sha1(a){return binb2b64(core_sha1(str2binb(a),8*a.length))}function str_sha1(a){return binb2str(core_sha1(str2binb(a),8*a.length))}function b64_hmac_sha1(a,b){return binb2b64(core_hmac_sha1(a,b))}function str_hmac_sha1(a,b){return binb2str(core_hmac_sha1(a,b))}function core_sha1(a,b){a[b>>5]|=128<<24-b%32,a[(b+64>>9<<4)+15]=b;var c,d,e,f,g,h,i,j,k=new Array(80),l=1732584193,m=-271733879,n=-1732584194,o=271733878,p=-1009589776;for(c=0;cd;d++)k[d]=16>d?a[c+d]:rol(k[d-3]^k[d-8]^k[d-14]^k[d-16],1),e=safe_add(safe_add(rol(l,5),sha1_ft(d,m,n,o)),safe_add(safe_add(p,k[d]),sha1_kt(d))),p=o,o=n,n=rol(m,30),m=l,l=e;l=safe_add(l,f),m=safe_add(m,g),n=safe_add(n,h),o=safe_add(o,i),p=safe_add(p,j)}return[l,m,n,o,p]}function sha1_ft(a,b,c,d){return 20>a?b&c|~b&d:40>a?b^c^d:60>a?b&c|b&d|c&d:b^c^d}function sha1_kt(a){return 20>a?1518500249:40>a?1859775393:60>a?-1894007588:-899497514}function core_hmac_sha1(a,b){var c=str2binb(a);c.length>16&&(c=core_sha1(c,8*a.length));for(var d=new Array(16),e=new Array(16),f=0;16>f;f++)d[f]=909522486^c[f],e[f]=1549556828^c[f];var g=core_sha1(d.concat(str2binb(b)),512+8*b.length);return core_sha1(e.concat(g),672)}function safe_add(a,b){var c=(65535&a)+(65535&b),d=(a>>16)+(b>>16)+(c>>16);return d<<16|65535&c}function rol(a,b){return a<>>32-b}function str2binb(a){for(var b=[],c=255,d=0;d<8*a.length;d+=8)b[d>>5]|=(a.charCodeAt(d/8)&c)<<24-d%32;return b}function binb2str(a){for(var b="",c=255,d=0;d<32*a.length;d+=8)b+=String.fromCharCode(a[d>>5]>>>24-d%32&c);return b}function binb2b64(a){for(var b,c,d="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",e="",f=0;f<4*a.length;f+=3)for(b=(a[f>>2]>>8*(3-f%4)&255)<<16|(a[f+1>>2]>>8*(3-(f+1)%4)&255)<<8|a[f+2>>2]>>8*(3-(f+2)%4)&255,c=0;4>c;c++)e+=8*f+6*c>32*a.length?"=":d.charAt(b>>6*(3-c)&63);return e}var Base64=function(){var a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",b={encode:function(b){var c,d,e,f,g,h,i,j="",k=0;do c=b.charCodeAt(k++),d=b.charCodeAt(k++),e=b.charCodeAt(k++),f=c>>2,g=(3&c)<<4|d>>4,h=(15&d)<<2|e>>6,i=63&e,isNaN(d)?h=i=64:isNaN(e)&&(i=64),j=j+a.charAt(f)+a.charAt(g)+a.charAt(h)+a.charAt(i);while(k>4,d=(15&g)<<4|h>>2,e=(3&h)<<6|i,j+=String.fromCharCode(c),64!=h&&(j+=String.fromCharCode(d)),64!=i&&(j+=String.fromCharCode(e));while(k>16)+(b>>16)+(c>>16);return d<<16|65535&c},b=function(a,b){return a<>>32-b},c=function(a){for(var b=[],c=0;c<8*a.length;c+=8)b[c>>5]|=(255&a.charCodeAt(c/8))<>5]>>>c%32&255);return b},e=function(a){for(var b="0123456789abcdef",c="",d=0;d<4*a.length;d++)c+=b.charAt(a[d>>2]>>d%4*8+4&15)+b.charAt(a[d>>2]>>d%4*8&15);return c},f=function(c,d,e,f,g,h){return a(b(a(a(d,c),a(f,h)),g),e)},g=function(a,b,c,d,e,g,h){return f(b&c|~b&d,a,b,e,g,h)},h=function(a,b,c,d,e,g,h){return f(b&d|c&~d,a,b,e,g,h)},i=function(a,b,c,d,e,g,h){return f(b^c^d,a,b,e,g,h)},j=function(a,b,c,d,e,g,h){return f(c^(b|~d),a,b,e,g,h)},k=function(b,c){b[c>>5]|=128<>>9<<4)+14]=c;for(var d,e,f,k,l=1732584193,m=-271733879,n=-1732584194,o=271733878,p=0;pc?Math.ceil(c):Math.floor(c),0>c&&(c+=b);b>c;c++)if(c in this&&this[c]===a)return c;return-1}),function(a){function b(a,b){return new f.Builder(a,b)}function c(a){return new f.Builder("message",a)}function d(a){return new f.Builder("iq",a)}function e(a){return new f.Builder("presence",a)}var f;f={VERSION:"1.1.3",NS:{HTTPBIND:"http://jabber.org/protocol/httpbind",BOSH:"urn:xmpp:xbosh",CLIENT:"jabber:client",AUTH:"jabber:iq:auth",ROSTER:"jabber:iq:roster",PROFILE:"jabber:iq:profile",DISCO_INFO:"http://jabber.org/protocol/disco#info",DISCO_ITEMS:"http://jabber.org/protocol/disco#items",MUC:"http://jabber.org/protocol/muc",SASL:"urn:ietf:params:xml:ns:xmpp-sasl",STREAM:"http://etherx.jabber.org/streams",BIND:"urn:ietf:params:xml:ns:xmpp-bind",SESSION:"urn:ietf:params:xml:ns:xmpp-session",VERSION:"jabber:iq:version",STANZAS:"urn:ietf:params:xml:ns:xmpp-stanzas",XHTML_IM:"http://jabber.org/protocol/xhtml-im",XHTML:"http://www.w3.org/1999/xhtml"},XHTML:{tags:["a","blockquote","br","cite","em","img","li","ol","p","span","strong","ul","body"],attributes:{a:["href"],blockquote:["style"],br:[],cite:["style"],em:[],img:["src","alt","style","height","width"],li:["style"],ol:["style"],p:["style"],span:["style"],strong:[],ul:["style"],body:[]},css:["background-color","color","font-family","font-size","font-style","font-weight","margin-left","margin-right","text-align","text-decoration"],validTag:function(a){for(var b=0;b0)for(var c=0;c/g,">"),a=a.replace(/'/g,"'"),a=a.replace(/"/g,""")},xmlTextNode:function(a){return f.xmlGenerator().createTextNode(a)},xmlHtmlNode:function(a){var b;if(window.DOMParser){var c=new DOMParser;b=c.parseFromString(a,"text/xml")}else b=new ActiveXObject("Microsoft.XMLDOM"),b.async="false",b.loadXML(a);return b},getText:function(a){if(!a)return null;var b="";0===a.childNodes.length&&a.nodeType==f.ElementType.TEXT&&(b+=a.nodeValue);for(var c=0;c0&&(h=i.join("; "),c.setAttribute(g,h))}else c.setAttribute(g,h);for(b=0;b/g,"\\3e").replace(/@/g,"\\40")},unescapeNode:function(a){return a.replace(/\\20/g," ").replace(/\\22/g,'"').replace(/\\26/g,"&").replace(/\\27/g,"'").replace(/\\2f/g,"/").replace(/\\3a/g,":").replace(/\\3c/g,"<").replace(/\\3e/g,">").replace(/\\40/g,"@").replace(/\\5c/g,"\\")},getNodeFromJid:function(a){return a.indexOf("@")<0?null:a.split("@")[0]},getDomainFromJid:function(a){var b=f.getBareJidFromJid(a);if(b.indexOf("@")<0)return b;var c=b.split("@");return c.splice(0,1),c.join("@")},getResourceFromJid:function(a){var b=a.split("/");return b.length<2?null:(b.splice(0,1),b.join("/"))},getBareJidFromJid:function(a){return a?a.split("/")[0]:null},log:function(){},debug:function(a){this.log(this.LogLevel.DEBUG,a)},info:function(a){this.log(this.LogLevel.INFO,a)},warn:function(a){this.log(this.LogLevel.WARN,a)},error:function(a){this.log(this.LogLevel.ERROR,a)},fatal:function(a){this.log(this.LogLevel.FATAL,a)},serialize:function(a){var b;if(!a)return null;"function"==typeof a.tree&&(a=a.tree());var c,d,e=a.nodeName;for(a.getAttribute("_realname")&&(e=a.getAttribute("_realname")),b="<"+e,c=0;c/g,">").replace(/0){for(b+=">",c=0;c"}b+=""+e+">"}else b+="/>";return b},_requestId:0,_connectionPlugins:{},addConnectionPlugin:function(a,b){f._connectionPlugins[a]=b}},f.Builder=function(a,b){("presence"==a||"message"==a||"iq"==a)&&(b&&!b.xmlns?b.xmlns=f.NS.CLIENT:b||(b={xmlns:f.NS.CLIENT})),this.nodeTree=f.xmlElement(a,b),this.node=this.nodeTree},f.Builder.prototype={tree:function(){return this.nodeTree},toString:function(){return f.serialize(this.nodeTree)},up:function(){return this.node=this.node.parentNode,this},attrs:function(a){for(var b in a)a.hasOwnProperty(b)&&this.node.setAttribute(b,a[b]);return this},c:function(a,b,c){var d=f.xmlElement(a,b,c);return this.node.appendChild(d),c||(this.node=d),this},cnode:function(a){var b,c=f.xmlGenerator();try{b=void 0!==c.importNode}catch(d){b=!1}var e=b?c.importNode(a,!0):f.copyElement(a);return this.node.appendChild(e),this.node=e,this},t:function(a){var b=f.xmlTextNode(a);return this.node.appendChild(b),this},h:function(a){var b=document.createElement("body");b.innerHTML=a;for(var c=f.createHtml(b);c.childNodes.length>0;)this.node.appendChild(c.childNodes[0]);return this}},f.Handler=function(a,b,c,d,e,g,h){this.handler=a,this.ns=b,this.name=c,this.type=d,this.id=e,this.options=h||{matchBare:!1},this.options.matchBare||(this.options.matchBare=!1),this.from=this.options.matchBare?g?f.getBareJidFromJid(g):null:g,this.user=!0},f.Handler.prototype={isMatch:function(a){var b,c=null;if(c=this.options.matchBare?f.getBareJidFromJid(a.getAttribute("from")):a.getAttribute("from"),b=!1,this.ns){var d=this;f.forEachChild(a,null,function(a){a.getAttribute("xmlns")==d.ns&&(b=!0)}),b=b||a.getAttribute("xmlns")==this.ns}else b=!0;return!b||this.name&&!f.isTagEqual(a,this.name)||this.type&&a.getAttribute("type")!=this.type||this.id&&a.getAttribute("id")!=this.id||this.from&&c!=this.from?!1:!0},run:function(a){var b=null;try{b=this.handler(a)}catch(c){throw c.sourceURL?f.fatal("error: "+this.handler+" "+c.sourceURL+":"+c.line+" - "+c.name+": "+c.message):c.fileName?("undefined"!=typeof console&&(console.trace(),console.error(this.handler," - error - ",c,c.message)),f.fatal("error: "+this.handler+" "+c.fileName+":"+c.lineNumber+" - "+c.name+": "+c.message)):f.fatal("error: "+c.message+"\n"+c.stack),c}return b},toString:function(){return"{Handler: "+this.handler+"("+this.name+","+this.id+","+this.ns+")}"}},f.TimedHandler=function(a,b){this.period=a,this.handler=b,this.lastCalled=(new Date).getTime(),this.user=!0},f.TimedHandler.prototype={run:function(){return this.lastCalled=(new Date).getTime(),this.handler()},reset:function(){this.lastCalled=(new Date).getTime()},toString:function(){return"{TimedHandler: "+this.handler+"("+this.period+")}"}},f.Connection=function(a,b){this.service=a,this.options=b||{};var c=this.options.protocol||"";this._proto=0===a.indexOf("ws:")||0===a.indexOf("wss:")||0===c.indexOf("ws")?new f.Websocket(this):new f.Bosh(this),this.jid="",this.domain=null,this.features=null,this._sasl_data={},this.do_session=!1,this.do_bind=!1,this.timedHandlers=[],this.handlers=[],this.removeTimeds=[],this.removeHandlers=[],this.addTimeds=[],this.addHandlers=[],this._authentication={},this._idleTimeout=null,this._disconnectTimeout=null,this.do_authentication=!0,this.authenticated=!1,this.disconnecting=!1,this.connected=!1,this.errors=0,this.paused=!1,this._data=[],this._uniqueId=0,this._sasl_success_handler=null,this._sasl_failure_handler=null,this._sasl_challenge_handler=null,this.maxRetries=5,this._idleTimeout=setTimeout(this._onIdle.bind(this),100);for(var d in f._connectionPlugins)if(f._connectionPlugins.hasOwnProperty(d)){var e=f._connectionPlugins[d],g=function(){};g.prototype=e,this[d]=new g,this[d].init(this)}},f.Connection.prototype={reset:function(){this._proto._reset(),this.do_session=!1,this.do_bind=!1,this.timedHandlers=[],this.handlers=[],this.removeTimeds=[],this.removeHandlers=[],this.addTimeds=[],this.addHandlers=[],this._authentication={},this.authenticated=!1,this.disconnecting=!1,this.connected=!1,this.errors=0,this._requests=[],this._uniqueId=0},pause:function(){this.paused=!0},resume:function(){this.paused=!1},getUniqueId:function(a){return"string"==typeof a||"number"==typeof a?++this._uniqueId+":"+a:++this._uniqueId+""},connect:function(a,b,c,d,e,g){this.jid=a,this.authzid=f.getBareJidFromJid(this.jid),this.authcid=f.getNodeFromJid(this.jid),this.pass=b,this.servtype="xmpp",this.connect_callback=c,this.disconnecting=!1,this.connected=!1,this.authenticated=!1,this.errors=0,this.domain=f.getDomainFromJid(this.jid),this._changeConnectStatus(f.Status.CONNECTING,null),this._proto._connect(d,e,g)},attach:function(a,b,c,d,e,f,g){this._proto._attach(a,b,c,d,e,f,g)},xmlInput:function(){},xmlOutput:function(){},rawInput:function(){},rawOutput:function(){},send:function(a){if(null!==a){if("function"==typeof a.sort)for(var b=0;b0;)e=this.removeHandlers.pop(),d=this.handlers.indexOf(e),d>=0&&this.handlers.splice(d,1);for(;this.addHandlers.length>0;)this.handlers.push(this.addHandlers.pop());if(this.disconnecting&&this._proto._emptyQueue())return this._doDisconnect(),void 0;var g,h,i=c.getAttribute("type");if(null!==i&&"terminate"==i){if(this.disconnecting)return;return g=c.getAttribute("condition"),h=c.getElementsByTagName("conflict"),null!==g?("remote-stream-error"==g&&h.length>0&&(g="conflict"),this._changeConnectStatus(f.Status.CONNFAIL,g)):this._changeConnectStatus(f.Status.CONNFAIL,"unknown"),this.disconnect("unknown stream-error"),void 0}var j=this;f.forEachChild(c,null,function(a){var b,c;for(c=j.handlers,j.handlers=[],b=0;b0;g||(g=d.getElementsByTagName("features").length>0);var h,i,j=d.getElementsByTagName("mechanism"),k=[],l=!1;if(!g)return this._proto._no_auth_received(b),void 0;if(j.length>0)for(h=0;h0,(l=this._authentication.legacy_auth||k.length>0)?(this.do_authentication!==!1&&this.authenticate(k),void 0):(this._proto._no_auth_received(b),void 0)}}},authenticate:function(a){var c;for(c=0;ca[e].prototype.priority&&(e=g);if(e!=c){var h=a[c];a[c]=a[e],a[e]=h}}var i=!1;for(c=0;c0&&(b="conflict"),this._changeConnectStatus(f.Status.AUTHFAIL,b),!1}var e,g=a.getElementsByTagName("bind");return g.length>0?(e=g[0].getElementsByTagName("jid"),e.length>0&&(this.jid=f.getText(e[0]),this.do_session?(this._addSysHandler(this._sasl_session_cb.bind(this),null,null,null,"_session_auth_2"),this.send(d({type:"set",id:"_session_auth_2"}).c("session",{xmlns:f.NS.SESSION}).tree())):(this.authenticated=!0,this._changeConnectStatus(f.Status.CONNECTED,null))),void 0):(f.info("SASL binding failed."),this._changeConnectStatus(f.Status.AUTHFAIL,null),!1)},_sasl_session_cb:function(a){if("result"==a.getAttribute("type"))this.authenticated=!0,this._changeConnectStatus(f.Status.CONNECTED,null);else if("error"==a.getAttribute("type"))return f.info("Session creation failed."),this._changeConnectStatus(f.Status.AUTHFAIL,null),!1;return!1},_sasl_failure_cb:function(){return this._sasl_success_handler&&(this.deleteHandler(this._sasl_success_handler),this._sasl_success_handler=null),this._sasl_challenge_handler&&(this.deleteHandler(this._sasl_challenge_handler),this._sasl_challenge_handler=null),this._sasl_mechanism&&this._sasl_mechanism.onFailure(),this._changeConnectStatus(f.Status.AUTHFAIL,null),!1},_auth2_cb:function(a){return"result"==a.getAttribute("type")?(this.authenticated=!0,this._changeConnectStatus(f.Status.CONNECTED,null)):"error"==a.getAttribute("type")&&(this._changeConnectStatus(f.Status.AUTHFAIL,null),this.disconnect("authentication failed")),!1},_addSysTimedHandler:function(a,b){var c=new f.TimedHandler(a,b);return c.user=!1,this.addTimeds.push(c),c},_addSysHandler:function(a,b,c,d,e){var g=new f.Handler(a,b,c,d,e);return g.user=!1,this.addHandlers.push(g),g},_onDisconnectTimeout:function(){return f.info("_onDisconnectTimeout was called"),this._proto._onDisconnectTimeout(),this._doDisconnect(),!1},_onIdle:function(){for(var a,b,c,d;this.addTimeds.length>0;)this.timedHandlers.push(this.addTimeds.pop());for(;this.removeTimeds.length>0;)b=this.removeTimeds.pop(),a=this.timedHandlers.indexOf(b),a>=0&&this.timedHandlers.splice(a,1);var e=(new Date).getTime();for(d=[],a=0;a=c-e?b.run()&&d.push(b):d.push(b));this.timedHandlers=d,clearTimeout(this._idleTimeout),this._proto._onIdle(),this.connected&&(this._idleTimeout=setTimeout(this._onIdle.bind(this),100))}},a&&a(f,b,c,d,e),f.SASLMechanism=function(a,b,c){this.name=a,this.isClientFirst=b,this.priority=c},f.SASLMechanism.prototype={test:function(){return!0},onStart:function(a){this._connection=a},onChallenge:function(){throw new Error("You should implement challenge handling!")},onFailure:function(){this._connection=null},onSuccess:function(){this._connection=null}},f.SASLAnonymous=function(){},f.SASLAnonymous.prototype=new f.SASLMechanism("ANONYMOUS",!1,10),f.SASLAnonymous.test=function(a){return null===a.authcid},f.Connection.prototype.mechanisms[f.SASLAnonymous.prototype.name]=f.SASLAnonymous,f.SASLPlain=function(){},f.SASLPlain.prototype=new f.SASLMechanism("PLAIN",!0,20),f.SASLPlain.test=function(a){return null!==a.authcid},f.SASLPlain.prototype.onChallenge=function(a){var b=a.authzid;return b+="\x00",b+=a.authcid,b+="\x00",b+=a.pass},f.Connection.prototype.mechanisms[f.SASLPlain.prototype.name]=f.SASLPlain,f.SASLSHA1=function(){},f.SASLSHA1.prototype=new f.SASLMechanism("SCRAM-SHA-1",!0,40),f.SASLSHA1.test=function(a){return null!==a.authcid},f.SASLSHA1.prototype.onChallenge=function(a,b,c){var d=c||MD5.hexdigest(1234567890*Math.random()),e="n="+a.authcid;return e+=",r=",e+=d,a._sasl_data.cnonce=d,a._sasl_data["client-first-message-bare"]=e,e="n,,"+e,this.onChallenge=function(a,b){for(var c,d,e,f,g,h,i,j,k,l,m,n="c=biws,",o=a._sasl_data["client-first-message-bare"]+","+b+",",p=a._sasl_data.cnonce,q=/([a-z]+)=([^,]+)(,|$)/;b.match(q);){var r=b.match(q);switch(b=b.replace(r[0],""),r[1]){case"r":c=r[2];break;case"s":d=r[2];break;case"i":e=r[2]}}if(c.substr(0,p.length)!==p)return a._sasl_data={},a._sasl_failure_cb();for(n+="r="+c,o+=n,d=Base64.decode(d),d+="\x00\x00\x00",f=h=core_hmac_sha1(a.pass,d),i=1;e>i;i++){for(g=core_hmac_sha1(a.pass,binb2str(h)),j=0;5>j;j++)f[j]^=g[j];h=g}for(f=binb2str(f),k=core_hmac_sha1(f,"Client Key"),l=str_hmac_sha1(f,"Server Key"),m=core_hmac_sha1(str_sha1(binb2str(k)),o),a._sasl_data["server-signature"]=b64_hmac_sha1(l,o),j=0;5>j;j++)k[j]^=m[j];return n+=",p="+Base64.encode(binb2str(k))}.bind(this),e},f.Connection.prototype.mechanisms[f.SASLSHA1.prototype.name]=f.SASLSHA1,f.SASLMD5=function(){},f.SASLMD5.prototype=new f.SASLMechanism("DIGEST-MD5",!1,30),f.SASLMD5.test=function(a){return null!==a.authcid},f.SASLMD5.prototype._quote=function(a){return'"'+a.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'},f.SASLMD5.prototype.onChallenge=function(a,b,c){for(var d,e=/([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/,f=c||MD5.hexdigest(""+1234567890*Math.random()),g="",h=null,i="",j="";b.match(e);)switch(d=b.match(e),b=b.replace(d[0],""),d[2]=d[2].replace(/^"(.+)"$/,"$1"),d[1]){case"realm":g=d[2];
+break;case"nonce":i=d[2];break;case"qop":j=d[2];break;case"host":h=d[2]}var k=a.servtype+"/"+a.domain;null!==h&&(k=k+"/"+h);var l=MD5.hash(a.authcid+":"+g+":"+this._connection.pass)+":"+i+":"+f,m="AUTHENTICATE:"+k,n="";return n+="charset=utf-8,",n+="username="+this._quote(a.authcid)+",",n+="realm="+this._quote(g)+",",n+="nonce="+this._quote(i)+",",n+="nc=00000001,",n+="cnonce="+this._quote(f)+",",n+="digest-uri="+this._quote(k)+",",n+="response="+MD5.hexdigest(MD5.hexdigest(l)+":"+i+":00000001:"+f+":auth:"+MD5.hexdigest(m))+",",n+="qop=auth",this.onChallenge=function(){return""}.bind(this),n},f.Connection.prototype.mechanisms[f.SASLMD5.prototype.name]=f.SASLMD5}(function(){window.Strophe=arguments[0],window.$build=arguments[1],window.$msg=arguments[2],window.$iq=arguments[3],window.$pres=arguments[4]}),Strophe.Request=function(a,b,c,d){this.id=++Strophe._requestId,this.xmlData=a,this.data=Strophe.serialize(a),this.origFunc=b,this.func=b,this.rid=c,this.date=0/0,this.sends=d||0,this.abort=!1,this.dead=null,this.age=function(){if(!this.date)return 0;var a=new Date;return(a-this.date)/1e3},this.timeDead=function(){if(!this.dead)return 0;var a=new Date;return(a-this.dead)/1e3},this.xhr=this._newXHR()},Strophe.Request.prototype={getResponse:function(){var a=null;if(this.xhr.responseXML&&this.xhr.responseXML.documentElement){if(a=this.xhr.responseXML.documentElement,"parsererror"==a.tagName)throw Strophe.error("invalid response received"),Strophe.error("responseText: "+this.xhr.responseText),Strophe.error("responseXML: "+Strophe.serialize(this.xhr.responseXML)),"parsererror"}else this.xhr.responseText&&(Strophe.error("invalid response received"),Strophe.error("responseText: "+this.xhr.responseText),Strophe.error("responseXML: "+Strophe.serialize(this.xhr.responseXML)));return a},_newXHR:function(){var a=null;return window.XMLHttpRequest?(a=new XMLHttpRequest,a.overrideMimeType&&a.overrideMimeType("text/xml")):window.ActiveXObject&&(a=new ActiveXObject("Microsoft.XMLHTTP")),a.onreadystatechange=this.func.bind(null,this),a}},Strophe.Bosh=function(a){this._conn=a,this.rid=Math.floor(4294967295*Math.random()),this.sid=null,this.hold=1,this.wait=60,this.window=5,this._requests=[]},Strophe.Bosh.prototype={strip:null,_buildBody:function(){var a=$build("body",{rid:this.rid++,xmlns:Strophe.NS.HTTPBIND});return null!==this.sid&&a.attrs({sid:this.sid}),a},_reset:function(){this.rid=Math.floor(4294967295*Math.random()),this.sid=null},_connect:function(a,b,c){this.wait=a||this.wait,this.hold=b||this.hold;var d=this._buildBody().attrs({to:this._conn.domain,"xml:lang":"en",wait:this.wait,hold:this.hold,content:"text/xml; charset=utf-8",ver:"1.6","xmpp:version":"1.0","xmlns:xmpp":Strophe.NS.BOSH});c&&d.attrs({route:c});var e=this._conn._connect_cb;this._requests.push(new Strophe.Request(d.tree(),this._onRequestStateChange.bind(this,e.bind(this._conn)),d.tree().getAttribute("rid"))),this._throttledRequestHandler()},_attach:function(a,b,c,d,e,f,g){this._conn.jid=a,this.sid=b,this.rid=c,this._conn.connect_callback=d,this._conn.domain=Strophe.getDomainFromJid(this._conn.jid),this._conn.authenticated=!0,this._conn.connected=!0,this.wait=e||this.wait,this.hold=f||this.hold,this.window=g||this.window,this._conn._changeConnectStatus(Strophe.Status.ATTACHED,null)},_connect_cb:function(a){var b,c,d=a.getAttribute("type");if(null!==d&&"terminate"==d)return Strophe.error("BOSH-Connection failed: "+b),b=a.getAttribute("condition"),c=a.getElementsByTagName("conflict"),null!==b?("remote-stream-error"==b&&c.length>0&&(b="conflict"),this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,b)):this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"unknown"),this._conn._doDisconnect(),Strophe.Status.CONNFAIL;this.sid||(this.sid=a.getAttribute("sid"));var e=a.getAttribute("requests");e&&(this.window=parseInt(e,10));var f=a.getAttribute("hold");f&&(this.hold=parseInt(f,10));var g=a.getAttribute("wait");g&&(this.wait=parseInt(g,10))},_disconnect:function(a){this._sendTerminate(a)},_doDisconnect:function(){this.sid=null,this.rid=Math.floor(4294967295*Math.random())},_emptyQueue:function(){return 0===this._requests.length},_hitError:function(a){this.errors++,Strophe.warn("request errored, status: "+a+", number of errors: "+this.errors),this.errors>4&&this._onDisconnectTimeout()},_no_auth_received:function(a){a=a?a.bind(this._conn):this._conn._connect_cb.bind(this._conn);var b=this._buildBody();this._requests.push(new Strophe.Request(b.tree(),this._onRequestStateChange.bind(this,a.bind(this._conn)),b.tree().getAttribute("rid"))),this._throttledRequestHandler()},_onDisconnectTimeout:function(){for(var a;this._requests.length>0;)a=this._requests.pop(),a.abort=!0,a.xhr.abort(),a.xhr.onreadystatechange=function(){}},_onIdle:function(){var a=this._conn._data;if(this._conn.authenticated&&0===this._requests.length&&0===a.length&&!this._conn.disconnecting&&(Strophe.info("no requests during idle cycle, sending blank request"),a.push(null)),this._requests.length<2&&a.length>0&&!this._conn.paused){for(var b=this._buildBody(),c=0;c0){var d=this._requests[0].age();null!==this._requests[0].dead&&this._requests[0].timeDead()>Math.floor(Strophe.SECONDARY_TIMEOUT*this.wait)&&this._throttledRequestHandler(),d>Math.floor(Strophe.TIMEOUT*this.wait)&&(Strophe.warn("Request "+this._requests[0].id+" timed out, over "+Math.floor(Strophe.TIMEOUT*this.wait)+" seconds since last activity"),this._throttledRequestHandler())}},_onRequestStateChange:function(a,b){if(Strophe.debug("request id "+b.id+"."+b.sends+" state changed to "+b.xhr.readyState),b.abort)return b.abort=!1,void 0;var c;if(4==b.xhr.readyState){c=0;try{c=b.xhr.status}catch(d){}if("undefined"==typeof c&&(c=0),this.disconnecting&&c>=400)return this._hitError(c),void 0;var e=this._requests[0]==b,f=this._requests[1]==b;(c>0&&500>c||b.sends>5)&&(this._removeRequest(b),Strophe.debug("request id "+b.id+" should now be removed")),200==c?((f||e&&this._requests.length>0&&this._requests[0].age()>Math.floor(Strophe.SECONDARY_TIMEOUT*this.wait))&&this._restartRequest(0),Strophe.debug("request id "+b.id+"."+b.sends+" got 200"),a(b),this.errors=0):(Strophe.error("request id "+b.id+"."+b.sends+" error "+c+" happened"),(0===c||c>=400&&600>c||c>=12e3)&&(this._hitError(c),c>=400&&500>c&&(this._conn._changeConnectStatus(Strophe.Status.DISCONNECTING,null),this._conn._doDisconnect()))),c>0&&500>c||b.sends>5||this._throttledRequestHandler()}},_processRequest:function(a){var b=this,c=this._requests[a],d=-1;try{4==c.xhr.readyState&&(d=c.xhr.status)}catch(e){Strophe.error("caught an error in _requests["+a+"], reqStatus: "+d)}if("undefined"==typeof d&&(d=-1),c.sends>this.maxRetries)return this._onDisconnectTimeout(),void 0;var f=c.age(),g=!isNaN(f)&&f>Math.floor(Strophe.TIMEOUT*this.wait),h=null!==c.dead&&c.timeDead()>Math.floor(Strophe.SECONDARY_TIMEOUT*this.wait),i=4==c.xhr.readyState&&(1>d||d>=500);if((g||h||i)&&(h&&Strophe.error("Request "+this._requests[a].id+" timed out (secondary), restarting"),c.abort=!0,c.xhr.abort(),c.xhr.onreadystatechange=function(){},this._requests[a]=new Strophe.Request(c.xmlData,c.origFunc,c.rid,c.sends),c=this._requests[a]),0===c.xhr.readyState){Strophe.debug("request id "+c.id+"."+c.sends+" posting");try{c.xhr.open("POST",this._conn.service,this._conn.options.sync?!1:!0)}catch(j){return Strophe.error("XHR open failed."),this._conn.connected||this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"bad-service"),this._conn.disconnect(),void 0}var k=function(){if(c.date=new Date,b._conn.options.customHeaders){var a=b._conn.options.customHeaders;for(var d in a)a.hasOwnProperty(d)&&c.xhr.setRequestHeader(d,a[d])}c.xhr.send(c.data)};if(c.sends>1){var l=1e3*Math.min(Math.floor(Strophe.TIMEOUT*this.wait),Math.pow(c.sends,3));setTimeout(k,l)}else k();c.sends++,this._conn.xmlOutput!==Strophe.Connection.prototype.xmlOutput&&(c.xmlData.nodeName===this.strip&&c.xmlData.childNodes.length?this._conn.xmlOutput(c.xmlData.childNodes[0]):this._conn.xmlOutput(c.xmlData)),this._conn.rawOutput!==Strophe.Connection.prototype.rawOutput&&this._conn.rawOutput(c.data)}else Strophe.debug("_processRequest: "+(0===a?"first":"second")+" request has readyState of "+c.xhr.readyState)},_removeRequest:function(a){Strophe.debug("removing request");var b;for(b=this._requests.length-1;b>=0;b--)a==this._requests[b]&&this._requests.splice(b,1);a.xhr.onreadystatechange=function(){},this._throttledRequestHandler()},_restartRequest:function(a){var b=this._requests[a];null===b.dead&&(b.dead=new Date),this._processRequest(a)},_reqToData:function(a){try{return a.getResponse()}catch(b){if("parsererror"!=b)throw b;this._conn.disconnect("strophe-parsererror")}},_sendTerminate:function(a){Strophe.info("_sendTerminate was called");var b=this._buildBody().attrs({type:"terminate"});a&&b.cnode(a.tree());var c=new Strophe.Request(b.tree(),this._onRequestStateChange.bind(this,this._conn._dataRecv.bind(this._conn)),b.tree().getAttribute("rid"));this._requests.push(c),this._throttledRequestHandler()},_send:function(){clearTimeout(this._conn._idleTimeout),this._throttledRequestHandler(),this._conn._idleTimeout=setTimeout(this._conn._onIdle.bind(this._conn),100)},_sendRestart:function(){this._throttledRequestHandler(),clearTimeout(this._conn._idleTimeout)},_throttledRequestHandler:function(){this._requests?Strophe.debug("_throttledRequestHandler called with "+this._requests.length+" requests"):Strophe.debug("_throttledRequestHandler called with undefined requests"),this._requests&&0!==this._requests.length&&(this._requests.length>0&&this._processRequest(0),this._requests.length>1&&Math.abs(this._requests[0].rid-this._requests[1].rid)\s*)*/,"");if(""===b)return;b=a.data.replace(//,"");var c=(new DOMParser).parseFromString(b,"text/xml").documentElement;this._conn.xmlInput(c),this._conn.rawInput(a.data),this._handleStreamStart(c)&&(this._connect_cb(c),this.streamStart=a.data.replace(/^$/,""))}else{if(""===a.data)return this._conn.rawInput(a.data),this._conn.xmlInput(document.createElement("stream:stream")),this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"Received closing stream"),this._conn._doDisconnect(),void 0;var d=this._streamWrap(a.data),e=(new DOMParser).parseFromString(d,"text/xml").documentElement;this.socket.onmessage=this._onMessage.bind(this),this._conn._connect_cb(e,null,a.data)}},_disconnect:function(a){if(this.socket.readyState!==WebSocket.CLOSED){a&&this._conn.send(a);var b="";this._conn.xmlOutput(document.createElement("stream:stream")),this._conn.rawOutput(b);try{this.socket.send(b)}catch(c){Strophe.info("Couldn't send closing stream tag.")}}this._conn._doDisconnect()},_doDisconnect:function(){Strophe.info("WebSockets _doDisconnect was called"),this._closeSocket()},_streamWrap:function(a){return this.streamStart+a+""},_closeSocket:function(){if(this.socket)try{this.socket.close()}catch(a){}this.socket=null},_emptyQueue:function(){return!0},_onClose:function(){this._conn.connected&&!this._conn.disconnecting?(Strophe.error("Websocket closed unexcectedly"),this._conn._doDisconnect()):Strophe.info("Websocket closed")},_no_auth_received:function(a){Strophe.error("Server did not send any auth methods"),this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"Server did not send any auth methods"),a&&(a=a.bind(this._conn))(),this._conn._doDisconnect()},_onDisconnectTimeout:function(){},_onError:function(a){Strophe.error("Websocket error "+a),this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"The WebSocket connection could not be established was disconnected."),this._disconnect()},_onIdle:function(){var a=this._conn._data;if(a.length>0&&!this._conn.paused){for(var b=0;b"===a.data){var d="";return this._conn.rawInput(d),this._conn.xmlInput(document.createElement("stream:stream")),this._conn.disconnecting||this._conn._doDisconnect(),void 0}if(0===a.data.search("/,""),b=(new DOMParser).parseFromString(c,"text/xml").documentElement,!this._handleStreamStart(b))return}else c=this._streamWrap(a.data),b=(new DOMParser).parseFromString(c,"text/xml").documentElement;if(!this._check_streamerror(b,Strophe.Status.ERROR))return this._conn.disconnecting&&"presence"===b.firstChild.nodeName&&"unavailable"===b.firstChild.getAttribute("type")?(this._conn.xmlInput(b),this._conn.rawInput(Strophe.serialize(b)),void 0):(this._conn._dataRecv(b,a.data),void 0)},_onOpen:function(){Strophe.info("Websocket open");var a=this._buildStream();this._conn.xmlOutput(a.tree());var b=this._removeClosingTag(a);this._conn.rawOutput(b),this.socket.send(b)},_removeClosingTag:function(a){var b=Strophe.serialize(a);return b=b.replace(/<(stream:stream .*[^\/])\/>$/,"<$1>")},_reqToData:function(a){return a},_send:function(){this._conn.flush()},_sendRestart:function(){clearTimeout(this._conn._idleTimeout),this._conn._onIdle.bind(this._conn)()}};Strophe.addConnectionPlugin("disco",{_connection:null,_identities:[],_features:[],_items:[],init:function(a){this._connection=a;this._identities=[];this._features=[];this._items=[];a.addHandler(this._onDiscoInfo.bind(this),Strophe.NS.DISCO_INFO,"iq","get",null,null);a.addHandler(this._onDiscoItems.bind(this),Strophe.NS.DISCO_ITEMS,"iq","get",null,null)},addIdentity:function(d,c,a,e){for(var b=0;bjingle'), '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();
+ $(document).trigger('callaccepted.jingle', [sess.sid]);
+ 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
+ var sess = new JingleSession(myjid || this.connection.jid,
+ 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
+ var self = this;
+ this.connection.sendIQ(
+ $iq({type: 'get', to: this.connection.domain})
+ .c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
+ function (res) {
+ var iceservers = [];
+ $(res).find('>services>service').each(function (idx, el) {
+ el = $(el);
+ var dict = {};
+ 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;
+ }
+ });
+ self.ice_config.iceServers = iceservers;
+ },
+ function (err) {
+ console.warn('getting turn credentials failed', err);
+ console.warn('is mod_turncredentials or similar installed?');
+ }
+ );
+ // implement push?
+ }
+});
diff --git a/libs/strophe/strophe.jingle.sdp.js b/libs/strophe/strophe.jingle.sdp.js
new file mode 100644
index 000000000..f1b9f940f
--- /dev/null
+++ b/libs/strophe/strophe.jingle.sdp.js
@@ -0,0 +1,801 @@
+/* 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) {
+ var self = this;
+ var lines = SDPUtil.find_lines(this.session, prefix);
+ lines.forEach(function(line) {
+ self.session = self.session.replace(line + '\r\n', '');
+ });
+ 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) {
+ var self = this;
+ var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
+ lines.forEach(function(line) {
+ self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
+ });
+ 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;
+ var self = this;
+ // 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);
+ elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics: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
+ 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();
+ }
+ }
+ this.RtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
+
+ 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:*
+ this.RtcpFbToJingle(i, elem, '*');
+
+ // 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
+ }
+
+ // map ice-ufrag/pwd, dtls fingerprint, candidates
+ this.TransportToJingle(i, elem);
+
+ 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;
+};
+
+SDP.prototype.TransportToJingle = function (mediaindex, elem) {
+ var i = mediaindex;
+ var tmp;
+ var self = this;
+ 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;
+ line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
+ 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);
+ 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();
+ }
+ });
+};
+
+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) {
+ var self = this;
+ 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) {
+ return content.getAttribute('name');
+ }).get();
+ if (contents.length > 0) {
+ self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
+ }
+ });
+ } 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) {
+ return content.getAttribute('name');
+ }).get();
+ if (group.getAttribute('type') !== null && contents.length > 0) {
+ self.raw += 'a=group:' + group.getAttribute('type') + ' ' + contents.join(' ') + '\r\n';
+ }
+ });
+ } 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) {
+ return content.getAttribute('name');
+ }).get();
+ if (bundle.length) {
+ this.raw += 'a=group:BUNDLE ' + bundle.join(' ') + '\r\n';
+ }
+ }
+
+ this.session = this.raw;
+ jingle.find('>content').each(function () {
+ var m = self.jingle2media($(this));
+ self.media.push(m);
+ });
+
+ // 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';
+ }
+ tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get();
+ 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
+ media += 'a=fingerprint:' + this.getAttribute('hash');
+ media += ' ' + $(this).text();
+ media += '\r\n';
+ if (this.getAttribute('setup')) {
+ media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
+ }
+ });
+ }
+ 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';
+
+ //
+ // 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 () {
+ 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');
+ }
+ media += '\r\n';
+ });
+ }
+ desc.find('payload-type').each(function () {
+ media += SDPUtil.build_rtpmap(this) + '\r\n';
+ if ($(this).find('>parameter').length) {
+ media += 'a=fmtp:' + this.getAttribute('id') + ' ';
+ media += $(this).find('parameter').map(function () { return (this.getAttribute('name') ? (this.getAttribute('name') + '=') : '') + this.getAttribute('value'); }).get().join(';');
+ media += '\r\n';
+ }
+ // xep-0293
+ media += self.RtcpFbFromJingle($(this), this.getAttribute('id'));
+ });
+
+ // 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 () {
+ media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
+ });
+
+ 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 () {
+ var ssrc = this.getAttribute('ssrc');
+ $(this).find('>parameter').each(function () {
+ media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
+ if (this.getAttribute('value') && this.getAttribute('value').length)
+ media += ':' + this.getAttribute('value');
+ 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
+ //
+ 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';
+ }
+};
diff --git a/libs/strophe/strophe.jingle.session.js b/libs/strophe/strophe.jingle.session.js
new file mode 100644
index 000000000..730162192
--- /dev/null
+++ b/libs/strophe/strophe.jingle.session.js
@@ -0,0 +1,858 @@
+/* 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.pendingop = null;
+
+ this.wait = true;
+}
+
+JingleSession.prototype.initiate = function (peerjid, isInitiator) {
+ var self = this;
+ 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;
+ //console.log('create PeerConnection ' + JSON.stringify(this.ice_config));
+ 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) {
+ self.sendIceCandidate(event.candidate);
+ };
+ this.peerconnection.onaddstream = function (event) {
+ self.remoteStream = event.stream;
+ self.remoteStreams.push(event.stream);
+ $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
+ };
+ this.peerconnection.onremovestream = function (event) {
+ self.remoteStream = null;
+ // FIXME: remove from this.remoteStreams
+ $(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
+ };
+ this.peerconnection.onsignalingstatechange = function (event) {
+ if (!(self && self.peerconnection)) return;
+ };
+ this.peerconnection.oniceconnectionstatechange = function (event) {
+ if (!(self && self.peerconnection)) return;
+ switch (self.peerconnection.iceConnectionState) {
+ case 'connected':
+ this.startTime = new Date();
+ break;
+ case 'disconnected':
+ this.stopTime = new Date();
+ break;
+ }
+ $(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
+ };
+ // add any local and relayed stream
+ this.localStreams.forEach(function(stream) {
+ self.peerconnection.addStream(stream);
+ });
+ this.relayedStreams.forEach(function(stream) {
+ self.peerconnection.addStream(stream);
+ });
+};
+
+JingleSession.prototype.accept = function () {
+ var self = this;
+ 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';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ error.source = 'answer';
+ $(document).trigger('error.jingle', [self.sid, error]);
+ },
+ 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 () {
+ //console.log('setLocalDescription success');
+ $(document).trigger('setLocalDescription.jingle', [self.sid]);
+ },
+ 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) {
+ var self = this;
+ if (candidate && !this.lasticecandidate) {
+ var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
+ var jcand = SDPUtil.candidateToJingle(candidate.candidate);
+ 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 20ms callout
+ window.setTimeout(function () {
+ if (self.drip_container.length === 0) return;
+ self.sendIceCandidates(self.drip_container);
+ self.drip_container = [];
+ }, 20);
+
+ }
+ this.drip_container.push(event.candidate);
+ return;
+ } else {
+ self.sendIceCandidate([event.candidate]);
+ }
+ }
+ } else {
+ //console.log('sendIceCandidate: last candidate.');
+ if (!this.usetrickle) {
+ //console.log('should send full offer now...');
+ 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 () {
+ //console.log('session initiate ack');
+ var ack = {};
+ ack.source = 'offer';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ self.state = 'error';
+ self.peerconnection.close();
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ error.source = 'offer';
+ $(document).trigger('error.jingle', [self.sid, error]);
+ },
+ 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.sendIceCandidates = function (candidates) {
+ console.log('sendIceCandidates', candidates);
+ var cand = $iq({to: this.peerjid, type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ action: 'transport-info',
+ initiator: this.initiator,
+ sid: this.sid});
+ for (var mid = 0; mid < this.localSDP.media.length; mid++) {
+ var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
+ if (cands.length > 0) {
+ var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
+ ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
+ cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
+ 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
+ if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
+ var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
+ 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
+ //console.log('was this the last candidate', this.lasticecandidate);
+ this.connection.sendIQ(cand,
+ function () {
+ var ack = {};
+ ack.source = 'transportinfo';
+ $(document).trigger('ack.jingle', [this.sid, ack]);
+ },
+ function (stanza) {
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ error.source = 'transportinfo';
+ $(document).trigger('error.jingle', [this.sid, error]);
+ },
+ 10000);
+};
+
+
+JingleSession.prototype.sendOffer = function () {
+ //console.log('sendOffer...');
+ var self = this;
+ this.peerconnection.createOffer(function (sdp) {
+ self.createdOffer(sdp);
+ },
+ function (e) {
+ console.error('createOffer failed', e);
+ },
+ this.media_constraints
+ );
+};
+
+JingleSession.prototype.createdOffer = function (sdp) {
+ //console.log('createdOffer', sdp);
+ var self = this;
+ 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';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ self.state = 'error';
+ self.peerconnection.close();
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ error.source = 'offer';
+ $(document).trigger('error.jingle', [self.sid, error]);
+ },
+ 10000);
+ }
+ sdp.sdp = this.localSDP.raw;
+ this.peerconnection.setLocalDescription(sdp,
+ function () {
+ $(document).trigger('setLocalDescription.jingle', [self.sid]);
+ //console.log('setLocalDescription success');
+ },
+ 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) {
+ //console.log('setting remote description... ', desctype);
+ 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 () {
+ //console.log('setRemoteDescription success');
+ },
+ function (e) {
+ console.error('setRemoteDescription error', e);
+ }
+ );
+};
+
+JingleSession.prototype.addIceCandidate = function (elem) {
+ var self = this;
+ 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 () {
+ 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:')) {
+ var tmp = $(this).find('transport');
+ self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
+ self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
+ tmp = $(this).find('transport>fingerprint');
+ if (tmp.length) {
+ self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
+ } else {
+ console.log('no dtls fingerprint (webrtc issue #1718?)');
+ self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
+ }
+ 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 {
+ this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
+ function() {
+ },
+ function(e) {
+ console.log('setRemoteDescription pranswer failed', e.toString());
+ });
+ } catch (e) {
+ console.error('setting pranswer failed', e);
+ }
+ } else {
+ //console.log('not yet setting pranswer');
+ }
+ }
+ // operate on each content element
+ elem.each(function () {
+ // would love to deactivate this, but firefox still requires it
+ var idx = -1;
+ var i;
+ 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) {
+ idx = i;
+ break;
+ }
+ }
+ if (idx == -1) { // fall back to localdescription
+ 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) {
+ 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 {
+ self.peerconnection.addIceCandidate(candidate);
+ } catch (e) {
+ console.error('addIceCandidate failed', e.toString(), line);
+ }
+ });
+ });
+};
+
+JingleSession.prototype.sendAnswer = function (provisional) {
+ //console.log('createAnswer', provisional);
+ var self = this;
+ this.peerconnection.createAnswer(
+ function (sdp) {
+ self.createdAnswer(sdp, provisional);
+ },
+ function (e) {
+ console.error('createAnswer failed', e);
+ },
+ this.media_constraints
+ );
+};
+
+JingleSession.prototype.createdAnswer = function (sdp, provisional) {
+ //console.log('createAnswer callback');
+ var self = this;
+ 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';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ error.source = 'answer';
+ $(document).trigger('error.jingle', [self.sid, error]);
+ },
+ 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 () {
+ $(document).trigger('setLocalDescription.jingle', [self.sid]);
+ //console.log('setLocalDescription success');
+ },
+ 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) {
+ var self = this,
+ 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 () {
+ self.peerconnection.close();
+ self.peerconnection = null;
+ self.terminate();
+ var ack = {};
+ ack.source = 'terminate';
+ $(document).trigger('ack.jingle', [self.sid, ack]);
+ },
+ function (stanza) {
+ var error = ($(stanza).find('error').length) ? {
+ code: $(stanza).find('error').attr('code'),
+ reason: $(stanza).find('error :first')[0].tagName,
+ }:{};
+ $(document).trigger('ack.jingle', [self.sid, error]);
+ },
+ 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);
+
+ var self = this;
+ $(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;
+ if (!self.addssrc[idx]) self.addssrc[idx] = '';
+ self.addssrc[idx] += lines;
+ });
+ 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);
+
+ var self = this;
+ $(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;
+ if (!self.removessrc[idx]) self.removessrc[idx] = '';
+ self.removessrc[idx] += lines;
+ });
+ sdp.raw = sdp.session + sdp.media.join('');
+ });
+ this.modifySources();
+};
+
+JingleSession.prototype.modifySources = function() {
+ var self = this;
+ if (this.peerconnection.signalingState == 'closed') return;
+ if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)) return;
+ if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
+ console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
+ this.wait = true;
+ window.setTimeout(function() { self.modifySources(); }, 250);
+ return;
+ }
+ if (this.wait) {
+ window.setTimeout(function() { self.modifySources(); }, 2500);
+ 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() {
+ self.peerconnection.createAnswer(
+ function(modifiedAnswer) {
+ // change video direction, see https://github.com/jitsi/jitmeet/issues/41
+ if (self.pendingop !== null) {
+ var sdp = new SDP(modifiedAnswer.sdp);
+ if (sdp.media.length > 1) {
+ switch(self.pendingop) {
+ case 'mute':
+ sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
+ break;
+ case 'unmute':
+ sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
+ break;
+ }
+ sdp.raw = sdp.session + sdp.media.join('');
+ modifiedAnswer.sdp = sdp.raw;
+ }
+ self.pendingop = null;
+ }
+
+ self.peerconnection.setLocalDescription(modifiedAnswer,
+ function() {
+ //console.log('modified setLocalDescription ok');
+ $(document).trigger('setLocalDescription.jingle', [self.sid]);
+ },
+ function(error) {
+ console.log('modified setLocalDescription failed');
+ }
+ );
+ },
+ function(error) {
+ console.log('modified answer failed');
+ }
+ );
+ },
+ function(error) {
+ console.log('modify failed');
+ }
+ );
+};
+
+// SDP-based mute by going recvonly/sendrecv
+// FIXME: should probably black out the screen as well
+JingleSession.prototype.hardMuteVideo = function (muted) {
+ this.pendingop = muted ? 'mute' : 'unmute';
+ this.modifySources();
+
+ this.connection.jingle.localStream.getVideoTracks().forEach(function (track) {
+ track.enabled = !muted;
+ });
+};
+
+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) {
+ var self = this;
+ 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 () {
+ if (self && self.peerconnection && self.peerconnection.getStats) {
+ self.peerconnection.getStats(function (stats) {
+ 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;
+ $(document).trigger('packetloss.jingle', [self.sid, loss]);
+ });
+ }
+ }, interval || 3000);
+ return this.statsinterval;
+};
diff --git a/libs/strophejingle.bundle.js b/libs/strophejingle.bundle.js
deleted file mode 100644
index f8dc7c313..000000000
--- a/libs/strophejingle.bundle.js
+++ /dev/null
@@ -1,2304 +0,0 @@
-/*! strophe.js v1.1.3 - built on 20-01-2014 */
-function b64_sha1(a){return binb2b64(core_sha1(str2binb(a),8*a.length))}function str_sha1(a){return binb2str(core_sha1(str2binb(a),8*a.length))}function b64_hmac_sha1(a,b){return binb2b64(core_hmac_sha1(a,b))}function str_hmac_sha1(a,b){return binb2str(core_hmac_sha1(a,b))}function core_sha1(a,b){a[b>>5]|=128<<24-b%32,a[(b+64>>9<<4)+15]=b;var c,d,e,f,g,h,i,j,k=new Array(80),l=1732584193,m=-271733879,n=-1732584194,o=271733878,p=-1009589776;for(c=0;cd;d++)k[d]=16>d?a[c+d]:rol(k[d-3]^k[d-8]^k[d-14]^k[d-16],1),e=safe_add(safe_add(rol(l,5),sha1_ft(d,m,n,o)),safe_add(safe_add(p,k[d]),sha1_kt(d))),p=o,o=n,n=rol(m,30),m=l,l=e;l=safe_add(l,f),m=safe_add(m,g),n=safe_add(n,h),o=safe_add(o,i),p=safe_add(p,j)}return[l,m,n,o,p]}function sha1_ft(a,b,c,d){return 20>a?b&c|~b&d:40>a?b^c^d:60>a?b&c|b&d|c&d:b^c^d}function sha1_kt(a){return 20>a?1518500249:40>a?1859775393:60>a?-1894007588:-899497514}function core_hmac_sha1(a,b){var c=str2binb(a);c.length>16&&(c=core_sha1(c,8*a.length));for(var d=new Array(16),e=new Array(16),f=0;16>f;f++)d[f]=909522486^c[f],e[f]=1549556828^c[f];var g=core_sha1(d.concat(str2binb(b)),512+8*b.length);return core_sha1(e.concat(g),672)}function safe_add(a,b){var c=(65535&a)+(65535&b),d=(a>>16)+(b>>16)+(c>>16);return d<<16|65535&c}function rol(a,b){return a<>>32-b}function str2binb(a){for(var b=[],c=255,d=0;d<8*a.length;d+=8)b[d>>5]|=(a.charCodeAt(d/8)&c)<<24-d%32;return b}function binb2str(a){for(var b="",c=255,d=0;d<32*a.length;d+=8)b+=String.fromCharCode(a[d>>5]>>>24-d%32&c);return b}function binb2b64(a){for(var b,c,d="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",e="",f=0;f<4*a.length;f+=3)for(b=(a[f>>2]>>8*(3-f%4)&255)<<16|(a[f+1>>2]>>8*(3-(f+1)%4)&255)<<8|a[f+2>>2]>>8*(3-(f+2)%4)&255,c=0;4>c;c++)e+=8*f+6*c>32*a.length?"=":d.charAt(b>>6*(3-c)&63);return e}var Base64=function(){var a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",b={encode:function(b){var c,d,e,f,g,h,i,j="",k=0;do c=b.charCodeAt(k++),d=b.charCodeAt(k++),e=b.charCodeAt(k++),f=c>>2,g=(3&c)<<4|d>>4,h=(15&d)<<2|e>>6,i=63&e,isNaN(d)?h=i=64:isNaN(e)&&(i=64),j=j+a.charAt(f)+a.charAt(g)+a.charAt(h)+a.charAt(i);while(k>4,d=(15&g)<<4|h>>2,e=(3&h)<<6|i,j+=String.fromCharCode(c),64!=h&&(j+=String.fromCharCode(d)),64!=i&&(j+=String.fromCharCode(e));while(k>16)+(b>>16)+(c>>16);return d<<16|65535&c},b=function(a,b){return a<>>32-b},c=function(a){for(var b=[],c=0;c<8*a.length;c+=8)b[c>>5]|=(255&a.charCodeAt(c/8))<>5]>>>c%32&255);return b},e=function(a){for(var b="0123456789abcdef",c="",d=0;d<4*a.length;d++)c+=b.charAt(a[d>>2]>>d%4*8+4&15)+b.charAt(a[d>>2]>>d%4*8&15);return c},f=function(c,d,e,f,g,h){return a(b(a(a(d,c),a(f,h)),g),e)},g=function(a,b,c,d,e,g,h){return f(b&c|~b&d,a,b,e,g,h)},h=function(a,b,c,d,e,g,h){return f(b&d|c&~d,a,b,e,g,h)},i=function(a,b,c,d,e,g,h){return f(b^c^d,a,b,e,g,h)},j=function(a,b,c,d,e,g,h){return f(c^(b|~d),a,b,e,g,h)},k=function(b,c){b[c>>5]|=128<>>9<<4)+14]=c;for(var d,e,f,k,l=1732584193,m=-271733879,n=-1732584194,o=271733878,p=0;pc?Math.ceil(c):Math.floor(c),0>c&&(c+=b);b>c;c++)if(c in this&&this[c]===a)return c;return-1}),function(a){function b(a,b){return new f.Builder(a,b)}function c(a){return new f.Builder("message",a)}function d(a){return new f.Builder("iq",a)}function e(a){return new f.Builder("presence",a)}var f;f={VERSION:"1.1.3",NS:{HTTPBIND:"http://jabber.org/protocol/httpbind",BOSH:"urn:xmpp:xbosh",CLIENT:"jabber:client",AUTH:"jabber:iq:auth",ROSTER:"jabber:iq:roster",PROFILE:"jabber:iq:profile",DISCO_INFO:"http://jabber.org/protocol/disco#info",DISCO_ITEMS:"http://jabber.org/protocol/disco#items",MUC:"http://jabber.org/protocol/muc",SASL:"urn:ietf:params:xml:ns:xmpp-sasl",STREAM:"http://etherx.jabber.org/streams",BIND:"urn:ietf:params:xml:ns:xmpp-bind",SESSION:"urn:ietf:params:xml:ns:xmpp-session",VERSION:"jabber:iq:version",STANZAS:"urn:ietf:params:xml:ns:xmpp-stanzas",XHTML_IM:"http://jabber.org/protocol/xhtml-im",XHTML:"http://www.w3.org/1999/xhtml"},XHTML:{tags:["a","blockquote","br","cite","em","img","li","ol","p","span","strong","ul","body"],attributes:{a:["href"],blockquote:["style"],br:[],cite:["style"],em:[],img:["src","alt","style","height","width"],li:["style"],ol:["style"],p:["style"],span:["style"],strong:[],ul:["style"],body:[]},css:["background-color","color","font-family","font-size","font-style","font-weight","margin-left","margin-right","text-align","text-decoration"],validTag:function(a){for(var b=0;b0)for(var c=0;c/g,">"),a=a.replace(/'/g,"'"),a=a.replace(/"/g,""")},xmlTextNode:function(a){return f.xmlGenerator().createTextNode(a)},xmlHtmlNode:function(a){var b;if(window.DOMParser){var c=new DOMParser;b=c.parseFromString(a,"text/xml")}else b=new ActiveXObject("Microsoft.XMLDOM"),b.async="false",b.loadXML(a);return b},getText:function(a){if(!a)return null;var b="";0===a.childNodes.length&&a.nodeType==f.ElementType.TEXT&&(b+=a.nodeValue);for(var c=0;c0&&(h=i.join("; "),c.setAttribute(g,h))}else c.setAttribute(g,h);for(b=0;b/g,"\\3e").replace(/@/g,"\\40")},unescapeNode:function(a){return a.replace(/\\20/g," ").replace(/\\22/g,'"').replace(/\\26/g,"&").replace(/\\27/g,"'").replace(/\\2f/g,"/").replace(/\\3a/g,":").replace(/\\3c/g,"<").replace(/\\3e/g,">").replace(/\\40/g,"@").replace(/\\5c/g,"\\")},getNodeFromJid:function(a){return a.indexOf("@")<0?null:a.split("@")[0]},getDomainFromJid:function(a){var b=f.getBareJidFromJid(a);if(b.indexOf("@")<0)return b;var c=b.split("@");return c.splice(0,1),c.join("@")},getResourceFromJid:function(a){var b=a.split("/");return b.length<2?null:(b.splice(0,1),b.join("/"))},getBareJidFromJid:function(a){return a?a.split("/")[0]:null},log:function(){},debug:function(a){this.log(this.LogLevel.DEBUG,a)},info:function(a){this.log(this.LogLevel.INFO,a)},warn:function(a){this.log(this.LogLevel.WARN,a)},error:function(a){this.log(this.LogLevel.ERROR,a)},fatal:function(a){this.log(this.LogLevel.FATAL,a)},serialize:function(a){var b;if(!a)return null;"function"==typeof a.tree&&(a=a.tree());var c,d,e=a.nodeName;for(a.getAttribute("_realname")&&(e=a.getAttribute("_realname")),b="<"+e,c=0;c/g,">").replace(/0){for(b+=">",c=0;c"}b+=""+e+">"}else b+="/>";return b},_requestId:0,_connectionPlugins:{},addConnectionPlugin:function(a,b){f._connectionPlugins[a]=b}},f.Builder=function(a,b){("presence"==a||"message"==a||"iq"==a)&&(b&&!b.xmlns?b.xmlns=f.NS.CLIENT:b||(b={xmlns:f.NS.CLIENT})),this.nodeTree=f.xmlElement(a,b),this.node=this.nodeTree},f.Builder.prototype={tree:function(){return this.nodeTree},toString:function(){return f.serialize(this.nodeTree)},up:function(){return this.node=this.node.parentNode,this},attrs:function(a){for(var b in a)a.hasOwnProperty(b)&&this.node.setAttribute(b,a[b]);return this},c:function(a,b,c){var d=f.xmlElement(a,b,c);return this.node.appendChild(d),c||(this.node=d),this},cnode:function(a){var b,c=f.xmlGenerator();try{b=void 0!==c.importNode}catch(d){b=!1}var e=b?c.importNode(a,!0):f.copyElement(a);return this.node.appendChild(e),this.node=e,this},t:function(a){var b=f.xmlTextNode(a);return this.node.appendChild(b),this},h:function(a){var b=document.createElement("body");b.innerHTML=a;for(var c=f.createHtml(b);c.childNodes.length>0;)this.node.appendChild(c.childNodes[0]);return this}},f.Handler=function(a,b,c,d,e,g,h){this.handler=a,this.ns=b,this.name=c,this.type=d,this.id=e,this.options=h||{matchBare:!1},this.options.matchBare||(this.options.matchBare=!1),this.from=this.options.matchBare?g?f.getBareJidFromJid(g):null:g,this.user=!0},f.Handler.prototype={isMatch:function(a){var b,c=null;if(c=this.options.matchBare?f.getBareJidFromJid(a.getAttribute("from")):a.getAttribute("from"),b=!1,this.ns){var d=this;f.forEachChild(a,null,function(a){a.getAttribute("xmlns")==d.ns&&(b=!0)}),b=b||a.getAttribute("xmlns")==this.ns}else b=!0;return!b||this.name&&!f.isTagEqual(a,this.name)||this.type&&a.getAttribute("type")!=this.type||this.id&&a.getAttribute("id")!=this.id||this.from&&c!=this.from?!1:!0},run:function(a){var b=null;try{b=this.handler(a)}catch(c){throw c.sourceURL?f.fatal("error: "+this.handler+" "+c.sourceURL+":"+c.line+" - "+c.name+": "+c.message):c.fileName?("undefined"!=typeof console&&(console.trace(),console.error(this.handler," - error - ",c,c.message)),f.fatal("error: "+this.handler+" "+c.fileName+":"+c.lineNumber+" - "+c.name+": "+c.message)):f.fatal("error: "+c.message+"\n"+c.stack),c}return b},toString:function(){return"{Handler: "+this.handler+"("+this.name+","+this.id+","+this.ns+")}"}},f.TimedHandler=function(a,b){this.period=a,this.handler=b,this.lastCalled=(new Date).getTime(),this.user=!0},f.TimedHandler.prototype={run:function(){return this.lastCalled=(new Date).getTime(),this.handler()},reset:function(){this.lastCalled=(new Date).getTime()},toString:function(){return"{TimedHandler: "+this.handler+"("+this.period+")}"}},f.Connection=function(a,b){this.service=a,this.options=b||{};var c=this.options.protocol||"";this._proto=0===a.indexOf("ws:")||0===a.indexOf("wss:")||0===c.indexOf("ws")?new f.Websocket(this):new f.Bosh(this),this.jid="",this.domain=null,this.features=null,this._sasl_data={},this.do_session=!1,this.do_bind=!1,this.timedHandlers=[],this.handlers=[],this.removeTimeds=[],this.removeHandlers=[],this.addTimeds=[],this.addHandlers=[],this._authentication={},this._idleTimeout=null,this._disconnectTimeout=null,this.do_authentication=!0,this.authenticated=!1,this.disconnecting=!1,this.connected=!1,this.errors=0,this.paused=!1,this._data=[],this._uniqueId=0,this._sasl_success_handler=null,this._sasl_failure_handler=null,this._sasl_challenge_handler=null,this.maxRetries=5,this._idleTimeout=setTimeout(this._onIdle.bind(this),100);for(var d in f._connectionPlugins)if(f._connectionPlugins.hasOwnProperty(d)){var e=f._connectionPlugins[d],g=function(){};g.prototype=e,this[d]=new g,this[d].init(this)}},f.Connection.prototype={reset:function(){this._proto._reset(),this.do_session=!1,this.do_bind=!1,this.timedHandlers=[],this.handlers=[],this.removeTimeds=[],this.removeHandlers=[],this.addTimeds=[],this.addHandlers=[],this._authentication={},this.authenticated=!1,this.disconnecting=!1,this.connected=!1,this.errors=0,this._requests=[],this._uniqueId=0},pause:function(){this.paused=!0},resume:function(){this.paused=!1},getUniqueId:function(a){return"string"==typeof a||"number"==typeof a?++this._uniqueId+":"+a:++this._uniqueId+""},connect:function(a,b,c,d,e,g){this.jid=a,this.authzid=f.getBareJidFromJid(this.jid),this.authcid=f.getNodeFromJid(this.jid),this.pass=b,this.servtype="xmpp",this.connect_callback=c,this.disconnecting=!1,this.connected=!1,this.authenticated=!1,this.errors=0,this.domain=f.getDomainFromJid(this.jid),this._changeConnectStatus(f.Status.CONNECTING,null),this._proto._connect(d,e,g)},attach:function(a,b,c,d,e,f,g){this._proto._attach(a,b,c,d,e,f,g)},xmlInput:function(){},xmlOutput:function(){},rawInput:function(){},rawOutput:function(){},send:function(a){if(null!==a){if("function"==typeof a.sort)for(var b=0;b0;)e=this.removeHandlers.pop(),d=this.handlers.indexOf(e),d>=0&&this.handlers.splice(d,1);for(;this.addHandlers.length>0;)this.handlers.push(this.addHandlers.pop());if(this.disconnecting&&this._proto._emptyQueue())return this._doDisconnect(),void 0;var g,h,i=c.getAttribute("type");if(null!==i&&"terminate"==i){if(this.disconnecting)return;return g=c.getAttribute("condition"),h=c.getElementsByTagName("conflict"),null!==g?("remote-stream-error"==g&&h.length>0&&(g="conflict"),this._changeConnectStatus(f.Status.CONNFAIL,g)):this._changeConnectStatus(f.Status.CONNFAIL,"unknown"),this.disconnect("unknown stream-error"),void 0}var j=this;f.forEachChild(c,null,function(a){var b,c;for(c=j.handlers,j.handlers=[],b=0;b0;g||(g=d.getElementsByTagName("features").length>0);var h,i,j=d.getElementsByTagName("mechanism"),k=[],l=!1;if(!g)return this._proto._no_auth_received(b),void 0;if(j.length>0)for(h=0;h0,(l=this._authentication.legacy_auth||k.length>0)?(this.do_authentication!==!1&&this.authenticate(k),void 0):(this._proto._no_auth_received(b),void 0)}}},authenticate:function(a){var c;for(c=0;ca[e].prototype.priority&&(e=g);if(e!=c){var h=a[c];a[c]=a[e],a[e]=h}}var i=!1;for(c=0;c0&&(b="conflict"),this._changeConnectStatus(f.Status.AUTHFAIL,b),!1}var e,g=a.getElementsByTagName("bind");return g.length>0?(e=g[0].getElementsByTagName("jid"),e.length>0&&(this.jid=f.getText(e[0]),this.do_session?(this._addSysHandler(this._sasl_session_cb.bind(this),null,null,null,"_session_auth_2"),this.send(d({type:"set",id:"_session_auth_2"}).c("session",{xmlns:f.NS.SESSION}).tree())):(this.authenticated=!0,this._changeConnectStatus(f.Status.CONNECTED,null))),void 0):(f.info("SASL binding failed."),this._changeConnectStatus(f.Status.AUTHFAIL,null),!1)},_sasl_session_cb:function(a){if("result"==a.getAttribute("type"))this.authenticated=!0,this._changeConnectStatus(f.Status.CONNECTED,null);else if("error"==a.getAttribute("type"))return f.info("Session creation failed."),this._changeConnectStatus(f.Status.AUTHFAIL,null),!1;return!1},_sasl_failure_cb:function(){return this._sasl_success_handler&&(this.deleteHandler(this._sasl_success_handler),this._sasl_success_handler=null),this._sasl_challenge_handler&&(this.deleteHandler(this._sasl_challenge_handler),this._sasl_challenge_handler=null),this._sasl_mechanism&&this._sasl_mechanism.onFailure(),this._changeConnectStatus(f.Status.AUTHFAIL,null),!1},_auth2_cb:function(a){return"result"==a.getAttribute("type")?(this.authenticated=!0,this._changeConnectStatus(f.Status.CONNECTED,null)):"error"==a.getAttribute("type")&&(this._changeConnectStatus(f.Status.AUTHFAIL,null),this.disconnect("authentication failed")),!1},_addSysTimedHandler:function(a,b){var c=new f.TimedHandler(a,b);return c.user=!1,this.addTimeds.push(c),c},_addSysHandler:function(a,b,c,d,e){var g=new f.Handler(a,b,c,d,e);return g.user=!1,this.addHandlers.push(g),g},_onDisconnectTimeout:function(){return f.info("_onDisconnectTimeout was called"),this._proto._onDisconnectTimeout(),this._doDisconnect(),!1},_onIdle:function(){for(var a,b,c,d;this.addTimeds.length>0;)this.timedHandlers.push(this.addTimeds.pop());for(;this.removeTimeds.length>0;)b=this.removeTimeds.pop(),a=this.timedHandlers.indexOf(b),a>=0&&this.timedHandlers.splice(a,1);var e=(new Date).getTime();for(d=[],a=0;a=c-e?b.run()&&d.push(b):d.push(b));this.timedHandlers=d,clearTimeout(this._idleTimeout),this._proto._onIdle(),this.connected&&(this._idleTimeout=setTimeout(this._onIdle.bind(this),100))}},a&&a(f,b,c,d,e),f.SASLMechanism=function(a,b,c){this.name=a,this.isClientFirst=b,this.priority=c},f.SASLMechanism.prototype={test:function(){return!0},onStart:function(a){this._connection=a},onChallenge:function(){throw new Error("You should implement challenge handling!")},onFailure:function(){this._connection=null},onSuccess:function(){this._connection=null}},f.SASLAnonymous=function(){},f.SASLAnonymous.prototype=new f.SASLMechanism("ANONYMOUS",!1,10),f.SASLAnonymous.test=function(a){return null===a.authcid},f.Connection.prototype.mechanisms[f.SASLAnonymous.prototype.name]=f.SASLAnonymous,f.SASLPlain=function(){},f.SASLPlain.prototype=new f.SASLMechanism("PLAIN",!0,20),f.SASLPlain.test=function(a){return null!==a.authcid},f.SASLPlain.prototype.onChallenge=function(a){var b=a.authzid;return b+="\x00",b+=a.authcid,b+="\x00",b+=a.pass},f.Connection.prototype.mechanisms[f.SASLPlain.prototype.name]=f.SASLPlain,f.SASLSHA1=function(){},f.SASLSHA1.prototype=new f.SASLMechanism("SCRAM-SHA-1",!0,40),f.SASLSHA1.test=function(a){return null!==a.authcid},f.SASLSHA1.prototype.onChallenge=function(a,b,c){var d=c||MD5.hexdigest(1234567890*Math.random()),e="n="+a.authcid;return e+=",r=",e+=d,a._sasl_data.cnonce=d,a._sasl_data["client-first-message-bare"]=e,e="n,,"+e,this.onChallenge=function(a,b){for(var c,d,e,f,g,h,i,j,k,l,m,n="c=biws,",o=a._sasl_data["client-first-message-bare"]+","+b+",",p=a._sasl_data.cnonce,q=/([a-z]+)=([^,]+)(,|$)/;b.match(q);){var r=b.match(q);switch(b=b.replace(r[0],""),r[1]){case"r":c=r[2];break;case"s":d=r[2];break;case"i":e=r[2]}}if(c.substr(0,p.length)!==p)return a._sasl_data={},a._sasl_failure_cb();for(n+="r="+c,o+=n,d=Base64.decode(d),d+="\x00\x00\x00",f=h=core_hmac_sha1(a.pass,d),i=1;e>i;i++){for(g=core_hmac_sha1(a.pass,binb2str(h)),j=0;5>j;j++)f[j]^=g[j];h=g}for(f=binb2str(f),k=core_hmac_sha1(f,"Client Key"),l=str_hmac_sha1(f,"Server Key"),m=core_hmac_sha1(str_sha1(binb2str(k)),o),a._sasl_data["server-signature"]=b64_hmac_sha1(l,o),j=0;5>j;j++)k[j]^=m[j];return n+=",p="+Base64.encode(binb2str(k))}.bind(this),e},f.Connection.prototype.mechanisms[f.SASLSHA1.prototype.name]=f.SASLSHA1,f.SASLMD5=function(){},f.SASLMD5.prototype=new f.SASLMechanism("DIGEST-MD5",!1,30),f.SASLMD5.test=function(a){return null!==a.authcid},f.SASLMD5.prototype._quote=function(a){return'"'+a.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'},f.SASLMD5.prototype.onChallenge=function(a,b,c){for(var d,e=/([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/,f=c||MD5.hexdigest(""+1234567890*Math.random()),g="",h=null,i="",j="";b.match(e);)switch(d=b.match(e),b=b.replace(d[0],""),d[2]=d[2].replace(/^"(.+)"$/,"$1"),d[1]){case"realm":g=d[2];
-break;case"nonce":i=d[2];break;case"qop":j=d[2];break;case"host":h=d[2]}var k=a.servtype+"/"+a.domain;null!==h&&(k=k+"/"+h);var l=MD5.hash(a.authcid+":"+g+":"+this._connection.pass)+":"+i+":"+f,m="AUTHENTICATE:"+k,n="";return n+="charset=utf-8,",n+="username="+this._quote(a.authcid)+",",n+="realm="+this._quote(g)+",",n+="nonce="+this._quote(i)+",",n+="nc=00000001,",n+="cnonce="+this._quote(f)+",",n+="digest-uri="+this._quote(k)+",",n+="response="+MD5.hexdigest(MD5.hexdigest(l)+":"+i+":00000001:"+f+":auth:"+MD5.hexdigest(m))+",",n+="qop=auth",this.onChallenge=function(){return""}.bind(this),n},f.Connection.prototype.mechanisms[f.SASLMD5.prototype.name]=f.SASLMD5}(function(){window.Strophe=arguments[0],window.$build=arguments[1],window.$msg=arguments[2],window.$iq=arguments[3],window.$pres=arguments[4]}),Strophe.Request=function(a,b,c,d){this.id=++Strophe._requestId,this.xmlData=a,this.data=Strophe.serialize(a),this.origFunc=b,this.func=b,this.rid=c,this.date=0/0,this.sends=d||0,this.abort=!1,this.dead=null,this.age=function(){if(!this.date)return 0;var a=new Date;return(a-this.date)/1e3},this.timeDead=function(){if(!this.dead)return 0;var a=new Date;return(a-this.dead)/1e3},this.xhr=this._newXHR()},Strophe.Request.prototype={getResponse:function(){var a=null;if(this.xhr.responseXML&&this.xhr.responseXML.documentElement){if(a=this.xhr.responseXML.documentElement,"parsererror"==a.tagName)throw Strophe.error("invalid response received"),Strophe.error("responseText: "+this.xhr.responseText),Strophe.error("responseXML: "+Strophe.serialize(this.xhr.responseXML)),"parsererror"}else this.xhr.responseText&&(Strophe.error("invalid response received"),Strophe.error("responseText: "+this.xhr.responseText),Strophe.error("responseXML: "+Strophe.serialize(this.xhr.responseXML)));return a},_newXHR:function(){var a=null;return window.XMLHttpRequest?(a=new XMLHttpRequest,a.overrideMimeType&&a.overrideMimeType("text/xml")):window.ActiveXObject&&(a=new ActiveXObject("Microsoft.XMLHTTP")),a.onreadystatechange=this.func.bind(null,this),a}},Strophe.Bosh=function(a){this._conn=a,this.rid=Math.floor(4294967295*Math.random()),this.sid=null,this.hold=1,this.wait=60,this.window=5,this._requests=[]},Strophe.Bosh.prototype={strip:null,_buildBody:function(){var a=$build("body",{rid:this.rid++,xmlns:Strophe.NS.HTTPBIND});return null!==this.sid&&a.attrs({sid:this.sid}),a},_reset:function(){this.rid=Math.floor(4294967295*Math.random()),this.sid=null},_connect:function(a,b,c){this.wait=a||this.wait,this.hold=b||this.hold;var d=this._buildBody().attrs({to:this._conn.domain,"xml:lang":"en",wait:this.wait,hold:this.hold,content:"text/xml; charset=utf-8",ver:"1.6","xmpp:version":"1.0","xmlns:xmpp":Strophe.NS.BOSH});c&&d.attrs({route:c});var e=this._conn._connect_cb;this._requests.push(new Strophe.Request(d.tree(),this._onRequestStateChange.bind(this,e.bind(this._conn)),d.tree().getAttribute("rid"))),this._throttledRequestHandler()},_attach:function(a,b,c,d,e,f,g){this._conn.jid=a,this.sid=b,this.rid=c,this._conn.connect_callback=d,this._conn.domain=Strophe.getDomainFromJid(this._conn.jid),this._conn.authenticated=!0,this._conn.connected=!0,this.wait=e||this.wait,this.hold=f||this.hold,this.window=g||this.window,this._conn._changeConnectStatus(Strophe.Status.ATTACHED,null)},_connect_cb:function(a){var b,c,d=a.getAttribute("type");if(null!==d&&"terminate"==d)return Strophe.error("BOSH-Connection failed: "+b),b=a.getAttribute("condition"),c=a.getElementsByTagName("conflict"),null!==b?("remote-stream-error"==b&&c.length>0&&(b="conflict"),this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,b)):this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"unknown"),this._conn._doDisconnect(),Strophe.Status.CONNFAIL;this.sid||(this.sid=a.getAttribute("sid"));var e=a.getAttribute("requests");e&&(this.window=parseInt(e,10));var f=a.getAttribute("hold");f&&(this.hold=parseInt(f,10));var g=a.getAttribute("wait");g&&(this.wait=parseInt(g,10))},_disconnect:function(a){this._sendTerminate(a)},_doDisconnect:function(){this.sid=null,this.rid=Math.floor(4294967295*Math.random())},_emptyQueue:function(){return 0===this._requests.length},_hitError:function(a){this.errors++,Strophe.warn("request errored, status: "+a+", number of errors: "+this.errors),this.errors>4&&this._onDisconnectTimeout()},_no_auth_received:function(a){a=a?a.bind(this._conn):this._conn._connect_cb.bind(this._conn);var b=this._buildBody();this._requests.push(new Strophe.Request(b.tree(),this._onRequestStateChange.bind(this,a.bind(this._conn)),b.tree().getAttribute("rid"))),this._throttledRequestHandler()},_onDisconnectTimeout:function(){for(var a;this._requests.length>0;)a=this._requests.pop(),a.abort=!0,a.xhr.abort(),a.xhr.onreadystatechange=function(){}},_onIdle:function(){var a=this._conn._data;if(this._conn.authenticated&&0===this._requests.length&&0===a.length&&!this._conn.disconnecting&&(Strophe.info("no requests during idle cycle, sending blank request"),a.push(null)),this._requests.length<2&&a.length>0&&!this._conn.paused){for(var b=this._buildBody(),c=0;c0){var d=this._requests[0].age();null!==this._requests[0].dead&&this._requests[0].timeDead()>Math.floor(Strophe.SECONDARY_TIMEOUT*this.wait)&&this._throttledRequestHandler(),d>Math.floor(Strophe.TIMEOUT*this.wait)&&(Strophe.warn("Request "+this._requests[0].id+" timed out, over "+Math.floor(Strophe.TIMEOUT*this.wait)+" seconds since last activity"),this._throttledRequestHandler())}},_onRequestStateChange:function(a,b){if(Strophe.debug("request id "+b.id+"."+b.sends+" state changed to "+b.xhr.readyState),b.abort)return b.abort=!1,void 0;var c;if(4==b.xhr.readyState){c=0;try{c=b.xhr.status}catch(d){}if("undefined"==typeof c&&(c=0),this.disconnecting&&c>=400)return this._hitError(c),void 0;var e=this._requests[0]==b,f=this._requests[1]==b;(c>0&&500>c||b.sends>5)&&(this._removeRequest(b),Strophe.debug("request id "+b.id+" should now be removed")),200==c?((f||e&&this._requests.length>0&&this._requests[0].age()>Math.floor(Strophe.SECONDARY_TIMEOUT*this.wait))&&this._restartRequest(0),Strophe.debug("request id "+b.id+"."+b.sends+" got 200"),a(b),this.errors=0):(Strophe.error("request id "+b.id+"."+b.sends+" error "+c+" happened"),(0===c||c>=400&&600>c||c>=12e3)&&(this._hitError(c),c>=400&&500>c&&(this._conn._changeConnectStatus(Strophe.Status.DISCONNECTING,null),this._conn._doDisconnect()))),c>0&&500>c||b.sends>5||this._throttledRequestHandler()}},_processRequest:function(a){var b=this,c=this._requests[a],d=-1;try{4==c.xhr.readyState&&(d=c.xhr.status)}catch(e){Strophe.error("caught an error in _requests["+a+"], reqStatus: "+d)}if("undefined"==typeof d&&(d=-1),c.sends>this.maxRetries)return this._onDisconnectTimeout(),void 0;var f=c.age(),g=!isNaN(f)&&f>Math.floor(Strophe.TIMEOUT*this.wait),h=null!==c.dead&&c.timeDead()>Math.floor(Strophe.SECONDARY_TIMEOUT*this.wait),i=4==c.xhr.readyState&&(1>d||d>=500);if((g||h||i)&&(h&&Strophe.error("Request "+this._requests[a].id+" timed out (secondary), restarting"),c.abort=!0,c.xhr.abort(),c.xhr.onreadystatechange=function(){},this._requests[a]=new Strophe.Request(c.xmlData,c.origFunc,c.rid,c.sends),c=this._requests[a]),0===c.xhr.readyState){Strophe.debug("request id "+c.id+"."+c.sends+" posting");try{c.xhr.open("POST",this._conn.service,this._conn.options.sync?!1:!0)}catch(j){return Strophe.error("XHR open failed."),this._conn.connected||this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"bad-service"),this._conn.disconnect(),void 0}var k=function(){if(c.date=new Date,b._conn.options.customHeaders){var a=b._conn.options.customHeaders;for(var d in a)a.hasOwnProperty(d)&&c.xhr.setRequestHeader(d,a[d])}c.xhr.send(c.data)};if(c.sends>1){var l=1e3*Math.min(Math.floor(Strophe.TIMEOUT*this.wait),Math.pow(c.sends,3));setTimeout(k,l)}else k();c.sends++,this._conn.xmlOutput!==Strophe.Connection.prototype.xmlOutput&&(c.xmlData.nodeName===this.strip&&c.xmlData.childNodes.length?this._conn.xmlOutput(c.xmlData.childNodes[0]):this._conn.xmlOutput(c.xmlData)),this._conn.rawOutput!==Strophe.Connection.prototype.rawOutput&&this._conn.rawOutput(c.data)}else Strophe.debug("_processRequest: "+(0===a?"first":"second")+" request has readyState of "+c.xhr.readyState)},_removeRequest:function(a){Strophe.debug("removing request");var b;for(b=this._requests.length-1;b>=0;b--)a==this._requests[b]&&this._requests.splice(b,1);a.xhr.onreadystatechange=function(){},this._throttledRequestHandler()},_restartRequest:function(a){var b=this._requests[a];null===b.dead&&(b.dead=new Date),this._processRequest(a)},_reqToData:function(a){try{return a.getResponse()}catch(b){if("parsererror"!=b)throw b;this._conn.disconnect("strophe-parsererror")}},_sendTerminate:function(a){Strophe.info("_sendTerminate was called");var b=this._buildBody().attrs({type:"terminate"});a&&b.cnode(a.tree());var c=new Strophe.Request(b.tree(),this._onRequestStateChange.bind(this,this._conn._dataRecv.bind(this._conn)),b.tree().getAttribute("rid"));this._requests.push(c),this._throttledRequestHandler()},_send:function(){clearTimeout(this._conn._idleTimeout),this._throttledRequestHandler(),this._conn._idleTimeout=setTimeout(this._conn._onIdle.bind(this._conn),100)},_sendRestart:function(){this._throttledRequestHandler(),clearTimeout(this._conn._idleTimeout)},_throttledRequestHandler:function(){this._requests?Strophe.debug("_throttledRequestHandler called with "+this._requests.length+" requests"):Strophe.debug("_throttledRequestHandler called with undefined requests"),this._requests&&0!==this._requests.length&&(this._requests.length>0&&this._processRequest(0),this._requests.length>1&&Math.abs(this._requests[0].rid-this._requests[1].rid)\s*)*/,"");if(""===b)return;b=a.data.replace(//,"");var c=(new DOMParser).parseFromString(b,"text/xml").documentElement;this._conn.xmlInput(c),this._conn.rawInput(a.data),this._handleStreamStart(c)&&(this._connect_cb(c),this.streamStart=a.data.replace(/^$/,""))}else{if(""===a.data)return this._conn.rawInput(a.data),this._conn.xmlInput(document.createElement("stream:stream")),this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"Received closing stream"),this._conn._doDisconnect(),void 0;var d=this._streamWrap(a.data),e=(new DOMParser).parseFromString(d,"text/xml").documentElement;this.socket.onmessage=this._onMessage.bind(this),this._conn._connect_cb(e,null,a.data)}},_disconnect:function(a){if(this.socket.readyState!==WebSocket.CLOSED){a&&this._conn.send(a);var b="";this._conn.xmlOutput(document.createElement("stream:stream")),this._conn.rawOutput(b);try{this.socket.send(b)}catch(c){Strophe.info("Couldn't send closing stream tag.")}}this._conn._doDisconnect()},_doDisconnect:function(){Strophe.info("WebSockets _doDisconnect was called"),this._closeSocket()},_streamWrap:function(a){return this.streamStart+a+""},_closeSocket:function(){if(this.socket)try{this.socket.close()}catch(a){}this.socket=null},_emptyQueue:function(){return!0},_onClose:function(){this._conn.connected&&!this._conn.disconnecting?(Strophe.error("Websocket closed unexcectedly"),this._conn._doDisconnect()):Strophe.info("Websocket closed")},_no_auth_received:function(a){Strophe.error("Server did not send any auth methods"),this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"Server did not send any auth methods"),a&&(a=a.bind(this._conn))(),this._conn._doDisconnect()},_onDisconnectTimeout:function(){},_onError:function(a){Strophe.error("Websocket error "+a),this._conn._changeConnectStatus(Strophe.Status.CONNFAIL,"The WebSocket connection could not be established was disconnected."),this._disconnect()},_onIdle:function(){var a=this._conn._data;if(a.length>0&&!this._conn.paused){for(var b=0;b"===a.data){var d="";return this._conn.rawInput(d),this._conn.xmlInput(document.createElement("stream:stream")),this._conn.disconnecting||this._conn._doDisconnect(),void 0}if(0===a.data.search("/,""),b=(new DOMParser).parseFromString(c,"text/xml").documentElement,!this._handleStreamStart(b))return}else c=this._streamWrap(a.data),b=(new DOMParser).parseFromString(c,"text/xml").documentElement;if(!this._check_streamerror(b,Strophe.Status.ERROR))return this._conn.disconnecting&&"presence"===b.firstChild.nodeName&&"unavailable"===b.firstChild.getAttribute("type")?(this._conn.xmlInput(b),this._conn.rawInput(Strophe.serialize(b)),void 0):(this._conn._dataRecv(b,a.data),void 0)},_onOpen:function(){Strophe.info("Websocket open");var a=this._buildStream();this._conn.xmlOutput(a.tree());var b=this._removeClosingTag(a);this._conn.rawOutput(b),this.socket.send(b)},_removeClosingTag:function(a){var b=Strophe.serialize(a);return b=b.replace(/<(stream:stream .*[^\/])\/>$/,"<$1>")},_reqToData:function(a){return a},_send:function(){this._conn.flush()},_sendRestart:function(){clearTimeout(this._conn._idleTimeout),this._conn._onIdle.bind(this._conn)()}};Strophe.addConnectionPlugin("disco",{_connection:null,_identities:[],_features:[],_items:[],init:function(a){this._connection=a;this._identities=[];this._features=[];this._items=[];a.addHandler(this._onDiscoInfo.bind(this),Strophe.NS.DISCO_INFO,"iq","get",null,null);a.addHandler(this._onDiscoItems.bind(this),Strophe.NS.DISCO_ITEMS,"iq","get",null,null)},addIdentity:function(d,c,a,e){for(var b=0;b self.maxstats) {
- self.stats[id].values.shift();
- self.stats[id].times.shift();
- }
- self.stats[id].endTime = now;
- });
- }
- });
-
- }, 1000);
- }
-};
-
-dumpSDP = function(description) {
- return 'type: ' + description.type + '\r\n' + description.sdp;
-}
-
-if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
- 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.id);
- this.peerconnection.addStream(stream);
-};
-
-TraceablePeerConnection.prototype.removeStream = function (stream) {
- this.trace('removeStream', stream.id);
- this.peerconnection.removeStream(stream);
-};
-
-TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
- this.trace('createDataChannel', label, opts);
- this.peerconnection.createDataChannel(label, opts);
-}
-
-TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
- var self = this;
- this.trace('setLocalDescription', dumpSDP(description));
- this.peerconnection.setLocalDescription(description,
- function () {
- self.trace('setLocalDescriptionOnSuccess');
- successCallback();
- },
- function (err) {
- self.trace('setLocalDescriptionOnFailure', err);
- failureCallback(err);
- }
- );
- /*
- if (this.statsinterval === null && this.maxstats > 0) {
- // start gathering stats
- }
- */
-};
-
-TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
- var self = this;
- this.trace('setRemoteDescription', dumpSDP(description));
- this.peerconnection.setRemoteDescription(description,
- function () {
- self.trace('setRemoteDescriptionOnSuccess');
- successCallback();
- },
- function (err) {
- self.trace('setRemoteDescriptionOnFailure', err);
- failureCallback(err);
- }
- );
- /*
- if (this.statsinterval === null && this.maxstats > 0) {
- // start gathering stats
- }
- */
-};
-
-TraceablePeerConnection.prototype.close = function () {
- this.trace('stop');
- if (this.statsinterval !== null) {
- window.clearInterval(this.statsinterval);
- this.statsinterval = null;
- }
- this.peerconnection.close();
-};
-
-TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
- var self = this;
- this.trace('createOffer', JSON.stringify(constraints, null, ' '));
- this.peerconnection.createOffer(
- function (offer) {
- self.trace('createOfferOnSuccess', dumpSDP(offer));
- successCallback(offer);
- },
- function(err) {
- self.trace('createOfferOnFailure', err);
- failureCallback(err);
- },
- constraints
- );
-};
-
-TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
- var self = this;
- this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
- this.peerconnection.createAnswer(
- function (answer) {
- self.trace('createAnswerOnSuccess', dumpSDP(answer));
- successCallback(answer);
- },
- function(err) {
- self.trace('createAnswerOnFailure', err);
- failureCallback(err);
- },
- constraints
- );
-};
-
-TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
- var self = this;
- this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
- 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, errback) {
- if (navigator.mozGetUserMedia) {
- // ignore for now...
- } else {
- this.peerconnection.getStats(callback);
- }
-};
-
-// 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));
- },
- // DTLS should now be enabled by default but..
- pc_constraints: {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]}
- };
- 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();
- $(document).trigger('callaccepted.jingle', [sess.sid]);
- 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
- var sess = new JingleSession(myjid || this.connection.jid,
- 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
- var self = this;
- this.connection.sendIQ(
- $iq({type: 'get', to: this.connection.domain})
- .c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
- function (res) {
- var iceservers = [];
- $(res).find('>services>service').each(function (idx, el) {
- el = $(el);
- var dict = {};
- 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;
- }
- });
- self.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) {
- var self = this;
- var lines = SDPUtil.find_lines(this.session, prefix);
- lines.forEach(function(line) {
- self.session = self.session.replace(line + '\r\n', '');
- });
- 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) {
- var self = this;
- var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
- lines.forEach(function(line) {
- self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
- });
- 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;
- var self = this;
- // 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);
- elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics: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
- 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();
- }
- }
- this.RtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
-
- 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:*
- this.RtcpFbToJingle(i, elem, '*');
-
- // 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
- }
-
- // map ice-ufrag/pwd, dtls fingerprint, candidates
- this.TransportToJingle(i, elem);
-
- 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;
-};
-
-SDP.prototype.TransportToJingle = function (mediaindex, elem) {
- var i = mediaindex;
- var tmp;
- var self = this;
- 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;
- line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
- 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);
- 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();
- }
- });
-};
-
-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) {
- var self = this;
- 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) {
- return content.getAttribute('name');
- }).get();
- if (contents.length > 0) {
- self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
- }
- });
- } 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) {
- return content.getAttribute('name');
- }).get();
- if (group.getAttribute('type') !== null && contents.length > 0) {
- self.raw += 'a=group:' + group.getAttribute('type') + ' ' + contents.join(' ') + '\r\n';
- }
- });
- } 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) {
- return content.getAttribute('name');
- }).get();
- if (bundle.length) {
- this.raw += 'a=group:BUNDLE ' + bundle.join(' ') + '\r\n';
- }
- }
-
- this.session = this.raw;
- jingle.find('>content').each(function () {
- var m = self.jingle2media($(this));
- self.media.push(m);
- });
-
- // 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';
- }
- tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get();
- 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
- media += 'a=fingerprint:' + this.getAttribute('hash');
- media += ' ' + $(this).text();
- media += '\r\n';
- if (this.getAttribute('setup')) {
- media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
- }
- });
- }
- 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';
-
- //
- // 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 () {
- 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');
- }
- media += '\r\n';
- });
- }
- desc.find('payload-type').each(function () {
- media += SDPUtil.build_rtpmap(this) + '\r\n';
- if ($(this).find('>parameter').length) {
- media += 'a=fmtp:' + this.getAttribute('id') + ' ';
- media += $(this).find('parameter').map(function () { return (this.getAttribute('name') ? (this.getAttribute('name') + '=') : '') + this.getAttribute('value'); }).get().join(';');
- media += '\r\n';
- }
- // xep-0293
- media += self.RtcpFbFromJingle($(this), this.getAttribute('id'));
- });
-
- // 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 () {
- media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
- });
-
- 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 () {
- var ssrc = this.getAttribute('ssrc');
- $(this).find('>parameter').each(function () {
- media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
- if (this.getAttribute('value') && this.getAttribute('value').length)
- media += ':' + this.getAttribute('value');
- 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
- //
- 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.pendingop = null;
-
- this.wait = true;
-}
-
-JingleSession.prototype.initiate = function (peerjid, isInitiator) {
- var self = this;
- 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;
- //console.log('create PeerConnection ' + JSON.stringify(this.ice_config));
- 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) {
- self.sendIceCandidate(event.candidate);
- };
- this.peerconnection.onaddstream = function (event) {
- self.remoteStream = event.stream;
- self.remoteStreams.push(event.stream);
- $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
- };
- this.peerconnection.onremovestream = function (event) {
- self.remoteStream = null;
- // FIXME: remove from this.remoteStreams
- $(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
- };
- this.peerconnection.onsignalingstatechange = function (event) {
- if (!(self && self.peerconnection)) return;
- };
- this.peerconnection.oniceconnectionstatechange = function (event) {
- if (!(self && self.peerconnection)) return;
- switch (self.peerconnection.iceConnectionState) {
- case 'connected':
- this.startTime = new Date();
- break;
- case 'disconnected':
- this.stopTime = new Date();
- break;
- }
- $(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
- };
- // add any local and relayed stream
- this.localStreams.forEach(function(stream) {
- self.peerconnection.addStream(stream);
- });
- this.relayedStreams.forEach(function(stream) {
- self.peerconnection.addStream(stream);
- });
-};
-
-JingleSession.prototype.accept = function () {
- var self = this;
- 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';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- error.source = 'answer';
- $(document).trigger('error.jingle', [self.sid, error]);
- },
- 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 () {
- //console.log('setLocalDescription success');
- $(document).trigger('setLocalDescription.jingle', [self.sid]);
- },
- 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) {
- var self = this;
- if (candidate && !this.lasticecandidate) {
- var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
- var jcand = SDPUtil.candidateToJingle(candidate.candidate);
- 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 20ms callout
- window.setTimeout(function () {
- if (self.drip_container.length === 0) return;
- self.sendIceCandidates(self.drip_container);
- self.drip_container = [];
- }, 20);
-
- }
- this.drip_container.push(event.candidate);
- return;
- } else {
- self.sendIceCandidate([event.candidate]);
- }
- }
- } else {
- //console.log('sendIceCandidate: last candidate.');
- if (!this.usetrickle) {
- //console.log('should send full offer now...');
- 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 () {
- //console.log('session initiate ack');
- var ack = {};
- ack.source = 'offer';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- self.state = 'error';
- self.peerconnection.close();
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- error.source = 'offer';
- $(document).trigger('error.jingle', [self.sid, error]);
- },
- 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.sendIceCandidates = function (candidates) {
- console.log('sendIceCandidates', candidates);
- var cand = $iq({to: this.peerjid, type: 'set'})
- .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
- action: 'transport-info',
- initiator: this.initiator,
- sid: this.sid});
- for (var mid = 0; mid < this.localSDP.media.length; mid++) {
- var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
- if (cands.length > 0) {
- var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
- ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
- cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
- 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
- if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
- var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
- 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
- //console.log('was this the last candidate', this.lasticecandidate);
- this.connection.sendIQ(cand,
- function () {
- var ack = {};
- ack.source = 'transportinfo';
- $(document).trigger('ack.jingle', [this.sid, ack]);
- },
- function (stanza) {
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- error.source = 'transportinfo';
- $(document).trigger('error.jingle', [this.sid, error]);
- },
- 10000);
-};
-
-
-JingleSession.prototype.sendOffer = function () {
- //console.log('sendOffer...');
- var self = this;
- this.peerconnection.createOffer(function (sdp) {
- self.createdOffer(sdp);
- },
- function (e) {
- console.error('createOffer failed', e);
- },
- this.media_constraints
- );
-};
-
-JingleSession.prototype.createdOffer = function (sdp) {
- //console.log('createdOffer', sdp);
- var self = this;
- 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';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- self.state = 'error';
- self.peerconnection.close();
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- error.source = 'offer';
- $(document).trigger('error.jingle', [self.sid, error]);
- },
- 10000);
- }
- sdp.sdp = this.localSDP.raw;
- this.peerconnection.setLocalDescription(sdp,
- function () {
- $(document).trigger('setLocalDescription.jingle', [self.sid]);
- //console.log('setLocalDescription success');
- },
- 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) {
- //console.log('setting remote description... ', desctype);
- 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 () {
- //console.log('setRemoteDescription success');
- },
- function (e) {
- console.error('setRemoteDescription error', e);
- }
- );
-};
-
-JingleSession.prototype.addIceCandidate = function (elem) {
- var self = this;
- 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 () {
- 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:')) {
- var tmp = $(this).find('transport');
- self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
- self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
- tmp = $(this).find('transport>fingerprint');
- if (tmp.length) {
- self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
- } else {
- console.log('no dtls fingerprint (webrtc issue #1718?)');
- self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
- }
- 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 {
- this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
- function() {
- },
- function(e) {
- console.log('setRemoteDescription pranswer failed', e.toString());
- });
- } catch (e) {
- console.error('setting pranswer failed', e);
- }
- } else {
- //console.log('not yet setting pranswer');
- }
- }
- // operate on each content element
- elem.each(function () {
- // would love to deactivate this, but firefox still requires it
- var idx = -1;
- var i;
- 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) {
- idx = i;
- break;
- }
- }
- if (idx == -1) { // fall back to localdescription
- 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) {
- 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 {
- self.peerconnection.addIceCandidate(candidate);
- } catch (e) {
- console.error('addIceCandidate failed', e.toString(), line);
- }
- });
- });
-};
-
-JingleSession.prototype.sendAnswer = function (provisional) {
- //console.log('createAnswer', provisional);
- var self = this;
- this.peerconnection.createAnswer(
- function (sdp) {
- self.createdAnswer(sdp, provisional);
- },
- function (e) {
- console.error('createAnswer failed', e);
- },
- this.media_constraints
- );
-};
-
-JingleSession.prototype.createdAnswer = function (sdp, provisional) {
- //console.log('createAnswer callback');
- var self = this;
- 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';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- error.source = 'answer';
- $(document).trigger('error.jingle', [self.sid, error]);
- },
- 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 () {
- $(document).trigger('setLocalDescription.jingle', [self.sid]);
- //console.log('setLocalDescription success');
- },
- 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) {
- var self = this,
- 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 () {
- self.peerconnection.close();
- self.peerconnection = null;
- self.terminate();
- var ack = {};
- ack.source = 'terminate';
- $(document).trigger('ack.jingle', [self.sid, ack]);
- },
- function (stanza) {
- var error = ($(stanza).find('error').length) ? {
- code: $(stanza).find('error').attr('code'),
- reason: $(stanza).find('error :first')[0].tagName,
- }:{};
- $(document).trigger('ack.jingle', [self.sid, error]);
- },
- 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);
-
- var self = this;
- $(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;
- if (!self.addssrc[idx]) self.addssrc[idx] = '';
- self.addssrc[idx] += lines;
- });
- 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);
-
- var self = this;
- $(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;
- if (!self.removessrc[idx]) self.removessrc[idx] = '';
- self.removessrc[idx] += lines;
- });
- sdp.raw = sdp.session + sdp.media.join('');
- });
- this.modifySources();
-};
-
-JingleSession.prototype.modifySources = function() {
- var self = this;
- if (this.peerconnection.signalingState == 'closed') return;
- if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)) return;
- if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
- console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
- this.wait = true;
- window.setTimeout(function() { self.modifySources(); }, 250);
- return;
- }
- if (this.wait) {
- window.setTimeout(function() { self.modifySources(); }, 2500);
- 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() {
- self.peerconnection.createAnswer(
- function(modifiedAnswer) {
- // change video direction, see https://github.com/jitsi/jitmeet/issues/41
- if (self.pendingop !== null) {
- var sdp = new SDP(modifiedAnswer.sdp);
- if (sdp.media.length > 1) {
- switch(self.pendingop) {
- case 'mute':
- sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
- break;
- case 'unmute':
- sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
- break;
- }
- sdp.raw = sdp.session + sdp.media.join('');
- modifiedAnswer.sdp = sdp.raw;
- }
- self.pendingop = null;
- }
-
- self.peerconnection.setLocalDescription(modifiedAnswer,
- function() {
- //console.log('modified setLocalDescription ok');
- $(document).trigger('setLocalDescription.jingle', [self.sid]);
- },
- function(error) {
- console.log('modified setLocalDescription failed');
- }
- );
- },
- function(error) {
- console.log('modified answer failed');
- }
- );
- },
- function(error) {
- console.log('modify failed');
- }
- );
-};
-
-// SDP-based mute by going recvonly/sendrecv
-// FIXME: should probably black out the screen as well
-JingleSession.prototype.hardMuteVideo = function (muted) {
- this.pendingop = muted ? 'mute' : 'unmute';
- this.modifySources();
-
- this.connection.jingle.localStream.getVideoTracks().forEach(function (track) {
- track.enabled = !muted;
- });
-};
-
-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) {
- var self = this;
- 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 () {
- if (self && self.peerconnection && self.peerconnection.getStats) {
- self.peerconnection.getStats(function (stats) {
- 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;
- $(document).trigger('packetloss.jingle', [self.sid, loss]);
- });
- }
- }, interval || 3000);
- return this.statsinterval;
-};
-