diff --git a/app.js b/app.js index c162eafdb..04d163a7c 100644 --- a/app.js +++ b/app.js @@ -725,8 +725,8 @@ function toggleVideo() { if (!sess) { return; } - sess.pendingop = ismuted ? 'unmute' : 'mute'; - sess.modifySources(); + sess.peerconnection.pendingop = ismuted ? 'unmute' : 'mute'; + sess.peerconnection.modifySources(); } function toggleAudio() { diff --git a/libs/colibri/colibri.focus.js b/libs/colibri/colibri.focus.js index 0b549016b..c12231a60 100644 --- a/libs/colibri/colibri.focus.js +++ b/libs/colibri/colibri.focus.js @@ -40,7 +40,10 @@ function ColibriFocus(connection, bridgejid) { this.peers = []; this.confid = null; - this.peerconnection = null; + this.peerconnection + = new TraceablePeerConnection( + this.connection.jingle.ice_config, + this.connection.jingle.pc_constraints); // media types of the conference this.media = ['audio', 'video']; @@ -51,13 +54,6 @@ function ColibriFocus(connection, bridgejid) { this.channels = []; this.remotessrc = {}; - // ssrc lines to be added on next update - this.addssrc = []; - // ssrc lines to be removed on next update - this.removessrc = []; - // pending mute/unmute video op that modify local description - this.pendingop = null; - // container for candidates from the focus // gathered before confid is known this.drip_container = []; @@ -81,7 +77,6 @@ ColibriFocus.prototype.makeConference = function (peers) { self.channels.push([]); }); - this.peerconnection = new TraceablePeerConnection(this.connection.jingle.ice_config, this.connection.jingle.pc_constraints); this.peerconnection.addStream(this.connection.jingle.localStream); this.peerconnection.oniceconnectionstatechange = function (event) { console.warn('ice connection state changed to', self.peerconnection.iceConnectionState); @@ -651,9 +646,11 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) // ACT 4: add new a=ssrc lines to local remotedescription for (channel = 0; channel < this.channels[participant].length; channel++) { //if (channel == 0) continue; FIXME: does not work as intended - if (!this.addssrc[channel]) this.addssrc[channel] = ''; if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) { - this.addssrc[channel] += SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'; + this.peerconnection.enqueueAddSsrc( + channel, + SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n' + ); } } this.modifySources(); @@ -764,8 +761,7 @@ ColibriFocus.prototype.terminate = function (session, reason) { } var ssrcs = this.remotessrc[session.peerjid]; for (var i = 0; i < ssrcs.length; i++) { - if (!this.removessrc[i]) this.removessrc[i] = ''; - this.removessrc[i] += ssrcs[i]; + this.peerconnection.enqueueRemoveSsrc(i, ssrcs[i]); } // remove from this.peers this.peers.splice(participant, 1); @@ -796,7 +792,7 @@ ColibriFocus.prototype.terminate = function (session, reason) { for (var j = 0; j < ssrcs.length; j++) { sdp.media[j] = 'a=mid:' + contents[j] + '\r\n'; sdp.media[j] += ssrcs[j]; - this.removessrc[j] += ssrcs[j]; + this.peerconnection.enqueueRemoveSsrc(j, ssrcs[j]); } this.sendSSRCUpdate(sdp, session.peerjid, false); @@ -806,126 +802,14 @@ ColibriFocus.prototype.terminate = function (session, reason) { ColibriFocus.prototype.modifySources = function () { var self = this; - if (this.peerconnection.signalingState == 'closed') return; - if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)) return; - - // FIXME: this is a big hack - // https://code.google.com/p/webrtc/issues/detail?id=2688 - if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) { - console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState); - window.setTimeout(function () { self.modifySources(); }, 250); - this.wait = true; - 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.peerconnection.modifySources(function(){ + $(document).trigger('setLocalDescription.jingle', [self.sid]); }); - 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 () { - console.log('setModifiedRemoteDescription ok'); - self.peerconnection.createAnswer( - function (modifiedAnswer) { - console.log('modifiedAnswer created'); - - // 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; - } - - // FIXME: pushing down an answer while ice connection state - // is still checking is bad... - //console.log(self.peerconnection.iceConnectionState); - - // trying to work around another chrome bug - //modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass'); - self.peerconnection.setLocalDescription(modifiedAnswer, - function () { - console.log('setModifiedLocalDescription ok'); - $(document).trigger('setLocalDescription.jingle', [self.sid]); - }, - function (error) { - console.log('setModifiedLocalDescription failed', error); - } - ); - }, - function (error) { - console.log('createModifiedAnswer failed', error); - } - ); - }, - function (error) { - console.log('setModifiedRemoteDescription failed', error); - } - ); - /* - * now that we have a passive focus, this way is bad again! :-) - this.peerconnection.createOffer( - function (modifiedOffer) { - console.log('created (un)modified offer'); - self.peerconnection.setLocalDescription(modifiedOffer, - function () { - console.log('setModifiedLocalDescription ok'); - self.peerconnection.setRemoteDescription( - new RTCSessionDescription({type: 'answer', sdp: sdp.raw }), - function () { - console.log('setModifiedRemoteDescription ok'); - }, - function (error) { - console.log('setModifiedRemoteDescription failed'); - } - ); - $(document).trigger('setLocalDescription.jingle', [self.sid]); - }, - function (error) { - console.log('setModifiedLocalDescription failed'); - } - ); - }, - function (error) { - console.log('creating (un)modified offerfailed'); - } - ); - */ }; ColibriFocus.prototype.hardMuteVideo = function (muted) { - this.pendingop = muted ? 'mute' : 'unmute'; - this.modifySources(); + + this.peerconnection.hardMuteVideo(muted); this.connection.jingle.localStream.getVideoTracks().forEach(function (track) { track.enabled = !muted; diff --git a/libs/strophe/strophe.jingle.adapter.js b/libs/strophe/strophe.jingle.adapter.js index 113df4f08..3f4cc1314 100644 --- a/libs/strophe/strophe.jingle.adapter.js +++ b/libs/strophe/strophe.jingle.adapter.js @@ -7,6 +7,24 @@ function TraceablePeerConnection(ice_config, constraints) { this.statsinterval = null; this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable + /** + * Array of ssrcs that will be added on next modifySources call. + * @type {Array} + */ + this.addssrc = []; + /** + * Array of ssrcs that will be added on next modifySources call. + * @type {Array} + */ + this.removessrc = []; + /** + * Pending operation that will be done during modifySources call. + * Currently 'mute'/'unmute' operations are supported. + * + * @type {String} + */ + this.pendingop = null; + // override as desired this.trace = function(what, info) { //console.warn('WTRACE', what, info); @@ -163,6 +181,173 @@ TraceablePeerConnection.prototype.setRemoteDescription = function (description, */ }; +TraceablePeerConnection.prototype.hardMuteVideo = function (muted) { + this.pendingop = muted ? 'mute' : 'unmute'; + this.modifySources(); +}; + +TraceablePeerConnection.prototype.enqueueAddSsrc = function(channel, ssrcLines) { + if (!this.addssrc[channel]) { + this.addssrc[channel] = ''; + } + this.addssrc[channel] += ssrcLines; +} + +TraceablePeerConnection.prototype.addSource = function (elem) { + console.log('addssrc', new Date().getTime()); + console.log('ice', this.iceConnectionState); + var sdp = new SDP(this.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; + self.enqueueAddSsrc(idx, lines); + }); + sdp.raw = sdp.session + sdp.media.join(''); + }); + this.modifySources(); +}; + +TraceablePeerConnection.prototype.enqueueRemoveSsrc = function(channel, ssrcLines) { + if (!this.removessrc[channel]){ + this.removessrc[channel] = ''; + } + this.removessrc[channel] += ssrcLines; +} + +TraceablePeerConnection.prototype.removeSource = function (elem) { + console.log('removessrc', new Date().getTime()); + console.log('ice', this.iceConnectionState); + var sdp = new SDP(this.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; + self.enqueueRemoveSsrc(idx, lines); + }); + sdp.raw = sdp.session + sdp.media.join(''); + }); + this.modifySources(); +}; + +TraceablePeerConnection.prototype.modifySources = function(successCallback) { + var self = this; + if (this.signalingState == 'closed') return; + if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)){ + if(successCallback){ + successCallback(); + } + return; + } + + // FIXME: this is a big hack + // https://code.google.com/p/webrtc/issues/detail?id=2688 + if (!(this.signalingState == 'stable' && this.iceConnectionState == 'connected')) { + console.warn('modifySources not yet', this.signalingState, this.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.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.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}), + function() { + self.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.setLocalDescription(modifiedAnswer, + function() { + //console.log('modified setLocalDescription ok'); + if(successCallback){ + successCallback(); + } + }, + function(error) { + console.error('modified setLocalDescription failed', error); + } + ); + }, + function(error) { + console.error('modified answer failed', error); + } + ); + }, + function(error) { + console.error('modify failed', error); + } + ); +}; + TraceablePeerConnection.prototype.close = function () { this.trace('stop'); if (this.statsinterval !== null) { diff --git a/libs/strophe/strophe.jingle.session.js b/libs/strophe/strophe.jingle.session.js index 730162192..3c8576923 100644 --- a/libs/strophe/strophe.jingle.session.js +++ b/libs/strophe/strophe.jingle.session.js @@ -9,7 +9,19 @@ function JingleSession(me, sid, connection) { this.isInitiator = null; this.peerjid = null; this.state = null; + /** + * Peer connection instance. + * @type {TraceablePeerConnection} + */ this.peerconnection = null; + //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); + } + this.remoteStream = null; this.localSDP = null; this.remoteSDP = null; @@ -35,10 +47,6 @@ function JingleSession(me, sid, connection) { this.reason = null; - this.addssrc = []; - this.removessrc = []; - this.pendingop = null; - this.wait = true; } @@ -54,16 +62,6 @@ JingleSession.prototype.initiate = function (peerjid, isInitiator) { 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; @@ -635,150 +633,27 @@ JingleSession.prototype.sendTerminate = function (reason, text) { 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(); + this.peerconnection.addSource(elem); }; 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(); + this.peerconnection.removeSource(elem); }; 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.peerconnection.modifySources(function(){ + $(document).trigger('setLocalDescription.jingle', [self.sid]); }); - 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.peerconnection.hardMuteVideo(muted); this.connection.jingle.localStream.getVideoTracks().forEach(function (track) { track.enabled = !muted;