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:
commit
715749ead8
90
app.js
90
app.js
|
@ -20,6 +20,11 @@ var statsCollector = null;
|
|||
*/
|
||||
var ssrc2videoType = {};
|
||||
var videoSrcToSsrc = {};
|
||||
/**
|
||||
* Currently focused video "src"(displayed in large video).
|
||||
* @type {String}
|
||||
*/
|
||||
var focusedVideoSrc = null;
|
||||
var mutedAudios = {};
|
||||
|
||||
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) {
|
||||
// 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]);
|
||||
|
||||
|
@ -509,6 +572,12 @@ $(document).bind('callincoming.jingle', function (event, sid) {
|
|||
|
||||
startRtpStatsCollector();
|
||||
|
||||
// Bind data channel listener in case we're a regular participant
|
||||
if (config.openSctp)
|
||||
{
|
||||
bindDataChannelListener(sess.peerconnection);
|
||||
}
|
||||
|
||||
// TODO: check affiliation and/or role
|
||||
console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
|
||||
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)
|
||||
{
|
||||
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) {
|
||||
|
@ -528,7 +603,8 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) {
|
|||
videoelem.show();
|
||||
resizeThumbnails();
|
||||
|
||||
updateLargeVideo(videoelem.attr('src'), 1);
|
||||
if (!focusedVideoSrc)
|
||||
updateLargeVideo(videoelem.attr('src'), 1);
|
||||
|
||||
showFocusIndicator();
|
||||
}
|
||||
|
@ -551,7 +627,7 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) {
|
|||
var directions = {};
|
||||
var localSDP = new SDP(sess.peerconnection.localDescription.sdp);
|
||||
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:')) {
|
||||
// assumes a single local ssrc
|
||||
|
@ -647,6 +723,16 @@ $(document).bind('left.muc', function (event, jid) {
|
|||
}
|
||||
}, 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);
|
||||
|
||||
if (focus == null
|
||||
|
|
|
@ -12,5 +12,6 @@ var config = {
|
|||
desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
|
||||
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer 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
|
||||
};
|
|
@ -52,6 +52,10 @@
|
|||
z-index: 3;
|
||||
}
|
||||
|
||||
#remoteVideos .videocontainer.videoContainerFocused {
|
||||
border: 3px solid #388396;
|
||||
}
|
||||
|
||||
#localVideoWrapper {
|
||||
display:inline-block;
|
||||
-webkit-mask-box-image: url(../images/videomask.svg);
|
||||
|
|
|
@ -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);
|
||||
};*/
|
||||
}
|
|
@ -24,6 +24,7 @@
|
|||
<script src="muc.js?v=10"></script><!-- simple MUC library -->
|
||||
<script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
|
||||
<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="chat.js?v=4"></script><!-- chat logic -->
|
||||
<script src="util.js?v=3"></script><!-- utility functions -->
|
||||
|
|
|
@ -44,8 +44,27 @@ function ColibriFocus(connection, bridgejid) {
|
|||
this.peers = [];
|
||||
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
|
||||
this.media = ['audio', 'video'];
|
||||
if (config.openSctp)
|
||||
{
|
||||
this.media = ['audio', 'video', 'data'];
|
||||
}
|
||||
else
|
||||
{
|
||||
this.media = ['audio', 'video'];
|
||||
}
|
||||
|
||||
this.connection.jingle.sessions[this.sid] = this;
|
||||
this.mychannel = [];
|
||||
|
@ -151,17 +170,29 @@ ColibriFocus.prototype._makeConference = function () {
|
|||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
|
||||
|
||||
this.media.forEach(function (name) {
|
||||
var isData = name === 'data';
|
||||
var channel = isData ? 'sctpconnection' : 'channel';
|
||||
|
||||
elem.c('content', {name: name});
|
||||
elem.c('channel', {
|
||||
|
||||
elem.c(channel, {
|
||||
initiator: 'true',
|
||||
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++) {
|
||||
elem.c('channel', {
|
||||
elem.c(channel, {
|
||||
initiator: 'true',
|
||||
expire: '15',
|
||||
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
|
||||
});
|
||||
|
@ -209,8 +240,13 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
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++) {
|
||||
tmp = $(remotecontents[i]).find('>channel').get();
|
||||
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++) {
|
||||
|
@ -223,7 +259,55 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
|
||||
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;
|
||||
var channel;
|
||||
/*
|
||||
|
@ -262,12 +346,17 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
// get the mixed ssrc
|
||||
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
||||
// 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') + ' ' + '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') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
|
||||
} else {
|
||||
}
|
||||
else if (!isData)
|
||||
{
|
||||
// make chrome happy... '3735928559' == 0xDEADBEEF
|
||||
// FIXME: this currently appears as two streams, should be one
|
||||
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
|
||||
|
@ -308,21 +397,41 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
|
||||
var localSDP = new SDP(self.peerconnection.localDescription.sdp);
|
||||
localSDP.media.forEach(function (media, channel) {
|
||||
var name = SDPUtil.parse_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('channel', {
|
||||
initiator: 'true',
|
||||
expire: '15',
|
||||
id: self.mychannel[channel].attr('id'),
|
||||
endpoint: 'fix_me_focus_endpoint'
|
||||
});
|
||||
|
||||
// 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();
|
||||
if (name !== 'data')
|
||||
{
|
||||
elem.c('channel', {
|
||||
initiator: 'true',
|
||||
expire: self.channelExpire,
|
||||
id: self.mychannel[channel].attr('id'),
|
||||
endpoint: self.myMucResource
|
||||
});
|
||||
|
||||
// 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,
|
||||
endpoint: self.myMucResource,
|
||||
port: sctpPort
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
localSDP.TransportToJingle(channel, elem);
|
||||
|
@ -336,7 +445,9 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
// ...
|
||||
},
|
||||
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') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\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
|
||||
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\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
|
||||
ColibriFocus.prototype.addNewParticipant = function (peer) {
|
||||
var self = this;
|
||||
if (this.confid === 0) {
|
||||
if (this.confid === 0 || !this.peerconnection.localDescription)
|
||||
{
|
||||
// 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 () {
|
||||
self.addNewParticipant(peer);
|
||||
}, 250);
|
||||
|
@ -506,14 +628,26 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
|
|||
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_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('channel', {
|
||||
if (name !== 'data')
|
||||
{
|
||||
elem.c('channel', {
|
||||
initiator: 'true',
|
||||
expire:'15',
|
||||
expire: self.channelExpire,
|
||||
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
|
||||
});
|
||||
|
||||
|
@ -521,7 +655,15 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
|
|||
function (result) {
|
||||
var contents = $(result).find('>conference>content').get();
|
||||
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.initiate(peer, true);
|
||||
|
@ -535,37 +677,52 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
|
|||
// update the channel description (payload-types + dtls fp) for a participant
|
||||
ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
|
||||
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++) {
|
||||
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
|
||||
change.c('channel', {
|
||||
id: $(this.channels[participant][channel]).attr('id'),
|
||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||
expire: '15'
|
||||
});
|
||||
for (channel = 0; channel < this.channels[participant].length; channel++)
|
||||
{
|
||||
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
|
||||
});
|
||||
|
||||
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();
|
||||
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();
|
||||
});
|
||||
*/
|
||||
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
|
||||
remoteSDP.TransportToJingle(channel, change);
|
||||
|
||||
change.up(); // end of channel
|
||||
change.up(); // end of channel/sctpconnection
|
||||
change.up(); // end of content
|
||||
}
|
||||
this.connection.sendIQ(change,
|
||||
|
@ -679,8 +836,11 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
|
|||
this.remotessrc[session.peerjid] = [];
|
||||
for (channel = 0; channel < this.channels[participant].length; channel++) {
|
||||
//if (channel == 0) continue; FIXME: does not work as intended
|
||||
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';
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -706,14 +866,27 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
|
|||
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||
$(elem).each(function () {
|
||||
var name = $(this).attr('name');
|
||||
|
||||
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('channel', {
|
||||
id: $(self.channels[participant][channel]).attr('id'),
|
||||
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
|
||||
expire: '15'
|
||||
});
|
||||
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', {
|
||||
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
|
||||
expire: self.channelExpire
|
||||
});
|
||||
}
|
||||
$(this).find('>transport').each(function () {
|
||||
change.c('transport', {
|
||||
ufrag: $(this).attr('ufrag'),
|
||||
|
@ -733,7 +906,7 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
|
|||
});
|
||||
change.up(); // end of transport
|
||||
});
|
||||
change.up(); // end of channel
|
||||
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
|
||||
|
@ -773,21 +946,35 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) {
|
|||
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++) {
|
||||
for (var mid = 0; mid < localSDP.media.length; mid++)
|
||||
{
|
||||
var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
|
||||
if (cands.length > 0) {
|
||||
mycands.c('content', {name: cands[0].sdpMid });
|
||||
mycands.c('channel', {
|
||||
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
|
||||
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
|
||||
expire: '15'
|
||||
});
|
||||
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', {
|
||||
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'});
|
||||
for (var i = 0; i < cands.length; i++) {
|
||||
mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
|
||||
}
|
||||
mycands.up(); // transport
|
||||
mycands.up(); // channel
|
||||
mycands.up(); // channel / sctpconnection
|
||||
mycands.up(); // content
|
||||
}
|
||||
}
|
||||
|
@ -818,13 +1005,26 @@ ColibriFocus.prototype.terminate = function (session, reason) {
|
|||
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++) {
|
||||
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
|
||||
change.c('channel', {
|
||||
id: $(this.channels[participant][channel]).attr('id'),
|
||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||
expire: '0'
|
||||
});
|
||||
change.up(); // end of 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', {
|
||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||
expire: '0'
|
||||
});
|
||||
}
|
||||
change.up(); // end of channel/sctpconnection
|
||||
change.up(); // end of content
|
||||
}
|
||||
this.connection.sendIQ(change,
|
||||
|
|
|
@ -32,7 +32,7 @@ function TraceablePeerConnection(ice_config, constraints) {
|
|||
this.switchstreams = false;
|
||||
|
||||
// override as desired
|
||||
this.trace = function(what, info) {
|
||||
this.trace = function (what, info) {
|
||||
//console.warn('WTRACE', what, info);
|
||||
self.updateLog.push({
|
||||
time: new Date(),
|
||||
|
@ -144,8 +144,8 @@ TraceablePeerConnection.prototype.removeStream = function (stream) {
|
|||
|
||||
TraceablePeerConnection.prototype.createDataChannel = function (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) {
|
||||
var self = this;
|
||||
|
|
|
@ -155,7 +155,10 @@ SDP.prototype.toJingle = function (elem, thecreator) {
|
|||
}
|
||||
for (i = 0; i < this.media.length; i++) {
|
||||
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;
|
||||
}
|
||||
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
|
||||
|
@ -171,12 +174,14 @@ SDP.prototype.toJingle = function (elem, thecreator) {
|
|||
elem.attrs({ name: mid });
|
||||
|
||||
// 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();
|
||||
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',
|
||||
{xmlns: 'urn:xmpp:jingle:apps:rtp:1',
|
||||
media: mline.media });
|
||||
|
@ -304,6 +309,26 @@ SDP.prototype.TransportToJingle = function (mediaindex, elem) {
|
|||
var self = this;
|
||||
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
|
||||
var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
|
||||
fingerprints.forEach(function(line) {
|
||||
|
@ -438,6 +463,8 @@ SDP.prototype.jingle2media = function (content) {
|
|||
ssrc = desc.attr('ssrc'),
|
||||
self = this,
|
||||
tmp;
|
||||
var sctp = content.find(
|
||||
'>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
|
||||
|
||||
tmp = { media: desc.attr('media') };
|
||||
tmp.port = '1';
|
||||
|
@ -446,14 +473,35 @@ SDP.prototype.jingle2media = function (content) {
|
|||
tmp.port = '0';
|
||||
}
|
||||
if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
|
||||
tmp.proto = 'RTP/SAVPF';
|
||||
if (sctp.length)
|
||||
tmp.proto = 'DTLS/SCTP';
|
||||
else
|
||||
tmp.proto = 'RTP/SAVPF';
|
||||
} else {
|
||||
tmp.proto = 'RTP/AVPF';
|
||||
}
|
||||
tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get();
|
||||
media += SDPUtil.build_mline(tmp) + '\r\n';
|
||||
if (!sctp.length)
|
||||
{
|
||||
tmp.fmt = desc.find('payload-type').map(
|
||||
function () { return this.getAttribute('id'); }).get();
|
||||
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 += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
|
||||
if (!sctp.length)
|
||||
media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
|
||||
tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
|
||||
if (tmp.length) {
|
||||
if (tmp.attr('ufrag')) {
|
||||
|
|
|
@ -90,6 +90,20 @@ SDPUtil = {
|
|||
data.channels = parts.length ? parts.shift() : '1';
|
||||
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) {
|
||||
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
|
||||
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
|
||||
|
|
Loading…
Reference in New Issue