Adds support for sctp data channels + user interface for active speaker. Makes the toolbar more visible. Moves toolbar and videolayout related code in separate classes.

This commit is contained in:
yanas 2014-06-12 20:59:47 +03:00
commit 715749ead8
10 changed files with 541 additions and 94 deletions

88
app.js
View File

@ -20,6 +20,11 @@ var statsCollector = null;
*/ */
var ssrc2videoType = {}; var ssrc2videoType = {};
var videoSrcToSsrc = {}; var videoSrcToSsrc = {};
/**
* Currently focused video "src"(displayed in large video).
* @type {String}
*/
var focusedVideoSrc = null;
var mutedAudios = {}; var mutedAudios = {};
var localVideoSrc = null; var localVideoSrc = null;
@ -358,7 +363,65 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
} }
}); });
/**
* Returns the JID of the user to whom given <tt>videoSrc</tt> belongs.
* @param videoSrc the video "src" identifier.
* @returns {null | String} the JID of the user to whom given <tt>videoSrc</tt>
* belongs.
*/
function getJidFromVideoSrc(videoSrc)
{
if (videoSrc === localVideoSrc)
return connection.emuc.myroomjid;
var ssrc = videoSrcToSsrc[videoSrc];
if (!ssrc)
{
return null;
}
return ssrc2jid[ssrc];
}
/**
* Gets the selector of video thumbnail container for the user identified by
* given <tt>userJid</tt>
* @param userJid user's Jid for whom we want to get the video container.
*/
function getParticipantContainer(userJid)
{
if (!userJid)
return null;
if (userJid === connection.emuc.myroomjid)
return $("#localVideoContainer");
else
return $("#participant_" + Strophe.getResourceFromJid(userJid));
}
function handleVideoThumbClicked(videoSrc) { function handleVideoThumbClicked(videoSrc) {
// Restore style for previously focused video
var oldContainer =
getParticipantContainer(
getJidFromVideoSrc(focusedVideoSrc));
if (oldContainer)
oldContainer.removeClass("videoContainerFocused");
// Unlock
if (focusedVideoSrc === videoSrc)
{
focusedVideoSrc = null;
return;
}
// Lock new video
focusedVideoSrc = videoSrc;
var userJid = getJidFromVideoSrc(videoSrc);
if (userJid)
{
var container = getParticipantContainer(userJid);
container.addClass("videoContainerFocused");
}
$(document).trigger("video.selected", [false]); $(document).trigger("video.selected", [false]);
@ -509,6 +572,12 @@ $(document).bind('callincoming.jingle', function (event, sid) {
startRtpStatsCollector(); startRtpStatsCollector();
// Bind data channel listener in case we're a regular participant
if (config.openSctp)
{
bindDataChannelListener(sess.peerconnection);
}
// TODO: check affiliation and/or role // TODO: check affiliation and/or role
console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]); console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
sess.usedrip = true; // not-so-naive trickle ice sess.usedrip = true; // not-so-naive trickle ice
@ -520,6 +589,12 @@ $(document).bind('callincoming.jingle', function (event, sid) {
$(document).bind('conferenceCreated.jingle', function (event, focus) $(document).bind('conferenceCreated.jingle', function (event, focus)
{ {
startRtpStatsCollector(); startRtpStatsCollector();
// Bind data channel listener in case we're the focus
if (config.openSctp)
{
bindDataChannelListener(focus.peerconnection);
}
}); });
$(document).bind('callactive.jingle', function (event, videoelem, sid) { $(document).bind('callactive.jingle', function (event, videoelem, sid) {
@ -528,6 +603,7 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) {
videoelem.show(); videoelem.show();
resizeThumbnails(); resizeThumbnails();
if (!focusedVideoSrc)
updateLargeVideo(videoelem.attr('src'), 1); updateLargeVideo(videoelem.attr('src'), 1);
showFocusIndicator(); showFocusIndicator();
@ -551,7 +627,7 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) {
var directions = {}; var directions = {};
var localSDP = new SDP(sess.peerconnection.localDescription.sdp); var localSDP = new SDP(sess.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media) { localSDP.media.forEach(function (media) {
var type = SDPUtil.parse_mline(media.split('\r\n')[0]).media; var type = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
if (SDPUtil.find_line(media, 'a=ssrc:')) { if (SDPUtil.find_line(media, 'a=ssrc:')) {
// assumes a single local ssrc // assumes a single local ssrc
@ -647,6 +723,16 @@ $(document).bind('left.muc', function (event, jid) {
} }
}, 10); }, 10);
// Unlock large video
if (focusedVideoSrc)
{
if (getJidFromVideoSrc(focusedVideoSrc) === jid)
{
console.info("Focused video owner has left the conference");
focusedVideoSrc = null;
}
}
connection.jingle.terminateByJid(jid); connection.jingle.terminateByJid(jid);
if (focus == null if (focus == null

View File

@ -12,5 +12,6 @@ var config = {
desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable. desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
minChromeExtVersion: '0.1', // Required version of Chrome extension minChromeExtVersion: '0.1', // Required version of Chrome extension
enableRtpStats: false // Enables RTP stats processing enableRtpStats: false, // Enables RTP stats processing
openSctp: true //Toggle to enable/disable SCTP channels
}; };

View File

@ -52,6 +52,10 @@
z-index: 3; z-index: 3;
} }
#remoteVideos .videocontainer.videoContainerFocused {
border: 3px solid #388396;
}
#localVideoWrapper { #localVideoWrapper {
display:inline-block; display:inline-block;
-webkit-mask-box-image: url(../images/videomask.svg); -webkit-mask-box-image: url(../images/videomask.svg);

90
data_channels.js Normal file
View File

@ -0,0 +1,90 @@
/* global connection, Strophe, updateLargeVideo, focusedVideoSrc*/
/**
* Callback triggered by PeerConnection when new data channel is opened
* on the bridge.
* @param event the event info object.
*/
function onDataChannel(event)
{
var dataChannel = event.channel;
dataChannel.onopen = function ()
{
console.info("Data channel opened by the bridge !!!", dataChannel);
// Code sample for sending string and/or binary data
// Sends String message to the bridge
//dataChannel.send("Hello bridge!");
// Sends 12 bytes binary message to the bridge
//dataChannel.send(new ArrayBuffer(12));
};
dataChannel.onerror = function (error)
{
console.error("Data Channel Error:", error, dataChannel);
};
dataChannel.onmessage = function (event)
{
var msgData = event.data;
console.info("Got Data Channel Message:", msgData, dataChannel);
// Active speaker event
if (msgData.indexOf('activeSpeaker') === 0 && !focusedVideoSrc)
{
// Endpoint ID from the bridge
var endpointId = msgData.split(":")[1];
console.info("New active speaker: " + endpointId);
var container = document.getElementById(
'participant_' + endpointId);
// Local video will not have container found, but that's ok
// since we don't want to switch to local video
if (container)
{
var video = container.getElementsByTagName("video");
if (video.length)
{
updateLargeVideo(video[0].src);
}
}
}
};
dataChannel.onclose = function ()
{
console.info("The Data Channel closed", dataChannel);
};
}
/**
* Binds "ondatachannel" event listener to given PeerConnection instance.
* @param peerConnection WebRTC peer connection instance.
*/
function bindDataChannelListener(peerConnection)
{
peerConnection.ondatachannel = onDataChannel;
// Sample code for opening new data channel from Jitsi Meet to the bridge.
// Although it's not a requirement to open separate channels from both bridge
// and peer as single channel can be used for sending and receiving data.
// So either channel opened by the bridge or the one opened here is enough
// for communication with the bridge.
/*var dataChannelOptions =
{
reliable: true
};
var dataChannel
= peerConnection.createDataChannel("myChannel", dataChannelOptions);
// Can be used only when is in open state
dataChannel.onopen = function ()
{
dataChannel.send("My channel !!!");
};
dataChannel.onmessage = function (event)
{
var msgData = event.data;
console.info("Got My Data Channel Message:", msgData, dataChannel);
};*/
}

View File

@ -24,6 +24,7 @@
<script src="muc.js?v=10"></script><!-- simple MUC library --> <script src="muc.js?v=10"></script><!-- simple MUC library -->
<script src="estos_log.js?v=2"></script><!-- simple stanza logger --> <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
<script src="desktopsharing.js?v=1"></script><!-- desktop sharing --> <script src="desktopsharing.js?v=1"></script><!-- desktop sharing -->
<script src="data_channels.js?v=1"></script><!-- data channels -->
<script src="app.js?v=26"></script><!-- application logic --> <script src="app.js?v=26"></script><!-- application logic -->
<script src="chat.js?v=4"></script><!-- chat logic --> <script src="chat.js?v=4"></script><!-- chat logic -->
<script src="util.js?v=3"></script><!-- utility functions --> <script src="util.js?v=3"></script><!-- utility functions -->

View File

@ -44,8 +44,27 @@ function ColibriFocus(connection, bridgejid) {
this.peers = []; this.peers = [];
this.confid = null; 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 = 60;
// media types of the conference // media types of the conference
if (config.openSctp)
{
this.media = ['audio', 'video', 'data'];
}
else
{
this.media = ['audio', 'video']; this.media = ['audio', 'video'];
}
this.connection.jingle.sessions[this.sid] = this; this.connection.jingle.sessions[this.sid] = this;
this.mychannel = []; this.mychannel = [];
@ -151,17 +170,29 @@ ColibriFocus.prototype._makeConference = function () {
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'}); elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
this.media.forEach(function (name) { this.media.forEach(function (name) {
var isData = name === 'data';
var channel = isData ? 'sctpconnection' : 'channel';
elem.c('content', {name: name}); elem.c('content', {name: name});
elem.c('channel', {
elem.c(channel, {
initiator: 'true', initiator: 'true',
expire: '15', expire: '15',
endpoint: 'fix_me_focus_endpoint'}).up(); endpoint: self.myMucResource
});
if (isData)
elem.attrs({port: 5000});
elem.up();// end of channel
for (var j = 0; j < self.peers.length; j++) { for (var j = 0; j < self.peers.length; j++) {
elem.c('channel', { elem.c(channel, {
initiator: 'true', initiator: 'true',
expire: '15', expire: '15',
endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/')) endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/'))
}).up(); });
if (isData)
elem.attrs({port: 5000});
elem.up(); // end of channel
} }
elem.up(); // end of content elem.up(); // end of content
}); });
@ -209,8 +240,13 @@ ColibriFocus.prototype.createdConference = function (result) {
this.confid = $(result).find('>conference').attr('id'); this.confid = $(result).find('>conference').attr('id');
var remotecontents = $(result).find('>conference>content').get(); var remotecontents = $(result).find('>conference>content').get();
var numparticipants = 0; var numparticipants = 0;
for (var i = 0; i < remotecontents.length; i++) { for (var i = 0; i < remotecontents.length; i++)
tmp = $(remotecontents[i]).find('>channel').get(); {
var contentName = $(remotecontents[i]).attr('name');
var channelName
= contentName !== 'data' ? '>channel' : '>sctpconnection';
tmp = $(remotecontents[i]).find(channelName).get();
this.mychannel.push($(tmp.shift())); this.mychannel.push($(tmp.shift()));
numparticipants = tmp.length; numparticipants = tmp.length;
for (j = 0; j < tmp.length; j++) { for (j = 0; j < tmp.length; j++) {
@ -223,7 +259,55 @@ ColibriFocus.prototype.createdConference = function (result) {
console.log('remote channels', this.channels); console.log('remote channels', this.channels);
var bridgeSDP = new SDP('v=0\r\no=- 5151055458874951233 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n'); // 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 */
'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' +
/* 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 goog-remb\r\n' +
'a=rtpmap:116 red/90000\r\n' +
'a=rtpmap:117 ulpfec/90000\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; bridgeSDP.media.length = this.mychannel.length;
var channel; var channel;
/* /*
@ -262,12 +346,17 @@ ColibriFocus.prototype.createdConference = function (result) {
// get the mixed ssrc // get the mixed ssrc
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
// FIXME: check rtp-level-relay-type // FIXME: check rtp-level-relay-type
if (tmp.length) {
var isData = bridgeSDP.media[channel].indexOf('application') !== -1;
if (!isData && tmp.length)
{
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n'; bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n'; bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n'; bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n'; bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
} else { }
else if (!isData)
{
// make chrome happy... '3735928559' == 0xDEADBEEF // make chrome happy... '3735928559' == 0xDEADBEEF
// FIXME: this currently appears as two streams, should be one // FIXME: this currently appears as two streams, should be one
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n'; bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
@ -308,22 +397,42 @@ ColibriFocus.prototype.createdConference = function (result) {
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid}); elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
var localSDP = new SDP(self.peerconnection.localDescription.sdp); var localSDP = new SDP(self.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) { localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
elem.c('content', {name: name}); elem.c('content', {name: name});
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
if (name !== 'data')
{
elem.c('channel', { elem.c('channel', {
initiator: 'true', initiator: 'true',
expire: '15', expire: self.channelExpire,
id: self.mychannel[channel].attr('id'), id: self.mychannel[channel].attr('id'),
endpoint: 'fix_me_focus_endpoint' endpoint: self.myMucResource
}); });
// FIXME: should reuse code from .toJingle // 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++)
for (var j = 0; j < mline.fmt.length; j++) { {
var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]); var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
if (rtpmap)
{
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap)); elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
elem.up(); 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,
endpoint: self.myMucResource,
port: sctpPort
}
);
}
localSDP.TransportToJingle(channel, elem); localSDP.TransportToJingle(channel, elem);
@ -336,7 +445,9 @@ ColibriFocus.prototype.createdConference = function (result) {
// ... // ...
}, },
function (error) { function (error) {
console.warn(error); console.error(
"ERROR setLocalDescription succeded",
error, elem);
} }
); );
@ -421,7 +532,10 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n'; sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n'; sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n'; sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
} else { }
// No SSRCs for 'data', comes when j == 2
else if (j < 2)
{
// make chrome happy... '3735928559' == 0xDEADBEEF // make chrome happy... '3735928559' == 0xDEADBEEF
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n'; sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n'; sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
@ -490,9 +604,17 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
// pull in a new participant into the conference // pull in a new participant into the conference
ColibriFocus.prototype.addNewParticipant = function (peer) { ColibriFocus.prototype.addNewParticipant = function (peer) {
var self = this; var self = this;
if (this.confid === 0) { if (this.confid === 0 || !this.peerconnection.localDescription)
{
// bad state // bad state
console.log('confid does not exist yet, postponing', peer); 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 () { window.setTimeout(function () {
self.addNewParticipant(peer); self.addNewParticipant(peer);
}, 250); }, 250);
@ -506,14 +628,26 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
var localSDP = new SDP(this.peerconnection.localDescription.sdp); var localSDP = new SDP(this.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) { localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
elem.c('content', {name: name}); elem.c('content', {name: name});
if (name !== 'data')
{
elem.c('channel', { elem.c('channel', {
initiator: 'true', initiator: 'true',
expire:'15', expire: self.channelExpire,
endpoint: peer.substr(1 + peer.lastIndexOf('/')) endpoint: peer.substr(1 + peer.lastIndexOf('/'))
}); });
elem.up(); // end of channel }
else
{
elem.c('sctpconnection', {
endpoint: peer.substr(1 + peer.lastIndexOf('/')),
initiator: 'true',
expire: self.channelExpire,
port: 5000
});
}
elem.up(); // end of channel/sctpconnection
elem.up(); // end of content elem.up(); // end of content
}); });
@ -521,7 +655,15 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
function (result) { function (result) {
var contents = $(result).find('>conference>content').get(); var contents = $(result).find('>conference>content').get();
for (var i = 0; i < contents.length; i++) { for (var i = 0; i < contents.length; i++) {
tmp = $(contents[i]).find('>channel').get(); 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]; self.channels[index][i] = tmp[0];
} }
self.initiate(peer, true); self.initiate(peer, true);
@ -535,14 +677,19 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
// update the channel description (payload-types + dtls fp) for a participant // update the channel description (payload-types + dtls fp) for a participant
ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) { ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
console.log('change allocation for', this.confid); console.log('change allocation for', this.confid);
var self = this;
var change = $iq({to: this.bridgejid, type: 'set'}); var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); 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++)
change.c('content', {name: channel === 0 ? 'audio' : 'video'}); {
var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:'));
change.c('content', {name: name});
if (name !== 'data')
{
change.c('channel', { change.c('channel', {
id: $(this.channels[participant][channel]).attr('id'), id: $(this.channels[participant][channel]).attr('id'),
endpoint: $(this.channels[participant][channel]).attr('endpoint'), endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: '15' expire: self.channelExpire
}); });
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:'); var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
@ -562,10 +709,20 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
*/ */
change.up(); change.up();
}); });
}
else
{
var sctpmap = SDPUtil.find_line(remoteSDP.media[channel], 'a=sctpmap:');
change.c('sctpconnection', {
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: self.channelExpire,
port: SDPUtil.parse_sctpmap(sctpmap)[0]
});
}
// now add transport // now add transport
remoteSDP.TransportToJingle(channel, change); remoteSDP.TransportToJingle(channel, change);
change.up(); // end of channel change.up(); // end of channel/sctpconnection
change.up(); // end of content change.up(); // end of content
} }
this.connection.sendIQ(change, this.connection.sendIQ(change,
@ -679,8 +836,11 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
this.remotessrc[session.peerjid] = []; this.remotessrc[session.peerjid] = [];
for (channel = 0; channel < this.channels[participant].length; channel++) { for (channel = 0; channel < this.channels[participant].length; channel++) {
//if (channel == 0) continue; FIXME: does not work as intended //if (channel == 0) continue; FIXME: does not work as intended
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) { if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length)
this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'; {
this.remotessrc[session.peerjid][channel] =
SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:')
.join('\r\n') + '\r\n';
} }
} }
@ -706,14 +866,27 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
$(elem).each(function () { $(elem).each(function () {
var name = $(this).attr('name'); var name = $(this).attr('name');
var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc 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}); change.c('content', {name: name});
if (name !== 'data')
{
change.c('channel', { change.c('channel', {
id: $(self.channels[participant][channel]).attr('id'), id: $(self.channels[participant][channel]).attr('id'),
endpoint: $(self.channels[participant][channel]).attr('endpoint'), endpoint: $(self.channels[participant][channel]).attr('endpoint'),
expire: '15' expire: self.channelExpire
}); });
}
else
{
change.c('sctpconnection', {
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
expire: self.channelExpire
});
}
$(this).find('>transport').each(function () { $(this).find('>transport').each(function () {
change.c('transport', { change.c('transport', {
ufrag: $(this).attr('ufrag'), ufrag: $(this).attr('ufrag'),
@ -733,7 +906,7 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
}); });
change.up(); // end of transport change.up(); // end of transport
}); });
change.up(); // end of channel change.up(); // end of channel/sctpconnection
change.up(); // end of content change.up(); // end of content
}); });
// FIXME: need to check if there is at least one candidate when filtering TCP ones // FIXME: need to check if there is at least one candidate when filtering TCP ones
@ -773,21 +946,35 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) {
mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); 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 // FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
var localSDP = new SDP(this.peerconnection.localDescription.sdp); var localSDP = new SDP(this.peerconnection.localDescription.sdp);
for (var mid = 0; mid < localSDP.media.length; mid++) { for (var mid = 0; mid < localSDP.media.length; mid++)
{
var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; }); var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
if (cands.length > 0) { if (cands.length > 0)
mycands.c('content', {name: cands[0].sdpMid }); {
var name = cands[0].sdpMid;
mycands.c('content', {name: name });
if (name !== 'data')
{
mycands.c('channel', { mycands.c('channel', {
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'), id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'), endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
expire: '15' expire: self.channelExpire
}); });
}
else
{
mycands.c('sctpconnection', {
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'}); mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
for (var i = 0; i < cands.length; i++) { for (var i = 0; i < cands.length; i++) {
mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up(); mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
} }
mycands.up(); // transport mycands.up(); // transport
mycands.up(); // channel mycands.up(); // channel / sctpconnection
mycands.up(); // content mycands.up(); // content
} }
} }
@ -818,13 +1005,26 @@ ColibriFocus.prototype.terminate = function (session, reason) {
var change = $iq({to: this.bridgejid, type: 'set'}); var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (var channel = 0; channel < this.channels[participant].length; channel++) { for (var channel = 0; channel < this.channels[participant].length; channel++) {
change.c('content', {name: channel === 0 ? 'audio' : 'video'}); var name = channel === 0 ? 'audio' : 'video';
if (channel == 2)
name = 'data';
change.c('content', {name: name});
if (name !== 'data')
{
change.c('channel', { change.c('channel', {
id: $(this.channels[participant][channel]).attr('id'), id: $(this.channels[participant][channel]).attr('id'),
endpoint: $(this.channels[participant][channel]).attr('endpoint'), endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: '0' expire: '0'
}); });
change.up(); // end of channel }
else
{
change.c('sctpconnection', {
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: '0'
});
}
change.up(); // end of channel/sctpconnection
change.up(); // end of content change.up(); // end of content
} }
this.connection.sendIQ(change, this.connection.sendIQ(change,

View File

@ -32,7 +32,7 @@ function TraceablePeerConnection(ice_config, constraints) {
this.switchstreams = false; 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);
self.updateLog.push({ self.updateLog.push({
time: new Date(), time: new Date(),
@ -144,8 +144,8 @@ TraceablePeerConnection.prototype.removeStream = function (stream) {
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) { TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
this.trace('createDataChannel', label, opts); this.trace('createDataChannel', label, opts);
this.peerconnection.createDataChannel(label, opts); return this.peerconnection.createDataChannel(label, opts);
} };
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) { TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
var self = this; var self = this;

View File

@ -155,7 +155,10 @@ SDP.prototype.toJingle = function (elem, thecreator) {
} }
for (i = 0; i < this.media.length; i++) { for (i = 0; i < this.media.length; i++) {
mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]); mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
if (!(mline.media == 'audio' || mline.media == 'video')) { if (!(mline.media === 'audio' ||
mline.media === 'video' ||
mline.media === 'application'))
{
continue; continue;
} }
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) { if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
@ -171,12 +174,14 @@ SDP.prototype.toJingle = function (elem, thecreator) {
elem.attrs({ name: mid }); elem.attrs({ name: mid });
// old BUNDLE plan, to be removed // old BUNDLE plan, to be removed
if (bundle.indexOf(mid) != -1) { if (bundle.indexOf(mid) !== -1) {
elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up(); elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
bundle.splice(bundle.indexOf(mid), 1); bundle.splice(bundle.indexOf(mid), 1);
} }
} }
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length) {
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length)
{
elem.c('description', elem.c('description',
{xmlns: 'urn:xmpp:jingle:apps:rtp:1', {xmlns: 'urn:xmpp:jingle:apps:rtp:1',
media: mline.media }); media: mline.media });
@ -304,6 +309,26 @@ SDP.prototype.TransportToJingle = function (mediaindex, elem) {
var self = this; var self = this;
elem.c('transport'); elem.c('transport');
// XEP-0343 DTLS/SCTP
if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
{
var sctpmap = SDPUtil.find_line(
this.media[i], 'a=sctpmap:', self.session);
if (sctpmap)
{
var sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
elem.c('sctpmap',
{
xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
number: sctpAttrs[0], /* SCTP port */
protocol: sctpAttrs[1], /* protocol */
});
// Optional stream count attribute
if (sctpAttrs.length > 2)
elem.attrs({ streams: sctpAttrs[2]});
elem.up();
}
}
// XEP-0320 // XEP-0320
var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session); var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
fingerprints.forEach(function(line) { fingerprints.forEach(function(line) {
@ -438,6 +463,8 @@ SDP.prototype.jingle2media = function (content) {
ssrc = desc.attr('ssrc'), ssrc = desc.attr('ssrc'),
self = this, self = this,
tmp; tmp;
var sctp = content.find(
'>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
tmp = { media: desc.attr('media') }; tmp = { media: desc.attr('media') };
tmp.port = '1'; tmp.port = '1';
@ -446,13 +473,34 @@ SDP.prototype.jingle2media = function (content) {
tmp.port = '0'; tmp.port = '0';
} }
if (content.find('>transport>fingerprint').length || desc.find('encryption').length) { if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
if (sctp.length)
tmp.proto = 'DTLS/SCTP';
else
tmp.proto = 'RTP/SAVPF'; tmp.proto = 'RTP/SAVPF';
} else { } else {
tmp.proto = 'RTP/AVPF'; tmp.proto = 'RTP/AVPF';
} }
tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get(); if (!sctp.length)
{
tmp.fmt = desc.find('payload-type').map(
function () { return this.getAttribute('id'); }).get();
media += SDPUtil.build_mline(tmp) + '\r\n'; media += SDPUtil.build_mline(tmp) + '\r\n';
}
else
{
media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
media += 'a=sctpmap:' + sctp.attr('number') +
' ' + sctp.attr('protocol');
var streamCount = sctp.attr('streams');
if (streamCount)
media += ' ' + streamCount + '\r\n';
else
media += '\r\n';
}
media += 'c=IN IP4 0.0.0.0\r\n'; media += 'c=IN IP4 0.0.0.0\r\n';
if (!sctp.length)
media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n'; media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]'); tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
if (tmp.length) { if (tmp.length) {

View File

@ -90,6 +90,20 @@ SDPUtil = {
data.channels = parts.length ? parts.shift() : '1'; data.channels = parts.length ? parts.shift() : '1';
return data; return data;
}, },
/**
* Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
* @param line eg. "a=sctpmap:5000 webrtc-datachannel"
* @returns [SCTP port number, protocol, streams]
*/
parse_sctpmap: function (line)
{
var parts = line.substring(10).split(' ');
var sctpPort = parts[0];
var protocol = parts[1];
// Stream count is optional
var streamCount = parts.length > 2 ? parts[2] : null;
return [sctpPort, protocol, streamCount];// SCTP port
},
build_rtpmap: function (el) { build_rtpmap: function (el) {
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate'); var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') { if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {

3
muc.js
View File

@ -21,6 +21,9 @@ Strophe.addConnectionPlugin('emuc', {
}, },
doJoin: function (jid, password) { doJoin: function (jid, password) {
this.myroomjid = jid; this.myroomjid = jid;
console.info("Joined MUC as " + this.myroomjid);
this.initPresenceMap(this.myroomjid); this.initPresenceMap(this.myroomjid);
if (!this.roomjid) { if (!this.roomjid) {