diff --git a/app.js b/app.js index 193d44cb0..efdd6d14c 100644 --- a/app.js +++ b/app.js @@ -166,6 +166,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { var sess = connection.jingle.sessions[sid]; if (data.stream.id === 'mixedmslabel') return; videoTracks = data.stream.getVideoTracks(); + console.log("waiting..", videoTracks, selector[0]); if (videoTracks.length === 0 || selector[0].currentTime > 0) { RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF? $(document).trigger('callactive.jingle', [selector, sid]); @@ -437,6 +438,8 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) { }); console.log('new ssrcs', newssrcs); + // Have to clear presence map to get rid of removed streams + connection.emuc.clearPresenceMedia(); var i = 0; Object.keys(newssrcs).forEach(function (mtype) { i++; @@ -542,6 +545,14 @@ $(document).bind('left.muc', function (event, jid) { }); $(document).bind('presence.muc', function (event, jid, info, pres) { + + // Remove old ssrcs coming from the jid + Object.keys(ssrc2jid).forEach(function(ssrc){ + if(ssrc2jid[ssrc] == jid){ + delete ssrc2jid[ssrc]; + } + }); + $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) { //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc')); ssrc2jid[ssrc.getAttribute('ssrc')] = jid; @@ -1191,6 +1202,9 @@ function showToolbar() { // TODO: Enable settings functionality. Need to uncomment the settings button in index.html. // $('#settingsButton').css({visibility:"visible"}); } + if(isDesktopSharingEnabled()) { + $('#desktopsharing').css({display:"inline"}); + } } /* diff --git a/config.js b/config.js index 98dc30d7c..344cd45ea 100644 --- a/config.js +++ b/config.js @@ -8,5 +8,6 @@ var config = { // useStunTurn: true, // use XEP-0215 to fetch STUN and TURN server // useIPv6: true, // ipv6 support. use at your own risk useNicks: false, - bosh: '//lambada.jitsi.net/http-bind' // FIXME: use xep-0156 for that + bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that + chromeDesktopSharing: false // Desktop sharing is disabled by default }; diff --git a/desktopsharing.js b/desktopsharing.js new file mode 100644 index 000000000..e0ac06787 --- /dev/null +++ b/desktopsharing.js @@ -0,0 +1,77 @@ +/** + * Indicates that desktop stream is currently in use(for toggle purpose). + * @type {boolean} + */ +var isUsingScreenStream = false; +/** + * Indicates that switch stream operation is in progress and prevent from triggering new events. + * @type {boolean} + */ +var switchInProgress = false; + +/** + * @returns {boolean} true if desktop sharing feature is available and enabled. + */ +function isDesktopSharingEnabled() { + // Desktop sharing must be enabled in config and works on chrome only. + // Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled. + return config.chromeDesktopSharing && RTC.browser == 'chrome'; +} + +/* + * Toggles screen sharing. + */ +function toggleScreenSharing() { + if (!(connection && connection.connected + && !switchInProgress + && getConferenceHandler().peerconnection.signalingState == 'stable' + && getConferenceHandler().peerconnection.iceConnectionState == 'connected')) { + return; + } + switchInProgress = true; + + // Only the focus is able to set a shared key. + if(!isUsingScreenStream) + { + // Enable screen stream + getUserMediaWithConstraints( + ['screen'], + function(stream){ + isUsingScreenStream = true; + gotScreenStream(stream); + }, + getSwitchStreamFailed + ); + } else { + // Disable screen stream + getUserMediaWithConstraints( + ['video'], + function(stream) { + isUsingScreenStream = false; + gotScreenStream(stream); + }, + getSwitchStreamFailed, config.resolution || '360' + ); + } +} + +function getSwitchStreamFailed(error) { + console.error("Failed to obtain the stream to switch to", error); + switchInProgress = false; +} + +function gotScreenStream(stream) { + var oldStream = connection.jingle.localVideo; + + change_local_video(stream); + + // FIXME: will block switchInProgress on true value in case of exception + getConferenceHandler().switchStreams(stream, oldStream, onDesktopStreamEnabled); +} + +function onDesktopStreamEnabled() { + // Wait a moment before enabling the button + window.setTimeout(function() { + switchInProgress = false; + }, 3000); +} diff --git a/index.html b/index.html index f3de5fe2c..a2904c8d0 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,7 @@ + @@ -15,6 +16,7 @@ + @@ -54,6 +56,10 @@
+ diff --git a/libs/colibri/colibri.focus.js b/libs/colibri/colibri.focus.js index 84e9473fe..7e1060339 100644 --- a/libs/colibri/colibri.focus.js +++ b/libs/colibri/colibri.focus.js @@ -38,7 +38,7 @@ ColibriFocus.prototype = Object.create(SessionBase.prototype); function ColibriFocus(connection, bridgejid) { - SessionBase.call(this, connection); + SessionBase.call(this, connection, Math.random().toString(36).substr(2, 12)); this.bridgejid = bridgejid; this.peers = []; @@ -47,7 +47,6 @@ function ColibriFocus(connection, bridgejid) { // media types of the conference this.media = ['audio', 'video']; - this.sid = Math.random().toString(36).substr(2, 12); this.connection.jingle.sessions[this.sid] = this; this.mychannel = []; this.channels = []; @@ -556,67 +555,86 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) { // tell everyone about a new participants a=ssrc lines (isadd is true) // or a leaving participants a=ssrc lines -// FIXME: should not take an SDP, but rather the a=ssrc lines and probably a=mid -ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) { +ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd) { var self = this; this.peers.forEach(function (peerjid) { - if (peerjid == jid) return; - console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', jid); + if (peerjid == fromJid) return; + console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', fromJid); if (!self.remotessrc[peerjid]) { // FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept // possibly, this.remoteSSRC[session.peerjid] does not exist yet console.warn('do we really want to bother', peerjid, 'with updates yet?'); } - var channel; var peersess = self.connection.jingle.jid2session[peerjid]; - var modify = $iq({to: peerjid, type: 'set'}) - .c('jingle', { - xmlns: 'urn:xmpp:jingle:1', - action: isadd ? 'addsource' : 'removesource', - initiator: peersess.initiator, - sid: peersess.sid - } - ); - // 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++) { - modified = true; - tmp = SDPUtil.find_lines(sdp.media[channel], 'a=ssrc:'); - modify.c('content', {name: SDPUtil.parse_mid(SDPUtil.find_line(sdp.media[channel], 'a=mid:'))}); - modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' }); - // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly - tmp.forEach(function (line) { - var idx = line.indexOf(' '); - var linessrc = line.substr(0, idx).substr(7); - modify.attrs({ssrc: linessrc}); + if(!peersess){ + console.warn('no session with peer: '+peerjid+' yet...'); + return; + } - var kv = line.substr(idx + 1); - modify.c('parameter'); - if (kv.indexOf(':') == -1) { - modify.attrs({ name: kv }); - } else { - modify.attrs({ name: kv.split(':', 2)[0] }); - modify.attrs({ value: kv.split(':', 2)[1] }); - } - modify.up(); - }); - modify.up(); // end of source - modify.up(); // end of content - } - if (modified) { - self.connection.sendIQ(modify, - function (res) { - console.warn('got modify result'); - }, - function (err) { - console.warn('got modify error', err); - } - ); + self.sendSSRCUpdateIq(sdpMediaSsrcs, peersess.sid, peersess.initiator, peerjid, isadd); + }); +}; + +/** + * Overrides SessionBase.addSource. + * + * @param elem proprietary 'add source' Jingle request(XML node). + * @param fromJid JID of the participant to whom new ssrcs belong. + */ +ColibriFocus.prototype.addSource = function (elem, fromJid) { + + var self = this; + this.peerconnection.addSource(elem); + + var peerSsrc = this.remotessrc[fromJid]; + //console.log("On ADD", self.addssrc, peerSsrc); + this.peerconnection.addssrc.forEach(function(val, idx){ + if(!peerSsrc[idx]){ + // add ssrc + peerSsrc[idx] = val; } else { - console.log('modification not necessary'); + if(peerSsrc[idx].indexOf(val) == -1){ + peerSsrc[idx] = peerSsrc[idx]+val; + } } }); + + var oldRemoteSdp = new SDP(this.peerconnection.remoteDescription.sdp); + this.modifySources(function(){ + // Notify other participants about added ssrc + var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp); + var newSSRCs = oldRemoteSdp.getNewMedia(remoteSDP); + self.sendSSRCUpdate(newSSRCs, fromJid, true); + }); +}; + +/** + * Overrides SessionBase.removeSource. + * + * @param elem proprietary 'remove source' Jingle request(XML node). + * @param fromJid JID of the participant to whom removed ssrcs belong. + */ +ColibriFocus.prototype.removeSource = function (elem, fromJid) { + + var self = this; + this.peerconnection.removeSource(elem); + + var peerSsrc = this.remotessrc[fromJid]; + //console.log("On REMOVE", self.removessrc, peerSsrc); + this.peerconnection.removessrc.forEach(function(val, idx){ + if(peerSsrc[idx]){ + // Remove ssrc + peerSsrc[idx] = peerSsrc[idx].replace(val, ''); + } + }); + + var oldSDP = new SDP(self.peerconnection.remoteDescription.sdp); + this.modifySources(function(){ + // Notify other participants about removed ssrc + var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp); + var removedSSRCs = remoteSDP.getNewMedia(oldSDP); + self.sendSSRCUpdate(removedSSRCs, fromJid, false); + }); }; ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) { @@ -630,7 +648,7 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) this.updateChannel(remoteSDP, participant); // ACT 2: tell anyone else about the new SSRCs - this.sendSSRCUpdate(remoteSDP, session.peerjid, true); + this.sendSSRCUpdate(remoteSDP.getMediaSsrcMap(), session.peerjid, true); // ACT 3: note the SSRCs this.remotessrc[session.peerjid] = []; @@ -792,7 +810,7 @@ ColibriFocus.prototype.terminate = function (session, reason) { sdp.media[j] += ssrcs[j]; this.peerconnection.enqueueRemoveSsrc(j, ssrcs[j]); } - this.sendSSRCUpdate(sdp, session.peerjid, false); + this.sendSSRCUpdate(sdp.getMediaSsrcMap(), session.peerjid, false); delete this.remotessrc[session.peerjid]; this.modifySources(); diff --git a/libs/colibri/colibri.session.js b/libs/colibri/colibri.session.js index 1adbd5e46..7fd58c84d 100644 --- a/libs/colibri/colibri.session.js +++ b/libs/colibri/colibri.session.js @@ -61,6 +61,14 @@ ColibriSession.prototype.accept = function () { console.log('ColibriSession.accept'); }; +ColibriSession.prototype.addSource = function (elem, fromJid) { + this.colibri.addSource(elem, fromJid); +}; + +ColibriSession.prototype.removeSource = function (elem, fromJid) { + this.colibri.removeSource(elem, fromJid); +}; + ColibriSession.prototype.terminate = function (reason) { this.colibri.terminate(this, reason); }; diff --git a/libs/strophe/strophe.jingle.adapter.js b/libs/strophe/strophe.jingle.adapter.js index a492e03ca..40200040c 100644 --- a/libs/strophe/strophe.jingle.adapter.js +++ b/libs/strophe/strophe.jingle.adapter.js @@ -25,6 +25,12 @@ function TraceablePeerConnection(ice_config, constraints) { */ this.pendingop = null; + /** + * Flag indicates that peer connection stream have changed and modifySources should proceed. + * @type {boolean} + */ + this.switchstreams = false; + // override as desired this.trace = function(what, info) { //console.warn('WTRACE', what, info); @@ -197,6 +203,7 @@ TraceablePeerConnection.prototype.addSource = function (elem) { console.log('addssrc', new Date().getTime()); console.log('ice', this.iceConnectionState); var sdp = new SDP(this.remoteDescription.sdp); + var mySdp = new SDP(this.peerconnection.localDescription.sdp); var self = this; $(elem).each(function (idx, content) { @@ -205,6 +212,15 @@ TraceablePeerConnection.prototype.addSource = function (elem) { tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); tmp.each(function () { var ssrc = $(this).attr('ssrc'); + if(mySdp.containsSSRC(ssrc)){ + /** + * This happens when multiple participants change their streams at the same time and + * ColibriFocus.modifySources have to wait for stable state. In the meantime multiple + * addssrc are scheduled for update IQ. See + */ + console.warn("Got add stream request for my own ssrc: "+ssrc); + return; + } $(this).find('>parameter').each(function () { lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name'); if ($(this).attr('value') && $(this).attr('value').length) @@ -233,6 +249,7 @@ TraceablePeerConnection.prototype.removeSource = function (elem) { console.log('removessrc', new Date().getTime()); console.log('ice', this.iceConnectionState); var sdp = new SDP(this.remoteDescription.sdp); + var mySdp = new SDP(this.peerconnection.localDescription.sdp); var self = this; $(elem).each(function (idx, content) { @@ -241,6 +258,11 @@ TraceablePeerConnection.prototype.removeSource = function (elem) { tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); tmp.each(function () { var ssrc = $(this).attr('ssrc'); + // This should never happen, but can be useful for bug detection + if(mySdp.containsSSRC(ssrc)){ + console.error("Got remove stream request for my own ssrc: "+ssrc); + return; + } $(this).find('>parameter').each(function () { lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name'); if ($(this).attr('value') && $(this).attr('value').length) @@ -261,7 +283,8 @@ TraceablePeerConnection.prototype.removeSource = function (elem) { TraceablePeerConnection.prototype.modifySources = function(successCallback) { var self = this; if (this.signalingState == 'closed') return; - if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)){ + if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){ + // There is nothing to do since scheduled job might have been executed by another succeeding call if(successCallback){ successCallback(); } @@ -282,6 +305,9 @@ TraceablePeerConnection.prototype.modifySources = function(successCallback) { return; } + // Reset switch streams flag + this.switchstreams = false; + var sdp = new SDP(this.remoteDescription.sdp); // add sources @@ -486,8 +512,11 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res } if (um.indexOf('screen') >= 0) { constraints.video = { - "mandatory": { - "chromeMediaSource": "screen" + mandatory: { + chromeMediaSource: "screen", + maxWidth: window.screen.width, + maxHeight: window.screen.height, + maxFrameRate: 3 } }; } diff --git a/libs/strophe/strophe.jingle.js b/libs/strophe/strophe.jingle.js index 160b4bb6e..21ae246aa 100644 --- a/libs/strophe/strophe.jingle.js +++ b/libs/strophe/strophe.jingle.js @@ -141,10 +141,10 @@ Strophe.addConnectionPlugin('jingle', { } break; case 'addsource': // FIXME: proprietary - sess.addSource($(iq).find('>jingle>content')); + sess.addSource($(iq).find('>jingle>content'), fromJid); break; case 'removesource': // FIXME: proprietary - sess.removeSource($(iq).find('>jingle>content')); + sess.removeSource($(iq).find('>jingle>content'), fromJid); break; default: console.warn('jingle action not implemented', action); diff --git a/libs/strophe/strophe.jingle.sdp.js b/libs/strophe/strophe.jingle.sdp.js index f1b9f940f..245c9de26 100644 --- a/libs/strophe/strophe.jingle.sdp.js +++ b/libs/strophe/strophe.jingle.sdp.js @@ -11,7 +11,75 @@ function SDP(sdp) { this.session = this.media.shift() + '\r\n'; this.raw = this.session + this.media.join(''); } - +/** + * Returns map of MediaChannel mapped per channel idx. + */ +SDP.prototype.getMediaSsrcMap = function() { + var self = this; + var media_ssrcs = {}; + for (channelNum = 0; channelNum < self.media.length; channelNum++) { + modified = true; + tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc:'); + var type = SDPUtil.parse_mid(SDPUtil.find_line(self.media[channelNum], 'a=mid:')); + var channel = new MediaChannel(channelNum, type); + media_ssrcs[channelNum] = channel; + tmp.forEach(function (line) { + var linessrc = line.substring(7).split(' ')[0]; + // allocate new ChannelSsrc + if(!channel.ssrcs[linessrc]) { + channel.ssrcs[linessrc] = new ChannelSsrc(linessrc, type); + } + channel.ssrcs[linessrc].lines.push(line); + }); + } + return media_ssrcs; +} +/** + * Returns true if this SDP contains given SSRC. + * @param ssrc the ssrc to check. + * @returns {boolean} true if this SDP contains given SSRC. + */ +SDP.prototype.containsSSRC = function(ssrc) { + var channels = this.getMediaSsrcMap(); + var contains = false; + Object.keys(channels).forEach(function(chNumber){ + var channel = channels[chNumber]; + //console.log("Check", channel, ssrc); + if(Object.keys(channel.ssrcs).indexOf(ssrc) != -1){ + contains = true; + } + }); + return contains; +} +/** + * Returns map of MediaChannel that contains only media not contained in otherSdp. Mapped by channel idx. + * @param otherSdp the other SDP to check ssrc with. + */ +SDP.prototype.getNewMedia = function(otherSdp) { + var myMedia = this.getMediaSsrcMap(); + var othersMedia = otherSdp.getMediaSsrcMap(); + var newMedia = {}; + Object.keys(othersMedia).forEach(function(channelNum) { + var myChannel = myMedia[channelNum]; + var othersChannel = othersMedia[channelNum]; + if(!myChannel && othersChannel) { + // Add whole channel + newMedia[channelNum] = othersChannel; + return; + } + // Look for new ssrcs accross the channel + Object.keys(othersChannel.ssrcs).forEach(function(ssrc) { + if(Object.keys(myChannel.ssrcs).indexOf(ssrc) === -1) { + // Allocate channel if we've found ssrc that doesn't exist in our channel + if(!newMedia[channelNum]){ + newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType); + } + newMedia[channelNum].ssrcs[ssrc] = othersChannel.ssrcs[ssrc]; + } + }) + }); + return newMedia; +} // remove iSAC and CN from SDP SDP.prototype.mangle = function () { var i, j, mline, lines, rtpmap, newdesc; @@ -485,317 +553,4 @@ SDP.prototype.jingle2media = function (content) { } } 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'; - } -}; +}; \ No newline at end of file diff --git a/libs/strophe/strophe.jingle.sdp.util.js b/libs/strophe/strophe.jingle.sdp.util.js new file mode 100644 index 000000000..c5ad5e5bf --- /dev/null +++ b/libs/strophe/strophe.jingle.sdp.util.js @@ -0,0 +1,353 @@ +/** + * Contains utility classes used in SDP class. + * + */ + +/** + * Class holds a=ssrc lines and media type a=mid + * @param ssrc synchronization source identifier number(a=ssrc lines from SDP) + * @param type media type eg. "audio" or "video"(a=mid frm SDP) + * @constructor + */ +function ChannelSsrc(ssrc, type) { + this.ssrc = ssrc; + this.type = type; + this.lines = []; +} + +/** + * Helper class represents media channel. Is a container for ChannelSsrc, holds channel idx and media type. + * @param channelNumber channel idx in SDP media array. + * @param mediaType media type(a=mid) + * @constructor + */ +function MediaChannel(channelNumber, mediaType) { + /** + * SDP channel number + * @type {*} + */ + this.chNumber = channelNumber; + /** + * Channel media type(a=mid) + * @type {*} + */ + this.mediaType = mediaType; + /** + * The maps of ssrc numbers to ChannelSsrc objects. + */ + this.ssrcs = {}; +} + +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 index a6c75978f..96631ebbf 100644 --- a/libs/strophe/strophe.jingle.session.js +++ b/libs/strophe/strophe.jingle.session.js @@ -3,10 +3,9 @@ JingleSession.prototype = Object.create(SessionBase.prototype); function JingleSession(me, sid, connection) { - SessionBase.call(this, connection); + SessionBase.call(this, connection, sid); this.me = me; - this.sid = sid; this.initiator = null; this.responder = null; this.isInitiator = null; @@ -155,6 +154,22 @@ JingleSession.prototype.accept = function () { ); }; +/** + * Implements SessionBase.sendSSRCUpdate. + */ +JingleSession.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isadd) { + + var self = this; + console.log('tell', self.peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from' + self.me); + + if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')){ + console.log("Too early to send updates"); + return; + } + + this.sendSSRCUpdateIq(sdpMediaSsrcs, self.sid, self.initiator, self.peerjid, isadd); +}; + JingleSession.prototype.terminate = function (reason) { this.state = 'ended'; this.reason = reason; diff --git a/libs/strophe/strophe.jingle.sessionbase.js b/libs/strophe/strophe.jingle.sessionbase.js index 6388269e5..b35d2a6fe 100644 --- a/libs/strophe/strophe.jingle.sessionbase.js +++ b/libs/strophe/strophe.jingle.sessionbase.js @@ -1,11 +1,13 @@ /** * Base class for ColibriFocus and JingleSession. * @param connection Strophe connection object + * @param sid my session identifier(resource) * @constructor */ -function SessionBase(connection){ +function SessionBase(connection, sid){ this.connection = connection; + this.sid = sid; this.peerconnection = new TraceablePeerConnection( connection.jingle.ice_config, @@ -13,26 +15,158 @@ function SessionBase(connection){ } -SessionBase.prototype.modifySources = function() { +SessionBase.prototype.modifySources = function (successCallback) { var self = this; this.peerconnection.modifySources(function(){ $(document).trigger('setLocalDescription.jingle', [self.sid]); + if(successCallback) { + successCallback(); + } }); }; -SessionBase.prototype.addSource = function (elem) { +SessionBase.prototype.addSource = function (elem, fromJid) { this.peerconnection.addSource(elem); this.modifySources(); }; -SessionBase.prototype.removeSource = function (elem) { +SessionBase.prototype.removeSource = function (elem, fromJid) { this.peerconnection.removeSource(elem); this.modifySources(); }; +/** + * Switches video streams. + * @param new_stream new stream that will be used as video of this session. + * @param oldStream old video stream of this session. + * @param success_callback callback executed after successful stream switch. + */ +SessionBase.prototype.switchStreams = function (new_stream, oldStream, success_callback) { + + var self = this; + + // Remember SDP to figure out added/removed SSRCs + var oldSdp = new SDP(self.peerconnection.localDescription.sdp); + + self.peerconnection.removeStream(oldStream); + + self.connection.jingle.localVideo = new_stream; + self.peerconnection.addStream(self.connection.jingle.localVideo); + + self.connection.jingle.localStreams = []; + self.connection.jingle.localStreams.push(self.connection.jingle.localAudio); + self.connection.jingle.localStreams.push(self.connection.jingle.localVideo); + + self.peerconnection.switchstreams = true; + self.modifySources(function() { + console.log('modify sources done'); + + var newSdp = new SDP(self.peerconnection.localDescription.sdp); + console.log("SDPs", oldSdp, newSdp); + self.notifyMySSRCUpdate(oldSdp, newSdp); + + success_callback(); + }); +}; + +/** + * Figures out added/removed ssrcs and send update IQs. + * @param old_sdp SDP object for old description. + * @param new_sdp SDP object for new description. + */ +SessionBase.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { + + var old_media = old_sdp.getMediaSsrcMap(); + var new_media = new_sdp.getMediaSsrcMap(); + //console.log("old/new medias: ", old_media, new_media); + + var toAdd = old_sdp.getNewMedia(new_sdp); + var toRemove = new_sdp.getNewMedia(old_sdp); + //console.log("to add", toAdd); + //console.log("to remove", toRemove); + if(Object.keys(toRemove).length > 0){ + this.sendSSRCUpdate(toRemove, null, false); + } + if(Object.keys(toAdd).length > 0){ + this.sendSSRCUpdate(toAdd, null, true); + } +}; + +/** + * Empty method that does nothing by default. It should send SSRC update IQs to session participants. + * @param sdpMediaSsrcs array of + * @param fromJid + * @param isAdd + */ +SessionBase.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isAdd) { + //FIXME: put default implementation here(maybe from JingleSession?) +} + +/** + * Sends SSRC update IQ. + * @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove. + * @param sid session identifier that will be put into the IQ. + * @param initiator initiator identifier. + * @param toJid destination Jid + * @param isAdd indicates if this is remove or add operation. + */ +SessionBase.prototype.sendSSRCUpdateIq = function(sdpMediaSsrcs, sid, initiator, toJid, isAdd) { + + var self = this; + var modify = $iq({to: toJid, type: 'set'}) + .c('jingle', { + xmlns: 'urn:xmpp:jingle:1', + action: isAdd ? 'addsource' : 'removesource', + initiator: initiator, + sid: sid + } + ); + // FIXME: only announce video ssrcs since we mix audio and dont need + // the audio ssrcs therefore + var modified = false; + Object.keys(sdpMediaSsrcs).forEach(function(channelNum){ + modified = true; + var channel = sdpMediaSsrcs[channelNum]; + modify.c('content', {name: channel.mediaType}); + // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly + // generate sources from lines + Object.keys(channel.ssrcs).forEach(function(ssrcNum) { + var mediaSsrc = channel.ssrcs[ssrcNum]; + modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' }); + modify.attrs({ssrc: mediaSsrc.ssrc}); + // iterate over ssrc lines + mediaSsrc.lines.forEach(function (line) { + var idx = line.indexOf(' '); + var kv = line.substr(idx + 1); + modify.c('parameter'); + if (kv.indexOf(':') == -1) { + modify.attrs({ name: kv }); + } else { + modify.attrs({ name: kv.split(':', 2)[0] }); + modify.attrs({ value: kv.split(':', 2)[1] }); + } + modify.up(); // end of parameter + }); + modify.up(); // end of source + }); + modify.up(); // end of content + }); + if (modified) { + self.connection.sendIQ(modify, + function (res) { + console.info('got modify result', res); + }, + function (err) { + console.error('got modify error', err); + } + ); + } else { + console.log('modification not necessary'); + } +}; // SDP-based mute by going recvonly/sendrecv // FIXME: should probably black out the screen as well diff --git a/muc.js b/muc.js index 576c26cb5..42b798a13 100644 --- a/muc.js +++ b/muc.js @@ -232,6 +232,14 @@ Strophe.addConnectionPlugin('emuc', { this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs; this.presMap['source' + sourceNumber + '_direction'] = direction; }, + clearPresenceMedia: function () { + var self = this; + Object.keys(this.presMap).forEach( function(key) { + if(key.indexOf('source') != -1) { + delete self.presMap[key]; + } + }); + }, addPreziToPresence: function (url, currentSlide) { this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi'; this.presMap['preziurl'] = url;