Fixes desktop sharing when used with simulcast.

This commit is contained in:
George Politis 2014-11-11 15:46:13 +01:00
parent ee1c221e6d
commit a0092b78ca
5 changed files with 279 additions and 156 deletions

View File

@ -11,8 +11,8 @@
<meta itemprop="image" content="/images/jitsilogo.png"/>
<script src="libs/jquery-2.1.1.min.js"></script>
<script src="config.js?v=5"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="simulcast.js?v=6"></script><!-- simulcast handling -->
<script src="libs/strophe/strophe.jingle.adapter.js?v=2"></script><!-- strophe.jingle bundles -->
<script src="simulcast.js?v=7"></script><!-- simulcast handling -->
<script src="libs/strophe/strophe.jingle.adapter.js?v=3"></script><!-- strophe.jingle bundles -->
<script src="libs/strophe/strophe.min.js?v=1"></script>
<script src="libs/strophe/strophe.disco.min.js?v=1"></script>
<script src="libs/strophe/strophe.caps.jsonly.min.js?v=1"></script>
@ -22,7 +22,7 @@
<script src="libs/strophe/strophe.jingle.sessionbase.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.session.js?v=2"></script>
<script src="libs/strophe/strophe.util.js"></script>
<script src="libs/colibri/colibri.focus.js?v=11"></script><!-- colibri focus implementation -->
<script src="libs/colibri/colibri.focus.js?v=12"></script><!-- colibri focus implementation -->
<script src="libs/colibri/colibri.session.js?v=1"></script>
<script src="libs/jquery-ui.js"></script>
<script src="libs/rayo.js?v=1"></script>

View File

