Makes it possible to switch video streams during the session. Adds desktop sharing feature for chrome.

This commit is contained in:
paweldomas 2014-03-13 13:22:54 +01:00
parent a5951df0d9
commit 3e34df8730
13 changed files with 798 additions and 380 deletions

14
app.js
View File

@ -166,6 +166,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
var sess = connection.jingle.sessions[sid]; var sess = connection.jingle.sessions[sid];
if (data.stream.id === 'mixedmslabel') return; if (data.stream.id === 'mixedmslabel') return;
videoTracks = data.stream.getVideoTracks(); videoTracks = data.stream.getVideoTracks();
console.log("waiting..", videoTracks, selector[0]);
if (videoTracks.length === 0 || selector[0].currentTime > 0) { if (videoTracks.length === 0 || selector[0].currentTime > 0) {
RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF? RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
$(document).trigger('callactive.jingle', [selector, sid]); $(document).trigger('callactive.jingle', [selector, sid]);
@ -437,6 +438,8 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) {
}); });
console.log('new ssrcs', newssrcs); console.log('new ssrcs', newssrcs);
// Have to clear presence map to get rid of removed streams
connection.emuc.clearPresenceMedia();
var i = 0; var i = 0;
Object.keys(newssrcs).forEach(function (mtype) { Object.keys(newssrcs).forEach(function (mtype) {
i++; i++;
@ -542,6 +545,14 @@ $(document).bind('left.muc', function (event, jid) {
}); });
$(document).bind('presence.muc', function (event, jid, info, pres) { $(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) { $(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')); //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
ssrc2jid[ssrc.getAttribute('ssrc')] = jid; ssrc2jid[ssrc.getAttribute('ssrc')] = jid;
@ -1191,6 +1202,9 @@ function showToolbar() {
// TODO: Enable settings functionality. Need to uncomment the settings button in index.html. // TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
// $('#settingsButton').css({visibility:"visible"}); // $('#settingsButton').css({visibility:"visible"});
} }
if(isDesktopSharingEnabled()) {
$('#desktopsharing').css({display:"inline"});
}
} }
/* /*

View File

@ -8,5 +8,6 @@ var config = {
// useStunTurn: true, // use XEP-0215 to fetch STUN and TURN server // useStunTurn: true, // use XEP-0215 to fetch STUN and TURN server
// useIPv6: true, // ipv6 support. use at your own risk // useIPv6: true, // ipv6 support. use at your own risk
useNicks: false, 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
}; };

77
desktopsharing.js Normal file
View File

@ -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} <tt>true</tt> 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);
}

View File

@ -6,6 +6,7 @@
<script src="libs/strophe/strophe.jingle.bundle.js?v=8"></script> <script src="libs/strophe/strophe.jingle.bundle.js?v=8"></script>
<script src="libs/strophe/strophe.jingle.js?v=1"></script> <script src="libs/strophe/strophe.jingle.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.sdp.js?v=1"></script> <script src="libs/strophe/strophe.jingle.sdp.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.sdp.util.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.sessionbase.js?v=1"></script> <script src="libs/strophe/strophe.jingle.sessionbase.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.session.js?v=1"></script> <script src="libs/strophe/strophe.jingle.session.js?v=1"></script>
<script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation --> <script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
@ -15,6 +16,7 @@
<script src="estos_log.js?v=2"></script><!-- simple stanza logger --> <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
<script src="app.js?v=23"></script><!-- application logic --> <script src="app.js?v=23"></script><!-- application logic -->
<script src="chat.js?v=3"></script><!-- chat logic --> <script src="chat.js?v=3"></script><!-- chat logic -->
<script src="desktopsharing.js?v=1"></script><!-- desktop sharing logic -->
<script src="util.js?v=2"></script><!-- utility functions --> <script src="util.js?v=2"></script><!-- utility functions -->
<script src="etherpad.js?v=5"></script><!-- etherpad plugin --> <script src="etherpad.js?v=5"></script><!-- etherpad plugin -->
<script src="smileys.js?v=1"></script><!-- smiley images --> <script src="smileys.js?v=1"></script><!-- smiley images -->
@ -54,6 +56,10 @@
<a class="button" onclick='Etherpad.toggleEtherpad(0);'><i title="Open shared document" class="fa fa-file-text fa-lg"></i></a> <a class="button" onclick='Etherpad.toggleEtherpad(0);'><i title="Open shared document" class="fa fa-file-text fa-lg"></i></a>
</span> </span>
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<span id="desktopsharing" style="display: none">
<a class="button" onclick="toggleScreenSharing();"><i title="Share screen" class="fa fa-desktop fa-lg"></i></a>
<div class="header_button_separator"></div>
</span>
<a class="button" onclick='toggleFullScreen();'><i title="Enter / Exit Full Screen" class="fa fa-arrows-alt fa-lg"></i></a> <a class="button" onclick='toggleFullScreen();'><i title="Enter / Exit Full Screen" class="fa fa-arrows-alt fa-lg"></i></a>
</span> </span>
</div> </div>

View File

@ -38,7 +38,7 @@
ColibriFocus.prototype = Object.create(SessionBase.prototype); ColibriFocus.prototype = Object.create(SessionBase.prototype);
function ColibriFocus(connection, bridgejid) { function ColibriFocus(connection, bridgejid) {
SessionBase.call(this, connection); SessionBase.call(this, connection, Math.random().toString(36).substr(2, 12));
this.bridgejid = bridgejid; this.bridgejid = bridgejid;
this.peers = []; this.peers = [];
@ -47,7 +47,6 @@ function ColibriFocus(connection, bridgejid) {
// media types of the conference // media types of the conference
this.media = ['audio', 'video']; this.media = ['audio', 'video'];
this.sid = Math.random().toString(36).substr(2, 12);
this.connection.jingle.sessions[this.sid] = this; this.connection.jingle.sessions[this.sid] = this;
this.mychannel = []; this.mychannel = [];
this.channels = []; this.channels = [];
@ -556,67 +555,86 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
// tell everyone about a new participants a=ssrc lines (isadd is true) // tell everyone about a new participants a=ssrc lines (isadd is true)
// or a leaving participants a=ssrc lines // 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 (sdpMediaSsrcs, fromJid, isadd) {
ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) {
var self = this; var self = this;
this.peers.forEach(function (peerjid) { this.peers.forEach(function (peerjid) {
if (peerjid == jid) return; if (peerjid == fromJid) return;
console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', jid); console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', fromJid);
if (!self.remotessrc[peerjid]) { if (!self.remotessrc[peerjid]) {
// FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept // 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 // possibly, this.remoteSSRC[session.peerjid] does not exist yet
console.warn('do we really want to bother', peerjid, 'with updates yet?'); console.warn('do we really want to bother', peerjid, 'with updates yet?');
} }
var channel;
var peersess = self.connection.jingle.jid2session[peerjid]; var peersess = self.connection.jingle.jid2session[peerjid];
var modify = $iq({to: peerjid, type: 'set'}) if(!peersess){
.c('jingle', { console.warn('no session with peer: '+peerjid+' yet...');
xmlns: 'urn:xmpp:jingle:1', return;
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});
var kv = line.substr(idx + 1); self.sendSSRCUpdateIq(sdpMediaSsrcs, peersess.sid, peersess.initiator, peerjid, isadd);
modify.c('parameter'); });
if (kv.indexOf(':') == -1) { };
modify.attrs({ name: kv });
} else { /**
modify.attrs({ name: kv.split(':', 2)[0] }); * Overrides SessionBase.addSource.
modify.attrs({ value: kv.split(':', 2)[1] }); *
} * @param elem proprietary 'add source' Jingle request(XML node).
modify.up(); * @param fromJid JID of the participant to whom new ssrcs belong.
}); */
modify.up(); // end of source ColibriFocus.prototype.addSource = function (elem, fromJid) {
modify.up(); // end of content
} var self = this;
if (modified) { this.peerconnection.addSource(elem);
self.connection.sendIQ(modify,
function (res) { var peerSsrc = this.remotessrc[fromJid];
console.warn('got modify result'); //console.log("On ADD", self.addssrc, peerSsrc);
}, this.peerconnection.addssrc.forEach(function(val, idx){
function (err) { if(!peerSsrc[idx]){
console.warn('got modify error', err); // add ssrc
} peerSsrc[idx] = val;
);
} else { } 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) { ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
@ -630,7 +648,7 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
this.updateChannel(remoteSDP, participant); this.updateChannel(remoteSDP, participant);
// ACT 2: tell anyone else about the new SSRCs // 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 // ACT 3: note the SSRCs
this.remotessrc[session.peerjid] = []; this.remotessrc[session.peerjid] = [];
@ -792,7 +810,7 @@ ColibriFocus.prototype.terminate = function (session, reason) {
sdp.media[j] += ssrcs[j]; sdp.media[j] += ssrcs[j];
this.peerconnection.enqueueRemoveSsrc(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]; delete this.remotessrc[session.peerjid];
this.modifySources(); this.modifySources();

View File

@ -61,6 +61,14 @@ ColibriSession.prototype.accept = function () {
console.log('ColibriSession.accept'); 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) { ColibriSession.prototype.terminate = function (reason) {
this.colibri.terminate(this, reason); this.colibri.terminate(this, reason);
}; };

View File

@ -25,6 +25,12 @@ function TraceablePeerConnection(ice_config, constraints) {
*/ */
this.pendingop = null; this.pendingop = null;
/**
* Flag indicates that peer connection stream have changed and modifySources should proceed.
* @type {boolean}
*/
this.switchstreams = false;
// override as desired // override as desired
this.trace = function(what, info) { this.trace = function(what, info) {
//console.warn('WTRACE', what, info); //console.warn('WTRACE', what, info);
@ -197,6 +203,7 @@ TraceablePeerConnection.prototype.addSource = function (elem) {
console.log('addssrc', new Date().getTime()); console.log('addssrc', new Date().getTime());
console.log('ice', this.iceConnectionState); console.log('ice', this.iceConnectionState);
var sdp = new SDP(this.remoteDescription.sdp); var sdp = new SDP(this.remoteDescription.sdp);
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
var self = this; var self = this;
$(elem).each(function (idx, content) { $(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 = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
tmp.each(function () { tmp.each(function () {
var ssrc = $(this).attr('ssrc'); 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 () { $(this).find('>parameter').each(function () {
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name'); lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
if ($(this).attr('value') && $(this).attr('value').length) 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('removessrc', new Date().getTime());
console.log('ice', this.iceConnectionState); console.log('ice', this.iceConnectionState);
var sdp = new SDP(this.remoteDescription.sdp); var sdp = new SDP(this.remoteDescription.sdp);
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
var self = this; var self = this;
$(elem).each(function (idx, content) { $(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 = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
tmp.each(function () { tmp.each(function () {
var ssrc = $(this).attr('ssrc'); 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 () { $(this).find('>parameter').each(function () {
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name'); lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
if ($(this).attr('value') && $(this).attr('value').length) if ($(this).attr('value') && $(this).attr('value').length)
@ -261,7 +283,8 @@ TraceablePeerConnection.prototype.removeSource = function (elem) {
TraceablePeerConnection.prototype.modifySources = function(successCallback) { TraceablePeerConnection.prototype.modifySources = function(successCallback) {
var self = this; var self = this;
if (this.signalingState == 'closed') return; 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){ if(successCallback){
successCallback(); successCallback();
} }
@ -282,6 +305,9 @@ TraceablePeerConnection.prototype.modifySources = function(successCallback) {
return; return;
} }
// Reset switch streams flag
this.switchstreams = false;
var sdp = new SDP(this.remoteDescription.sdp); var sdp = new SDP(this.remoteDescription.sdp);
// add sources // add sources
@ -486,8 +512,11 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res
} }
if (um.indexOf('screen') >= 0) { if (um.indexOf('screen') >= 0) {
constraints.video = { constraints.video = {
"mandatory": { mandatory: {
"chromeMediaSource": "screen" chromeMediaSource: "screen",
maxWidth: window.screen.width,
maxHeight: window.screen.height,
maxFrameRate: 3
} }
}; };
} }

View File

@ -141,10 +141,10 @@ Strophe.addConnectionPlugin('jingle', {
} }
break; break;
case 'addsource': // FIXME: proprietary case 'addsource': // FIXME: proprietary
sess.addSource($(iq).find('>jingle>content')); sess.addSource($(iq).find('>jingle>content'), fromJid);
break; break;
case 'removesource': // FIXME: proprietary case 'removesource': // FIXME: proprietary
sess.removeSource($(iq).find('>jingle>content')); sess.removeSource($(iq).find('>jingle>content'), fromJid);
break; break;
default: default:
console.warn('jingle action not implemented', action); console.warn('jingle action not implemented', action);

View File

@ -11,7 +11,75 @@ function SDP(sdp) {
this.session = this.media.shift() + '\r\n'; this.session = this.media.shift() + '\r\n';
this.raw = this.session + this.media.join(''); 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 <tt>true</tt> if this SDP contains given SSRC.
* @param ssrc the ssrc to check.
* @returns {boolean} <tt>true</tt> 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 <tt>otherSdp</tt>. 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 // remove iSAC and CN from SDP
SDP.prototype.mangle = function () { SDP.prototype.mangle = function () {
var i, j, mline, lines, rtpmap, newdesc; var i, j, mline, lines, rtpmap, newdesc;
@ -485,317 +553,4 @@ SDP.prototype.jingle2media = function (content) {
} }
} }
return media; return media;
}; };
SDPUtil = {
iceparams: function (mediadesc, sessiondesc) {
var data = null;
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
data = {
ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
};
}
return data;
},
parse_iceufrag: function (line) {
return line.substring(12);
},
build_iceufrag: function (frag) {
return 'a=ice-ufrag:' + frag;
},
parse_icepwd: function (line) {
return line.substring(10);
},
build_icepwd: function (pwd) {
return 'a=ice-pwd:' + pwd;
},
parse_mid: function (line) {
return line.substring(6);
},
parse_mline: function (line) {
var parts = line.substring(2).split(' '),
data = {};
data.media = parts.shift();
data.port = parts.shift();
data.proto = parts.shift();
if (parts[parts.length - 1] === '') { // trailing whitespace
parts.pop();
}
data.fmt = parts;
return data;
},
build_mline: function (mline) {
return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
},
parse_rtpmap: function (line) {
var parts = line.substring(9).split(' '),
data = {};
data.id = parts.shift();
parts = parts[0].split('/');
data.name = parts.shift();
data.clockrate = parts.shift();
data.channels = parts.length ? parts.shift() : '1';
return data;
},
build_rtpmap: function (el) {
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
line += '/' + el.getAttribute('channels');
}
return line;
},
parse_crypto: function (line) {
var parts = line.substring(9).split(' '),
data = {};
data.tag = parts.shift();
data['crypto-suite'] = parts.shift();
data['key-params'] = parts.shift();
if (parts.length) {
data['session-params'] = parts.join(' ');
}
return data;
},
parse_fingerprint: function (line) { // RFC 4572
var parts = line.substring(14).split(' '),
data = {};
data.hash = parts.shift();
data.fingerprint = parts.shift();
// TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
return data;
},
parse_fmtp: function (line) {
var parts = line.split(' '),
i, key, value,
data = [];
parts.shift();
parts = parts.join(' ').split(';');
for (i = 0; i < parts.length; i++) {
key = parts[i].split('=')[0];
while (key.length && key[0] == ' ') {
key = key.substring(1);
}
value = parts[i].split('=')[1];
if (key && value) {
data.push({name: key, value: value});
} else if (key) {
// rfc 4733 (DTMF) style stuff
data.push({name: '', value: key});
}
}
return data;
},
parse_icecandidate: function (line) {
var candidate = {},
elems = line.split(' ');
candidate.foundation = elems[0].substring(12);
candidate.component = elems[1];
candidate.protocol = elems[2].toLowerCase();
candidate.priority = elems[3];
candidate.ip = elems[4];
candidate.port = elems[5];
// elems[6] => "typ"
candidate.type = elems[7];
candidate.generation = 0; // default value, may be overwritten below
for (var i = 8; i < elems.length; i += 2) {
switch (elems[i]) {
case 'raddr':
candidate['rel-addr'] = elems[i + 1];
break;
case 'rport':
candidate['rel-port'] = elems[i + 1];
break;
case 'generation':
candidate.generation = elems[i + 1];
break;
default: // TODO
console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
}
}
candidate.network = '1';
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
return candidate;
},
build_icecandidate: function (cand) {
var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
line += ' ';
switch (cand.type) {
case 'srflx':
case 'prflx':
case 'relay':
if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
line += 'raddr';
line += ' ';
line += cand['rel-addr'];
line += ' ';
line += 'rport';
line += ' ';
line += cand['rel-port'];
line += ' ';
}
break;
}
line += 'generation';
line += ' ';
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
return line;
},
parse_ssrc: function (desc) {
// proprietary mapping of a=ssrc lines
// TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
// and parse according to that
var lines = desc.split('\r\n'),
data = {};
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, 7) == 'a=ssrc:') {
var idx = lines[i].indexOf(' ');
data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
}
}
return data;
},
parse_rtcpfb: function (line) {
var parts = line.substr(10).split(' ');
var data = {};
data.pt = parts.shift();
data.type = parts.shift();
data.params = parts;
return data;
},
parse_extmap: function (line) {
var parts = line.substr(9).split(' ');
var data = {};
data.value = parts.shift();
if (data.value.indexOf('/') != -1) {
data.direction = data.value.substr(data.value.indexOf('/') + 1);
data.value = data.value.substr(0, data.value.indexOf('/'));
} else {
data.direction = 'both';
}
data.uri = parts.shift();
data.params = parts;
return data;
},
find_line: function (haystack, needle, sessionpart) {
var lines = haystack.split('\r\n');
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, needle.length) == needle) {
return lines[i];
}
}
if (!sessionpart) {
return false;
}
// search session part
lines = sessionpart.split('\r\n');
for (var j = 0; j < lines.length; j++) {
if (lines[j].substring(0, needle.length) == needle) {
return lines[j];
}
}
return false;
},
find_lines: function (haystack, needle, sessionpart) {
var lines = haystack.split('\r\n'),
needles = [];
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, needle.length) == needle)
needles.push(lines[i]);
}
if (needles.length || !sessionpart) {
return needles;
}
// search session part
lines = sessionpart.split('\r\n');
for (var j = 0; j < lines.length; j++) {
if (lines[j].substring(0, needle.length) == needle) {
needles.push(lines[j]);
}
}
return needles;
},
candidateToJingle: function (line) {
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
if (line.substring(0, 12) != 'a=candidate:') {
console.log('parseCandidate called with a line that is not a candidate line');
console.log(line);
return null;
}
if (line.substring(line.length - 2) == '\r\n') // chomp it
line = line.substring(0, line.length - 2);
var candidate = {},
elems = line.split(' '),
i;
if (elems[6] != 'typ') {
console.log('did not find typ in the right place');
console.log(line);
return null;
}
candidate.foundation = elems[0].substring(12);
candidate.component = elems[1];
candidate.protocol = elems[2].toLowerCase();
candidate.priority = elems[3];
candidate.ip = elems[4];
candidate.port = elems[5];
// elems[6] => "typ"
candidate.type = elems[7];
for (i = 8; i < elems.length; i += 2) {
switch (elems[i]) {
case 'raddr':
candidate['rel-addr'] = elems[i + 1];
break;
case 'rport':
candidate['rel-port'] = elems[i + 1];
break;
case 'generation':
candidate.generation = elems[i + 1];
break;
default: // TODO
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
}
}
candidate.network = '1';
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
return candidate;
},
candidateFromJingle: function (cand) {
var line = 'a=candidate:';
line += cand.getAttribute('foundation');
line += ' ';
line += cand.getAttribute('component');
line += ' ';
line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
line += ' ';
line += cand.getAttribute('priority');
line += ' ';
line += cand.getAttribute('ip');
line += ' ';
line += cand.getAttribute('port');
line += ' ';
line += 'typ';
line += ' ' + cand.getAttribute('type');
line += ' ';
switch (cand.getAttribute('type')) {
case 'srflx':
case 'prflx':
case 'relay':
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
line += 'raddr';
line += ' ';
line += cand.getAttribute('rel-addr');
line += ' ';
line += 'rport';
line += ' ';
line += cand.getAttribute('rel-port');
line += ' ';
}
break;
}
line += 'generation';
line += ' ';
line += cand.getAttribute('generation') || '0';
return line + '\r\n';
}
};

View File

@ -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
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
if (line.substring(0, 12) != 'a=candidate:') {
console.log('parseCandidate called with a line that is not a candidate line');
console.log(line);
return null;
}
if (line.substring(line.length - 2) == '\r\n') // chomp it
line = line.substring(0, line.length - 2);
var candidate = {},
elems = line.split(' '),
i;
if (elems[6] != 'typ') {
console.log('did not find typ in the right place');
console.log(line);
return null;
}
candidate.foundation = elems[0].substring(12);
candidate.component = elems[1];
candidate.protocol = elems[2].toLowerCase();
candidate.priority = elems[3];
candidate.ip = elems[4];
candidate.port = elems[5];
// elems[6] => "typ"
candidate.type = elems[7];
for (i = 8; i < elems.length; i += 2) {
switch (elems[i]) {
case 'raddr':
candidate['rel-addr'] = elems[i + 1];
break;
case 'rport':
candidate['rel-port'] = elems[i + 1];
break;
case 'generation':
candidate.generation = elems[i + 1];
break;
default: // TODO
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
}
}
candidate.network = '1';
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
return candidate;
},
candidateFromJingle: function (cand) {
var line = 'a=candidate:';
line += cand.getAttribute('foundation');
line += ' ';
line += cand.getAttribute('component');
line += ' ';
line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
line += ' ';
line += cand.getAttribute('priority');
line += ' ';
line += cand.getAttribute('ip');
line += ' ';
line += cand.getAttribute('port');
line += ' ';
line += 'typ';
line += ' ' + cand.getAttribute('type');
line += ' ';
switch (cand.getAttribute('type')) {
case 'srflx':
case 'prflx':
case 'relay':
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
line += 'raddr';
line += ' ';
line += cand.getAttribute('rel-addr');
line += ' ';
line += 'rport';
line += ' ';
line += cand.getAttribute('rel-port');
line += ' ';
}
break;
}
line += 'generation';
line += ' ';
line += cand.getAttribute('generation') || '0';
return line + '\r\n';
}
};

View File

@ -3,10 +3,9 @@
JingleSession.prototype = Object.create(SessionBase.prototype); JingleSession.prototype = Object.create(SessionBase.prototype);
function JingleSession(me, sid, connection) { function JingleSession(me, sid, connection) {
SessionBase.call(this, connection); SessionBase.call(this, connection, sid);
this.me = me; this.me = me;
this.sid = sid;
this.initiator = null; this.initiator = null;
this.responder = null; this.responder = null;
this.isInitiator = 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) { JingleSession.prototype.terminate = function (reason) {
this.state = 'ended'; this.state = 'ended';
this.reason = reason; this.reason = reason;

View File

@ -1,11 +1,13 @@
/** /**
* Base class for ColibriFocus and JingleSession. * Base class for ColibriFocus and JingleSession.
* @param connection Strophe connection object * @param connection Strophe connection object
* @param sid my session identifier(resource)
* @constructor * @constructor
*/ */
function SessionBase(connection){ function SessionBase(connection, sid){
this.connection = connection; this.connection = connection;
this.sid = sid;
this.peerconnection this.peerconnection
= new TraceablePeerConnection( = new TraceablePeerConnection(
connection.jingle.ice_config, connection.jingle.ice_config,
@ -13,26 +15,158 @@ function SessionBase(connection){
} }
SessionBase.prototype.modifySources = function() { SessionBase.prototype.modifySources = function (successCallback) {
var self = this; var self = this;
this.peerconnection.modifySources(function(){ this.peerconnection.modifySources(function(){
$(document).trigger('setLocalDescription.jingle', [self.sid]); $(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.peerconnection.addSource(elem);
this.modifySources(); this.modifySources();
}; };
SessionBase.prototype.removeSource = function (elem) { SessionBase.prototype.removeSource = function (elem, fromJid) {
this.peerconnection.removeSource(elem); this.peerconnection.removeSource(elem);
this.modifySources(); 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 // SDP-based mute by going recvonly/sendrecv
// FIXME: should probably black out the screen as well // FIXME: should probably black out the screen as well

8
muc.js
View File

@ -232,6 +232,14 @@ Strophe.addConnectionPlugin('emuc', {
this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs; this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs;
this.presMap['source' + sourceNumber + '_direction'] = direction; 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) { addPreziToPresence: function (url, currentSlide) {
this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi'; this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi';
this.presMap['preziurl'] = url; this.presMap['preziurl'] = url;