Merge branch 'master' of github.com:jitsi/jitsi-meet
Conflicts: app.js index.html libs/colibri/colibri.focus.js libs/modules/statistics.bundle.js moderator.js modules/UI/videolayout/VideoLayout.js muc.js
This commit is contained in:
commit
6ce48a5b7b
2
Makefile
2
Makefile
|
@ -16,7 +16,7 @@ $(MODULES): *.js
|
|||
$(BROWSERIFY) $(FLAGS) $(MODULE_DIR)/$@/$@.js -s $@ -o $(OUTPUT_DIR)/$@.bundle.js
|
||||
|
||||
clean:
|
||||
@rm $(OUTPUT_DIR)/*.bundle.js
|
||||
@rm -f $(OUTPUT_DIR)/*.bundle.js
|
||||
|
||||
deploy:
|
||||
@mkdir -p $(DEPLOY_DIR) && cp $(OUTPUT_DIR)/*.bundle.js $(DEPLOY_DIR)
|
||||
|
|
34
app.js
34
app.js
|
@ -2,6 +2,10 @@
|
|||
/* application specific logic */
|
||||
var connection = null;
|
||||
var authenticatedUser = false;
|
||||
/* Initial "authentication required" dialog */
|
||||
var authDialog = null;
|
||||
/* Loop retry ID that wits for other user to create the room */
|
||||
var authRetryId = null;
|
||||
var activecall = null;
|
||||
var nickname = null;
|
||||
var focusMucJid = null;
|
||||
|
@ -57,6 +61,7 @@ var sessionTerminated = false;
|
|||
|
||||
function init() {
|
||||
|
||||
|
||||
RTC.addStreamListener(maybeDoJoin, StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
|
||||
RTC.start();
|
||||
|
||||
|
@ -150,6 +155,17 @@ function doJoin() {
|
|||
|
||||
function doJoinAfterFocus() {
|
||||
|
||||
// Close authentication dialog if opened
|
||||
if (authDialog) {
|
||||
messageHandler.closeDialog();
|
||||
authDialog = null;
|
||||
}
|
||||
// Clear retry interval, so that we don't call 'doJoinAfterFocus' twice
|
||||
if (authRetryId) {
|
||||
window.clearTimeout(authRetryId);
|
||||
authRetryId = null;
|
||||
}
|
||||
|
||||
var roomjid;
|
||||
roomjid = roomName;
|
||||
|
||||
|
@ -999,21 +1015,15 @@ function hangup() {
|
|||
|
||||
}
|
||||
|
||||
$.prompt("Session Terminated",
|
||||
{
|
||||
title: "You hung up the call",
|
||||
persistent: true,
|
||||
buttons: {
|
||||
"Join again": true
|
||||
},
|
||||
closeText: '',
|
||||
submit: function(event, value, message, formVals)
|
||||
messageHandler.openDialog(
|
||||
"Session Terminated",
|
||||
"You hung up the call",
|
||||
true,
|
||||
{ "Join again": true },
|
||||
function(event, value, message, formVals)
|
||||
{
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ var config = {
|
|||
//anonymousdomain: 'guest.example.com',
|
||||
muc: 'conference.jitsi-meet.example.com', // FIXME: use XEP-0030
|
||||
bridge: 'jitsi-videobridge.jitsi-meet.example.com', // FIXME: use XEP-0030
|
||||
//jirecon: 'jirecon.jitsi-meet.example.com',
|
||||
//call_control: 'callcontrol.jitsi-meet.example.com',
|
||||
//focus: 'focus.jitsi-meet.example.com' - defaults to 'focus.jitsi-meet.example.com'
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*.html /usr/share/jitsi-meet/
|
||||
*.ico /usr/share/jitsi-meet/
|
||||
libs /usr/share/jitsi-meet/
|
||||
service /usr/share/jitsi-meet/
|
||||
css /usr/share/jitsi-meet/
|
||||
sounds /usr/share/jitsi-meet/
|
||||
fonts /usr/share/jitsi-meet/
|
||||
|
|
|
@ -12,6 +12,9 @@
|
|||
%:
|
||||
dh $@
|
||||
|
||||
# we skip making Makefile exists for updating browserify modules when developing
|
||||
override_dh_auto_build:
|
||||
|
||||
override_dh_install:
|
||||
dh_installdirs
|
||||
dh_install -X/config.js
|
||||
|
|
12
index.html
12
index.html
|
@ -17,13 +17,9 @@
|
|||
<script src="libs/strophe/strophe.disco.min.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.caps.jsonly.min.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.jingle.js?v=2"></script>
|
||||
<script src="libs/strophe/strophe.jingle.sdp.js?v=2"></script>
|
||||
<script src="libs/strophe/strophe.jingle.sdp.util.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.jingle.sessionbase.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.jingle.session.js?v=2"></script>
|
||||
<script src="libs/strophe/strophe.jingle.sdp.js?v=4"></script>
|
||||
<script src="libs/strophe/strophe.jingle.session.js?v=4"></script>
|
||||
<script src="libs/strophe/strophe.util.js"></script>
|
||||
<script src="libs/colibri/colibri.focus.js?v=12"></script><!-- colibri focus implementation -->
|
||||
<script src="libs/colibri/colibri.session.js?v=1"></script>
|
||||
<script src="libs/jquery-ui.js"></script>
|
||||
<script src="libs/rayo.js?v=1"></script>
|
||||
<script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||
|
@ -40,7 +36,7 @@
|
|||
<script src="muc.js?v=17"></script><!-- simple MUC library -->
|
||||
<script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
|
||||
<script src="desktopsharing.js?v=3"></script><!-- desktop sharing -->
|
||||
<script src="app.js?v=22"></script><!-- application logic -->
|
||||
<script src="app.js?v=24"></script><!-- application logic -->
|
||||
<script src="util.js?v=7"></script><!-- utility functions -->
|
||||
<script src="moderatemuc.js?v=4"></script><!-- moderator plugin -->
|
||||
<script src="analytics.js?v=1"></script><!-- google analytics plugin -->
|
||||
|
@ -48,7 +44,7 @@
|
|||
<script src="moderator.js?v=2"></script><!-- media stream -->
|
||||
<script src="tracking.js?v=1"></script><!-- tracking -->
|
||||
<script src="api_connector.js?v=2"></script>
|
||||
<script src="keyboard_shortcut.js?v=1"></script>
|
||||
<script src="keyboard_shortcut.js?v=3"></script>
|
||||
<link rel="stylesheet" href="css/font.css?v=6"/>
|
||||
<link rel="stylesheet" href="css/toastr.css?v=1">
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=30"/>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,94 +0,0 @@
|
|||
/* 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.
|
||||
*/
|
||||
// A colibri session is similar to a jingle session, it just implements some things differently
|
||||
// FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
|
||||
function ColibriSession(me, sid, connection) {
|
||||
this.me = me;
|
||||
this.sid = sid;
|
||||
this.connection = connection;
|
||||
//this.peerconnection = null;
|
||||
//this.mychannel = null;
|
||||
//this.channels = null;
|
||||
this.peerjid = null;
|
||||
|
||||
this.colibri = null;
|
||||
}
|
||||
|
||||
// implementation of JingleSession interface
|
||||
ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
|
||||
this.peerjid = peerjid;
|
||||
};
|
||||
|
||||
ColibriSession.prototype.sendOffer = function (offer) {
|
||||
console.log('ColibriSession.sendOffer');
|
||||
};
|
||||
|
||||
|
||||
ColibriSession.prototype.accept = function () {
|
||||
console.log('ColibriSession.accept');
|
||||
};
|
||||
|
||||
ColibriSession.prototype.addSource = function (elem, fromJid) {
|
||||
this.colibri.addSource(elem, fromJid);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.removeSource = function (elem, fromJid) {
|
||||
this.colibri.removeSource(elem, fromJid);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.terminate = function (reason) {
|
||||
this.colibri.terminate(this, reason);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.active = function () {
|
||||
console.log('ColibriSession.active');
|
||||
};
|
||||
|
||||
ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
|
||||
this.colibri.setRemoteDescription(this, elem, desctype);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.addIceCandidate = function (elem) {
|
||||
this.colibri.addIceCandidate(this, elem);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
|
||||
console.log('ColibriSession.sendAnswer');
|
||||
};
|
||||
|
||||
ColibriSession.prototype.sendTerminate = function (reason, text) {
|
||||
this.colibri.sendTerminate(this, reason, text);
|
||||
};
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -24,7 +24,7 @@ Strophe.addConnectionPlugin('rayo',
|
|||
var req = $iq(
|
||||
{
|
||||
type: 'set',
|
||||
to: config.hosts.call_control
|
||||
to: focusMucJid
|
||||
}
|
||||
);
|
||||
req.c('dial',
|
||||
|
|
|
@ -7,30 +7,6 @@ function TraceablePeerConnection(ice_config, constraints) {
|
|||
this.statsinterval = null;
|
||||
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
||||
|
||||
/**
|
||||
* Array of ssrcs that will be added on next modifySources call.
|
||||
* @type {Array}
|
||||
*/
|
||||
this.addssrc = [];
|
||||
/**
|
||||
* Array of ssrcs that will be added on next modifySources call.
|
||||
* @type {Array}
|
||||
*/
|
||||
this.removessrc = [];
|
||||
/**
|
||||
* Pending operation that will be done during modifySources call.
|
||||
* Currently 'mute'/'unmute' operations are supported.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.pendingop = null;
|
||||
|
||||
/**
|
||||
* Flag indicates that peer connection stream have changed and modifySources should proceed.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.switchstreams = false;
|
||||
|
||||
// override as desired
|
||||
this.trace = function (what, info) {
|
||||
//console.warn('WTRACE', what, info);
|
||||
|
@ -213,232 +189,6 @@ TraceablePeerConnection.prototype.setRemoteDescription = function (description,
|
|||
*/
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.hardMuteVideo = function (muted) {
|
||||
this.pendingop = muted ? 'mute' : 'unmute';
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.enqueueAddSsrc = function(channel, ssrcLines) {
|
||||
if (!this.addssrc[channel]) {
|
||||
this.addssrc[channel] = '';
|
||||
}
|
||||
this.addssrc[channel] += ssrcLines;
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.addSource = function (elem) {
|
||||
console.log('addssrc', new Date().getTime());
|
||||
console.log('ice', this.iceConnectionState);
|
||||
var sdp = new SDP(this.remoteDescription.sdp);
|
||||
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
|
||||
|
||||
var self = this;
|
||||
$(elem).each(function (idx, content) {
|
||||
var name = $(content).attr('name');
|
||||
var lines = '';
|
||||
tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
|
||||
var semantics = this.getAttribute('semantics');
|
||||
var ssrcs = $(this).find('>source').map(function () {
|
||||
return this.getAttribute('ssrc');
|
||||
}).get();
|
||||
|
||||
if (ssrcs.length != 0) {
|
||||
lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
|
||||
}
|
||||
});
|
||||
tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
|
||||
tmp.each(function () {
|
||||
var ssrc = $(this).attr('ssrc');
|
||||
if(mySdp.containsSSRC(ssrc)){
|
||||
/**
|
||||
* This happens when multiple participants change their streams at the same time and
|
||||
* ColibriFocus.modifySources have to wait for stable state. In the meantime multiple
|
||||
* addssrc are scheduled for update IQ. See
|
||||
*/
|
||||
console.warn("Got add stream request for my own ssrc: "+ssrc);
|
||||
return;
|
||||
}
|
||||
$(this).find('>parameter').each(function () {
|
||||
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
||||
if ($(this).attr('value') && $(this).attr('value').length)
|
||||
lines += ':' + $(this).attr('value');
|
||||
lines += '\r\n';
|
||||
});
|
||||
});
|
||||
sdp.media.forEach(function(media, idx) {
|
||||
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
||||
return;
|
||||
sdp.media[idx] += lines;
|
||||
self.enqueueAddSsrc(idx, lines);
|
||||
});
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
});
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.enqueueRemoveSsrc = function(channel, ssrcLines) {
|
||||
if (!this.removessrc[channel]){
|
||||
this.removessrc[channel] = '';
|
||||
}
|
||||
this.removessrc[channel] += ssrcLines;
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.removeSource = function (elem) {
|
||||
console.log('removessrc', new Date().getTime());
|
||||
console.log('ice', this.iceConnectionState);
|
||||
var sdp = new SDP(this.remoteDescription.sdp);
|
||||
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
|
||||
|
||||
var self = this;
|
||||
$(elem).each(function (idx, content) {
|
||||
var name = $(content).attr('name');
|
||||
var lines = '';
|
||||
tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
|
||||
var semantics = this.getAttribute('semantics');
|
||||
var ssrcs = $(this).find('>source').map(function () {
|
||||
return this.getAttribute('ssrc');
|
||||
}).get();
|
||||
|
||||
if (ssrcs.length != 0) {
|
||||
lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
|
||||
}
|
||||
});
|
||||
tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
|
||||
tmp.each(function () {
|
||||
var ssrc = $(this).attr('ssrc');
|
||||
// This should never happen, but can be useful for bug detection
|
||||
if(mySdp.containsSSRC(ssrc)){
|
||||
console.error("Got remove stream request for my own ssrc: "+ssrc);
|
||||
return;
|
||||
}
|
||||
$(this).find('>parameter').each(function () {
|
||||
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
||||
if ($(this).attr('value') && $(this).attr('value').length)
|
||||
lines += ':' + $(this).attr('value');
|
||||
lines += '\r\n';
|
||||
});
|
||||
});
|
||||
sdp.media.forEach(function(media, idx) {
|
||||
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
||||
return;
|
||||
sdp.media[idx] += lines;
|
||||
self.enqueueRemoveSsrc(idx, lines);
|
||||
});
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
});
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.modifySources = function(successCallback) {
|
||||
var self = this;
|
||||
if (this.signalingState == 'closed') return;
|
||||
if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){
|
||||
// There is nothing to do since scheduled job might have been executed by another succeeding call
|
||||
if(successCallback){
|
||||
successCallback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: this is a big hack
|
||||
// https://code.google.com/p/webrtc/issues/detail?id=2688
|
||||
// ^ has been fixed.
|
||||
if (!(this.signalingState == 'stable' && this.iceConnectionState == 'connected')) {
|
||||
console.warn('modifySources not yet', this.signalingState, this.iceConnectionState);
|
||||
this.wait = true;
|
||||
window.setTimeout(function() { self.modifySources(successCallback); }, 250);
|
||||
return;
|
||||
}
|
||||
if (this.wait) {
|
||||
window.setTimeout(function() { self.modifySources(successCallback); }, 2500);
|
||||
this.wait = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset switch streams flag
|
||||
this.switchstreams = false;
|
||||
|
||||
var sdp = new SDP(this.remoteDescription.sdp);
|
||||
|
||||
// add sources
|
||||
this.addssrc.forEach(function(lines, idx) {
|
||||
sdp.media[idx] += lines;
|
||||
});
|
||||
this.addssrc = [];
|
||||
|
||||
// remove sources
|
||||
this.removessrc.forEach(function(lines, idx) {
|
||||
lines = lines.split('\r\n');
|
||||
lines.pop(); // remove empty last element;
|
||||
lines.forEach(function(line) {
|
||||
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
|
||||
});
|
||||
});
|
||||
this.removessrc = [];
|
||||
|
||||
// FIXME:
|
||||
// this was a hack for the situation when only one peer exists
|
||||
// in the conference.
|
||||
// check if still required and remove
|
||||
if (sdp.media[0])
|
||||
sdp.media[0] = sdp.media[0].replace('a=recvonly', 'a=sendrecv');
|
||||
if (sdp.media[1])
|
||||
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
|
||||
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
this.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
|
||||
function() {
|
||||
|
||||
if(self.signalingState == 'closed') {
|
||||
console.error("createAnswer attempt on closed state");
|
||||
return;
|
||||
}
|
||||
|
||||
self.createAnswer(
|
||||
function(modifiedAnswer) {
|
||||
// change video direction, see https://github.com/jitsi/jitmeet/issues/41
|
||||
if (self.pendingop !== null) {
|
||||
var sdp = new SDP(modifiedAnswer.sdp);
|
||||
if (sdp.media.length > 1) {
|
||||
switch(self.pendingop) {
|
||||
case 'mute':
|
||||
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
|
||||
break;
|
||||
case 'unmute':
|
||||
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
|
||||
break;
|
||||
}
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
modifiedAnswer.sdp = sdp.raw;
|
||||
}
|
||||
self.pendingop = null;
|
||||
}
|
||||
|
||||
// FIXME: pushing down an answer while ice connection state
|
||||
// is still checking is bad...
|
||||
//console.log(self.peerconnection.iceConnectionState);
|
||||
|
||||
// trying to work around another chrome bug
|
||||
//modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
|
||||
self.setLocalDescription(modifiedAnswer,
|
||||
function() {
|
||||
//console.log('modified setLocalDescription ok');
|
||||
if(successCallback){
|
||||
successCallback();
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
console.error('modified setLocalDescription failed', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(error) {
|
||||
console.error('modified answer failed', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(error) {
|
||||
console.error('modify failed', error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.close = function () {
|
||||
this.trace('stop');
|
||||
if (this.statsinterval !== null) {
|
||||
|
|
|
@ -17,27 +17,37 @@ function SDP(sdp) {
|
|||
SDP.prototype.getMediaSsrcMap = function() {
|
||||
var self = this;
|
||||
var media_ssrcs = {};
|
||||
for (channelNum = 0; channelNum < self.media.length; channelNum++) {
|
||||
modified = true;
|
||||
tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc:');
|
||||
var type = SDPUtil.parse_mid(SDPUtil.find_line(self.media[channelNum], 'a=mid:'));
|
||||
var channel = new MediaChannel(channelNum, type);
|
||||
media_ssrcs[channelNum] = channel;
|
||||
var tmp;
|
||||
for (var mediaindex = 0; mediaindex < self.media.length; mediaindex++) {
|
||||
tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc:');
|
||||
var mid = SDPUtil.parse_mid(SDPUtil.find_line(self.media[mediaindex], 'a=mid:'));
|
||||
var media = {
|
||||
mediaindex: mediaindex,
|
||||
mid: mid,
|
||||
ssrcs: {},
|
||||
ssrcGroups: []
|
||||
};
|
||||
media_ssrcs[mediaindex] = media;
|
||||
tmp.forEach(function (line) {
|
||||
var linessrc = line.substring(7).split(' ')[0];
|
||||
// allocate new ChannelSsrc
|
||||
if(!channel.ssrcs[linessrc]) {
|
||||
channel.ssrcs[linessrc] = new ChannelSsrc(linessrc, type);
|
||||
if(!media.ssrcs[linessrc]) {
|
||||
media.ssrcs[linessrc] = {
|
||||
ssrc: linessrc,
|
||||
lines: []
|
||||
};
|
||||
}
|
||||
channel.ssrcs[linessrc].lines.push(line);
|
||||
media.ssrcs[linessrc].lines.push(line);
|
||||
});
|
||||
tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc-group:');
|
||||
tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc-group:');
|
||||
tmp.forEach(function(line){
|
||||
var semantics = line.substr(0, idx).substr(13);
|
||||
var ssrcs = line.substr(14 + semantics.length).split(' ');
|
||||
if (ssrcs.length != 0) {
|
||||
var ssrcGroup = new ChannelSsrcGroup(semantics, ssrcs);
|
||||
channel.ssrcGroups.push(ssrcGroup);
|
||||
media.ssrcGroups.push({
|
||||
semantics: semantics,
|
||||
ssrcs: ssrcs
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -49,23 +59,28 @@ SDP.prototype.getMediaSsrcMap = function() {
|
|||
* @returns {boolean} <tt>true</tt> if this SDP contains given SSRC.
|
||||
*/
|
||||
SDP.prototype.containsSSRC = function(ssrc) {
|
||||
var channels = this.getMediaSsrcMap();
|
||||
var medias = this.getMediaSsrcMap();
|
||||
var contains = false;
|
||||
Object.keys(channels).forEach(function(chNumber){
|
||||
var channel = channels[chNumber];
|
||||
Object.keys(medias).forEach(function(mediaindex){
|
||||
var media = medias[mediaindex];
|
||||
//console.log("Check", channel, ssrc);
|
||||
if(Object.keys(channel.ssrcs).indexOf(ssrc) != -1){
|
||||
if(Object.keys(media.ssrcs).indexOf(ssrc) != -1){
|
||||
contains = true;
|
||||
}
|
||||
});
|
||||
return contains;
|
||||
};
|
||||
|
||||
function SDPDiffer(mySDP, otherSDP) {
|
||||
this.mySDP = mySDP;
|
||||
this.otherSDP = otherSDP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns map of MediaChannel that contains only media not contained in <tt>otherSdp</tt>. Mapped by channel idx.
|
||||
* @param otherSdp the other SDP to check ssrc with.
|
||||
*/
|
||||
SDP.prototype.getNewMedia = function(otherSdp) {
|
||||
SDPDiffer.prototype.getNewMedia = function() {
|
||||
|
||||
// this could be useful in Array.prototype.
|
||||
function arrayEquals(array) {
|
||||
|
@ -92,35 +107,40 @@ SDP.prototype.getNewMedia = function(otherSdp) {
|
|||
return true;
|
||||
}
|
||||
|
||||
var myMedia = this.getMediaSsrcMap();
|
||||
var othersMedia = otherSdp.getMediaSsrcMap();
|
||||
var myMedias = this.mySDP.getMediaSsrcMap();
|
||||
var othersMedias = this.otherSDP.getMediaSsrcMap();
|
||||
var newMedia = {};
|
||||
Object.keys(othersMedia).forEach(function(channelNum) {
|
||||
var myChannel = myMedia[channelNum];
|
||||
var othersChannel = othersMedia[channelNum];
|
||||
if(!myChannel && othersChannel) {
|
||||
Object.keys(othersMedias).forEach(function(othersMediaIdx) {
|
||||
var myMedia = myMedias[othersMediaIdx];
|
||||
var othersMedia = othersMedias[othersMediaIdx];
|
||||
if(!myMedia && othersMedia) {
|
||||
// Add whole channel
|
||||
newMedia[channelNum] = othersChannel;
|
||||
newMedia[othersMediaIdx] = othersMedia;
|
||||
return;
|
||||
}
|
||||
// Look for new ssrcs accross the channel
|
||||
Object.keys(othersChannel.ssrcs).forEach(function(ssrc) {
|
||||
if(Object.keys(myChannel.ssrcs).indexOf(ssrc) === -1) {
|
||||
Object.keys(othersMedia.ssrcs).forEach(function(ssrc) {
|
||||
if(Object.keys(myMedia.ssrcs).indexOf(ssrc) === -1) {
|
||||
// Allocate channel if we've found ssrc that doesn't exist in our channel
|
||||
if(!newMedia[channelNum]){
|
||||
newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType);
|
||||
if(!newMedia[othersMediaIdx]){
|
||||
newMedia[othersMediaIdx] = {
|
||||
mediaindex: othersMedia.mediaindex,
|
||||
mid: othersMedia.mid,
|
||||
ssrcs: {},
|
||||
ssrcGroups: []
|
||||
};
|
||||
}
|
||||
newMedia[channelNum].ssrcs[ssrc] = othersChannel.ssrcs[ssrc];
|
||||
newMedia[othersMediaIdx].ssrcs[ssrc] = othersMedia.ssrcs[ssrc];
|
||||
}
|
||||
});
|
||||
|
||||
// Look for new ssrc groups across the channels
|
||||
othersChannel.ssrcGroups.forEach(function(otherSsrcGroup){
|
||||
othersMedia.ssrcGroups.forEach(function(otherSsrcGroup){
|
||||
|
||||
// try to match the other ssrc-group with an ssrc-group of ours
|
||||
var matched = false;
|
||||
for (var i = 0; i < myChannel.ssrcGroups.length; i++) {
|
||||
var mySsrcGroup = myChannel.ssrcGroups[i];
|
||||
for (var i = 0; i < myMedia.ssrcGroups.length; i++) {
|
||||
var mySsrcGroup = myMedia.ssrcGroups[i];
|
||||
if (otherSsrcGroup.semantics == mySsrcGroup.semantics
|
||||
&& arrayEquals.apply(otherSsrcGroup.ssrcs, [mySsrcGroup.ssrcs])) {
|
||||
|
||||
|
@ -133,16 +153,88 @@ SDP.prototype.getNewMedia = function(otherSdp) {
|
|||
// Allocate channel if we've found an ssrc-group that doesn't
|
||||
// exist in our channel
|
||||
|
||||
if(!newMedia[channelNum]){
|
||||
newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType);
|
||||
if(!newMedia[othersMediaIdx]){
|
||||
newMedia[othersMediaIdx] = {
|
||||
mediaindex: othersMedia.mediaindex,
|
||||
mid: othersMedia.mid,
|
||||
ssrcs: {},
|
||||
ssrcGroups: []
|
||||
};
|
||||
}
|
||||
newMedia[channelNum].ssrcGroups.push(otherSsrcGroup);
|
||||
newMedia[othersMediaIdx].ssrcGroups.push(otherSsrcGroup);
|
||||
}
|
||||
});
|
||||
});
|
||||
return newMedia;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends SSRC update IQ.
|
||||
* @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove.
|
||||
* @param sid session identifier that will be put into the IQ.
|
||||
* @param initiator initiator identifier.
|
||||
* @param toJid destination Jid
|
||||
* @param isAdd indicates if this is remove or add operation.
|
||||
*/
|
||||
SDPDiffer.prototype.toJingle = function(modify) {
|
||||
var sdpMediaSsrcs = this.getNewMedia();
|
||||
var self = this;
|
||||
|
||||
// FIXME: only announce video ssrcs since we mix audio and dont need
|
||||
// the audio ssrcs therefore
|
||||
var modified = false;
|
||||
Object.keys(sdpMediaSsrcs).forEach(function(mediaindex){
|
||||
modified = true;
|
||||
var media = sdpMediaSsrcs[mediaindex];
|
||||
modify.c('content', {name: media.mid});
|
||||
|
||||
modify.c('description', {xmlns:'urn:xmpp:jingle:apps:rtp:1', media: media.mid});
|
||||
// FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
|
||||
// generate sources from lines
|
||||
Object.keys(media.ssrcs).forEach(function(ssrcNum) {
|
||||
var mediaSsrc = media.ssrcs[ssrcNum];
|
||||
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
||||
modify.attrs({ssrc: mediaSsrc.ssrc});
|
||||
// iterate over ssrc lines
|
||||
mediaSsrc.lines.forEach(function (line) {
|
||||
var idx = line.indexOf(' ');
|
||||
var kv = line.substr(idx + 1);
|
||||
modify.c('parameter');
|
||||
if (kv.indexOf(':') == -1) {
|
||||
modify.attrs({ name: kv });
|
||||
} else {
|
||||
modify.attrs({ name: kv.split(':', 2)[0] });
|
||||
modify.attrs({ value: kv.split(':', 2)[1] });
|
||||
}
|
||||
modify.up(); // end of parameter
|
||||
});
|
||||
modify.up(); // end of source
|
||||
});
|
||||
|
||||
// generate source groups from lines
|
||||
media.ssrcGroups.forEach(function(ssrcGroup) {
|
||||
if (ssrcGroup.ssrcs.length != 0) {
|
||||
|
||||
modify.c('ssrc-group', {
|
||||
semantics: ssrcGroup.semantics,
|
||||
xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
|
||||
});
|
||||
|
||||
ssrcGroup.ssrcs.forEach(function (ssrc) {
|
||||
modify.c('source', { ssrc: ssrc })
|
||||
.up(); // end of source
|
||||
});
|
||||
modify.up(); // end of ssrc-group
|
||||
}
|
||||
});
|
||||
|
||||
modify.up(); // end of description
|
||||
modify.up(); // end of content
|
||||
});
|
||||
|
||||
return modified;
|
||||
};
|
||||
|
||||
// remove iSAC and CN from SDP
|
||||
SDP.prototype.mangle = function () {
|
||||
var i, j, mline, lines, rtpmap, newdesc;
|
||||
|
@ -683,3 +775,353 @@ SDP.prototype.jingle2media = function (content) {
|
|||
|
||||
return media;
|
||||
};
|
||||
|
||||
SDPUtil = {
|
||||
iceparams: function (mediadesc, sessiondesc) {
|
||||
var data = null;
|
||||
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
|
||||
SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
|
||||
data = {
|
||||
ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
|
||||
pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
|
||||
};
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_iceufrag: function (line) {
|
||||
return line.substring(12);
|
||||
},
|
||||
build_iceufrag: function (frag) {
|
||||
return 'a=ice-ufrag:' + frag;
|
||||
},
|
||||
parse_icepwd: function (line) {
|
||||
return line.substring(10);
|
||||
},
|
||||
build_icepwd: function (pwd) {
|
||||
return 'a=ice-pwd:' + pwd;
|
||||
},
|
||||
parse_mid: function (line) {
|
||||
return line.substring(6);
|
||||
},
|
||||
parse_mline: function (line) {
|
||||
var parts = line.substring(2).split(' '),
|
||||
data = {};
|
||||
data.media = parts.shift();
|
||||
data.port = parts.shift();
|
||||
data.proto = parts.shift();
|
||||
if (parts[parts.length - 1] === '') { // trailing whitespace
|
||||
parts.pop();
|
||||
}
|
||||
data.fmt = parts;
|
||||
return data;
|
||||
},
|
||||
build_mline: function (mline) {
|
||||
return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
|
||||
},
|
||||
parse_rtpmap: function (line) {
|
||||
var parts = line.substring(9).split(' '),
|
||||
data = {};
|
||||
data.id = parts.shift();
|
||||
parts = parts[0].split('/');
|
||||
data.name = parts.shift();
|
||||
data.clockrate = parts.shift();
|
||||
data.channels = parts.length ? parts.shift() : '1';
|
||||
return data;
|
||||
},
|
||||
/**
|
||||
* 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') {
|
||||
line += '/' + el.getAttribute('channels');
|
||||
}
|
||||
return line;
|
||||
},
|
||||
parse_crypto: function (line) {
|
||||
var parts = line.substring(9).split(' '),
|
||||
data = {};
|
||||
data.tag = parts.shift();
|
||||
data['crypto-suite'] = parts.shift();
|
||||
data['key-params'] = parts.shift();
|
||||
if (parts.length) {
|
||||
data['session-params'] = parts.join(' ');
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_fingerprint: function (line) { // RFC 4572
|
||||
var parts = line.substring(14).split(' '),
|
||||
data = {};
|
||||
data.hash = parts.shift();
|
||||
data.fingerprint = parts.shift();
|
||||
// TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
|
||||
return data;
|
||||
},
|
||||
parse_fmtp: function (line) {
|
||||
var parts = line.split(' '),
|
||||
i, key, value,
|
||||
data = [];
|
||||
parts.shift();
|
||||
parts = parts.join(' ').split(';');
|
||||
for (i = 0; i < parts.length; i++) {
|
||||
key = parts[i].split('=')[0];
|
||||
while (key.length && key[0] == ' ') {
|
||||
key = key.substring(1);
|
||||
}
|
||||
value = parts[i].split('=')[1];
|
||||
if (key && value) {
|
||||
data.push({name: key, value: value});
|
||||
} else if (key) {
|
||||
// rfc 4733 (DTMF) style stuff
|
||||
data.push({name: '', value: key});
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_icecandidate: function (line) {
|
||||
var candidate = {},
|
||||
elems = line.split(' ');
|
||||
candidate.foundation = elems[0].substring(12);
|
||||
candidate.component = elems[1];
|
||||
candidate.protocol = elems[2].toLowerCase();
|
||||
candidate.priority = elems[3];
|
||||
candidate.ip = elems[4];
|
||||
candidate.port = elems[5];
|
||||
// elems[6] => "typ"
|
||||
candidate.type = elems[7];
|
||||
candidate.generation = 0; // default value, may be overwritten below
|
||||
for (var i = 8; i < elems.length; i += 2) {
|
||||
switch (elems[i]) {
|
||||
case 'raddr':
|
||||
candidate['rel-addr'] = elems[i + 1];
|
||||
break;
|
||||
case 'rport':
|
||||
candidate['rel-port'] = elems[i + 1];
|
||||
break;
|
||||
case 'generation':
|
||||
candidate.generation = elems[i + 1];
|
||||
break;
|
||||
case 'tcptype':
|
||||
candidate.tcptype = elems[i + 1];
|
||||
break;
|
||||
default: // TODO
|
||||
console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
||||
}
|
||||
}
|
||||
candidate.network = '1';
|
||||
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
||||
return candidate;
|
||||
},
|
||||
build_icecandidate: function (cand) {
|
||||
var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
|
||||
line += ' ';
|
||||
switch (cand.type) {
|
||||
case 'srflx':
|
||||
case 'prflx':
|
||||
case 'relay':
|
||||
if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
|
||||
line += 'raddr';
|
||||
line += ' ';
|
||||
line += cand['rel-addr'];
|
||||
line += ' ';
|
||||
line += 'rport';
|
||||
line += ' ';
|
||||
line += cand['rel-port'];
|
||||
line += ' ';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (cand.hasOwnAttribute('tcptype')) {
|
||||
line += 'tcptype';
|
||||
line += ' ';
|
||||
line += cand.tcptype;
|
||||
line += ' ';
|
||||
}
|
||||
line += 'generation';
|
||||
line += ' ';
|
||||
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
|
||||
return line;
|
||||
},
|
||||
parse_ssrc: function (desc) {
|
||||
// proprietary mapping of a=ssrc lines
|
||||
// TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
|
||||
// and parse according to that
|
||||
var lines = desc.split('\r\n'),
|
||||
data = {};
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].substring(0, 7) == 'a=ssrc:') {
|
||||
var idx = lines[i].indexOf(' ');
|
||||
data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_rtcpfb: function (line) {
|
||||
var parts = line.substr(10).split(' ');
|
||||
var data = {};
|
||||
data.pt = parts.shift();
|
||||
data.type = parts.shift();
|
||||
data.params = parts;
|
||||
return data;
|
||||
},
|
||||
parse_extmap: function (line) {
|
||||
var parts = line.substr(9).split(' ');
|
||||
var data = {};
|
||||
data.value = parts.shift();
|
||||
if (data.value.indexOf('/') != -1) {
|
||||
data.direction = data.value.substr(data.value.indexOf('/') + 1);
|
||||
data.value = data.value.substr(0, data.value.indexOf('/'));
|
||||
} else {
|
||||
data.direction = 'both';
|
||||
}
|
||||
data.uri = parts.shift();
|
||||
data.params = parts;
|
||||
return data;
|
||||
},
|
||||
find_line: function (haystack, needle, sessionpart) {
|
||||
var lines = haystack.split('\r\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].substring(0, needle.length) == needle) {
|
||||
return lines[i];
|
||||
}
|
||||
}
|
||||
if (!sessionpart) {
|
||||
return false;
|
||||
}
|
||||
// search session part
|
||||
lines = sessionpart.split('\r\n');
|
||||
for (var j = 0; j < lines.length; j++) {
|
||||
if (lines[j].substring(0, needle.length) == needle) {
|
||||
return lines[j];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
find_lines: function (haystack, needle, sessionpart) {
|
||||
var lines = haystack.split('\r\n'),
|
||||
needles = [];
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].substring(0, needle.length) == needle)
|
||||
needles.push(lines[i]);
|
||||
}
|
||||
if (needles.length || !sessionpart) {
|
||||
return needles;
|
||||
}
|
||||
// search session part
|
||||
lines = sessionpart.split('\r\n');
|
||||
for (var j = 0; j < lines.length; j++) {
|
||||
if (lines[j].substring(0, needle.length) == needle) {
|
||||
needles.push(lines[j]);
|
||||
}
|
||||
}
|
||||
return needles;
|
||||
},
|
||||
candidateToJingle: function (line) {
|
||||
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
|
||||
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
|
||||
if (line.indexOf('candidate:') === 0) {
|
||||
line = 'a=' + line;
|
||||
} else if (line.substring(0, 12) != 'a=candidate:') {
|
||||
console.log('parseCandidate called with a line that is not a candidate line');
|
||||
console.log(line);
|
||||
return null;
|
||||
}
|
||||
if (line.substring(line.length - 2) == '\r\n') // chomp it
|
||||
line = line.substring(0, line.length - 2);
|
||||
var candidate = {},
|
||||
elems = line.split(' '),
|
||||
i;
|
||||
if (elems[6] != 'typ') {
|
||||
console.log('did not find typ in the right place');
|
||||
console.log(line);
|
||||
return null;
|
||||
}
|
||||
candidate.foundation = elems[0].substring(12);
|
||||
candidate.component = elems[1];
|
||||
candidate.protocol = elems[2].toLowerCase();
|
||||
candidate.priority = elems[3];
|
||||
candidate.ip = elems[4];
|
||||
candidate.port = elems[5];
|
||||
// elems[6] => "typ"
|
||||
candidate.type = elems[7];
|
||||
|
||||
candidate.generation = '0'; // default, may be overwritten below
|
||||
for (i = 8; i < elems.length; i += 2) {
|
||||
switch (elems[i]) {
|
||||
case 'raddr':
|
||||
candidate['rel-addr'] = elems[i + 1];
|
||||
break;
|
||||
case 'rport':
|
||||
candidate['rel-port'] = elems[i + 1];
|
||||
break;
|
||||
case 'generation':
|
||||
candidate.generation = elems[i + 1];
|
||||
break;
|
||||
case 'tcptype':
|
||||
candidate.tcptype = elems[i + 1];
|
||||
break;
|
||||
default: // TODO
|
||||
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
||||
}
|
||||
}
|
||||
candidate.network = '1';
|
||||
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
||||
return candidate;
|
||||
},
|
||||
candidateFromJingle: function (cand) {
|
||||
var line = 'a=candidate:';
|
||||
line += cand.getAttribute('foundation');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('component');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
|
||||
line += ' ';
|
||||
line += cand.getAttribute('priority');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('ip');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('port');
|
||||
line += ' ';
|
||||
line += 'typ';
|
||||
line += ' ' + cand.getAttribute('type');
|
||||
line += ' ';
|
||||
switch (cand.getAttribute('type')) {
|
||||
case 'srflx':
|
||||
case 'prflx':
|
||||
case 'relay':
|
||||
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
|
||||
line += 'raddr';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('rel-addr');
|
||||
line += ' ';
|
||||
line += 'rport';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('rel-port');
|
||||
line += ' ';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (cand.getAttribute('protocol').toLowerCase() == 'tcp') {
|
||||
line += 'tcptype';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('tcptype');
|
||||
line += ' ';
|
||||
}
|
||||
line += 'generation';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('generation') || '0';
|
||||
return line + '\r\n';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,406 +0,0 @@
|
|||
/**
|
||||
* Contains utility classes used in SDP class.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class holds a=ssrc lines and media type a=mid
|
||||
* @param ssrc synchronization source identifier number(a=ssrc lines from SDP)
|
||||
* @param type media type eg. "audio" or "video"(a=mid frm SDP)
|
||||
* @constructor
|
||||
*/
|
||||
function ChannelSsrc(ssrc, type) {
|
||||
this.ssrc = ssrc;
|
||||
this.type = type;
|
||||
this.lines = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Class holds a=ssrc-group: lines
|
||||
* @param semantics
|
||||
* @param ssrcs
|
||||
* @constructor
|
||||
*/
|
||||
function ChannelSsrcGroup(semantics, ssrcs, line) {
|
||||
this.semantics = semantics;
|
||||
this.ssrcs = ssrcs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class represents media channel. Is a container for ChannelSsrc, holds channel idx and media type.
|
||||
* @param channelNumber channel idx in SDP media array.
|
||||
* @param mediaType media type(a=mid)
|
||||
* @constructor
|
||||
*/
|
||||
function MediaChannel(channelNumber, mediaType) {
|
||||
/**
|
||||
* SDP channel number
|
||||
* @type {*}
|
||||
*/
|
||||
this.chNumber = channelNumber;
|
||||
/**
|
||||
* Channel media type(a=mid)
|
||||
* @type {*}
|
||||
*/
|
||||
this.mediaType = mediaType;
|
||||
/**
|
||||
* The maps of ssrc numbers to ChannelSsrc objects.
|
||||
*/
|
||||
this.ssrcs = {};
|
||||
|
||||
/**
|
||||
* The array of ChannelSsrcGroup objects.
|
||||
* @type {Array}
|
||||
*/
|
||||
this.ssrcGroups = [];
|
||||
}
|
||||
|
||||
SDPUtil = {
|
||||
iceparams: function (mediadesc, sessiondesc) {
|
||||
var data = null;
|
||||
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
|
||||
SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
|
||||
data = {
|
||||
ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
|
||||
pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
|
||||
};
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_iceufrag: function (line) {
|
||||
return line.substring(12);
|
||||
},
|
||||
build_iceufrag: function (frag) {
|
||||
return 'a=ice-ufrag:' + frag;
|
||||
},
|
||||
parse_icepwd: function (line) {
|
||||
return line.substring(10);
|
||||
},
|
||||
build_icepwd: function (pwd) {
|
||||
return 'a=ice-pwd:' + pwd;
|
||||
},
|
||||
parse_mid: function (line) {
|
||||
return line.substring(6);
|
||||
},
|
||||
parse_mline: function (line) {
|
||||
var parts = line.substring(2).split(' '),
|
||||
data = {};
|
||||
data.media = parts.shift();
|
||||
data.port = parts.shift();
|
||||
data.proto = parts.shift();
|
||||
if (parts[parts.length - 1] === '') { // trailing whitespace
|
||||
parts.pop();
|
||||
}
|
||||
data.fmt = parts;
|
||||
return data;
|
||||
},
|
||||
build_mline: function (mline) {
|
||||
return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
|
||||
},
|
||||
parse_rtpmap: function (line) {
|
||||
var parts = line.substring(9).split(' '),
|
||||
data = {};
|
||||
data.id = parts.shift();
|
||||
parts = parts[0].split('/');
|
||||
data.name = parts.shift();
|
||||
data.clockrate = parts.shift();
|
||||
data.channels = parts.length ? parts.shift() : '1';
|
||||
return data;
|
||||
},
|
||||
/**
|
||||
* 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') {
|
||||
line += '/' + el.getAttribute('channels');
|
||||
}
|
||||
return line;
|
||||
},
|
||||
parse_crypto: function (line) {
|
||||
var parts = line.substring(9).split(' '),
|
||||
data = {};
|
||||
data.tag = parts.shift();
|
||||
data['crypto-suite'] = parts.shift();
|
||||
data['key-params'] = parts.shift();
|
||||
if (parts.length) {
|
||||
data['session-params'] = parts.join(' ');
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_fingerprint: function (line) { // RFC 4572
|
||||
var parts = line.substring(14).split(' '),
|
||||
data = {};
|
||||
data.hash = parts.shift();
|
||||
data.fingerprint = parts.shift();
|
||||
// TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
|
||||
return data;
|
||||
},
|
||||
parse_fmtp: function (line) {
|
||||
var parts = line.split(' '),
|
||||
i, key, value,
|
||||
data = [];
|
||||
parts.shift();
|
||||
parts = parts.join(' ').split(';');
|
||||
for (i = 0; i < parts.length; i++) {
|
||||
key = parts[i].split('=')[0];
|
||||
while (key.length && key[0] == ' ') {
|
||||
key = key.substring(1);
|
||||
}
|
||||
value = parts[i].split('=')[1];
|
||||
if (key && value) {
|
||||
data.push({name: key, value: value});
|
||||
} else if (key) {
|
||||
// rfc 4733 (DTMF) style stuff
|
||||
data.push({name: '', value: key});
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_icecandidate: function (line) {
|
||||
var candidate = {},
|
||||
elems = line.split(' ');
|
||||
candidate.foundation = elems[0].substring(12);
|
||||
candidate.component = elems[1];
|
||||
candidate.protocol = elems[2].toLowerCase();
|
||||
candidate.priority = elems[3];
|
||||
candidate.ip = elems[4];
|
||||
candidate.port = elems[5];
|
||||
// elems[6] => "typ"
|
||||
candidate.type = elems[7];
|
||||
candidate.generation = 0; // default value, may be overwritten below
|
||||
for (var i = 8; i < elems.length; i += 2) {
|
||||
switch (elems[i]) {
|
||||
case 'raddr':
|
||||
candidate['rel-addr'] = elems[i + 1];
|
||||
break;
|
||||
case 'rport':
|
||||
candidate['rel-port'] = elems[i + 1];
|
||||
break;
|
||||
case 'generation':
|
||||
candidate.generation = elems[i + 1];
|
||||
break;
|
||||
case 'tcptype':
|
||||
candidate.tcptype = elems[i + 1];
|
||||
break;
|
||||
default: // TODO
|
||||
console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
||||
}
|
||||
}
|
||||
candidate.network = '1';
|
||||
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
||||
return candidate;
|
||||
},
|
||||
build_icecandidate: function (cand) {
|
||||
var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
|
||||
line += ' ';
|
||||
switch (cand.type) {
|
||||
case 'srflx':
|
||||
case 'prflx':
|
||||
case 'relay':
|
||||
if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
|
||||
line += 'raddr';
|
||||
line += ' ';
|
||||
line += cand['rel-addr'];
|
||||
line += ' ';
|
||||
line += 'rport';
|
||||
line += ' ';
|
||||
line += cand['rel-port'];
|
||||
line += ' ';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (cand.hasOwnAttribute('tcptype')) {
|
||||
line += 'tcptype';
|
||||
line += ' ';
|
||||
line += cand.tcptype;
|
||||
line += ' ';
|
||||
}
|
||||
line += 'generation';
|
||||
line += ' ';
|
||||
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
|
||||
return line;
|
||||
},
|
||||
parse_ssrc: function (desc) {
|
||||
// proprietary mapping of a=ssrc lines
|
||||
// TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
|
||||
// and parse according to that
|
||||
var lines = desc.split('\r\n'),
|
||||
data = {};
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].substring(0, 7) == 'a=ssrc:') {
|
||||
var idx = lines[i].indexOf(' ');
|
||||
data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_rtcpfb: function (line) {
|
||||
var parts = line.substr(10).split(' ');
|
||||
var data = {};
|
||||
data.pt = parts.shift();
|
||||
data.type = parts.shift();
|
||||
data.params = parts;
|
||||
return data;
|
||||
},
|
||||
parse_extmap: function (line) {
|
||||
var parts = line.substr(9).split(' ');
|
||||
var data = {};
|
||||
data.value = parts.shift();
|
||||
if (data.value.indexOf('/') != -1) {
|
||||
data.direction = data.value.substr(data.value.indexOf('/') + 1);
|
||||
data.value = data.value.substr(0, data.value.indexOf('/'));
|
||||
} else {
|
||||
data.direction = 'both';
|
||||
}
|
||||
data.uri = parts.shift();
|
||||
data.params = parts;
|
||||
return data;
|
||||
},
|
||||
find_line: function (haystack, needle, sessionpart) {
|
||||
var lines = haystack.split('\r\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].substring(0, needle.length) == needle) {
|
||||
return lines[i];
|
||||
}
|
||||
}
|
||||
if (!sessionpart) {
|
||||
return false;
|
||||
}
|
||||
// search session part
|
||||
lines = sessionpart.split('\r\n');
|
||||
for (var j = 0; j < lines.length; j++) {
|
||||
if (lines[j].substring(0, needle.length) == needle) {
|
||||
return lines[j];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
find_lines: function (haystack, needle, sessionpart) {
|
||||
var lines = haystack.split('\r\n'),
|
||||
needles = [];
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].substring(0, needle.length) == needle)
|
||||
needles.push(lines[i]);
|
||||
}
|
||||
if (needles.length || !sessionpart) {
|
||||
return needles;
|
||||
}
|
||||
// search session part
|
||||
lines = sessionpart.split('\r\n');
|
||||
for (var j = 0; j < lines.length; j++) {
|
||||
if (lines[j].substring(0, needle.length) == needle) {
|
||||
needles.push(lines[j]);
|
||||
}
|
||||
}
|
||||
return needles;
|
||||
},
|
||||
candidateToJingle: function (line) {
|
||||
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
|
||||
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
|
||||
if (line.indexOf('candidate:') === 0) {
|
||||
line = 'a=' + line;
|
||||
} else if (line.substring(0, 12) != 'a=candidate:') {
|
||||
console.log('parseCandidate called with a line that is not a candidate line');
|
||||
console.log(line);
|
||||
return null;
|
||||
}
|
||||
if (line.substring(line.length - 2) == '\r\n') // chomp it
|
||||
line = line.substring(0, line.length - 2);
|
||||
var candidate = {},
|
||||
elems = line.split(' '),
|
||||
i;
|
||||
if (elems[6] != 'typ') {
|
||||
console.log('did not find typ in the right place');
|
||||
console.log(line);
|
||||
return null;
|
||||
}
|
||||
candidate.foundation = elems[0].substring(12);
|
||||
candidate.component = elems[1];
|
||||
candidate.protocol = elems[2].toLowerCase();
|
||||
candidate.priority = elems[3];
|
||||
candidate.ip = elems[4];
|
||||
candidate.port = elems[5];
|
||||
// elems[6] => "typ"
|
||||
candidate.type = elems[7];
|
||||
|
||||
candidate.generation = '0'; // default, may be overwritten below
|
||||
for (i = 8; i < elems.length; i += 2) {
|
||||
switch (elems[i]) {
|
||||
case 'raddr':
|
||||
candidate['rel-addr'] = elems[i + 1];
|
||||
break;
|
||||
case 'rport':
|
||||
candidate['rel-port'] = elems[i + 1];
|
||||
break;
|
||||
case 'generation':
|
||||
candidate.generation = elems[i + 1];
|
||||
break;
|
||||
case 'tcptype':
|
||||
candidate.tcptype = elems[i + 1];
|
||||
break;
|
||||
default: // TODO
|
||||
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
||||
}
|
||||
}
|
||||
candidate.network = '1';
|
||||
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
||||
return candidate;
|
||||
},
|
||||
candidateFromJingle: function (cand) {
|
||||
var line = 'a=candidate:';
|
||||
line += cand.getAttribute('foundation');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('component');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
|
||||
line += ' ';
|
||||
line += cand.getAttribute('priority');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('ip');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('port');
|
||||
line += ' ';
|
||||
line += 'typ';
|
||||
line += ' ' + cand.getAttribute('type');
|
||||
line += ' ';
|
||||
switch (cand.getAttribute('type')) {
|
||||
case 'srflx':
|
||||
case 'prflx':
|
||||
case 'relay':
|
||||
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
|
||||
line += 'raddr';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('rel-addr');
|
||||
line += ' ';
|
||||
line += 'rport';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('rel-port');
|
||||
line += ' ';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (cand.getAttribute('protocol').toLowerCase() == 'tcp') {
|
||||
line += 'tcptype';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('tcptype');
|
||||
line += ' ';
|
||||
}
|
||||
line += 'generation';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('generation') || '0';
|
||||
return line + '\r\n';
|
||||
}
|
||||
};
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
/* jshint -W117 */
|
||||
// Jingle stuff
|
||||
JingleSession.prototype = Object.create(SessionBase.prototype);
|
||||
function JingleSession(me, sid, connection) {
|
||||
|
||||
SessionBase.call(this, connection, sid);
|
||||
|
||||
this.me = me;
|
||||
this.sid = sid;
|
||||
this.connection = connection;
|
||||
this.initiator = null;
|
||||
this.responder = null;
|
||||
this.isInitiator = null;
|
||||
|
@ -35,8 +33,20 @@ function JingleSession(me, sid, connection) {
|
|||
|
||||
this.reason = null;
|
||||
|
||||
this.addssrc = [];
|
||||
this.removessrc = [];
|
||||
this.pendingop = null;
|
||||
this.switchstreams = false;
|
||||
|
||||
this.wait = true;
|
||||
this.localStreamsSSRC = null;
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the (local) video has been muted
|
||||
* in response to a user command in contrast to an automatic decision made
|
||||
* by the application logic.
|
||||
*/
|
||||
this.videoMuteByUser = false;
|
||||
}
|
||||
|
||||
JingleSession.prototype.initiate = function (peerjid, isInitiator) {
|
||||
|
@ -163,22 +173,6 @@ JingleSession.prototype.accept = function () {
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements SessionBase.sendSSRCUpdate.
|
||||
*/
|
||||
JingleSession.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isadd) {
|
||||
|
||||
var self = this;
|
||||
console.log('tell', self.peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from' + self.me);
|
||||
|
||||
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')){
|
||||
console.log("Too early to send updates");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendSSRCUpdateIq(sdpMediaSsrcs, self.sid, self.initiator, self.peerjid, isadd);
|
||||
};
|
||||
|
||||
JingleSession.prototype.terminate = function (reason) {
|
||||
this.state = 'ended';
|
||||
this.reason = reason;
|
||||
|
@ -675,6 +669,445 @@ JingleSession.prototype.sendTerminate = function (reason, text) {
|
|||
}
|
||||
};
|
||||
|
||||
JingleSession.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;
|
||||
}
|
||||
|
||||
console.log('addssrc', new Date().getTime());
|
||||
console.log('ice', this.peerconnection.iceConnectionState);
|
||||
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
|
||||
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
|
||||
|
||||
$(elem).each(function (idx, content) {
|
||||
var name = $(content).attr('name');
|
||||
var lines = '';
|
||||
tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
|
||||
var semantics = this.getAttribute('semantics');
|
||||
var ssrcs = $(this).find('>source').map(function () {
|
||||
return this.getAttribute('ssrc');
|
||||
}).get();
|
||||
|
||||
if (ssrcs.length != 0) {
|
||||
lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
|
||||
}
|
||||
});
|
||||
tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
|
||||
tmp.each(function () {
|
||||
var ssrc = $(this).attr('ssrc');
|
||||
if(mySdp.containsSSRC(ssrc)){
|
||||
/**
|
||||
* This happens when multiple participants change their streams at the same time and
|
||||
* ColibriFocus.modifySources have to wait for stable state. In the meantime multiple
|
||||
* addssrc are scheduled for update IQ. See
|
||||
*/
|
||||
console.warn("Got add stream request for my own ssrc: "+ssrc);
|
||||
return;
|
||||
}
|
||||
$(this).find('>parameter').each(function () {
|
||||
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
||||
if ($(this).attr('value') && $(this).attr('value').length)
|
||||
lines += ':' + $(this).attr('value');
|
||||
lines += '\r\n';
|
||||
});
|
||||
});
|
||||
sdp.media.forEach(function(media, idx) {
|
||||
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
||||
return;
|
||||
sdp.media[idx] += lines;
|
||||
if (!self.addssrc[idx]) self.addssrc[idx] = '';
|
||||
self.addssrc[idx] += lines;
|
||||
});
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
});
|
||||
this.modifySources();
|
||||
};
|
||||
|
||||
JingleSession.prototype.removeSource = function (elem, fromJid) {
|
||||
|
||||
var self = this;
|
||||
// FIXME: dirty waiting
|
||||
if (!this.peerconnection.localDescription)
|
||||
{
|
||||
console.warn("removeSource - localDescription not ready yet")
|
||||
setTimeout(function()
|
||||
{
|
||||
self.removeSource(elem, fromJid);
|
||||
},
|
||||
200
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('removessrc', new Date().getTime());
|
||||
console.log('ice', this.peerconnection.iceConnectionState);
|
||||
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
|
||||
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
|
||||
|
||||
$(elem).each(function (idx, content) {
|
||||
var name = $(content).attr('name');
|
||||
var lines = '';
|
||||
tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
|
||||
var semantics = this.getAttribute('semantics');
|
||||
var ssrcs = $(this).find('>source').map(function () {
|
||||
return this.getAttribute('ssrc');
|
||||
}).get();
|
||||
|
||||
if (ssrcs.length != 0) {
|
||||
lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
|
||||
}
|
||||
});
|
||||
tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
|
||||
tmp.each(function () {
|
||||
var ssrc = $(this).attr('ssrc');
|
||||
// This should never happen, but can be useful for bug detection
|
||||
if(mySdp.containsSSRC(ssrc)){
|
||||
console.error("Got remove stream request for my own ssrc: "+ssrc);
|
||||
return;
|
||||
}
|
||||
$(this).find('>parameter').each(function () {
|
||||
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
||||
if ($(this).attr('value') && $(this).attr('value').length)
|
||||
lines += ':' + $(this).attr('value');
|
||||
lines += '\r\n';
|
||||
});
|
||||
});
|
||||
sdp.media.forEach(function(media, idx) {
|
||||
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
||||
return;
|
||||
sdp.media[idx] += lines;
|
||||
if (!self.removessrc[idx]) self.removessrc[idx] = '';
|
||||
self.removessrc[idx] += lines;
|
||||
});
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
});
|
||||
this.modifySources();
|
||||
};
|
||||
|
||||
JingleSession.prototype.modifySources = function (successCallback) {
|
||||
var self = this;
|
||||
if (this.peerconnection.signalingState == 'closed') return;
|
||||
if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){
|
||||
// There is nothing to do since scheduled job might have been executed by another succeeding call
|
||||
$(document).trigger('setLocalDescription.jingle', [self.sid]);
|
||||
if(successCallback){
|
||||
successCallback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: this is a big hack
|
||||
// https://code.google.com/p/webrtc/issues/detail?id=2688
|
||||
// ^ has been fixed.
|
||||
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
|
||||
console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
|
||||
this.wait = true;
|
||||
window.setTimeout(function() { self.modifySources(successCallback); }, 250);
|
||||
return;
|
||||
}
|
||||
if (this.wait) {
|
||||
window.setTimeout(function() { self.modifySources(successCallback); }, 2500);
|
||||
this.wait = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset switch streams flag
|
||||
this.switchstreams = false;
|
||||
|
||||
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
|
||||
|
||||
// add sources
|
||||
this.addssrc.forEach(function(lines, idx) {
|
||||
sdp.media[idx] += lines;
|
||||
});
|
||||
this.addssrc = [];
|
||||
|
||||
// remove sources
|
||||
this.removessrc.forEach(function(lines, idx) {
|
||||
lines = lines.split('\r\n');
|
||||
lines.pop(); // remove empty last element;
|
||||
lines.forEach(function(line) {
|
||||
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
|
||||
});
|
||||
});
|
||||
this.removessrc = [];
|
||||
|
||||
// FIXME:
|
||||
// this was a hack for the situation when only one peer exists
|
||||
// in the conference.
|
||||
// check if still required and remove
|
||||
if (sdp.media[0])
|
||||
sdp.media[0] = sdp.media[0].replace('a=recvonly', 'a=sendrecv');
|
||||
if (sdp.media[1])
|
||||
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
|
||||
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
|
||||
function() {
|
||||
|
||||
if(self.signalingState == 'closed') {
|
||||
console.error("createAnswer attempt on closed state");
|
||||
return;
|
||||
}
|
||||
|
||||
self.peerconnection.createAnswer(
|
||||
function(modifiedAnswer) {
|
||||
// change video direction, see https://github.com/jitsi/jitmeet/issues/41
|
||||
if (self.pendingop !== null) {
|
||||
var sdp = new SDP(modifiedAnswer.sdp);
|
||||
if (sdp.media.length > 1) {
|
||||
switch(self.pendingop) {
|
||||
case 'mute':
|
||||
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
|
||||
break;
|
||||
case 'unmute':
|
||||
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
|
||||
break;
|
||||
}
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
modifiedAnswer.sdp = sdp.raw;
|
||||
}
|
||||
self.pendingop = null;
|
||||
}
|
||||
|
||||
// FIXME: pushing down an answer while ice connection state
|
||||
// is still checking is bad...
|
||||
//console.log(self.peerconnection.iceConnectionState);
|
||||
|
||||
// trying to work around another chrome bug
|
||||
//modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
|
||||
self.peerconnection.setLocalDescription(modifiedAnswer,
|
||||
function() {
|
||||
//console.log('modified setLocalDescription ok');
|
||||
$(document).trigger('setLocalDescription.jingle', [self.sid]);
|
||||
if(successCallback){
|
||||
successCallback();
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
console.error('modified setLocalDescription failed', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(error) {
|
||||
console.error('modified answer failed', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(error) {
|
||||
console.error('modify failed', 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.
|
||||
*/
|
||||
JingleSession.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, true);
|
||||
self.peerconnection.addStream(new_stream);
|
||||
}
|
||||
|
||||
self.connection.jingle.localVideo = new_stream;
|
||||
|
||||
self.connection.jingle.localStreams = [];
|
||||
|
||||
//in firefox we have only one stream object
|
||||
if(self.connection.jingle.localAudio != self.connection.jingle.localVideo)
|
||||
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.switchstreams = true;
|
||||
self.modifySources(function() {
|
||||
console.log('modify sources done');
|
||||
|
||||
success_callback();
|
||||
|
||||
var newSdp = new SDP(self.peerconnection.localDescription.sdp);
|
||||
console.log("SDPs", oldSdp, newSdp);
|
||||
self.notifyMySSRCUpdate(oldSdp, newSdp);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Figures out added/removed ssrcs and send update IQs.
|
||||
* @param old_sdp SDP object for old description.
|
||||
* @param new_sdp SDP object for new description.
|
||||
*/
|
||||
JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
|
||||
|
||||
if (!(this.peerconnection.signalingState == 'stable' &&
|
||||
this.peerconnection.iceConnectionState == 'connected')){
|
||||
console.log("Too early to send updates");
|
||||
return;
|
||||
}
|
||||
|
||||
// send source-remove IQ.
|
||||
sdpDiffer = new SDPDiffer(new_sdp, old_sdp);
|
||||
var remove = $iq({to: this.peerjid, type: 'set'})
|
||||
.c('jingle', {
|
||||
xmlns: 'urn:xmpp:jingle:1',
|
||||
action: 'source-remove',
|
||||
initiator: this.initiator,
|
||||
sid: this.sid
|
||||
}
|
||||
);
|
||||
var removed = sdpDiffer.toJingle(remove);
|
||||
if (removed) {
|
||||
this.connection.sendIQ(remove,
|
||||
function (res) {
|
||||
console.info('got remove result', res);
|
||||
},
|
||||
function (err) {
|
||||
console.error('got remove error', err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('removal not necessary');
|
||||
}
|
||||
|
||||
// send source-add IQ.
|
||||
var sdpDiffer = new SDPDiffer(old_sdp, new_sdp);
|
||||
var add = $iq({to: this.peerjid, type: 'set'})
|
||||
.c('jingle', {
|
||||
xmlns: 'urn:xmpp:jingle:1',
|
||||
action: 'source-add',
|
||||
initiator: this.initiator,
|
||||
sid: this.sid
|
||||
}
|
||||
);
|
||||
var added = sdpDiffer.toJingle(add);
|
||||
if (added) {
|
||||
this.connection.sendIQ(add,
|
||||
function (res) {
|
||||
console.info('got add result', res);
|
||||
},
|
||||
function (err) {
|
||||
console.error('got add error', err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('addition not necessary');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the (local) video is mute i.e. all video tracks are
|
||||
* disabled.
|
||||
*
|
||||
* @return <tt>true</tt> if the (local) video is mute i.e. all video tracks are
|
||||
* disabled; otherwise, <tt>false</tt>
|
||||
*/
|
||||
JingleSession.prototype.isVideoMute = function () {
|
||||
var tracks = connection.jingle.localVideo.getVideoTracks();
|
||||
var mute = true;
|
||||
|
||||
for (var i = 0; i < tracks.length; ++i) {
|
||||
if (tracks[i].enabled) {
|
||||
mute = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return mute;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mutes/unmutes the (local) video i.e. enables/disables all video tracks.
|
||||
*
|
||||
* @param mute <tt>true</tt> to mute the (local) video i.e. to disable all video
|
||||
* tracks; otherwise, <tt>false</tt>
|
||||
* @param callback a function to be invoked with <tt>mute</tt> after all video
|
||||
* tracks have been enabled/disabled. The function may, optionally, return
|
||||
* another function which is to be invoked after the whole mute/unmute operation
|
||||
* has completed successfully.
|
||||
* @param options an object which specifies optional arguments such as the
|
||||
* <tt>boolean</tt> key <tt>byUser</tt> with default value <tt>true</tt> which
|
||||
* specifies whether the method was initiated in response to a user command (in
|
||||
* contrast to an automatic decision made by the application logic)
|
||||
*/
|
||||
JingleSession.prototype.setVideoMute = function (mute, callback, options) {
|
||||
var byUser;
|
||||
|
||||
if (options) {
|
||||
byUser = options.byUser;
|
||||
if (typeof byUser === 'undefined') {
|
||||
byUser = true;
|
||||
}
|
||||
} else {
|
||||
byUser = true;
|
||||
}
|
||||
// The user's command to mute the (local) video takes precedence over any
|
||||
// automatic decision made by the application logic.
|
||||
if (byUser) {
|
||||
this.videoMuteByUser = mute;
|
||||
} else if (this.videoMuteByUser) {
|
||||
return;
|
||||
}
|
||||
if (mute == this.isVideoMute())
|
||||
{
|
||||
// Even if no change occurs, the specified callback is to be executed.
|
||||
// The specified callback may, optionally, return a successCallback
|
||||
// which is to be executed as well.
|
||||
var successCallback = callback(mute);
|
||||
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
} else {
|
||||
var tracks = connection.jingle.localVideo.getVideoTracks();
|
||||
|
||||
for (var i = 0; i < tracks.length; ++i) {
|
||||
tracks[i].enabled = !mute;
|
||||
}
|
||||
|
||||
this.hardMuteVideo(mute);
|
||||
|
||||
this.modifySources(callback(mute));
|
||||
}
|
||||
};
|
||||
|
||||
// SDP-based mute by going recvonly/sendrecv
|
||||
// FIXME: should probably black out the screen as well
|
||||
JingleSession.prototype.toggleVideoMute = function (callback) {
|
||||
setVideoMute(isVideoMute(), callback);
|
||||
};
|
||||
|
||||
JingleSession.prototype.hardMuteVideo = function (muted) {
|
||||
this.pendingop = muted ? 'mute' : 'unmute';
|
||||
};
|
||||
|
||||
JingleSession.prototype.sendMute = function (muted, content) {
|
||||
var info = $iq({to: this.peerjid,
|
||||
type: 'set'})
|
||||
|
@ -746,3 +1179,4 @@ JingleSession.prototype.getStats = function (interval) {
|
|||
}, interval || 3000);
|
||||
return this.statsinterval;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,321 +0,0 @@
|
|||
/**
|
||||
* Base class for ColibriFocus and JingleSession.
|
||||
* @param connection Strophe connection object
|
||||
* @param sid my session identifier(resource)
|
||||
* @constructor
|
||||
*/
|
||||
function SessionBase(connection, sid) {
|
||||
this.connection = connection;
|
||||
this.sid = sid;
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the (local) video has been muted
|
||||
* in response to a user command in contrast to an automatic decision made
|
||||
* by the application logic.
|
||||
*/
|
||||
this.videoMuteByUser = false;
|
||||
}
|
||||
|
||||
|
||||
SessionBase.prototype.modifySources = function (successCallback) {
|
||||
var self = this;
|
||||
if(this.peerconnection)
|
||||
this.peerconnection.modifySources(function(){
|
||||
$(document).trigger('setLocalDescription.jingle', [self.sid]);
|
||||
if(successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SessionBase.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);
|
||||
|
||||
this.modifySources();
|
||||
};
|
||||
|
||||
SessionBase.prototype.removeSource = function (elem, fromJid) {
|
||||
|
||||
var self = this;
|
||||
// FIXME: dirty waiting
|
||||
if (!this.peerconnection.localDescription)
|
||||
{
|
||||
console.warn("removeSource - localDescription not ready yet")
|
||||
setTimeout(function()
|
||||
{
|
||||
self.removeSource(elem, fromJid);
|
||||
},
|
||||
200
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peerconnection.removeSource(elem);
|
||||
|
||||
this.modifySources();
|
||||
};
|
||||
|
||||
/**
|
||||
* Switches video streams.
|
||||
* @param new_stream new stream that will be used as video of this session.
|
||||
* @param oldStream old video stream of this session.
|
||||
* @param success_callback callback executed after successful stream switch.
|
||||
*/
|
||||
SessionBase.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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, true);
|
||||
self.peerconnection.addStream(new_stream);
|
||||
}
|
||||
|
||||
self.connection.jingle.localVideo = new_stream;
|
||||
|
||||
self.connection.jingle.localStreams = [];
|
||||
|
||||
//in firefox we have only one stream object
|
||||
if(self.connection.jingle.localAudio != self.connection.jingle.localVideo)
|
||||
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');
|
||||
|
||||
success_callback();
|
||||
|
||||
var newSdp = new SDP(self.peerconnection.localDescription.sdp);
|
||||
console.log("SDPs", oldSdp, newSdp);
|
||||
self.notifyMySSRCUpdate(oldSdp, newSdp);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Figures out added/removed ssrcs and send update IQs.
|
||||
* @param old_sdp SDP object for old description.
|
||||
* @param new_sdp SDP object for new description.
|
||||
*/
|
||||
SessionBase.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
|
||||
|
||||
var old_media = old_sdp.getMediaSsrcMap();
|
||||
var new_media = new_sdp.getMediaSsrcMap();
|
||||
//console.log("old/new medias: ", old_media, new_media);
|
||||
|
||||
var toAdd = old_sdp.getNewMedia(new_sdp);
|
||||
var toRemove = new_sdp.getNewMedia(old_sdp);
|
||||
//console.log("to add", toAdd);
|
||||
//console.log("to remove", toRemove);
|
||||
if(Object.keys(toRemove).length > 0){
|
||||
this.sendSSRCUpdate(toRemove, null, false);
|
||||
}
|
||||
if(Object.keys(toAdd).length > 0){
|
||||
this.sendSSRCUpdate(toAdd, null, true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Empty method that does nothing by default. It should send SSRC update IQs to session participants.
|
||||
* @param sdpMediaSsrcs array of
|
||||
* @param fromJid
|
||||
* @param isAdd
|
||||
*/
|
||||
SessionBase.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isAdd) {
|
||||
//FIXME: put default implementation here(maybe from JingleSession?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends SSRC update IQ.
|
||||
* @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove.
|
||||
* @param sid session identifier that will be put into the IQ.
|
||||
* @param initiator initiator identifier.
|
||||
* @param toJid destination Jid
|
||||
* @param isAdd indicates if this is remove or add operation.
|
||||
*/
|
||||
SessionBase.prototype.sendSSRCUpdateIq = function(sdpMediaSsrcs, sid, initiator, toJid, isAdd) {
|
||||
|
||||
var self = this;
|
||||
var modify = $iq({to: toJid, type: 'set'})
|
||||
.c('jingle', {
|
||||
xmlns: 'urn:xmpp:jingle:1',
|
||||
action: isAdd ? 'source-add' : 'source-remove',
|
||||
initiator: initiator,
|
||||
sid: sid
|
||||
}
|
||||
);
|
||||
// FIXME: only announce video ssrcs since we mix audio and dont need
|
||||
// the audio ssrcs therefore
|
||||
var modified = false;
|
||||
Object.keys(sdpMediaSsrcs).forEach(function(channelNum){
|
||||
modified = true;
|
||||
var channel = sdpMediaSsrcs[channelNum];
|
||||
modify.c('content', {name: channel.mediaType});
|
||||
|
||||
modify.c('description', {xmlns:'urn:xmpp:jingle:apps:rtp:1', media: channel.mediaType});
|
||||
// FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
|
||||
// generate sources from lines
|
||||
Object.keys(channel.ssrcs).forEach(function(ssrcNum) {
|
||||
var mediaSsrc = channel.ssrcs[ssrcNum];
|
||||
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
||||
modify.attrs({ssrc: mediaSsrc.ssrc});
|
||||
// iterate over ssrc lines
|
||||
mediaSsrc.lines.forEach(function (line) {
|
||||
var idx = line.indexOf(' ');
|
||||
var kv = line.substr(idx + 1);
|
||||
modify.c('parameter');
|
||||
if (kv.indexOf(':') == -1) {
|
||||
modify.attrs({ name: kv });
|
||||
} else {
|
||||
modify.attrs({ name: kv.split(':', 2)[0] });
|
||||
modify.attrs({ value: kv.split(':', 2)[1] });
|
||||
}
|
||||
modify.up(); // end of parameter
|
||||
});
|
||||
modify.up(); // end of source
|
||||
});
|
||||
|
||||
// generate source groups from lines
|
||||
channel.ssrcGroups.forEach(function(ssrcGroup) {
|
||||
if (ssrcGroup.ssrcs.length != 0) {
|
||||
|
||||
modify.c('ssrc-group', {
|
||||
semantics: ssrcGroup.semantics,
|
||||
xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
|
||||
});
|
||||
|
||||
ssrcGroup.ssrcs.forEach(function (ssrc) {
|
||||
modify.c('source', { ssrc: ssrc })
|
||||
.up(); // end of source
|
||||
});
|
||||
modify.up(); // end of ssrc-group
|
||||
}
|
||||
});
|
||||
|
||||
modify.up(); // end of description
|
||||
modify.up(); // end of content
|
||||
});
|
||||
if (modified) {
|
||||
self.connection.sendIQ(modify,
|
||||
function (res) {
|
||||
console.info('got modify result', res);
|
||||
},
|
||||
function (err) {
|
||||
console.error('got modify error', err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('modification not necessary');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the (local) video is mute i.e. all video tracks are
|
||||
* disabled.
|
||||
*
|
||||
* @return <tt>true</tt> if the (local) video is mute i.e. all video tracks are
|
||||
* disabled; otherwise, <tt>false</tt>
|
||||
*/
|
||||
SessionBase.prototype.isVideoMute = function () {
|
||||
var tracks = connection.jingle.localVideo.getVideoTracks();
|
||||
var mute = true;
|
||||
|
||||
for (var i = 0; i < tracks.length; ++i) {
|
||||
if (tracks[i].enabled) {
|
||||
mute = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return mute;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mutes/unmutes the (local) video i.e. enables/disables all video tracks.
|
||||
*
|
||||
* @param mute <tt>true</tt> to mute the (local) video i.e. to disable all video
|
||||
* tracks; otherwise, <tt>false</tt>
|
||||
* @param callback a function to be invoked with <tt>mute</tt> after all video
|
||||
* tracks have been enabled/disabled. The function may, optionally, return
|
||||
* another function which is to be invoked after the whole mute/unmute operation
|
||||
* has completed successfully.
|
||||
* @param options an object which specifies optional arguments such as the
|
||||
* <tt>boolean</tt> key <tt>byUser</tt> with default value <tt>true</tt> which
|
||||
* specifies whether the method was initiated in response to a user command (in
|
||||
* contrast to an automatic decision made by the application logic)
|
||||
*/
|
||||
SessionBase.prototype.setVideoMute = function (mute, callback, options) {
|
||||
var byUser;
|
||||
|
||||
if (options) {
|
||||
byUser = options.byUser;
|
||||
if (typeof byUser === 'undefined') {
|
||||
byUser = true;
|
||||
}
|
||||
} else {
|
||||
byUser = true;
|
||||
}
|
||||
// The user's command to mute the (local) video takes precedence over any
|
||||
// automatic decision made by the application logic.
|
||||
if (byUser) {
|
||||
this.videoMuteByUser = mute;
|
||||
} else if (this.videoMuteByUser) {
|
||||
return;
|
||||
}
|
||||
if (mute == this.isVideoMute())
|
||||
{
|
||||
// Even if no change occurs, the specified callback is to be executed.
|
||||
// The specified callback may, optionally, return a successCallback
|
||||
// which is to be executed as well.
|
||||
var successCallback = callback(mute);
|
||||
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
} else {
|
||||
var tracks = connection.jingle.localVideo.getVideoTracks();
|
||||
|
||||
for (var i = 0; i < tracks.length; ++i) {
|
||||
tracks[i].enabled = !mute;
|
||||
}
|
||||
|
||||
if (this.peerconnection) {
|
||||
this.peerconnection.hardMuteVideo(mute);
|
||||
}
|
||||
|
||||
this.modifySources(callback(mute));
|
||||
}
|
||||
};
|
||||
|
||||
// SDP-based mute by going recvonly/sendrecv
|
||||
// FIXME: should probably black out the screen as well
|
||||
SessionBase.prototype.toggleVideoMute = function (callback) {
|
||||
setVideoMute(isVideoMute(), callback);
|
||||
};
|
26
moderator.js
26
moderator.js
|
@ -11,6 +11,10 @@ var Moderator = (function (my) {
|
|||
var getNextErrorTimeout = Util.createExpBackoffTimer(1000);
|
||||
// External authentication stuff
|
||||
var externalAuthEnabled = false;
|
||||
// Sip gateway can be enabled by configuring Jigasi host in config.js or
|
||||
// it will be enabled automatically if focus detects the component through
|
||||
// service discovery.
|
||||
var sipGatewayEnabled = config.hosts.call_control !== undefined;
|
||||
|
||||
my.isModerator = function () {
|
||||
return connection && connection.emuc.isModerator();
|
||||
|
@ -24,6 +28,10 @@ var Moderator = (function (my) {
|
|||
return externalAuthEnabled;
|
||||
};
|
||||
|
||||
my.isSipGatewayEnabled = function () {
|
||||
return sipGatewayEnabled;
|
||||
};
|
||||
|
||||
my.init = function () {
|
||||
Moderator.onLocalRoleChange = function (from, member, pres) {
|
||||
UI.onModeratorStatusChanged(Moderator.isModerator());
|
||||
|
@ -78,6 +86,14 @@ var Moderator = (function (my) {
|
|||
{ name: 'bridge', value: config.hosts.bridge})
|
||||
.up();
|
||||
}
|
||||
// Tell the focus we have Jigasi configured
|
||||
if (config.hosts.call_control !== undefined)
|
||||
{
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'call_control', value: config.hosts.call_control})
|
||||
.up();
|
||||
}
|
||||
if (config.channelLastN !== undefined)
|
||||
{
|
||||
elem.c(
|
||||
|
@ -127,7 +143,17 @@ var Moderator = (function (my) {
|
|||
if (extAuthParam.length) {
|
||||
externalAuthEnabled = extAuthParam.attr('value') === 'true';
|
||||
}
|
||||
|
||||
console.info("External authentication enabled: " + externalAuthEnabled);
|
||||
|
||||
// Check if focus has auto-detected Jigasi component(this will be also
|
||||
// included if we have passed our host from the config)
|
||||
if ($(resultIq).find(
|
||||
'>conference>property[name=\'sipGatewayEnabled\']').length) {
|
||||
sipGatewayEnabled = true;
|
||||
}
|
||||
|
||||
console.info("Sip gateway enabled: " + sipGatewayEnabled);
|
||||
};
|
||||
|
||||
// FIXME: we need to show the fact that we're waiting for the focus
|
||||
|
|
|
@ -15,6 +15,17 @@
|
|||
* @constructor
|
||||
*/
|
||||
function MediaStream(data, sid, ssrc, eventEmmiter, browser) {
|
||||
|
||||
// XXX(gp) to minimize headaches in the future, we should build our
|
||||
// abstractions around tracks and not streams. ORTC is track based API.
|
||||
// Mozilla expects m-lines to represent media tracks.
|
||||
//
|
||||
// Practically, what I'm saying is that we should have a MediaTrack class
|
||||
// and not a MediaStream class.
|
||||
//
|
||||
// Also, we should be able to associate multiple SSRCs with a MediaTrack as
|
||||
// a track might have an associated RTX and FEC sources.
|
||||
|
||||
this.sid = sid;
|
||||
this.stream = data.stream;
|
||||
this.peerjid = data.peerjid;
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
//This should be uncommented when app.js supports require
|
||||
//var RTCBrowserType = require("../../service/RTC/RTCBrowserType.js");
|
||||
|
||||
var constraints = {audio: false, video: false};
|
||||
|
||||
function setResolutionConstraints(resolution, isAndroid)
|
||||
function setResolutionConstraints(constraints, resolution, isAndroid)
|
||||
{
|
||||
if (resolution && !constraints.video || isAndroid) {
|
||||
constraints.video = { mandatory: {}, optional: [] };// same behaviour as true
|
||||
|
@ -58,8 +56,10 @@ function setResolutionConstraints(resolution, isAndroid)
|
|||
}
|
||||
|
||||
|
||||
function setConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid)
|
||||
function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid)
|
||||
{
|
||||
var constraints = {audio: false, video: false};
|
||||
|
||||
if (um.indexOf('video') >= 0) {
|
||||
constraints.video = { mandatory: {}, optional: [] };// same behaviour as true
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ function setConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid
|
|||
}
|
||||
}
|
||||
|
||||
setResolutionConstraints(resolution, isAndroid);
|
||||
setResolutionConstraints(constraints, resolution, isAndroid);
|
||||
|
||||
if (bandwidth) { // doesn't work currently, see webrtc issue 1846
|
||||
if (!constraints.video) constraints.video = {mandatory: {}, optional: []};//same behaviour as true
|
||||
|
@ -126,6 +126,8 @@ function setConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid
|
|||
if (!constraints.video) constraints.video = {mandatory: {}, optional: []};// same behaviour as true;
|
||||
constraints.video.mandatory.minFrameRate = fps;
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
|
||||
|
||||
|
@ -220,7 +222,8 @@ RTCUtils.prototype.getUserMediaWithConstraints = function(
|
|||
// Check if we are running on Android device
|
||||
var isAndroid = navigator.userAgent.indexOf('Android') != -1;
|
||||
|
||||
setConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid);
|
||||
var constraints = getConstraints(
|
||||
um, resolution, bandwidth, fps, desktopStream, isAndroid);
|
||||
|
||||
var isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
|
||||
|
@ -251,7 +254,8 @@ RTCUtils.prototype.getUserMediaWithConstraints = function(
|
|||
success_callback(stream);
|
||||
},
|
||||
function (error) {
|
||||
console.warn('Failed to get access to local media. Error ', error);
|
||||
console.warn('Failed to get access to local media. Error ',
|
||||
error, constraints);
|
||||
if (failure_callback) {
|
||||
failure_callback(error);
|
||||
}
|
||||
|
|
|
@ -406,23 +406,36 @@ UI.onPasswordReqiured = function (callback) {
|
|||
};
|
||||
|
||||
UI.onAuthenticationRequired = function () {
|
||||
// This is the loop that will wait for the room to be created by
|
||||
// someone else. 'auth_required.moderator' will bring us back here.
|
||||
authRetryId = window.setTimeout(
|
||||
function () {
|
||||
Moderator.allocateConferenceFocus(roomName, doJoinAfterFocus);
|
||||
}, 5000);
|
||||
// Show prompt only if it's not open
|
||||
if (authDialog !== null) {
|
||||
return;
|
||||
}
|
||||
// extract room name from 'room@muc.server.net'
|
||||
var room = roomName.substr(0, roomName.indexOf('@'));
|
||||
|
||||
messageHandler.openDialog(
|
||||
authDialog = messageHandler.openDialog(
|
||||
'Stop',
|
||||
'Authentication is required to create room:<br/>' + room,
|
||||
'Authentication is required to create room:<br/><b>' + room +
|
||||
'</b></br> You can either authenticate to create the room or ' +
|
||||
'just wait for someone else to do so.',
|
||||
true,
|
||||
{
|
||||
Authenticate: 'authNow',
|
||||
Close: 'close'
|
||||
Authenticate: 'authNow'
|
||||
},
|
||||
function (onSubmitEvent, submitValue) {
|
||||
console.info('On submit: ' + submitValue, submitValue);
|
||||
|
||||
// Do not close the dialog yet
|
||||
onSubmitEvent.preventDefault();
|
||||
|
||||
// Open login popup
|
||||
if (submitValue === 'authNow') {
|
||||
Toolbar.authenticateClicked();
|
||||
} else {
|
||||
Toolbar.showAuthenticateButton(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -164,15 +164,25 @@ var Toolbar = (function (my) {
|
|||
}
|
||||
|
||||
my.authenticateClicked = function () {
|
||||
// If auth window exists just bring it to the front
|
||||
if (authenticationWindow) {
|
||||
authenticationWindow.focus();
|
||||
return;
|
||||
}
|
||||
// Get authentication URL
|
||||
Moderator.getAuthUrl(function (url) {
|
||||
// Open popup with authentication URL
|
||||
authenticationWindow = messageHandler.openCenteredPopup(
|
||||
url, 500, 400,
|
||||
url, 910, 660,
|
||||
// On closed
|
||||
function () {
|
||||
// Close authentication dialog if opened
|
||||
if (authDialog) {
|
||||
messageHandler.closeDialog();
|
||||
authDialog = null;
|
||||
}
|
||||
// On popup closed - retry room allocation
|
||||
Moderator.allocateConferenceFocus(
|
||||
roomName, doJoinAfterFocus);
|
||||
Moderator.allocateConferenceFocus(roomName, doJoinAfterFocus);
|
||||
authenticationWindow = null;
|
||||
});
|
||||
if (!authenticationWindow) {
|
||||
|
@ -422,7 +432,7 @@ var Toolbar = (function (my) {
|
|||
|
||||
// Shows or hides SIP calls button
|
||||
my.showSipCallButton = function (show) {
|
||||
if (config.hosts.call_control && show) {
|
||||
if (Moderator.isSipGatewayEnabled() && show) {
|
||||
$('#sipCallButton').css({display: "inline"});
|
||||
} else {
|
||||
$('#sipCallButton').css({display: "none"});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* global $, jQuery */
|
||||
var messageHandler = (function(my) {
|
||||
|
||||
/**
|
||||
|
@ -53,15 +54,27 @@ var messageHandler = (function(my) {
|
|||
* @param submitFunction function to be called on submit
|
||||
* @param loadedFunction function to be called after the prompt is fully loaded
|
||||
*/
|
||||
my.openDialog = function(titleString, msgString, persistent, buttons, submitFunction, loadedFunction) {
|
||||
$.prompt(msgString, {
|
||||
my.openDialog = function (titleString, msgString, persistent, buttons,
|
||||
submitFunction, loadedFunction) {
|
||||
var args = {
|
||||
title: titleString,
|
||||
persistent: false,
|
||||
persistent: persistent,
|
||||
buttons: buttons,
|
||||
defaultButton: 1,
|
||||
loaded: loadedFunction,
|
||||
submit: submitFunction
|
||||
});
|
||||
};
|
||||
if (persistent) {
|
||||
args.closeText = '';
|
||||
}
|
||||
return $.prompt(msgString, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes currently opened dialog.
|
||||
*/
|
||||
my.closeDialog = function () {
|
||||
$.prompt.close();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -832,16 +832,7 @@ var VideoLayout = (function (my) {
|
|||
|
||||
var videoSpanId = 'participant_' + resourceJid;
|
||||
|
||||
if ($('#' + videoSpanId).length > 0) {
|
||||
// If there's been a focus change, make sure we add focus related
|
||||
// interface!!
|
||||
if (Moderator.isModerator() && !Moderator.isPeerModerator(peerJid)
|
||||
&& $('#remote_popupmenu_' + resourceJid).length <= 0) {
|
||||
addRemoteVideoMenu(peerJid,
|
||||
document.getElementById(videoSpanId));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!$('#' + videoSpanId).length) {
|
||||
var container =
|
||||
VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId, userId);
|
||||
Avatar.setUserAvatar(peerJid, userId);
|
||||
|
@ -1163,7 +1154,9 @@ var VideoLayout = (function (my) {
|
|||
* Shows a visual indicator for the moderator of the conference.
|
||||
*/
|
||||
my.showModeratorIndicator = function () {
|
||||
if (Moderator.isModerator()) {
|
||||
|
||||
var isModerator = Moderator.isModerator();
|
||||
if (isModerator) {
|
||||
var indicatorSpan = $('#localVideoContainer .focusindicator');
|
||||
|
||||
if (indicatorSpan.children().length === 0)
|
||||
|
@ -1172,38 +1165,48 @@ var VideoLayout = (function (my) {
|
|||
}
|
||||
}
|
||||
Object.keys(connection.emuc.members).forEach(function (jid) {
|
||||
var member = connection.emuc.members[jid];
|
||||
if (member.role === 'moderator') {
|
||||
var moderatorId
|
||||
= 'participant_' + Strophe.getResourceFromJid(jid);
|
||||
|
||||
var moderatorContainer
|
||||
= document.getElementById(moderatorId);
|
||||
|
||||
if (Strophe.getResourceFromJid(jid) === 'focus') {
|
||||
// Skip server side focus
|
||||
return;
|
||||
}
|
||||
if (!moderatorContainer) {
|
||||
console.error("No moderator container for " + jid);
|
||||
|
||||
var resourceJid = Strophe.getResourceFromJid(jid);
|
||||
var videoSpanId = 'participant_' + resourceJid;
|
||||
var videoContainer = document.getElementById(videoSpanId);
|
||||
|
||||
if (!videoContainer) {
|
||||
console.error("No video container for " + jid);
|
||||
return;
|
||||
}
|
||||
var menuSpan = $('#' + moderatorId + '>span.remotevideomenu');
|
||||
if (menuSpan.length) {
|
||||
removeRemoteVideoMenu(moderatorId);
|
||||
}
|
||||
|
||||
var member = connection.emuc.members[jid];
|
||||
|
||||
if (member.role === 'moderator') {
|
||||
// Remove menu if peer is moderator
|
||||
var menuSpan = $('#' + videoSpanId + '>span.remotevideomenu');
|
||||
if (menuSpan.length) {
|
||||
removeRemoteVideoMenu(videoSpanId);
|
||||
}
|
||||
// Show moderator indicator
|
||||
var indicatorSpan
|
||||
= $('#' + moderatorId + ' .focusindicator');
|
||||
= $('#' + videoSpanId + ' .focusindicator');
|
||||
|
||||
if (!indicatorSpan || indicatorSpan.length === 0) {
|
||||
indicatorSpan = document.createElement('span');
|
||||
indicatorSpan.className = 'focusindicator';
|
||||
|
||||
moderatorContainer.appendChild(indicatorSpan);
|
||||
videoContainer.appendChild(indicatorSpan);
|
||||
|
||||
createModeratorIndicatorElement(indicatorSpan);
|
||||
}
|
||||
} else if (isModerator) {
|
||||
// We are moderator, but user is not - add menu
|
||||
if ($('#remote_popupmenu_' + resourceJid).length <= 0) {
|
||||
addRemoteVideoMenu(
|
||||
jid,
|
||||
document.getElementById('participant_' + resourceJid));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -269,7 +269,7 @@ StatsCollector.prototype.start = function ()
|
|||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error("Unsupported key:" + e);
|
||||
console.error("Unsupported key:" + e, e);
|
||||
}
|
||||
|
||||
self.baselineStatsReport = self.currentStatsReport;
|
||||
|
@ -466,7 +466,7 @@ StatsCollector.prototype.processStatsReport = function () {
|
|||
if(!ssrc)
|
||||
continue;
|
||||
var jid = ssrc2jid[ssrc];
|
||||
if (!jid) {
|
||||
if (!jid && (Date.now() - now.timestamp) < 3000) {
|
||||
console.warn("No jid for ssrc: " + ssrc);
|
||||
continue;
|
||||
}
|
||||
|
@ -667,7 +667,7 @@ StatsCollector.prototype.processAudioLevelReport = function ()
|
|||
|
||||
var ssrc = getStatValue(now, 'ssrc');
|
||||
var jid = ssrc2jid[ssrc];
|
||||
if (!jid)
|
||||
if (!jid && (Date.now() - now.timestamp) < 3000)
|
||||
{
|
||||
console.warn("No jid for ssrc: " + ssrc);
|
||||
continue;
|
||||
|
|
49
muc.js
49
muc.js
|
@ -46,11 +46,46 @@ Strophe.addConnectionPlugin('emuc', {
|
|||
this.presMap.length = 0;
|
||||
this.connection.send(pres);
|
||||
},
|
||||
createNonAnonymousRoom: function() {
|
||||
// http://xmpp.org/extensions/xep-0045.html#createroom-reserved
|
||||
|
||||
var getForm = $iq({type: 'get', to: this.roomjid})
|
||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
|
||||
.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
|
||||
|
||||
this.connection.sendIQ(getForm, function (form){
|
||||
|
||||
if (!$(form).find(
|
||||
'>query>x[xmlns="jabber:x:data"]' +
|
||||
'>field[var="muc#roomconfig_whois"]').length) {
|
||||
|
||||
console.error('non-anonymous rooms not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
var formSubmit = $iq({to: this.roomjid, type: 'set'})
|
||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
|
||||
|
||||
formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
|
||||
|
||||
formSubmit.c('field', {'var': 'FORM_TYPE'})
|
||||
.c('value')
|
||||
.t('http://jabber.org/protocol/muc#roomconfig').up().up();
|
||||
|
||||
formSubmit.c('field', {'var': 'muc#roomconfig_whois'})
|
||||
.c('value').t('anyone').up().up();
|
||||
|
||||
this.connection.sendIQ(formSubmit);
|
||||
|
||||
}, function (error){
|
||||
console.error("Error getting room configuration form");
|
||||
});
|
||||
},
|
||||
onPresence: function (pres) {
|
||||
var from = pres.getAttribute('from');
|
||||
var type = pres.getAttribute('type');
|
||||
console.debug("Presence " + from + " - " + type);
|
||||
if (type != null) {
|
||||
|
||||
// What is this for? A workaround for something?
|
||||
if (pres.getAttribute('type')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -110,12 +145,8 @@ Strophe.addConnectionPlugin('emuc', {
|
|||
|
||||
// Parse status.
|
||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
|
||||
// http://xmpp.org/extensions/xep-0045.html#createroom-instant
|
||||
this.isOwner = true;
|
||||
var create = $iq({type: 'set', to: this.roomjid})
|
||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
|
||||
.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
|
||||
this.connection.sendIQ(create); // fire away
|
||||
this.createNonAnonymousRoom();
|
||||
}
|
||||
|
||||
// Parse roles.
|
||||
|
@ -177,8 +208,8 @@ Strophe.addConnectionPlugin('emuc', {
|
|||
UI.onMucRoleChanged(member.role, member.displayName);
|
||||
}
|
||||
}
|
||||
|
||||
// Always trigger presence to update bindings
|
||||
console.log('presence change from', from, pres);
|
||||
$(document).trigger('presence.muc', [from, member, pres]);
|
||||
|
||||
// Trigger status message update
|
||||
|
|
71
recording.js
71
recording.js
|
@ -4,21 +4,76 @@ var Recording = (function (my) {
|
|||
var recordingToken = null;
|
||||
var recordingEnabled;
|
||||
|
||||
/**
|
||||
* Whether to use a jirecon component for recording, or use the videobridge
|
||||
* through COLIBRI.
|
||||
*/
|
||||
var useJirecon = (typeof config.hosts.jirecon != "undefined");
|
||||
|
||||
/**
|
||||
* The ID of the jirecon recording session. Jirecon generates it when we
|
||||
* initially start recording, and it needs to be used in subsequent requests
|
||||
* to jirecon.
|
||||
*/
|
||||
var jireconRid = null;
|
||||
|
||||
my.setRecordingToken = function (token) {
|
||||
recordingToken = token;
|
||||
};
|
||||
|
||||
my.setRecording = function (state, token, callback) {
|
||||
if (useJirecon){
|
||||
this.setRecordingJirecon(state, token, callback);
|
||||
} else {
|
||||
this.setRecordingColibri(state, token, callback);
|
||||
}
|
||||
};
|
||||
|
||||
my.setRecordingJirecon = function (state, token, callback) {
|
||||
if (state == recordingEnabled){
|
||||
return;
|
||||
}
|
||||
|
||||
var iq = $iq({to: config.hosts.jirecon, type: 'set'})
|
||||
.c('recording', {xmlns: 'http://jitsi.org/protocol/jirecon',
|
||||
action: state ? 'start' : 'stop',
|
||||
mucjid: connection.emuc.roomjid});
|
||||
if (!state){
|
||||
iq.attrs({rid: jireconRid});
|
||||
}
|
||||
|
||||
console.log('Start recording');
|
||||
|
||||
connection.sendIQ(
|
||||
iq,
|
||||
function (result) {
|
||||
// TODO wait for an IQ with the real status, since this is
|
||||
// provisional?
|
||||
jireconRid = $(result).find('recording').attr('rid');
|
||||
console.log('Recording ' + (state ? 'started' : 'stopped') +
|
||||
'(jirecon)' + result);
|
||||
recordingEnabled = state;
|
||||
if (!state){
|
||||
jireconRid = null;
|
||||
}
|
||||
|
||||
callback(state);
|
||||
},
|
||||
function (error) {
|
||||
console.log('Failed to start recording, error: ', error);
|
||||
callback(recordingEnabled);
|
||||
});
|
||||
};
|
||||
|
||||
// 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.
|
||||
my.setRecording = function (state, token, callback) {
|
||||
var self = this;
|
||||
my.setRecordingColibri = function (state, token, callback) {
|
||||
var elem = $iq({to: focusMucJid, type: 'set'});
|
||||
elem.c('conference', {
|
||||
xmlns: 'http://jitsi.org/protocol/colibri'
|
||||
});
|
||||
elem.c('recording', {state: state, token: token});
|
||||
elem.up();
|
||||
|
||||
connection.sendIQ(elem,
|
||||
function (result) {
|
||||
|
@ -31,6 +86,7 @@ var Recording = (function (my) {
|
|||
},
|
||||
function (error) {
|
||||
console.warn(error);
|
||||
callback(recordingEnabled);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -43,11 +99,13 @@ var Recording = (function (my) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!recordingToken)
|
||||
// Jirecon does not (currently) support a token.
|
||||
if (!recordingToken && !useJirecon)
|
||||
{
|
||||
UI.messageHandler.openTwoButtonDialog(null,
|
||||
'<h2>Enter recording token</h2>' +
|
||||
'<input id="recordingToken" type="text" placeholder="token" autofocus>',
|
||||
'<input id="recordingToken" type="text" ' +
|
||||
'placeholder="token" autofocus>',
|
||||
false,
|
||||
"Save",
|
||||
function (e, v, m, f) {
|
||||
|
@ -63,7 +121,8 @@ var Recording = (function (my) {
|
|||
},
|
||||
function (event) {
|
||||
document.getElementById('recordingToken').focus();
|
||||
}
|
||||
},
|
||||
function () {}
|
||||
);
|
||||
|
||||
return;
|
||||
|
|
Loading…
Reference in New Issue