@ -551,82 +551,8 @@ ColibriFocus.prototype.createdConference = function (result) {
console.log('setLocalDescription succeeded.');
// make sure our presence is updated
$(document).trigger('setLocalDescription.jingle', [self.sid]);
var elem = $iq({to: self.bridgejid, type: 'get'});
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
var localSDP = new SDP(self.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
elem.c('content', {name: name});
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
if (name !== 'data')
{
elem.c('channel', {
initiator: 'true',
expire: self.channelExpire,
id: self.mychannel[channel].attr('id'),
endpoint: self.myMucResource
});
// signal (through COLIBRI) to the bridge
// the SSRC groups of the participant
// that plays the role of the focus
var ssrc_group_lines = SDPUtil.find_lines(media, 'a=ssrc-group:');
var idx = 0;
ssrc_group_lines.forEach(function(line) {
idx = line.indexOf(' ');
var semantics = line.substr(0, idx).substr(13);
var ssrcs = line.substr(14 + semantics.length).split(' ');
if (ssrcs.length != 0) {
elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
ssrcs.forEach(function(ssrc) {
elem.c('source', { ssrc: ssrc })
.up();
});
elem.up();
}
});
// FIXME: should reuse code from .toJingle
for (var j = 0; j < mline.fmt.length; j++)
{
var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
if (rtpmap)
{
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
elem.up();
}
}
}
else
{
var sctpmap = SDPUtil.find_line(media, 'a=sctpmap:' + mline.fmt[0]);
var sctpPort = SDPUtil.parse_sctpmap(sctpmap)[0];
elem.c("sctpconnection",
{
initiator: 'true',
expire: self.channelExpire,
id: self.mychannel[channel].attr('id'),
endpoint: self.myMucResource,
port: sctpPort
}
);
}
localSDP.TransportToJingle(channel, elem);
elem.up(); // end of channel
elem.up(); // end of content
});
self.connection.sendIQ(elem,
function (result) {
// ...
},
function (error) {
console.error(
"ERROR sending colibri message",
error, elem);
}
);
self.updateLocalChannel(localSDP);
// now initiate sessions
for (var i = 0; i < numparticipants; i++) {
@ -659,6 +585,96 @@ ColibriFocus.prototype.createdConference = function (result) {
};
ColibriFocus.prototype.updateLocalChannel = function(localSDP, parts) {
var self = this;
var elem = $iq({to: self.bridgejid, type: 'get'});
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
elem.c('content', {name: name});
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
if (name !== 'data') {
elem.c('channel', {
initiator: 'true',
expire: self.channelExpire,
id: self.mychannel[channel].attr('id'),
endpoint: self.myMucResource
});
if (!parts || parts.indexOf('sources') !== -1) {
// signal (through COLIBRI) to the bridge
// the SSRC groups of the participant
// that plays the role of the focus
var ssrc_group_lines = SDPUtil.find_lines(media, 'a=ssrc-group:');
var idx = 0;
var hasSIM = false;
ssrc_group_lines.forEach(function (line) {
idx = line.indexOf(' ');
var semantics = line.substr(0, idx).substr(13);
var ssrcs = line.substr(14 + semantics.length).split(' ');
if (ssrcs.length != 0) {
elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
ssrcs.forEach(function (ssrc) {
elem.c('source', { ssrc: ssrc })
.up();
});
elem.up();
}
});
if (!hasSIM && name == 'video') {
// disable simulcast with an empty ssrc-group element.
elem.c('ssrc-group', { semantics: 'SIM', xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
elem.up();
}
}
if (!parts || parts.indexOf('payload-type') !== -1) {
// FIXME: should reuse code from .toJingle
for (var j = 0; j < mline.fmt.length; j++) {
var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
if (rtpmap) {
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
elem.up();
}
}
}
}
else
{
var sctpmap = SDPUtil.find_line(media, 'a=sctpmap:' + mline.fmt[0]);
var sctpPort = SDPUtil.parse_sctpmap(sctpmap)[0];
elem.c("sctpconnection",
{
initiator: 'true',
expire: self.channelExpire,
id: self.mychannel[channel].attr('id'),
endpoint: self.myMucResource,
port: sctpPort
}
);
}
if (!parts || parts.indexOf('transport') !== -1) {
localSDP.TransportToJingle(channel, elem);
}
elem.up(); // end of channel
elem.up(); // end of content
});
self.connection.sendIQ(elem,
function (result) {
// ...
},
function (error) {
console.error(
"ERROR sending colibri message",
error, elem);
}
);
};
// send a session-initiate to a new participant
ColibriFocus.prototype.initiate = function (peer, isInitiator) {
var participant = this.peers.indexOf(peer);
@ -894,61 +910,73 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
};
// update the channel description (payload-types + dtls fp) for a participant
ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
ColibriFocus.prototype.updateRemoteChannel = function (remoteSDP, participant, parts) {
console.log('change allocation for', this.confid);
var self = this;
var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (channel = 0; channel < this.channels[participant].length; channel++)
{
for (channel = 0; channel < this.channels[participant].length; channel++) {
if (!remoteSDP.media[channel])
continue;
var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:'));
change.c('content', {name: name});
if (name !== 'data')
{
if (name !== 'data') {
change.c('channel', {
id: $(this.channels[participant][channel]).attr('id'),
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: self.channelExpire
});
// signal (throught COLIBRI) to the bridge the SSRC groups of this
// participant
var ssrc_group_lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
var idx = 0;
ssrc_group_lines.forEach(function(line) {
idx = line.indexOf(' ');
var semantics = line.substr(0, idx).substr(13);
var ssrcs = line.substr(14 + semantics.length).split(' ');
if (ssrcs.length != 0) {
change.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
ssrcs.forEach(function(ssrc) {
change.c('source', { ssrc: ssrc })
.up();
});
if (!parts || parts.indexOf('sources') !== -1) {
// signal (throught COLIBRI) to the bridge the SSRC groups of this
// participant
var ssrc_group_lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
var idx = 0;
var hasSIM = false;
ssrc_group_lines.forEach(function (line) {
idx = line.indexOf(' ');
var semantics = line.substr(0, idx).substr(13);
if (semantics == 'SIM') {
hasSIM = true;
}
var ssrcs = line.substr(14 + semantics.length).split(' ');
if (ssrcs.length != 0) {
change.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
ssrcs.forEach(function (ssrc) {
change.c('source', { ssrc: ssrc })
.up();
});
change.up();
}
});
if (!hasSIM && name == 'video') {
// disable simulcast with an empty ssrc-group element.
change.c('ssrc-group', { semantics: 'SIM', xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
change.up();
}
});
}
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
rtpmap.forEach(function (val) {
// TODO: too much copy-paste
var rtpmap = SDPUtil.parse_rtpmap(val);
change.c('payload-type', rtpmap);
//
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
/*
if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
for (var k = 0; k < tmp.length; k++) {
change.c('parameter', tmp[k]).up();
}
}
*/
change.up();
});
if (!parts || parts.indexOf('payload-type') !== -1) {
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
rtpmap.forEach(function (val) {
// TODO: too much copy-paste
var rtpmap = SDPUtil.parse_rtpmap(val);
change.c('payload-type', rtpmap);
//
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
/*
if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
for (var k = 0; k < tmp.length; k++) {
change.c('parameter', tmp[k]).up();
}
}
*/
change.up();
});
}
}
else
{
@ -960,8 +988,11 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
port: SDPUtil.parse_sctpmap(sctpmap)[0]
});
}
// now add transport
remoteSDP.TransportToJingle(channel, change);
if (!parts || parts.indexOf('transport') !== -1) {
// now add transport
remoteSDP.TransportToJingle(channel, change);
}
change.up(); // end of channel/sctpconnection
change.up(); // end of content
@ -976,6 +1007,57 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
);
};
/**
* 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.
*/
ColibriFocus.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
var self = this;
// Stop the stream to trigger onended event for old stream
oldStream.stop();
// Remember SDP to figure out added/removed SSRCs
var oldSdp = null;
if(self.peerconnection) {
if(self.peerconnection.localDescription) {
oldSdp = new SDP(self.peerconnection.localDescription.sdp);
}
self.peerconnection.removeStream(oldStream);
self.peerconnection.addStream(new_stream);
}
self.connection.jingle.localVideo = new_stream;
self.connection.jingle.localStreams = [];
self.connection.jingle.localStreams.push(self.connection.jingle.localAudio);
self.connection.jingle.localStreams.push(self.connection.jingle.localVideo);
// Conference is not active
if(!oldSdp || !self.peerconnection) {
success_callback();
return;
}
self.peerconnection.switchstreams = true;
self.modifySources(function() {
console.log('modify sources done');
var newSdp = new SDP(self.peerconnection.localDescription.sdp);
// change allocation on bridge
self.updateLocalChannel(newSdp, ['sources']);
console.log("SDPs", oldSdp, newSdp);
self.notifyMySSRCUpdate(oldSdp, newSdp);
success_callback();
});
};
// tell everyone about a new participants a=ssrc lines (isadd is true)
// or a leaving participants a=ssrc lines
ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd) {
@ -1010,7 +1092,7 @@ ColibriFocus.prototype.addSource = function (elem, fromJid) {
// FIXME: dirty waiting
if (!this.peerconnection.localDescription)
{
console.warn("addSource - localDescription not ready yet")
console.warn("addSource - localDescription not ready yet");
setTimeout(function() { self.addSource(elem, fromJid); }, 200);
return;
}
@ -1031,11 +1113,21 @@ ColibriFocus.prototype.addSource = function (elem, fromJid) {
});
var oldRemoteSdp = new SDP(this.peerconnection.remoteDescription.sdp);
this.modifySources(function(){
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);
// change allocation on bridge
if (peerSsrc[1] /* video */) {
// If the remote peer has changed its video sources, then we need to
// update the bridge with this information, in order for the
// simulcast manager of the remote peer to update its layers, and
// any associated receivers to adjust to the change.
var videoSDP = new SDP(['v=0', 'm=audio', 'a=mid:audio', peerSsrc[0]].join('\r\n') + ['m=video', 'a=mid:video', peerSsrc[1]].join('\r\n'));
var participant = self.peers.indexOf(fromJid);
self.updateRemoteChannel(videoSDP, participant, ['sources']);
}
});
};
@ -1073,6 +1165,16 @@ ColibriFocus.prototype.removeSource = function (elem, fromJid) {
var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp);
var removedSSRCs = remoteSDP.getNewMedia(oldSDP);
self.sendSSRCUpdate(removedSSRCs, fromJid, false);
// change allocation on bridge
if (peerSsrc[1] /* video */) {
// If the remote peer has changed its video sources, then we need to
// update the bridge with this information, in order for the
// simulcast manager of the remote peer to update its layers, and
// any associated receivers to adjust to the change.
var videoSDP = new SDP(['v=0', 'm=audio', 'a=mid:audio', peerSsrc[0]].join('\r\n') + ['m=video', 'a=mid:video', peerSsrc[1]].join('\r\n'));
var participant = self.peers.indexOf(fromJid);
self.updateRemoteChannel(videoSDP, participant, ['sources']);
}
});
};
@ -1084,7 +1186,7 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
remoteSDP.fromJingle(elem);
// ACT 1: change allocation on bridge
this.updateChannel(remoteSDP, participant);
this.updateRemoteChannel(remoteSDP, participant);
// ACT 2: tell anyone else about the new SSRCs
this.sendSSRCUpdate(remoteSDP.getMediaSsrcMap(), session.peerjid, true);

