1639 lines
60 KiB
JavaScript
1639 lines
60 KiB
JavaScript
/* colibri.js -- a COLIBRI focus
|
|
* The colibri spec has been submitted to the XMPP Standards Foundation
|
|
* for publications as a XMPP extensions:
|
|
* http://xmpp.org/extensions/inbox/colibri.html
|
|
*
|
|
* colibri.js is a participating focus, i.e. the focus participates
|
|
* in the conference. The conference itself can be ad-hoc, through a
|
|
* MUC, through PubSub, etc.
|
|
*
|
|
* colibri.js relies heavily on the strophe.jingle library available
|
|
* from https://github.com/ESTOS/strophe.jingle
|
|
* and interoperates with the Jitsi videobridge available from
|
|
* https://jitsi.org/Projects/JitsiVideobridge
|
|
*/
|
|
/*
|
|
Copyright (c) 2013 ESTOS GmbH
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
*/
|
|
/* jshint -W117 */
|
|
|
|
ColibriFocus.prototype = Object.create(SessionBase.prototype);
|
|
function ColibriFocus(connection, bridgejid) {
|
|
|
|
SessionBase.call(this, connection, Math.random().toString(36).substr(2, 12));
|
|
|
|
this.bridgejid = bridgejid;
|
|
this.peers = [];
|
|
this.remoteStreams = [];
|
|
this.confid = null;
|
|
|
|
/**
|
|
* Local XMPP resource used to join the multi user chat.
|
|
* @type {*}
|
|
*/
|
|
this.myMucResource = Strophe.getResourceFromJid(connection.emuc.myroomjid);
|
|
|
|
/**
|
|
* Default channel expire value in seconds.
|
|
* @type {number}
|
|
*/
|
|
this.channelExpire
|
|
= ('number' === typeof(config.channelExpire))
|
|
? config.channelExpire
|
|
: 15;
|
|
/**
|
|
* Default channel last-n value.
|
|
* @type {number}
|
|
*/
|
|
this.channelLastN
|
|
= ('number' === typeof(config.channelLastN)) ? config.channelLastN : -1;
|
|
|
|
// media types of the conference
|
|
if (config.openSctp)
|
|
this.media = ['audio', 'video', 'data'];
|
|
else
|
|
this.media = ['audio', 'video'];
|
|
|
|
this.connection.jingle.sessions[this.sid] = this;
|
|
this.bundledTransports = {};
|
|
this.mychannel = [];
|
|
this.channels = [];
|
|
this.remotessrc = {};
|
|
|
|
// container for candidates from the focus
|
|
// gathered before confid is known
|
|
this.drip_container = [];
|
|
|
|
// silly wait flag
|
|
this.wait = true;
|
|
|
|
this.recordingEnabled = false;
|
|
|
|
// stores information about the endpoints (i.e. display names) to
|
|
// be sent to the videobridge.
|
|
this.endpointsInfo = null;
|
|
}
|
|
|
|
// creates a conferences with an initial set of peers
|
|
ColibriFocus.prototype.makeConference = function (peers, errorCallback) {
|
|
var self = this;
|
|
if (this.confid !== null) {
|
|
console.error('makeConference called twice? Ignoring...');
|
|
// FIXME: just invite peers?
|
|
return;
|
|
}
|
|
this.confid = 0; // !null
|
|
this.peers = [];
|
|
peers.forEach(function (peer) {
|
|
self.peers.push(peer);
|
|
self.channels.push([]);
|
|
});
|
|
|
|
this.peerconnection
|
|
= new TraceablePeerConnection(
|
|
this.connection.jingle.ice_config,
|
|
this.connection.jingle.pc_constraints );
|
|
|
|
if(this.connection.jingle.localAudio) {
|
|
this.peerconnection.addStream(this.connection.jingle.localAudio);
|
|
}
|
|
if(this.connection.jingle.localVideo) {
|
|
this.peerconnection.addStream(this.connection.jingle.localVideo);
|
|
}
|
|
this.peerconnection.oniceconnectionstatechange = function (event) {
|
|
console.warn('ice connection state changed to', self.peerconnection.iceConnectionState);
|
|
/*
|
|
if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
|
|
console.log('adding new remote SSRCs from iceconnectionstatechange');
|
|
window.setTimeout(function() { self.modifySources(); }, 1000);
|
|
}
|
|
*/
|
|
$(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
|
|
};
|
|
this.peerconnection.onsignalingstatechange = function (event) {
|
|
console.warn(self.peerconnection.signalingState);
|
|
/*
|
|
if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
|
|
console.log('adding new remote SSRCs from signalingstatechange');
|
|
window.setTimeout(function() { self.modifySources(); }, 1000);
|
|
}
|
|
*/
|
|
};
|
|
this.peerconnection.onaddstream = function (event) {
|
|
// search the jid associated with this stream
|
|
Object.keys(self.remotessrc).forEach(function (jid) {
|
|
if (self.remotessrc[jid].join('\r\n').indexOf('mslabel:' + event.stream.id) != -1) {
|
|
event.peerjid = jid;
|
|
}
|
|
});
|
|
self.remoteStreams.push(event.stream);
|
|
$(document).trigger('remotestreamadded.jingle', [event, self.sid]);
|
|
};
|
|
this.peerconnection.onicecandidate = function (event) {
|
|
//console.log('focus onicecandidate', self.confid, new Date().getTime(), event.candidate);
|
|
if (!event.candidate) {
|
|
console.log('end of candidates');
|
|
return;
|
|
}
|
|
self.sendIceCandidate(event.candidate);
|
|
};
|
|
this._makeConference(errorCallback);
|
|
/*
|
|
this.peerconnection.createOffer(
|
|
function (offer) {
|
|
self.peerconnection.setLocalDescription(
|
|
offer,
|
|
function () {
|
|
// success
|
|
$(document).trigger('setLocalDescription.jingle', [self.sid]);
|
|
// FIXME: could call _makeConference here and trickle candidates later
|
|
self._makeConference();
|
|
},
|
|
function (error) {
|
|
console.log('setLocalDescription failed', error);
|
|
}
|
|
);
|
|
},
|
|
function (error) {
|
|
console.warn(error);
|
|
}
|
|
);
|
|
*/
|
|
};
|
|
|
|
// Sends a COLIBRI message which enables or disables (according to 'state') the
|
|
// recording on the bridge. Waits for the result IQ and calls 'callback' with
|
|
// the new recording state, according to the IQ.
|
|
ColibriFocus.prototype.setRecording = function(state, token, callback) {
|
|
var self = this;
|
|
var elem = $iq({to: this.bridgejid, type: 'set'});
|
|
elem.c('conference', {
|
|
xmlns: 'http://jitsi.org/protocol/colibri',
|
|
id: this.confid
|
|
});
|
|
elem.c('recording', {state: state, token: token});
|
|
elem.up();
|
|
|
|
this.connection.sendIQ(elem,
|
|
function (result) {
|
|
console.log('Set recording "', state, '". Result:', result);
|
|
var recordingElem = $(result).find('>conference>recording');
|
|
var newState = ('true' === recordingElem.attr('state'));
|
|
|
|
self.recordingEnabled = newState;
|
|
callback(newState);
|
|
},
|
|
function (error) {
|
|
console.warn(error);
|
|
}
|
|
);
|
|
};
|
|
|
|
/*
|
|
* Updates the display name for an endpoint with a specific jid.
|
|
* jid: the jid associated with the endpoint.
|
|
* displayName: the new display name for the endpoint.
|
|
*/
|
|
ColibriFocus.prototype.setEndpointDisplayName = function(jid, displayName) {
|
|
var endpointId = jid.substr(1 + jid.lastIndexOf('/'));
|
|
var update = false;
|
|
|
|
if (this.endpointsInfo === null) {
|
|
this.endpointsInfo = {};
|
|
}
|
|
|
|
var endpointInfo = this.endpointsInfo[endpointId];
|
|
if ('undefined' === typeof endpointInfo) {
|
|
endpointInfo = this.endpointsInfo[endpointId] = {};
|
|
}
|
|
|
|
if (endpointInfo['displayname'] !== displayName) {
|
|
endpointInfo['displayname'] = displayName;
|
|
update = true;
|
|
}
|
|
|
|
if (update) {
|
|
this.updateEndpoints();
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Sends a colibri message to the bridge that contains the
|
|
* current endpoints and their display names.
|
|
*/
|
|
ColibriFocus.prototype.updateEndpoints = function() {
|
|
if (this.confid === null
|
|
|| this.endpointsInfo === null) {
|
|
return;
|
|
}
|
|
|
|
if (this.confid === 0) {
|
|
// the colibri conference is currently initiating
|
|
var self = this;
|
|
window.setTimeout(function() { self.updateEndpoints()}, 1000);
|
|
return;
|
|
}
|
|
|
|
var elem = $iq({to: this.bridgejid, type: 'set'});
|
|
elem.c('conference', {
|
|
xmlns: 'http://jitsi.org/protocol/colibri',
|
|
id: this.confid
|
|
});
|
|
|
|
for (var id in this.endpointsInfo) {
|
|
elem.c('endpoint');
|
|
elem.attrs({ id: id,
|
|
displayname: this.endpointsInfo[id]['displayname']
|
|
});
|
|
elem.up();
|
|
}
|
|
|
|
//elem.up(); //conference
|
|
|
|
this.connection.sendIQ(
|
|
elem,
|
|
function (result) {},
|
|
function (error) { console.warn(error); }
|
|
);
|
|
};
|
|
|
|
ColibriFocus.prototype._makeConference = function (errorCallback) {
|
|
var self = this;
|
|
var elem = $iq({ to: this.bridgejid, type: 'get' });
|
|
elem.c('conference', { xmlns: 'http://jitsi.org/protocol/colibri' });
|
|
|
|
this.media.forEach(function (name) {
|
|
var elemName;
|
|
var elemAttrs = { initiator: 'true', expire: self.channelExpire };
|
|
|
|
if ('data' === name)
|
|
{
|
|
elemName = 'sctpconnection';
|
|
elemAttrs['port'] = 5000;
|
|
}
|
|
else
|
|
{
|
|
elemName = 'channel';
|
|
if ('video' === name) {
|
|
if (self.channelLastN >= 0) {
|
|
elemAttrs['last-n'] = self.channelLastN;
|
|
}
|
|
if (config.adaptiveLastN) {
|
|
elemAttrs['adaptive-last-n'] = 'true';
|
|
}
|
|
if (config.adaptiveSimulcast) {
|
|
elemAttrs['adaptive-simulcast'] = 'true';
|
|
}
|
|
}
|
|
}
|
|
|
|
elem.c('content', { name: name });
|
|
|
|
elem.c(elemName, elemAttrs);
|
|
elem.attrs({ endpoint: self.myMucResource });
|
|
if (config.useBundle) {
|
|
elem.attrs({ 'channel-bundle-id': self.myMucResource });
|
|
}
|
|
elem.up();// end of channel/sctpconnection
|
|
|
|
for (var j = 0; j < self.peers.length; j++) {
|
|
var peer = self.peers[j];
|
|
var peerEndpoint = peer.substr(1 + peer.lastIndexOf('/'));
|
|
|
|
elem.c(elemName, elemAttrs);
|
|
elem.attrs({ endpoint: peerEndpoint });
|
|
if (config.useBundle) {
|
|
elem.attrs({ 'channel-bundle-id': peerEndpoint });
|
|
}
|
|
elem.up(); // end of channel/sctpconnection
|
|
}
|
|
elem.up(); // end of content
|
|
});
|
|
|
|
if (this.endpointsInfo !== null) {
|
|
for (var id in this.endpointsInfo) {
|
|
elem.c('endpoint');
|
|
elem.attrs({ id: id,
|
|
displayname: this.endpointsInfo[id]['displayname']
|
|
});
|
|
elem.up();
|
|
}
|
|
}
|
|
|
|
/*
|
|
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
|
localSDP.media.forEach(function (media, channel) {
|
|
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
|
|
elem.c('content', {name: name});
|
|
elem.c('channel', {initiator: 'false', expire: self.channelExpire});
|
|
|
|
// FIXME: should reuse code from .toJingle
|
|
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
|
|
for (var j = 0; j < mline.fmt.length; j++) {
|
|
var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
|
|
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
|
|
elem.up();
|
|
}
|
|
|
|
localSDP.TransportToJingle(channel, elem);
|
|
|
|
elem.up(); // end of channel
|
|
for (j = 0; j < self.peers.length; j++) {
|
|
elem.c('channel', {initiator: 'true', expire: self.channelExpire }).up();
|
|
}
|
|
elem.up(); // end of content
|
|
});
|
|
*/
|
|
|
|
this.connection.sendIQ(elem,
|
|
function (result) {
|
|
self.createdConference(result);
|
|
},
|
|
function (error) {
|
|
console.warn(error);
|
|
errorCallback(error);
|
|
}
|
|
);
|
|
};
|
|
|
|
// callback when a colibri conference was created
|
|
ColibriFocus.prototype.createdConference = function (result) {
|
|
console.log('created a conference on the bridge');
|
|
var self = this;
|
|
var tmp;
|
|
|
|
this.confid = $(result).find('>conference').attr('id');
|
|
var remotecontents = $(result).find('>conference>content').get();
|
|
var numparticipants = 0;
|
|
for (var i = 0; i < remotecontents.length; i++)
|
|
{
|
|
var contentName = $(remotecontents[i]).attr('name');
|
|
var channelName
|
|
= contentName !== 'data' ? '>channel' : '>sctpconnection';
|
|
|
|
tmp = $(remotecontents[i]).find(channelName).get();
|
|
this.mychannel.push($(tmp.shift()));
|
|
numparticipants = tmp.length;
|
|
for (j = 0; j < tmp.length; j++) {
|
|
if (this.channels[j] === undefined) {
|
|
this.channels[j] = [];
|
|
}
|
|
this.channels[j].push(tmp[j]);
|
|
}
|
|
}
|
|
|
|
// save the 'transport' elements from 'channel-bundle'-s
|
|
var channelBundles = $(result).find('>conference>channel-bundle');
|
|
for (var i = 0; i < channelBundles.length; i++)
|
|
{
|
|
var endpointId = $(channelBundles[i]).attr('id');
|
|
this.bundledTransports[endpointId] = $(channelBundles[i]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
|
|
}
|
|
|
|
console.log('remote channels', this.channels);
|
|
|
|
// Notify that the focus has created the conference on the bridge
|
|
$(document).trigger('conferenceCreated.jingle', [self]);
|
|
|
|
var bridgeSDP = new SDP(
|
|
'v=0\r\n' +
|
|
'o=- 5151055458874951233 2 IN IP4 127.0.0.1\r\n' +
|
|
's=-\r\n' +
|
|
't=0 0\r\n' +
|
|
/* Audio */
|
|
(config.useBundle
|
|
? ('a=group:BUNDLE audio video' +
|
|
(config.openSctp ? ' data' : '') +
|
|
'\r\n')
|
|
: '') +
|
|
'm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\n' +
|
|
'c=IN IP4 0.0.0.0\r\n' +
|
|
'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
|
|
'a=mid:audio\r\n' +
|
|
'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n' +
|
|
'a=sendrecv\r\n' +
|
|
'a=rtpmap:111 opus/48000/2\r\n' +
|
|
'a=fmtp:111 minptime=10\r\n' +
|
|
'a=rtpmap:103 ISAC/16000\r\n' +
|
|
'a=rtpmap:104 ISAC/32000\r\n' +
|
|
'a=rtpmap:0 PCMU/8000\r\n' +
|
|
'a=rtpmap:8 PCMA/8000\r\n' +
|
|
'a=rtpmap:106 CN/32000\r\n' +
|
|
'a=rtpmap:105 CN/16000\r\n' +
|
|
'a=rtpmap:13 CN/8000\r\n' +
|
|
'a=rtpmap:126 telephone-event/8000\r\n' +
|
|
'a=maxptime:60\r\n' +
|
|
(config.useRtcpMux ? 'a=rtcp-mux\r\n' : '') +
|
|
/* Video */
|
|
'm=video 1 RTP/SAVPF 100 116 117\r\n' +
|
|
'c=IN IP4 0.0.0.0\r\n' +
|
|
'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
|
|
'a=mid:video\r\n' +
|
|
'a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n' +
|
|
'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' +
|
|
'a=sendrecv\r\n' +
|
|
'a=rtpmap:100 VP8/90000\r\n' +
|
|
'a=rtcp-fb:100 ccm fir\r\n' +
|
|
'a=rtcp-fb:100 nack\r\n' +
|
|
'a=rtcp-fb:100 nack pli\r\n' +
|
|
(config.enableFirefoxSupport? "" : 'a=rtcp-fb:100 goog-remb\r\n') +
|
|
'a=rtpmap:116 red/90000\r\n' +
|
|
'a=rtpmap:117 ulpfec/90000\r\n' +
|
|
(config.useRtcpMux ? 'a=rtcp-mux\r\n' : '') +
|
|
/* Data SCTP */
|
|
(config.openSctp ?
|
|
'm=application 1 DTLS/SCTP 5000\r\n' +
|
|
'c=IN IP4 0.0.0.0\r\n' +
|
|
'a=sctpmap:5000 webrtc-datachannel\r\n' +
|
|
'a=mid:data\r\n'
|
|
: '')
|
|
);
|
|
|
|
bridgeSDP.media.length = this.mychannel.length;
|
|
var channel;
|
|
/*
|
|
for (channel = 0; channel < bridgeSDP.media.length; channel++) {
|
|
bridgeSDP.media[channel] = '';
|
|
// unchanged lines
|
|
bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'm=') + '\r\n';
|
|
bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'c=') + '\r\n';
|
|
if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:')) {
|
|
bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:') + '\r\n';
|
|
}
|
|
if (SDPUtil.find_line(localSDP.media[channel], 'a=mid:')) {
|
|
bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=mid:') + '\r\n';
|
|
}
|
|
if (SDPUtil.find_line(localSDP.media[channel], 'a=sendrecv')) {
|
|
bridgeSDP.media[channel] += 'a=sendrecv\r\n';
|
|
}
|
|
if (SDPUtil.find_line(localSDP.media[channel], 'a=extmap:')) {
|
|
bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=extmap:').join('\r\n') + '\r\n';
|
|
}
|
|
|
|
// FIXME: should look at m-line and group the ids together
|
|
if (SDPUtil.find_line(localSDP.media[channel], 'a=rtpmap:')) {
|
|
bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtpmap:').join('\r\n') + '\r\n';
|
|
}
|
|
if (SDPUtil.find_line(localSDP.media[channel], 'a=fmtp:')) {
|
|
bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=fmtp:').join('\r\n') + '\r\n';
|
|
}
|
|
if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp-fb:')) {
|
|
bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtcp-fb:').join('\r\n') + '\r\n';
|
|
}
|
|
// FIXME: changed lines -- a=sendrecv direction, a=setup direction
|
|
}
|
|
*/
|
|
for (channel = 0; channel < bridgeSDP.media.length; channel++) {
|
|
// get the mixed ssrc
|
|
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
|
// FIXME: check rtp-level-relay-type
|
|
|
|
var name = bridgeSDP.media[channel].split(" ")[0].substr(2); // 'm=audio ...'
|
|
if (name === 'audio' || name === 'video') {
|
|
// make chrome happy... '3735928559' == 0xDEADBEEF
|
|
var ssrc = tmp.length ? tmp.attr('ssrc') : '3735928559';
|
|
|
|
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' cname:mixed\r\n';
|
|
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' label:mixedlabel' + name + '0\r\n';
|
|
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' msid:mixedmslabel mixedlabel' + name + '0\r\n';
|
|
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' mslabel:mixedmslabel\r\n';
|
|
}
|
|
|
|
// FIXME: should take code from .fromJingle
|
|
var channelBundleId = $(this.mychannel[channel]).attr('channel-bundle-id');
|
|
if (typeof channelBundleId != 'undefined') {
|
|
tmp = this.bundledTransports[channelBundleId];
|
|
} else {
|
|
tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
|
|
}
|
|
|
|
if (tmp.length) {
|
|
bridgeSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
|
|
bridgeSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
|
|
tmp.find('>candidate').each(function () {
|
|
bridgeSDP.media[channel] += SDPUtil.candidateFromJingle(this);
|
|
});
|
|
tmp = tmp.find('>fingerprint');
|
|
if (tmp.length) {
|
|
bridgeSDP.media[channel] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
|
|
bridgeSDP.media[channel] += 'a=setup:actpass\r\n'; // offer so always actpass
|
|
}
|
|
}
|
|
}
|
|
bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join('');
|
|
var bridgeDesc = new RTCSessionDescription({type: 'offer', sdp: bridgeSDP.raw});
|
|
bridgeDesc = simulcast.transformRemoteDescription(bridgeDesc);
|
|
|
|
this.peerconnection.setRemoteDescription(bridgeDesc,
|
|
function () {
|
|
console.log('setRemoteDescription success');
|
|
self.peerconnection.createAnswer(
|
|
function (answer) {
|
|
self.peerconnection.setLocalDescription(answer,
|
|
function () {
|
|
console.log('setLocalDescription succeeded.');
|
|
// make sure our presence is updated
|
|
$(document).trigger('setLocalDescription.jingle', [self.sid]);
|
|
var localSDP = new SDP(self.peerconnection.localDescription.sdp);
|
|
self.updateLocalChannel(localSDP);
|
|
|
|
// now initiate sessions
|
|
for (var i = 0; i < numparticipants; i++) {
|
|
self.initiate(self.peers[i], true);
|
|
}
|
|
|
|
// Notify we've created the conference
|
|
$(document).trigger(
|
|
'conferenceCreated.jingle', self);
|
|
},
|
|
function (error) {
|
|
console.warn('setLocalDescription failed.', error);
|
|
}
|
|
);
|
|
},
|
|
function (error) {
|
|
console.warn('createAnswer failed.', error);
|
|
}
|
|
);
|
|
/*
|
|
for (var i = 0; i < numparticipants; i++) {
|
|
self.initiate(self.peers[i], true);
|
|
}
|
|
*/
|
|
},
|
|
function (error) {
|
|
console.log('setRemoteDescription failed.', error);
|
|
}
|
|
);
|
|
|
|
};
|
|
|
|
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);
|
|
console.log('tell', peer, participant);
|
|
var sdp;
|
|
|
|
if (!(this.peerconnection !== null && this.peerconnection.signalingState == 'stable')) {
|
|
console.error('can not initiate a new session without a stable peerconnection');
|
|
return;
|
|
}
|
|
|
|
sdp = new SDP(this.peerconnection.remoteDescription.sdp);
|
|
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
|
// throw away stuff we don't want
|
|
// not needed with static offer
|
|
if (!config.useBundle) {
|
|
sdp.removeSessionLines('a=group:');
|
|
}
|
|
sdp.removeSessionLines('a=msid-semantic:'); // FIXME: not mapped over jingle anyway...
|
|
for (var i = 0; i < sdp.media.length; i++) {
|
|
if (!config.useRtcpMux) {
|
|
sdp.removeMediaLines(i, 'a=rtcp-mux');
|
|
}
|
|
sdp.removeMediaLines(i, 'a=ssrc:');
|
|
sdp.removeMediaLines(i, 'a=ssrc-group:');
|
|
sdp.removeMediaLines(i, 'a=crypto:');
|
|
sdp.removeMediaLines(i, 'a=candidate:');
|
|
sdp.removeMediaLines(i, 'a=ice-options:google-ice');
|
|
sdp.removeMediaLines(i, 'a=ice-ufrag:');
|
|
sdp.removeMediaLines(i, 'a=ice-pwd:');
|
|
sdp.removeMediaLines(i, 'a=fingerprint:');
|
|
sdp.removeMediaLines(i, 'a=setup:');
|
|
}
|
|
|
|
// add stuff we got from the bridge
|
|
for (var j = 0; j < sdp.media.length; j++) {
|
|
var chan = $(this.channels[participant][j]);
|
|
console.log('channel id', chan.attr('id'));
|
|
|
|
tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
|
|
|
var name = sdp.media[j].split(" ")[0].substr(2); // 'm=audio ...'
|
|
if (name === 'audio' || name === 'video') {
|
|
// make chrome happy... '3735928559' == 0xDEADBEEF
|
|
var ssrc = tmp.length ? tmp.attr('ssrc') : '3735928559';
|
|
|
|
sdp.media[j] += 'a=ssrc:' + ssrc + ' cname:mixed\r\n';
|
|
sdp.media[j] += 'a=ssrc:' + ssrc + ' label:mixedlabel' + name + '0\r\n';
|
|
sdp.media[j] += 'a=ssrc:' + ssrc + ' msid:mixedmslabel mixedlabel' + name + '0\r\n';
|
|
sdp.media[j] += 'a=ssrc:' + ssrc + ' mslabel:mixedmslabel\r\n';
|
|
}
|
|
|
|
// In the case of bundle, we add each candidate to all m= lines/jingle contents,
|
|
// just as chrome does
|
|
if (config.useBundle){
|
|
tmp = this.bundledTransports[chan.attr('channel-bundle-id')];
|
|
} else {
|
|
tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
|
|
}
|
|
|
|
if (tmp.length) {
|
|
if (tmp.attr('ufrag'))
|
|
sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
|
|
if (tmp.attr('pwd'))
|
|
sdp.media[j] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
|
|
// and the candidates...
|
|
tmp.find('>candidate').each(function () {
|
|
sdp.media[j] += SDPUtil.candidateFromJingle(this);
|
|
});
|
|
tmp = tmp.find('>fingerprint');
|
|
if (tmp.length) {
|
|
sdp.media[j] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
|
|
/*
|
|
if (tmp.attr('direction')) {
|
|
sdp.media[j] += 'a=setup:' + tmp.attr('direction') + '\r\n';
|
|
}
|
|
*/
|
|
sdp.media[j] += 'a=setup:actpass\r\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < sdp.media.length; i++) {
|
|
// re-add all remote a=ssrcs _and_ a=ssrc-group
|
|
for (var jid in this.remotessrc) {
|
|
if (jid == peer || !this.remotessrc[jid][i])
|
|
continue;
|
|
sdp.media[i] += this.remotessrc[jid][i];
|
|
}
|
|
|
|
// add local a=ssrc-group: lines
|
|
lines = SDPUtil.find_lines(localSDP.media[i], 'a=ssrc-group:');
|
|
if (lines.length != 0)
|
|
sdp.media[i] += lines.join('\r\n') + '\r\n';
|
|
|
|
// and local a=ssrc: lines
|
|
sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc:').join('\r\n') + '\r\n';
|
|
}
|
|
sdp.raw = sdp.session + sdp.media.join('');
|
|
|
|
// make a new colibri session and configure it
|
|
// FIXME: is it correct to use this.connection.jid when used in a MUC?
|
|
var sess = new ColibriSession(this.connection.jid,
|
|
Math.random().toString(36).substr(2, 12), // random string
|
|
this.connection);
|
|
sess.initiate(peer);
|
|
sess.colibri = this;
|
|
// We do not announce our audio per conference peer, so only video is set here
|
|
sess.localVideo = this.connection.jingle.localVideo;
|
|
sess.media_constraints = this.connection.jingle.media_constraints;
|
|
sess.pc_constraints = this.connection.jingle.pc_constraints;
|
|
sess.ice_config = this.connection.jingle.ice_config;
|
|
|
|
this.connection.jingle.sessions[sess.sid] = sess;
|
|
this.connection.jingle.jid2session[sess.peerjid] = sess;
|
|
|
|
// send a session-initiate
|
|
var init = $iq({to: peer, type: 'set'})
|
|
.c('jingle',
|
|
{xmlns: 'urn:xmpp:jingle:1',
|
|
action: 'session-initiate',
|
|
initiator: sess.me,
|
|
sid: sess.sid
|
|
}
|
|
);
|
|
sdp.toJingle(init, 'initiator');
|
|
this.connection.sendIQ(init,
|
|
function (res) {
|
|
console.log('got result');
|
|
},
|
|
function (err) {
|
|
console.log('got error');
|
|
}
|
|
);
|
|
};
|
|
|
|
// pull in a new participant into the conference
|
|
ColibriFocus.prototype.addNewParticipant = function (peer) {
|
|
var self = this;
|
|
if (this.confid === 0 || !this.peerconnection.localDescription)
|
|
{
|
|
// bad state
|
|
if (this.confid === 0)
|
|
{
|
|
console.error('confid does not exist yet, postponing', peer);
|
|
}
|
|
else
|
|
{
|
|
console.error('local description not ready yet, postponing', peer);
|
|
}
|
|
window.setTimeout(function () { self.addNewParticipant(peer); }, 250);
|
|
return;
|
|
}
|
|
var index = this.channels.length;
|
|
this.channels.push([]);
|
|
this.peers.push(peer);
|
|
|
|
var elem = $iq({to: this.bridgejid, type: 'get'});
|
|
elem.c(
|
|
'conference',
|
|
{ xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
|
|
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
|
localSDP.media.forEach(function (media, channel) {
|
|
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
|
|
var elemName;
|
|
var endpointId = peer.substr(1 + peer.lastIndexOf('/'));
|
|
var elemAttrs
|
|
= {
|
|
initiator: 'true',
|
|
expire: self.channelExpire,
|
|
endpoint: endpointId
|
|
};
|
|
if (config.useBundle) {
|
|
elemAttrs['channel-bundle-id'] = endpointId;
|
|
}
|
|
|
|
|
|
if ('data' == name)
|
|
{
|
|
elemName = 'sctpconnection';
|
|
elemAttrs['port'] = 5000;
|
|
}
|
|
else
|
|
{
|
|
elemName = 'channel';
|
|
if ('video' === name) {
|
|
if (self.channelLastN >= 0) {
|
|
elemAttrs['last-n'] = self.channelLastN;
|
|
}
|
|
if (config.adaptiveLastN) {
|
|
elemAttrs['adaptive-last-n'] = 'true';
|
|
}
|
|
if (config.adaptiveSimulcast) {
|
|
elemAttrs['adaptive-simulcast'] = 'true';
|
|
}
|
|
}
|
|
}
|
|
|
|
elem.c('content', { name: name });
|
|
elem.c(elemName, elemAttrs);
|
|
elem.up(); // end of channel/sctpconnection
|
|
elem.up(); // end of content
|
|
});
|
|
|
|
this.connection.sendIQ(elem,
|
|
function (result) {
|
|
var contents = $(result).find('>conference>content').get();
|
|
var i;
|
|
for (i = 0; i < contents.length; i++) {
|
|
var channelXml = $(contents[i]).find('>channel');
|
|
if (channelXml.length)
|
|
{
|
|
tmp = channelXml.get();
|
|
}
|
|
else
|
|
{
|
|
tmp = $(contents[i]).find('>sctpconnection').get();
|
|
}
|
|
self.channels[index][i] = tmp[0];
|
|
}
|
|
var channelBundles = $(result).find('>conference>channel-bundle');
|
|
for (i = 0; i < channelBundles.length; i++)
|
|
{
|
|
var endpointId = $(channelBundles[i]).attr('id');
|
|
self.bundledTransports[endpointId] = $(channelBundles[i]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
|
|
}
|
|
self.initiate(peer, true);
|
|
},
|
|
function (error) {
|
|
console.warn(error);
|
|
}
|
|
);
|
|
};
|
|
|
|
// update the channel description (payload-types + dtls fp) for a 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++) {
|
|
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') {
|
|
change.c('channel', {
|
|
id: $(this.channels[participant][channel]).attr('id'),
|
|
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
|
expire: self.channelExpire
|
|
});
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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
|
|
{
|
|
var sctpmap = SDPUtil.find_line(remoteSDP.media[channel], 'a=sctpmap:');
|
|
change.c('sctpconnection', {
|
|
id: $(this.channels[participant][channel]).attr('id'),
|
|
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
|
expire: self.channelExpire,
|
|
port: SDPUtil.parse_sctpmap(sctpmap)[0]
|
|
});
|
|
}
|
|
|
|
if (!parts || parts.indexOf('transport') !== -1) {
|
|
// now add transport
|
|
remoteSDP.TransportToJingle(channel, change);
|
|
}
|
|
|
|
change.up(); // end of channel/sctpconnection
|
|
change.up(); // end of content
|
|
}
|
|
this.connection.sendIQ(change,
|
|
function (res) {
|
|
console.log('got result');
|
|
},
|
|
function (err) {
|
|
console.log('got error');
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
var self = this;
|
|
this.peers.forEach(function (peerjid) {
|
|
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 peersess = self.connection.jingle.jid2session[peerjid];
|
|
if(!peersess){
|
|
console.warn('no session with peer: '+peerjid+' yet...');
|
|
return;
|
|
}
|
|
|
|
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;
|
|
// FIXME: dirty waiting
|
|
if (!this.peerconnection.localDescription)
|
|
{
|
|
console.warn("addSource - localDescription not ready yet");
|
|
setTimeout(function() { self.addSource(elem, fromJid); }, 200);
|
|
return;
|
|
}
|
|
|
|
this.peerconnection.addSource(elem);
|
|
|
|
// NOTE(gp) this could be a useful thing to have in every Array object.
|
|
var diffArray = function(a) {
|
|
return this.filter(function(i) {return a.indexOf(i) < 0;});
|
|
};
|
|
|
|
var peerSsrc = this.remotessrc[fromJid];
|
|
// console.log("On ADD", this.peerconnection.addssrc, peerSsrc);
|
|
this.peerconnection.addssrc.forEach(function(val, idx){
|
|
if(!peerSsrc[idx]){
|
|
// add ssrc
|
|
peerSsrc[idx] = val;
|
|
} else if (val) {
|
|
// NOTE(gp) we can't expect the lines in the removessrc SDP fragment
|
|
// to be in the same order as in the lines in the peerSsrc SDP
|
|
// fragment. So, here we remove the val lines and re-add them.
|
|
|
|
var lines = peerSsrc[idx].split('\r\n');
|
|
var diffLines = val.split('\r\n');
|
|
|
|
// Remove ssrc
|
|
peerSsrc[idx] = diffArray.apply(lines, [diffLines]).join('\r\n');
|
|
|
|
// Add ssrc
|
|
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);
|
|
// 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']);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
// FIXME: dirty waiting
|
|
if (!self.peerconnection.localDescription)
|
|
{
|
|
console.warn("removeSource - localDescription not ready yet");
|
|
setTimeout(function() { self.removeSource(elem, fromJid); }, 200);
|
|
return;
|
|
}
|
|
|
|
this.peerconnection.removeSource(elem);
|
|
|
|
// NOTE(gp) this could be a useful thing to have in every Array object.
|
|
var diffArray = function(a) {
|
|
return this.filter(function(i) {return a.indexOf(i) < 0;});
|
|
};
|
|
|
|
var peerSsrc = this.remotessrc[fromJid];
|
|
// console.log("On REMOVE", this.peerconnection.removessrc, peerSsrc);
|
|
this.peerconnection.removessrc.forEach(function(val, idx){
|
|
if(peerSsrc[idx] && val){
|
|
// NOTE(gp) we can't expect the lines in the removessrc SDP fragment
|
|
// to be in the same order as in the lines in the peerSsrc SDP
|
|
// fragment.
|
|
var lines = peerSsrc[idx].split('\r\n');
|
|
var diffLines = val.split('\r\n');
|
|
// Remove ssrc
|
|
peerSsrc[idx] = diffArray.apply(lines, [diffLines]).join('\r\n');
|
|
}
|
|
});
|
|
|
|
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);
|
|
// 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']);
|
|
}
|
|
});
|
|
};
|
|
|
|
ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
|
|
var participant = this.peers.indexOf(session.peerjid);
|
|
console.log('Colibri.setRemoteDescription from', session.peerjid, participant);
|
|
var remoteSDP = new SDP('');
|
|
var channel;
|
|
remoteSDP.fromJingle(elem);
|
|
|
|
// ACT 1: change allocation on bridge
|
|
this.updateRemoteChannel(remoteSDP, participant);
|
|
|
|
// ACT 2: tell anyone else about the new SSRCs
|
|
this.sendSSRCUpdate(remoteSDP.getMediaSsrcMap(), session.peerjid, true);
|
|
|
|
// ACT 3: note the SSRCs
|
|
this.remotessrc[session.peerjid] = [];
|
|
for (channel = 0; channel < this.channels[participant].length; channel++) {
|
|
//if (channel == 0) continue; FIXME: does not work as intended
|
|
if (!remoteSDP.media[channel])
|
|
continue;
|
|
|
|
var lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
|
|
if (lines.length != 0)
|
|
// prepend ssrc-groups
|
|
this.remotessrc[session.peerjid][channel] = lines.join('\r\n') + '\r\n';
|
|
|
|
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length)
|
|
{
|
|
if (!this.remotessrc[session.peerjid][channel])
|
|
this.remotessrc[session.peerjid][channel] = '';
|
|
|
|
this.remotessrc[session.peerjid][channel] +=
|
|
SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:')
|
|
.join('\r\n') + '\r\n';
|
|
}
|
|
}
|
|
|
|
// ACT 4: add new a=ssrc and s=ssrc-group lines to local remotedescription
|
|
for (channel = 0; channel < this.channels[participant].length; channel++) {
|
|
//if (channel == 0) continue; FIXME: does not work as intended
|
|
if (!remoteSDP.media[channel])
|
|
continue;
|
|
|
|
var lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
|
|
if (lines.length != 0)
|
|
this.peerconnection.enqueueAddSsrc(
|
|
channel, SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:').join('\r\n') + '\r\n');
|
|
|
|
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
|
|
this.peerconnection.enqueueAddSsrc(
|
|
channel,
|
|
SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'
|
|
);
|
|
}
|
|
}
|
|
this.modifySources();
|
|
};
|
|
|
|
// relay ice candidates to bridge using trickle
|
|
ColibriFocus.prototype.addIceCandidate = function (session, elem) {
|
|
var self = this;
|
|
var participant = this.peers.indexOf(session.peerjid);
|
|
//console.log('change transport allocation for', this.confid, session.peerjid, participant);
|
|
var change = $iq({to: this.bridgejid, type: 'set'});
|
|
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
|
$(elem).each(function () {
|
|
var name = $(this).attr('name');
|
|
|
|
// If we are using bundle, audio/video/data channel will have the same candidates, so only send them for
|
|
// the audio channel.
|
|
if (config.useBundle && name !== 'audio') {
|
|
return;
|
|
}
|
|
|
|
var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
|
|
if (name != 'audio' && name != 'video')
|
|
channel = 2; // name == 'data'
|
|
|
|
change.c('content', {name: name});
|
|
if (name !== 'data')
|
|
{
|
|
change.c('channel', {
|
|
id: $(self.channels[participant][channel]).attr('id'),
|
|
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
|
|
expire: self.channelExpire
|
|
});
|
|
}
|
|
else
|
|
{
|
|
change.c('sctpconnection', {
|
|
id: $(self.channels[participant][channel]).attr('id'),
|
|
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
|
|
expire: self.channelExpire
|
|
});
|
|
}
|
|
$(this).find('>transport').each(function () {
|
|
change.c('transport', {
|
|
ufrag: $(this).attr('ufrag'),
|
|
pwd: $(this).attr('pwd'),
|
|
xmlns: $(this).attr('xmlns')
|
|
});
|
|
if (config.useRtcpMux
|
|
&& 'channel' === change.node.parentNode.nodeName) {
|
|
change.c('rtcp-mux').up();
|
|
}
|
|
|
|
$(this).find('>candidate').each(function () {
|
|
/* not yet
|
|
if (this.getAttribute('protocol') == 'tcp' && this.getAttribute('port') == 0) {
|
|
// chrome generates TCP candidates with port 0
|
|
return;
|
|
}
|
|
*/
|
|
var line = SDPUtil.candidateFromJingle(this);
|
|
change.c('candidate', SDPUtil.candidateToJingle(line)).up();
|
|
});
|
|
change.up(); // end of transport
|
|
});
|
|
change.up(); // end of channel/sctpconnection
|
|
change.up(); // end of content
|
|
});
|
|
// FIXME: need to check if there is at least one candidate when filtering TCP ones
|
|
this.connection.sendIQ(change,
|
|
function (res) {
|
|
console.log('got result');
|
|
},
|
|
function (err) {
|
|
console.error('got error', err);
|
|
}
|
|
);
|
|
};
|
|
|
|
// send our own candidate to the bridge
|
|
ColibriFocus.prototype.sendIceCandidate = function (candidate) {
|
|
var self = this;
|
|
//console.log('candidate', candidate);
|
|
if (!candidate) {
|
|
console.log('end of candidates');
|
|
return;
|
|
}
|
|
if (this.drip_container.length === 0) {
|
|
// start 20ms callout
|
|
window.setTimeout(
|
|
function () {
|
|
if (self.drip_container.length === 0) return;
|
|
self.sendIceCandidates(self.drip_container);
|
|
self.drip_container = [];
|
|
},
|
|
20);
|
|
}
|
|
this.drip_container.push(candidate);
|
|
};
|
|
|
|
// sort and send multiple candidates
|
|
ColibriFocus.prototype.sendIceCandidates = function (candidates) {
|
|
var self = this;
|
|
var mycands = $iq({to: this.bridgejid, type: 'set'});
|
|
mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
|
// FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
|
|
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
|
for (var mid = 0; mid < localSDP.media.length; mid++)
|
|
{
|
|
var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
|
|
if (cands.length > 0)
|
|
{
|
|
var name = cands[0].sdpMid;
|
|
mycands.c('content', {name: name });
|
|
if (name !== 'data')
|
|
{
|
|
mycands.c('channel', {
|
|
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
|
|
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
|
|
expire: self.channelExpire
|
|
});
|
|
}
|
|
else
|
|
{
|
|
mycands.c('sctpconnection', {
|
|
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
|
|
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
|
|
port: $(this.mychannel[cands[0].sdpMLineIndex]).attr('port'),
|
|
expire: self.channelExpire
|
|
});
|
|
}
|
|
mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
|
|
if (config.useRtcpMux && name !== 'data') {
|
|
mycands.c('rtcp-mux').up();
|
|
}
|
|
for (var i = 0; i < cands.length; i++) {
|
|
mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
|
|
}
|
|
mycands.up(); // transport
|
|
mycands.up(); // channel / sctpconnection
|
|
mycands.up(); // content
|
|
}
|
|
}
|
|
console.log('send cands', candidates);
|
|
this.connection.sendIQ(mycands,
|
|
function (res) {
|
|
console.log('got result');
|
|
},
|
|
function (err) {
|
|
console.error('got error', err);
|
|
}
|
|
);
|
|
};
|
|
|
|
ColibriFocus.prototype.terminate = function (session, reason) {
|
|
console.log('remote session terminated from', session.peerjid);
|
|
var participant = this.peers.indexOf(session.peerjid);
|
|
if (!this.remotessrc[session.peerjid] || participant == -1) {
|
|
return;
|
|
}
|
|
var ssrcs = this.remotessrc[session.peerjid];
|
|
for (var i = 0; i < ssrcs.length; i++) {
|
|
this.peerconnection.enqueueRemoveSsrc(i, ssrcs[i]);
|
|
}
|
|
// remove from this.peers
|
|
this.peers.splice(participant, 1);
|
|
// expire channel on bridge
|
|
var change = $iq({to: this.bridgejid, type: 'set'});
|
|
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
|
for (var channel = 0; channel < this.channels[participant].length; channel++) {
|
|
var name = channel === 0 ? 'audio' : 'video';
|
|
if (channel == 2)
|
|
name = 'data';
|
|
change.c('content', {name: name});
|
|
if (name !== 'data')
|
|
{
|
|
change.c('channel', {
|
|
id: $(this.channels[participant][channel]).attr('id'),
|
|
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
|
expire: '0'
|
|
});
|
|
}
|
|
else
|
|
{
|
|
change.c('sctpconnection', {
|
|
id: $(this.channels[participant][channel]).attr('id'),
|
|
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
|
expire: '0'
|
|
});
|
|
}
|
|
change.up(); // end of channel/sctpconnection
|
|
change.up(); // end of content
|
|
}
|
|
this.connection.sendIQ(change,
|
|
function (res) {
|
|
console.log('got result');
|
|
},
|
|
function (err) {
|
|
console.error('got error', err);
|
|
}
|
|
);
|
|
// and remove from channels
|
|
this.channels.splice(participant, 1);
|
|
|
|
// tell everyone about the ssrcs to be removed
|
|
var sdp = new SDP('');
|
|
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
|
var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
|
|
for (var j = 0; j < ssrcs.length; j++) {
|
|
sdp.media[j] = 'a=mid:' + contents[j] + '\r\n';
|
|
sdp.media[j] += ssrcs[j];
|
|
this.peerconnection.enqueueRemoveSsrc(j, ssrcs[j]);
|
|
}
|
|
this.sendSSRCUpdate(sdp.getMediaSsrcMap(), session.peerjid, false);
|
|
|
|
delete this.remotessrc[session.peerjid];
|
|
this.modifySources();
|
|
};
|
|
|
|
ColibriFocus.prototype.sendTerminate = function (session, reason, text) {
|
|
var term = $iq({to: session.peerjid, type: 'set'})
|
|
.c('jingle',
|
|
{xmlns: 'urn:xmpp:jingle:1',
|
|
action: 'session-terminate',
|
|
initiator: session.me,
|
|
sid: session.sid})
|
|
.c('reason')
|
|
.c(reason || 'success');
|
|
|
|
if (text) {
|
|
term.up().c('text').t(text);
|
|
}
|
|
|
|
this.connection.sendIQ(term,
|
|
function () {
|
|
if (!session)
|
|
return;
|
|
|
|
if (session.peerconnection) {
|
|
session.peerconnection.close();
|
|
session.peerconnection = null;
|
|
}
|
|
|
|
session.terminate();
|
|
var ack = {};
|
|
ack.source = 'terminate';
|
|
$(document).trigger('ack.jingle', [session.sid, ack]);
|
|
},
|
|
function (stanza) {
|
|
var error = ($(stanza).find('error').length) ? {
|
|
code: $(stanza).find('error').attr('code'),
|
|
reason: $(stanza).find('error :first')[0].tagName,
|
|
}:{};
|
|
$(document).trigger('ack.jingle', [self.sid, error]);
|
|
},
|
|
10000);
|
|
if (this.statsinterval !== null) {
|
|
window.clearInterval(this.statsinterval);
|
|
this.statsinterval = null;
|
|
}
|
|
};
|
|
|
|
ColibriFocus.prototype.setRTCPTerminationStrategy = function (strategyFQN) {
|
|
var self = this;
|
|
|
|
// TODO(gp) maybe move the RTCP termination strategy element under the
|
|
// content or channel element.
|
|
var strategyIQ = $iq({to: this.bridgejid, type: 'set'});
|
|
strategyIQ.c('conference', {
|
|
xmlns: 'http://jitsi.org/protocol/colibri',
|
|
id: this.confid,
|
|
});
|
|
|
|
strategyIQ.c('rtcp-termination-strategy', {name: strategyFQN });
|
|
|
|
strategyIQ.c('content', {name: "video"});
|
|
strategyIQ.up(); // end of content
|
|
|
|
console.log('setting RTCP termination strategy', strategyFQN);
|
|
this.connection.sendIQ(strategyIQ,
|
|
function (res) {
|
|
console.log('got result');
|
|
},
|
|
function (err) {
|
|
console.error('got error', err);
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Sets the default value of the channel last-n attribute in this conference and
|
|
* updates/patches the existing channels.
|
|
*/
|
|
ColibriFocus.prototype.setChannelLastN = function (channelLastN) {
|
|
if (('number' === typeof(channelLastN))
|
|
&& (this.channelLastN !== channelLastN))
|
|
{
|
|
this.channelLastN = channelLastN;
|
|
|
|
// Update/patch the existing channels.
|
|
var patch = $iq({ to: this.bridgejid, type: 'set' });
|
|
|
|
patch.c(
|
|
'conference',
|
|
{ xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
|
|
patch.c('content', { name: 'video' });
|
|
patch.c(
|
|
'channel',
|
|
{
|
|
id: $(this.mychannel[1 /* video */]).attr('id'),
|
|
'last-n': this.channelLastN
|
|
});
|
|
patch.up(); // end of channel
|
|
for (var p = 0; p < this.channels.length; p++)
|
|
{
|
|
patch.c(
|
|
'channel',
|
|
{
|
|
id: $(this.channels[p][1 /* video */]).attr('id'),
|
|
'last-n': this.channelLastN
|
|
});
|
|
patch.up(); // end of channel
|
|
}
|
|
this.connection.sendIQ(
|
|
patch,
|
|
function (res) {
|
|
console.info('Set channel last-n succeeded:', res);
|
|
},
|
|
function (err) {
|
|
console.error('Set channel last-n failed:', err);
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sets the default value of the channel simulcast layer attribute in this
|
|
* conference and updates/patches the existing channels.
|
|
*/
|
|
ColibriFocus.prototype.setReceiveSimulcastLayer = function (receiveSimulcastLayer) {
|
|
if (('number' === typeof(receiveSimulcastLayer))
|
|
&& (this.receiveSimulcastLayer !== receiveSimulcastLayer))
|
|
{
|
|
// TODO(gp) be able to set the receiving simulcast layer on a per
|
|
// sender basis.
|
|
this.receiveSimulcastLayer = receiveSimulcastLayer;
|
|
|
|
// Update/patch the existing channels.
|
|
var patch = $iq({ to: this.bridgejid, type: 'set' });
|
|
|
|
patch.c(
|
|
'conference',
|
|
{ xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
|
|
patch.c('content', { name: 'video' });
|
|
patch.c(
|
|
'channel',
|
|
{
|
|
id: $(this.mychannel[1 /* video */]).attr('id'),
|
|
'receive-simulcast-layer': this.receiveSimulcastLayer
|
|
});
|
|
patch.up(); // end of channel
|
|
for (var p = 0; p < this.channels.length; p++)
|
|
{
|
|
patch.c(
|
|
'channel',
|
|
{
|
|
id: $(this.channels[p][1 /* video */]).attr('id'),
|
|
'receive-simulcast-layer': this.receiveSimulcastLayer
|
|
});
|
|
patch.up(); // end of channel
|
|
}
|
|
this.connection.sendIQ(
|
|
patch,
|
|
function (res) {
|
|
console.info('Set channel simulcast receive layer succeeded:', res);
|
|
},
|
|
function (err) {
|
|
console.error('Set channel simulcast receive layer failed:', err);
|
|
});
|
|
}
|
|
};
|
|
|