View File

@ -140,11 +140,13 @@ if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
TraceablePeerConnection.prototype.addStream = function (stream) {
this.trace('addStream', stream.id);
simulcast.resetSender();
this.peerconnection.addStream(stream);
};
TraceablePeerConnection.prototype.removeStream = function (stream) {
this.trace('removeStream', stream.id);
simulcast.resetSender();
this.peerconnection.removeStream(stream);
};
@ -204,7 +206,7 @@ TraceablePeerConnection.prototype.enqueueAddSsrc = function(channel, ssrcLines)
this.addssrc[channel] = '';
}
this.addssrc[channel] += ssrcLines;
}
};
TraceablePeerConnection.prototype.addSource = function (elem) {
console.log('addssrc', new Date().getTime());
@ -260,7 +262,7 @@ TraceablePeerConnection.prototype.enqueueRemoveSsrc = function(channel, ssrcLine
this.removessrc[channel] = '';
}
this.removessrc[channel] += ssrcLines;
}
};
TraceablePeerConnection.prototype.removeSource = function (elem) {
console.log('removessrc', new Date().getTime());

View File

@ -42,7 +42,7 @@ SDP.prototype.getMediaSsrcMap = function() {
});
}
return media_ssrcs;
}
};
/**
* Returns <tt>true</tt> if this SDP contains given SSRC.
* @param ssrc the ssrc to check.
@ -59,7 +59,8 @@ SDP.prototype.containsSSRC = function(ssrc) {
}
});
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.
@ -89,7 +90,7 @@ SDP.prototype.getNewMedia = function(otherSdp) {
}
}
return true;
};
}
var myMedia = this.getMediaSsrcMap();
var othersMedia = otherSdp.getMediaSsrcMap();
@ -111,7 +112,7 @@ SDP.prototype.getNewMedia = function(otherSdp) {
}
newMedia[channelNum].ssrcs[ssrc] = othersChannel.ssrcs[ssrc];
}
})
});
// Look for new ssrc groups across the channels
othersChannel.ssrcGroups.forEach(function(otherSsrcGroup){
@ -120,7 +121,7 @@ SDP.prototype.getNewMedia = function(otherSdp) {
var matched = false;
for (var i = 0; i < myChannel.ssrcGroups.length; i++) {
var mySsrcGroup = myChannel.ssrcGroups[i];
if (otherSsrcGroup.semantics == mySsrcGroup
if (otherSsrcGroup.semantics == mySsrcGroup.semantics
&& arrayEquals.apply(otherSsrcGroup.ssrcs, [mySsrcGroup.ssrcs])) {
matched = true;
@ -140,7 +141,8 @@ SDP.prototype.getNewMedia = function(otherSdp) {
});
});
return newMedia;
}
};
// remove iSAC and CN from SDP
SDP.prototype.mangle = function () {
var i, j, mline, lines, rtpmap, newdesc;

View File

@ -490,7 +490,6 @@ function SimulcastSender() {
this.logger = new SimulcastLogger('SimulcastSender', 1);
}
SimulcastSender.prototype._localVideoSourceCache = '';
SimulcastSender.prototype.displayedLocalVideoStream = null;
SimulcastSender.prototype._generateGuid = (function () {
@ -506,14 +505,6 @@ SimulcastSender.prototype._generateGuid = (function () {
};
}());
SimulcastSender.prototype._cacheLocalVideoSources = function (lines) {
this._localVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
};
SimulcastSender.prototype._restoreLocalVideoSources = function (lines) {
this.simulcastUtils._replaceVideoSources(lines, this._localVideoSourceCache);
};
// Returns a random integer between min (included) and max (excluded)
// Using Math.round() gives a non-uniform distribution!
SimulcastSender.prototype._generateRandomSSRC = function () {
@ -521,7 +512,37 @@ SimulcastSender.prototype._generateRandomSSRC = function () {
return Math.floor(Math.random() * (max - min)) + min;
};
SimulcastSender.prototype._appendSimulcastGroup = function (lines) {
SimulcastSender.prototype.getLocalVideoStream = function () {
return (this.displayedLocalVideoStream != null)
? this.displayedLocalVideoStream
// in case we have no simulcast at all, i.e. we didn't perform the GUM
: connection.jingle.localVideo;
};
function NativeSimulcastSender() {
SimulcastSender.call(this); // call the super constructor.
}
NativeSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
NativeSimulcastSender.prototype._localExplosionMap = {};
NativeSimulcastSender.prototype._isUsingScreenStream = false;
NativeSimulcastSender.prototype._localVideoSourceCache = '';
NativeSimulcastSender.prototype.reset = function () {
this._localExplosionMap = {};
this._isUsingScreenStream = isUsingScreenStream;
};
NativeSimulcastSender.prototype._cacheLocalVideoSources = function (lines) {
this._localVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
};
NativeSimulcastSender.prototype._restoreLocalVideoSources = function (lines) {
this.simulcastUtils._replaceVideoSources(lines, this._localVideoSourceCache);
};
NativeSimulcastSender.prototype._appendSimulcastGroup = function (lines) {
var videoSources, ssrcGroup, simSSRC, numOfSubs = 2, i, sb, msid;
this.logger.info('Appending simulcast group...');
@ -557,7 +578,7 @@ SimulcastSender.prototype._appendSimulcastGroup = function (lines) {
};
// Does the actual patching.
SimulcastSender.prototype._ensureSimulcastGroup = function (lines) {
NativeSimulcastSender.prototype._ensureSimulcastGroup = function (lines) {
this.logger.info('Ensuring simulcast group...');
@ -571,21 +592,6 @@ SimulcastSender.prototype._ensureSimulcastGroup = function (lines) {
}
};
SimulcastSender.prototype.getLocalVideoStream = function () {
return (this.displayedLocalVideoStream != null)
? this.displayedLocalVideoStream
// in case we have no simulcast at all, i.e. we didn't perform the GUM
: connection.jingle.localVideo;
};
function NativeSimulcastSender() {
SimulcastSender.call(this); // call the super constructor.
}
NativeSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
NativeSimulcastSender.prototype._localExplosionMap = {};
/**
* Produces a single stream with multiple tracks for local video sources.
*
@ -658,7 +664,7 @@ NativeSimulcastSender.prototype.getUserMedia = function (constraints, success, e
NativeSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
var sb;
if (!desc || desc == null) {
if (!desc || desc == null || this._isUsingScreenStream) {
return desc;
}
@ -686,6 +692,10 @@ NativeSimulcastSender.prototype.reverseTransformLocalDescription = function (des
*/
NativeSimulcastSender.prototype.transformAnswer = function (desc) {
if (!desc || desc == null || this._isUsingScreenStream) {
return desc;
}
var sb = desc.sdp.split('\r\n');
// Even if we have enabled native simulcasting previously
@ -734,7 +744,8 @@ SimulcastReceiver.prototype.transformRemoteDescription = function (desc) {
this._updateRemoteMaps(sb);
this._cacheRemoteVideoSources(sb);
// NOTE(gp) this needs to be called after updateRemoteMaps because we need the simulcast group in the _updateRemoteMaps() method.
// NOTE(gp) this needs to be called after updateRemoteMaps because we
// need the simulcast group in the _updateRemoteMaps() method.
this.simulcastUtils._removeSimulcastGroup(sb);
if (desc.sdp.indexOf('a=ssrc-group:SIM') !== -1) {
@ -1189,6 +1200,12 @@ SimulcastManager.prototype._setLocalVideoStreamEnabled = function(ssrc, enabled)
this.simulcastSender._setLocalVideoStreamEnabled(ssrc, enabled);
};
SimulcastManager.prototype.resetSender = function() {
if (typeof this.simulcastSender.reset === 'function'){
this.simulcastSender.reset();
}
};
/**
*
* @constructor