Commit 8bc04cbc authored by Dele Olajide's avatar Dele Olajide Committed by dele

OF-716 Modified build.xml to support plugins with embedded web apps

OF-716 Added Jitsi video cpnference web application to plugin


git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@13822 b35dd754-fafc-0310-a699-88a17e54d16e
parent c5d271b9
......@@ -1604,6 +1604,7 @@
<include name="database/**/*.sql"/>
<include name="i18n/*.properties"/>
<include name="web/**/*.*"/>
<include name="**/*.*"/>
<exclude name="web/WEB-INF/web.xml"/>
<exclude name="web/**/*.jsp"/>
<exclude name="web/**/*.jspf"/>
......
......@@ -44,10 +44,10 @@
Jitsi Video Bridge Plugin Changelog
</h1>
<p><b>1.0</b> -- Nov 30th, 2013</p>
<p><b>1.1</b> -- Dec 4th, 2013</p>
<ul>
<li>OF-716 Added to Openfire plugins </li>
<li>OF-716 Added to Openfire plugins with webrtc demo video application</li>
</ul>
<p><b>1.0</b> -- Apr 12, 2013</p>
......
......@@ -4,9 +4,9 @@
<class>org.jitsi.videobridge.openfire.PluginImpl</class>
<description>Integrates Jitsi Video Bridge into Openfire.</description>
<licenseType>other</licenseType>
<minServerVersion>3.0.0</minServerVersion>
<minServerVersion>3.9.0</minServerVersion>
<name>Jitsi Video Bridge</name>
<version>1.0</version>
<version>1.1</version>
<adminconsole>
<tab id="tab-server">
......
......@@ -63,6 +63,7 @@ Jitsi Videobridge does not mix the video channels into a composite video stream,
but only relays the received video channels to all call participants.
Therefore, while it does need to run on a server with good network bandwidth,
CPU horsepower is not that critical for performance.
A demo video conference application using WebRTC is included.
</p>
......@@ -76,5 +77,9 @@ plugin will then be automatically deployed. To upgrade to a new version, copy th
Under Server settings -> Jitsi Videobridge tab you can configure it.
<h2>How to use</h2>
To run the demo video conference application, point your browser at https://your_server:7443/videobridge
</body>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
</web-app>
\ No newline at end of file
/* jshint -W117 */
var bridgejid = 'jitsi-videobridge.' + window.location.hostname;
// static offer taken from chrome M31
var staticoffer = 'v=0\r\no=- 5151055458874951233 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n';
function Colibri(connection, bridgejid) {
this.connection = connection;
this.bridgejid = bridgejid;
this.peers = [];
this.peerconnection = null;
this.sid = Math.random().toString(36).substr(2, 12);
connection.jingle.sessions[this.sid] = this;
this.mychannel = [];
this.channels = [];
this.remotessrc = {};
// ssrc lines to be added on next update
this.addssrc = [];
// ssrc lines to be removed on next update
this.removessrc = [];
this.wait = true;
}
// creates a conferences with an initial set of peers
// FIXME: is the initial set required?
Colibri.prototype.makeConference = function (peers) {
var ob = this;
this.peers = [];
peers.forEach(function(peer) {
ob.peers.push(peer);
});
var elem = $iq({to: this.bridgejid, type: 'get'});
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
// FIXME: static contents are bad
var contents = ['audio', 'video'];
contents.forEach(function(name) {
elem.c('content', {creator: 'initiator', name: name});
elem.c('channel', {initiator: 'true'});
elem.up(); // end of channel
for (var j = 0; j < peers.length; j++) {
elem.c('channel', {initiator: 'true' }).up();
}
elem.up(); // end of content
});
this.connection.sendIQ(elem,
function(result) {
ob.createdConference(result);
},
function (error) {
console.warn(error);
}
);
};
// callback when a conference was created
Colibri.prototype.createdConference = function(result) {
console.log('created a conference on the bridge');
var tmp;
this.confid = $(result).find('>conference').attr('id');
this.remotecontents = $(result).find('>conference>content').get();
for (var i = 0; i < this.remotecontents.length; i++) {
tmp = $(this.remotecontents[i]).find('>channel').get();
this.mychannel.push($(tmp.shift()));
for (j = 0; j < tmp.length; j++) {
if (this.channels[j] === undefined) {
this.channels[j] = [];
}
this.channels[j].push(tmp[j]);
}
}
console.log('remote channels', this.channels);
var ob = this;
this.peers.forEach(function(peer, participant) {
ob.initiate(peer, true);
});
};
// send a session-initiate to a new participant
Colibri.prototype.initiate = function (peer, isInitiator) {
console.log('tell', peer, participant);
var participant = this.peers.indexOf(peer);
var sdp;
if (this.peerconnection != null && this.peerconnection.signalingState == 'stable') {
sdp = new SDP(this.peerconnection.remoteDescription.sdp);
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
// throw away stuff we don't want
// not needed with static offer
var line;
sdp.session = sdp.session.replace(SDPUtil.find_line(sdp.session, 'a=group:') + '\r\n', '');
sdp.session = sdp.session.replace(SDPUtil.find_line(sdp.session, 'a=msid-semantic:') + '\r\n', '');
for (var j = 0; j < sdp.media.length; j++) {
sdp.media[j] = sdp.media[j].replace('a=rtcp-mux\r\n', '');
while (SDPUtil.find_line(sdp.media[j], 'a=ssrc:')) {
sdp.media[j] = sdp.media[j].replace(SDPUtil.find_line(sdp.media[j], 'a=ssrc:') + '\r\n', '');
}
while (SDPUtil.find_line(sdp.media[j], 'a=crypto:')) {
sdp.media[j] = sdp.media[j].replace(SDPUtil.find_line(sdp.media[j], 'a=crypto:') + '\r\n', '');
}
while (SDPUtil.find_line(sdp.media[j], 'a=candidate:')) {
sdp.media[j] = sdp.media[j].replace(SDPUtil.find_line(sdp.media[j], 'a=candidate:') + '\r\n', '');
}
sdp.media[j] = sdp.media[j].replace('a=ice-options:google-ice\r\n', '');
sdp.media[j] = sdp.media[j].replace(SDPUtil.find_line(sdp.media[j], 'a=ice-ufrag:') + '\r\n', '');
sdp.media[j] = sdp.media[j].replace(SDPUtil.find_line(sdp.media[j], 'a=ice-pwd:') + '\r\n', '');
sdp.media[j] = sdp.media[j].replace(SDPUtil.find_line(sdp.media[j], 'a=fingerprint:') + '\r\n', '');
sdp.media[j] = sdp.media[j].replace(SDPUtil.find_line(sdp.media[j], 'a=setup:') + '\r\n', '');
// re-add all remote a=ssrcs
for (var jid in this.remotessrc) {
if (jid == peer) continue;
sdp.media[j] += this.remotessrc[jid][j];
}
// and local a=ssrc lines
sdp.media[j] += SDPUtil.find_lines(localSDP.media[j], 'a=ssrc').join('\r\n') + '\r\n';
}
sdp.raw = sdp.session + sdp.media.join('');
} else {
sdp = new SDP(staticoffer);
}
// make a new colibri session and configure it
var sess = new ColibriSession(this.connection.jid,
Math.random().toString(36).substr(2, 12), // random string
this.connection);
sess.initiate(peer);
sess.colibri = this;
sess.localStream = this.connection.jingle.localStream;
sess.media_constraints = this.connection.jingle.media_constraints;
sess.pc_constraints = this.connection.jingle.pc_constraints;
sess.ice_config = this.connection.ice_config;
connection.jingle.sessions[sess.sid] = sess;
connection.jingle.jid2session[sess.peerjid] = sess;
var init = $iq({to: sess.peerjid,
type: 'set'})
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
action: 'session-initiate',
initiator: sess.me,
sid: sess.sid });
// add stuff we got from the bridge
for (var j = 0; j < sdp.media.length; j++) {
var chan = $(this.channels[participant][j]);
console.log('channel id', chan.attr('id'));
tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
if (tmp.length) {
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' +'\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' +'\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabela0 mixedlabela0' +'\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabela0' +'\r\n';
} else {
// make chrome happy... '3735928559' == 0xDEADBEEF
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' +'\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' +'\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabelv0 mixedlabelv0' +'\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabelv0' +'\r\n';
}
tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
if (tmp.length) {
if (tmp.attr('ufrag'))
sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
if (tmp.attr('pwd'))
sdp.media[j] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
// and the candidates...
tmp.find('>candidate').each(function () {
sdp.media[j] += SDPUtil.candidateFromJingle(this);
});
tmp = tmp.find('>fingerprint');
if (tmp.length) {
sdp.media[j] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
/*
if (tmp.attr('direction')) {
sdp.media[j] += 'a=setup:' + tmp.attr('direction') + '\r\n';
}
*/
sdp.media[j] += 'a=setup:actpass\r\n';
}
}
}
sdp.toJingle(init, 'initiator');
this.connection.sendIQ(init,
function (res) {
console.log('got result');
},
function (err) {
console.log('got error');
}
);
}
// pull in a new participant into the conference
// FIXME: lots of duplicate code with makeconference
// note that here the id is set
// FIXME: does this needs safeguards?
Colibri.prototype.addNewParticipant = function(peer) {
var ob = this;
var index = this.channels.length;
this.channels.push([])
this.peers.push(peer);
var elem = $iq({to: this.bridgejid, type: 'get'});
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
var contents = ['audio', 'video'];
contents.forEach(function(name) {
elem.c('content', {creator: 'initiator', name: name});
elem.c('channel', {initiator: 'true'});
elem.up(); // end of channel
elem.up(); // end of content
});
this.connection.sendIQ(elem,
function(result) {
var contents = $(result).find('>conference>content').get();
for (var i = 0; i < contents.length; i++) {
tmp = $(contents[i]).find('>channel').get();
ob.channels[index][i] = tmp[0];
}
ob.initiate(peer, true);
},
function (error) {
console.warn(error);
}
);
}
// update the channel description (payload-types + dtls fp) for a participant
Colibri.prototype.updateChannel = function (remoteSDP, participant) {
console.log('change allocation for', this.confid);
var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (channel = 0; channel < this.channels[participant].length; channel++) {
change.c('content', {name: channel == 0 ? 'audio' : 'video'});
change.c('channel', {id: $(this.channels[participant][channel]).attr('id'), initiator: 'true'});
console.log('channel id', $(this.channels[participant][channel]).attr('id'));
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
rtpmap.forEach(function (val) {
// TODO: too much copy-paste
var rtpmap = SDPUtil.parse_rtpmap(val);
change.c('payload-type', rtpmap);
//
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
for (var k = 0; k < tmp.length; k++) {
change.c('parameter', tmp[k]).up();
}
}
change.up();
});
// now add transport
change.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
var fingerprints = SDPUtil.find_lines(remoteSDP.media[channel], 'a=fingerprint:', remoteSDP.session);
fingerprints.forEach(function (line) {
tmp = SDPUtil.parse_fingerprint(line);
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
change.c('fingerprint').t(tmp.fingerprint);
delete tmp.fingerprint;
line = SDPUtil.find_line(remoteSDP.media[channel], 'a=setup:', remoteSDP.session);
if (line) {
tmp.setup = line.substr(8);
}
change.attrs(tmp);
change.up();
});
var candidates = SDPUtil.find_lines(remoteSDP.media[channel], 'a=candidate:', remoteSDP.session);
candidates.forEach(function (line) {
var tmp = SDPUtil.candidateToJingle(line);
change.c('candidate', tmp).up();
});
tmp = SDPUtil.iceparams(remoteSDP.media[channel], remoteSDP.session);
if (tmp) {
change.attrs(tmp);
}
change.up(); // end of transport
change.up(); // end of channel
change.up(); // end of content
}
this.connection.sendIQ(change,
function (res) {
console.log('got result');
},
function (err) {
console.log('got error');
}
);
};
// tell everyone about a new participants a=ssrc lines (isadd is true)
// or a leaving participants a=ssrc lines
// FIXME: should not take an SDP, but rather the a=ssrc lines and probably a=mid
Colibri.prototype.sendSSRCUpdate = function(sdp, exclude, isadd) {
var ob = this;
this.peers.forEach(function(peerjid) {
console.warn('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', exclude);
if (peerjid == exclude) return;
var channel;
var peersess = ob.connection.jingle.jid2session[peerjid];
var modify = $iq({to: peerjid, type: 'set'})
.c('jingle', {
xmlns: 'urn:xmpp:jingle:1',
action: isadd ? 'addsource' : 'removesource',
initiator: peersess.initiator,
sid: peersess.sid
}
);
for (channel = 0; channel < sdp.media.length; channel++) {
tmp = SDPUtil.find_lines(sdp.media[channel], 'a=ssrc:');
modify.c('content', {name: SDPUtil.parse_mid(SDPUtil.find_line(sdp.media[channel], 'a=mid:'))});
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
// FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
tmp.forEach(function(line) {
var idx = line.indexOf(' ');
var linessrc = line.substr(0, idx).substr(7);
modify.attrs({ssrc:linessrc});
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();
});
modify.up(); // end of source
modify.up(); // end of content
}
ob.connection.sendIQ(modify,
function (res) {
console.warn('got modify result');
},
function (err) {
console.warn('got modify error');
}
);
});
};
Colibri.prototype.setRemoteDescription = function(session, elem, desctype) {
var participant = this.peers.indexOf(session.peerjid);
console.log('Colibri.setRemoteDescription from', session.peerjid, participant);
var ob = this;
var remoteSDP = new SDP('');
var tmp;
var channel;
remoteSDP.fromJingle(elem);
// ACT 1: change allocation on bridge
this.updateChannel(remoteSDP, participant);
// ACT 1.1: tell anyone else about the new SSRCs
this.sendSSRCUpdate(remoteSDP, session.peerjid, true);
// ACT 1.2: note the SSRCs
this.remotessrc[session.peerjid] = [];
for (channel = 0; channel < this.channels[participant].length; channel++) {
this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
}
// ACT 2: set remote description
console.log('set the remote description');
for (channel = 0; channel < this.channels[participant].length; channel++) {
while (SDPUtil.find_line(remoteSDP.media[channel], 'a=candidate:')) {
remoteSDP.media[channel] = remoteSDP.media[channel].replace(SDPUtil.find_line(remoteSDP.media[channel], 'a=candidate:') + '\r\n', '');
}
remoteSDP.media[channel] = remoteSDP.media[channel].replace(SDPUtil.find_line(remoteSDP.media[channel], 'a=ice-ufrag:') + '\r\n', '');
remoteSDP.media[channel] = remoteSDP.media[channel].replace(SDPUtil.find_line(remoteSDP.media[channel], 'a=ice-pwd:') + '\r\n', '');
remoteSDP.media[channel] = remoteSDP.media[channel].replace(SDPUtil.find_line(remoteSDP.media[channel], 'a=fingerprint:') + '\r\n', '');
remoteSDP.media[channel] = remoteSDP.media[channel].replace(SDPUtil.find_line(remoteSDP.media[channel], 'a=setup:') + '\r\n', '');
// omitting it just for audio does not work
if (this.peerconnection != null) {
if (!this.addssrc[channel]) this.addssrc[channel] = '';
this.addssrc[channel] += SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
while (SDPUtil.find_line(remoteSDP.media[channel], 'a=ssrc:')) {
remoteSDP.media[channel] = remoteSDP.media[channel].replace(SDPUtil.find_line(remoteSDP.media[channel], 'a=ssrc:') + '\r\n', '');
}
}
// get the mixed ssrc
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
if (tmp.length) {
remoteSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' +'\r\n';
remoteSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' +'\r\n';
remoteSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabela0 mixedlabela0' +'\r\n';
remoteSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabela0' +'\r\n';
}
tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
if (tmp.length) {
remoteSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
remoteSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
tmp.find('>candidate').each(function() {
remoteSDP.media[channel] += SDPUtil.candidateFromJingle(this);
});
tmp = tmp.find('>fingerprint');
if (tmp.length) {
remoteSDP.media[channel] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
if (tmp.attr('direction')) {
remoteSDP.media[channel]+= 'a=setup:' + tmp.attr('direction') + '\r\n';
}
}
}
}
remoteSDP.raw = remoteSDP.session + remoteSDP.media.join('');
// ACT 3: reinitialize peerconnection
// FIXME: this should happen earlier
if (this.peerconnection === null) {
this.peerconnection = new RTC.peerconnection(connection.jingle.ice_config, connection.jingle.pc_constraints);
this.peerconnection.oniceconnectionstatechange = function (event) {
console.warn('ice connection state changed to', ob.peerconnection.iceConnectionState);
/*
if (ob.peerconnection.signalingState == 'stable' && ob.peerconnection.iceConnectionState == 'connected') {
console.log('adding new remote SSRCs from iceconnectionstatechange');
window.setTimeout(function() { ob.modifySources(); }, 1000);
}
*/
}
this.peerconnection.onsignalingstatechange = function (event) {
console.warn(ob.peerconnection.signalingState);
/*
if (ob.peerconnection.signalingState == 'stable' && ob.peerconnection.iceConnectionState == 'connected') {
console.log('adding new remote SSRCs from signalingstatechange');
window.setTimeout(function() { ob.modifySources(); }, 1000);
}
*/
}
this.peerconnection.addStream(connection.jingle.localStream);
var remote = new RTCSessionDescription({type:'offer', sdp:remoteSDP.raw});
this.peerconnection.onaddstream = function (event) {
ob.remoteStream = event.stream;
$(document).trigger('remotestreamadded.jingle', [event, ob.sid]);
};
this.peerconnection.onicecandidate = function (event) {
ob.sendIceCandidate(event.candidate);
};
this.peerconnection.setRemoteDescription(remote,
function () {
console.log('setRemoteDescription success');
ob.peerconnection.createAnswer(
function (answer) {
console.log('now do the dance with the bridge...')
ob.peerconnection.setLocalDescription(answer,
function() {
console.log('setLocalDescription success');
},
function(error) {
console.error('setLocalDescription failed', error);
}
);
var mydesc = $iq({to: ob.bridgejid, type: 'set'});
var localSDP = new SDP(answer.sdp);
mydesc.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: ob.confid});
for (var channel = 0; channel < localSDP.media.length; channel++) {
console.log('my channel', $(ob.mychannel[channel]).attr('id'));
mydesc.c('content', {name: channel == 0 ? 'audio' : 'video' });
mydesc.c('channel', {id: $(ob.mychannel[channel]).attr('id'), initiator: 'true'});
var mline = SDPUtil.parse_mline(localSDP.media[channel].split('\r\n')[0]);
for (j = 0; j < mline.fmt.length; j++) {
rtpmap = SDPUtil.find_line(localSDP.media[channel], 'a=rtpmap:' + mline.fmt[j]);
mydesc.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
if (SDPUtil.find_line(localSDP.media[channel], 'a=fmtp:' + mline.fmt[j])) {
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(localSDP.media[channel], 'a=fmtp:' + mline.fmt[j]));
for (k = 0; k < tmp.length; k++) {
mydesc.c('parameter', tmp[k]).up();
}
}
mydesc.up();
}
mydesc.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
tmp = SDPUtil.iceparams(localSDP.media[channel], localSDP.session);
if (tmp) {
mydesc.attrs(tmp);
var fingerprints = SDPUtil.find_lines(localSDP.media[channel], 'a=fingerprint:', localSDP.session);
fingerprints.forEach(function (line) {
tmp = SDPUtil.parse_fingerprint(line);
//tmp.xmlns = 'urn:xmpp:tmp:jingle:apps:dtls:0';
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
mydesc.c('fingerprint').t(tmp.fingerprint);
delete tmp.fingerprint;
line = SDPUtil.find_line(localSDP.media[channel], 'a=setup:', ob.session);
if (line) {
tmp.setup = line.substr(8);
}
mydesc.attrs(tmp);
mydesc.up();
});
// XEP-0176
if (SDPUtil.find_line(localSDP.media[channel], 'a=candidate:', localSDP.session)) { // add any a=candidate lines
lines = SDPUtil.find_lines(localSDP.media[channel], 'a=candidate:', localSDP.session);
for (j = 0; j < lines.length; j++) {
tmp = SDPUtil.candidateToJingle(lines[j]);
mydesc.c('candidate', tmp).up();
}
}
mydesc.up(); // end of transport
mydesc.up(); // end of channel
mydesc.up(); // end of content
}
}
ob.connection.sendIQ(mydesc,
function (res) {
console.log('got result');
},
function (err) {
console.log('got error');
}
);
// tell everyone our actual ssrc
ob.sendSSRCUpdate(localSDP, null, true);
},
function (error) {
console.warn(error);
}
);
},
function (error) {
console.error('setRemoteDescription failed', error);
}
);
}
// ACT 4: add new a=ssrc lines to local remotedescription
this.modifySources();
};
// relay ice candidates to bridge using trickle
Colibri.prototype.addIceCandidate = function (session, elem) {
var ob = this;
var participant = this.peers.indexOf(session.peerjid);
console.log('change transport allocation for', this.confid, session.peerjid, participant);
var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
$(elem).each(function() {
var name = $(this).attr('name');
var channel = name == 'audio' ? 0 : 1;
change.c('content', {name: name});
change.c('channel', {id: $(ob.channels[participant][channel]).attr('id'), initiator: 'true'});
$(this).find('>transport').each(function() {
change.c('transport', {
ufrag: $(this).attr('ufrag'),
pwd: $(this).attr('pwd'),
xmlns: $(this).attr('xmlns')
});
$(this).find('>candidate').each(function () {
/* not yet
if (this.getAttribute('protocol') == 'tcp' && this.getAttribute('port') == 0) {
// chrome generates TCP candidates with port 0
return;
}
*/
var line = SDPUtil.candidateFromJingle(this);
change.c('candidate', SDPUtil.candidateToJingle(line)).up();
});
change.up(); // end of transport
});
change.up(); // end of channel
change.up(); // end of content
});
// FIXME: need to check if there is at least one candidate when filtering TCP ones
this.connection.sendIQ(change,
function (res) {
console.log('got result');
},
function (err) {
console.warn('got error');
}
);
};
// send our own candidate to the bridge
Colibri.prototype.sendIceCandidate = function(candidate) {
//console.log('candidate', candidate);
if (!candidate) {
console.log('end of candidates');
return;
}
var mycands = $iq({to: this.bridgejid, type: 'set'});
mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
mycands.c('content', {name: candidate.sdpMid });
mycands.c('channel', {id: $(this.mychannel[candidate.sdpMLineIndex]).attr('id'), initiator: 'true'});
mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
tmp = SDPUtil.candidateToJingle(candidate.candidate);
mycands.c('candidate', tmp).up();
this.connection.sendIQ(mycands,
function (res) {
console.log('got result');
},
function (err) {
console.warn('got error');
}
);
};
Colibri.prototype.terminate = function (session, reason) {
console.log('remote session terminated from', session.peerjid);
var participant = this.peers.indexOf(session.peerjid);
if (!this.remotessrc[session.peerjid] || participant == -1) {
return;
}
console.log('remote ssrcs:', this.remotessrc[session.peerjid]);
var ssrcs = this.remotessrc[session.peerjid];
for (var i = 0; i < ssrcs.length; i++) {
if (!this.removessrc[i]) this.removessrc[i] = '';
this.removessrc[i] += ssrcs[i];
}
// remove from this.peers
this.peers.splice(participant, 1);
// and from channels -- this wil timeout on the bridge
// FIXME: this suggests keeping parallel arrays is wrong
this.channels.splice(participant, 1);
// tell everyone about the ssrcs to be removed
var sdp = new SDP('');
var contents = ['audio', 'video'];
for (var j = 0; j < ssrcs.length; j++) {
sdp.media[j] = 'a=mid:' + contents[j] + '\r\n';
sdp.media[j] += ssrcs[j];
this.removessrc[j] += ssrcs[j];
}
this.sendSSRCUpdate(sdp, session.peerjid, false);
delete this.remotessrc[session.peerjid];
this.modifySources();
}
Colibri.prototype.modifySources = function() {
var ob = this;
if (!(this.addssrc.length || this.removessrc.length)) return;
// FIXME: this is a big hack
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
window.setTimeout(function() { ob.modifySources(); }, 250);
this.wait = true;
return;
}
if (this.wait) {
window.setTimeout(function() { ob.modifySources(); }, 2500);
this.wait = false;
return;
}
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
// mangle SDP a little
// remove the msid-semantics from the remote description, if any
if (SDPUtil.find_line(sdp.session, 'a=msid-semantic:')) {
sdp.session = sdp.session.replace(SDPUtil.find_line(sdp.session, 'a=msid-semantic:') + '\r\n', '');
}
// 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 = [];
sdp.raw = sdp.session + sdp.media.join('');
this.peerconnection.setRemoteDescription(
new RTCSessionDescription({type: 'offer', sdp: sdp.raw }),
function() {
console.log('setModifiedRemoteDescription ok');
ob.peerconnection.createAnswer(
function(modifiedAnswer) {
console.log('modifiedAnswer created');
// FIXME: pushing down an answer while ice connection state
// is still checking is bad...
console.log(ob.peerconnection.iceConnectionState);
ob.peerconnection.setLocalDescription(modifiedAnswer,
function() {
console.log('setModifiedLocalDescription ok');
},
function(error) {
console.log('setModifiedLocalDescription failed');
}
);
},
function(error) {
console.log('createModifiedAnswer failed');
}
);
},
function(error) {
console.log('setModifiedRemoteDescription failed');
}
);
}
// 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;
};
ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
console.log('ColibriSession.initiate');
this.peerjid = peerjid;
};
ColibriSession.prototype.sendOffer = function (offer) {
console.log('ColibriSession.sendOffer');
};
ColibriSession.prototype.accept = function () {
console.log('ColibriSession.accept');
};
ColibriSession.prototype.terminate = function (reason) {
console.log('ColibriSession.terminate');
this.colibri.terminate(this, reason);
};
ColibriSession.prototype.active = function () {
console.log('ColibriSession.active');
};
ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
console.log('ColibriSession.setRemoteDescription');
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) {
console.log('ColibriSession.sendTerminate');
};
\ No newline at end of file
<html>
<head>
<title>Jitsi Videobridge</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="strophejingle.bundle.js"></script>
<script src="colibri.js"></script>
<script src="videobridge.js"></script>
<script src="muc.js"></script>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<style type='text/css'>
html,body{margin:0px;}
#largeVideo {width:1280px;height:720px;margin-left:auto;margin-right:auto;display:block}
#localVideo {-webkit-transform: scale(-1,1);}
#remoteVideos {text-align:center;height:180px;}
#remoteVideos video {width:320;height:180;}
#spacer {height:40px;}
#settings {display:none;}
#header {text-align:center;visibility:hidden;}
#nowebrtc {display:none;}
</style>
</head>
<body>
<div id="header" onclick='window.prompt("Share this link with anyone you want to invite to join you:", window.location.href);return;'>
<i class='fa fa-external-link'>&nbsp;</i>Others can join you by just going to <span id='roomurl'></span>
</div>
<div id="settings">
<h1>Connection Settings</h1>
<form id="loginInfo">
<label>JID: <input id="jid" type="text" name="jid" placeholder="me@example.com"/></label>
<label>Password: <input id="password" type="password" name="password" placeholder="secret"/></label>
<input id="connect" type="submit" value="Connect" />
</form>
</div>
<video id="largeVideo" autoplay oncontextmenu="return false;"></video>
<div id="spacer"></div>
<div id="remoteVideos">
<video id="localVideo" autoplay oncontextmenu="return false;" muted/>
</div>
<script>
var connection = null;
var master = null;
var RTC = setupRTC();
if (RTC == null) {
alert('Sorry, your browser is not WebRTC enabled!'); // BAO
window.location.href = 'about:blank';
} else if (RTC.browser != 'chrome') {
alert('Sorry, only Chrome supported for now!'); // BAO
window.location.href = 'about:blank';
}
var RTCPeerconnection = RTC.peerconnection;
document.getElementById('jid').value = window.location.hostname;
var connType = "BOSH"; // change this line to WEBSOCKETS for Openfire websockets
if (connType == "WEBSOCKETS")
connection = new Openfire.Connection(window.location.protocol + '//' + window.location.host + '/http-bind/'); // BAO
else
connection = new Strophe.Connection(window.location.protocol + '//' + window.location.host + '/http-bind/'); // BAO
window.connection.resource = Math.random().toString(36).substr(2, 20);
connection.rawInput = function (data) { console.log('RECV: ' + data); };
connection.rawOutput = function (data) { console.log('SEND: ' + data); };
connection.jingle.pc_constraints = RTC.pc_constraints;
var jid = document.getElementById('jid').value || window.location.hostname;
connection.connect(jid, document.getElementById('password').value, function(status) {
if (status == Strophe.Status.CONNECTED) {
console.log('connected');
connection.send($pres()); // BAO
getUserMediaWithConstraints(['audio','video'], '360');
document.getElementById('connect').disabled = true;
} else {
console.log('status', status);
}
});
$(document).bind('mediaready.jingle', function(event, stream) {
connection.emuc.doJoin();
connection.jingle.localStream = stream;
RTC.attachMediaStream($('#localVideo'), stream);
document.getElementById('localVideo').muted = true;
document.getElementById('localVideo').autoplay = true;
document.getElementById('localVideo').volume = 0;
document.getElementById('largeVideo').volume = 0;
document.getElementById('largeVideo').src = document.getElementById('localVideo').src;
});
//$(document).bind('mediafailure.jingle', onMediaFailure);
$(document).bind('remotestreamadded.jingle', function(event, data, sid) {
// TODO: when adding a video, make sure they fit in a single row
function waitForRemoteVideo(selector, sid) {
var sess = connection.jingle.sessions[sid];
videoTracks = data.stream.getVideoTracks();
if (videoTracks.length === 0 || selector[0].currentTime > 0) {
RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
$(document).trigger('callactive.jingle', [selector, sid]);
console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState);
} else {
setTimeout(function() { waitForRemoteVideo(selector, sid); }, 100);
}
}
var sess = connection.jingle.sessions[sid];
var vid = document.createElement('video');
var id = 'remoteVideo_' + sid + '_' + data.stream.id;
vid.id = id;
vid.autoplay = true;
vid.oncontextmenu = function() { return false; };
var remotes = document.getElementById('remoteVideos');
remotes.appendChild(vid);
var sel = $('#' + id);
sel.hide();
RTC.attachMediaStream(sel, data.stream);
waitForRemoteVideo(sel, sid);
data.stream.onended = function() {
console.log('stream ended', this.id);
var src = $('#' + id).attr('src');
$('#' + id).remove();
if (src === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
// ... well, if nobody else is left, this picks the local video
var pick = $('#remoteVideos :visible:last').get(0);
// mute if localvideo
document.getElementById('largeVideo').volume = pick.volume;
document.getElementById('largeVideo').src = pick.src;
}
}
// FIXME: hover is bad, this causes flicker. How about moving this?
// remember that moving this in the DOM requires to play() again
sel.hover(
function() {
console.log('hover in', $(this).attr('src'));
if ($('#largeVideo').attr('src') != $(this).attr('src')) {
document.getElementById('largeVideo').volume = 1;
$('#largeVideo').attr('src', $(this).attr('src'));
}
},
function() {
//console.log('hover out', $(this).attr('src'));
//$('#largeVideo').attr('src', null);
}
);
});
$(document).bind('callincoming.jingle', function(event, sid) {
var sess = connection.jingle.sessions[sid];
sess.sendAnswer();
sess.accept();
});
$(document).bind('callactive.jingle', function(event, videoelem, sid) {
console.log('call active');
if (videoelem.attr('id').indexOf('mixedmslabel') == -1) {
// ignore mixedmslabela0 and v0
videoelem.show();
document.getElementById('largeVideo').volume = 1;
$('#largeVideo').attr('src', videoelem.attr('src'));
}
});
$(document).bind('callterminated.jingle', function(event, sid, reason) {
// FIXME
});
function resizeLarge() {
var availableHeight = window.innerHeight;
availableHeight -= $('#remoteVideos').height();
availableHeight -= 100; // padding + link ontop
var availableWidth = window.innerWidth;
var aspectRatio = 16.0 / 9.0;
if (availableHeight < availableWidth / aspectRatio) {
availableWidth = availableHeight * aspectRatio;
}
if (availableWidth < 0 || availableHeight < 0) return;
$('#largeVideo').width(availableWidth);
$('#largeVideo').height(availableWidth/aspectRatio);
}
$(document).ready(function() {
resizeLarge();
$(window).resize(function() {
resizeLarge();
});
});
</script>
</body>
</html>
......@@ -17,10 +17,17 @@ import org.jitsi.util.*;
import org.jitsi.videobridge.*;
import org.jivesoftware.openfire.container.*;
import org.jivesoftware.util.*;
import org.jivesoftware.openfire.http.HttpBindManager;
import org.slf4j.*;
import org.slf4j.Logger;
import org.xmpp.component.*;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.webapp.WebAppContext;
/**
* Implements <tt>org.jivesoftware.openfire.container.Plugin</tt> to integrate
* Jitsi Video Bridge into Openfire.
......@@ -37,6 +44,12 @@ public class PluginImpl
*/
private static final Logger Log = LoggerFactory.getLogger(PluginImpl.class);
/**
* The name of the property that contains the name of video conference application
*/
public static final String VIDEO_CONFERENCE_PROPERTY_NAME
= "org.jitsi.videobridge.video.conference.name";
/**
* The name of the property that contains the maximum port number that we'd
* like our RTP managers to bind upon.
......@@ -122,6 +135,22 @@ public class PluginImpl
System.setProperty("net.java.sip.communicator.SC_HOME_DIR_LOCATION", pluginDirectory.getPath());
System.setProperty("net.java.sip.communicator.SC_HOME_DIR_NAME", ".");
// start video conference web application
try {
String appName = JiveGlobals.getProperty(VIDEO_CONFERENCE_PROPERTY_NAME, "videobridge");
Log.info("Initialize Web App " + appName);
ContextHandlerCollection contexts = HttpBindManager.getInstance().getContexts();
WebAppContext context = new WebAppContext(contexts, pluginDirectory.getPath(), "/" + appName);
context.setWelcomeFiles(new String[]{"index.html"});
}
catch(Exception e) {
Log.error( "Jitsi Videobridge web app initialize error", e);
}
// Let's check for custom configuration
String maxVal = JiveGlobals.getProperty(MAX_PORT_NUMBER_PROPERTY_NAME);
String minVal = JiveGlobals.getProperty(MIN_PORT_NUMBER_PROPERTY_NAME);
......
var CONFERENCEDOMAIN = 'conference.' + window.location.hostname; // BAO
Strophe.addConnectionPlugin('emuc', {
connection: null,
roomjid: null,
myroomjid: null,
list_members: [],
isOwner: false,
init: function (conn) {
this.connection = conn;
},
doJoin: function () {
var roomnode = urlParam("r"); // BAO
console.log("roomnode = " + roomnode);
if (!roomnode) {
roomnode = Math.random().toString(36).substr(2, 20);
window.history.pushState('VideoChat', 'Room: ' + roomnode, window.location.pathname + "?r=" + roomnode);
}
if (this.roomjid == null) {
this.roomjid = roomnode + '@' + CONFERENCEDOMAIN;
}
this.myroomjid = this.roomjid + '/' + Strophe.getNodeFromJid(this.connection.jid);
console.log('joining', this.roomjid);
// muc stuff
this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true});
this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true});
this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true});
this.connection.send($pres({to: this.myroomjid }).c('x', {xmlns: 'http://jabber.org/protocol/muc'}));
},
onPresence: function (pres) {
var from = pres.getAttribute('from'),
type = pres.getAttribute('type');
if (type != null) {
return true;
}
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.send(create); // fire away
}
if (from == this.myroomjid) {
this.onJoinComplete();
} else if (this.list_members.indexOf(from) == -1) {
// new participant
this.list_members.push(from);
// FIXME: belongs into an event so we can separate emuc and colibri
if (master !== null) {
// FIXME: this should prepare the video
if (master.peers.length == 0) {
console.log('make new conference with', from);
master.makeConference(this.list_members);
} else {
console.log('invite', from, 'into conference');
master.addNewParticipant(from);
}
}
} else {
console.log('presence change from', from);
}
return true;
},
onPresenceUnavailable: function (pres) {
// FIXME: first part doesn't belong into EMUC
// FIXME: this should actually hide the video already for a nicer UX
this.connection.jingle.terminateByJid($(pres).attr('from'));
/*
if (Object.keys(this.connection.jingle.sessions).length == 0) {
console.log('everyone left');
}
*/
for (var i = 0; i < this.list_members.length; i++) {
if (this.list_members[i] == $(pres).attr('from')) {
this.list_members.splice(i, 1);
break;
}
}
if (this.list_members.length == 0) {
console.log('everyone left');
}
return true;
},
onPresenceError: function(pres) {
var ob = this;
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
window.setTimeout(function() {
var given = window.prompt('Password required');
if (given != null) {
ob.connection.send($pres({to: ob.myroomjid }).c('x', {xmlns: 'http://jabber.org/protocol/muc'}).c('password').t(given));
} else {
// user aborted
}
}, 50);
} else {
console.warn('onPresError ', pres);
}
return true;
},
onJoinComplete: function() {
console.log('onJoinComplete');
$('#roomurl').text(window.location.href);
$('#header').css('visibility', 'visible');
if (this.list_members.length < 1) {
// FIXME: belongs into an event so we can separate emuc and colibri
master = new Colibri(connection, bridgejid);
return;
}
},
lockRoom: function(key) {
//http://xmpp.org/extensions/xep-0045.html#roomconfig
var ob = this;
this.connection.sendIQ($iq({to:this.roomjid, type:'get'}).c('query', {xmlns:'http://jabber.org/protocol/muc#owner'}),
function(res) {
if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
var formsubmit = $iq({to:ob.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_roomsecret'}).c('value').t(key).up().up();
// FIXME: is muc#roomconfig_passwordprotectedroom required?
this.connection.sendIQ(formsubmit,
function(res) {
console.log('set room password');
},
function(err) {
console.warn('setting password failed', err);
}
);
} else {
console.warn('room passwords not supported');
}
},
function(err) {
console.warn('setting password failed', err);
}
);
}
});
$(window).bind('beforeunload', function() {
if (connection && connection.connected) {
// ensure signout
$.ajax({
type: 'POST',
url: '/http-bind',
async: false,
cache: false,
contentType: 'application/xml',
data: "<body rid='" + connection.rid + "' xmlns='http://jabber.org/protocol/httpbind' sid='" + connection.sid + "' type='terminate'><presence xmlns='jabber:client' type='unavailable'/></body>",
success: function(data) {
console.log('signed out');
console.log(data);
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log('signout error', textStatus + ' (' + errorThrown + ')');
}
});
}
})
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
function urlParam(name)
{
var results = new RegExp('[\\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (!results) { return undefined; }
return results[1] || undefined;
}
var Openfire = {};
/** Class: Openfire.Connection
* WebSockets Connection Manager for Openfire
*
* Thie class manages an WebSockets connection
* to an Openfire XMPP server through the WebSockets plugin and dispatches events to the user callbacks as
* data arrives. It uses the server side Openfire authentication
*
* After creating a Openfire object, the user will typically
* call connect() with a user supplied callback to handle connection level
* events like authentication failure, disconnection, or connection
* complete.
*
* To send data to the connection, use send(doc) or sendRaw(text)
*
* Use xmlInput(doc) and RawInput(text) overrideable function to receive XML data coming into the
* connection.
*
* The user will also have several event handlers defined by using
* addHandler() and addTimedHandler(). These will allow the user code to
* respond to interesting stanzas or do something periodically with the
* connection. These handlers will be active once authentication is
* finished.
*
* Create and initialize a Openfire object.
*
*
* Returns:
* A new Openfire object.
*/
Openfire.Connection = function(url)
{
if (!window.WebSocket)
{
window.WebSocket=window.MozWebSocket;
if (!window.WebSocket)
{
var msg = "WebSocket not supported by this browser";
alert(msg);
throw Error(msg);
}
}
this.host = url.indexOf("/") < 0 ? url : url.split("/")[2];
this.protocol = url.indexOf("/") < 0 ? "wss:" : (url.split("/")[0] == "http:") ? "ws:" : "wss:";
this.jid = "";
this.resource = "ofchat";
this.streamId = null;
// handler lists
this.timedHandlers = [];
this.handlers = [];
this.removeTimeds = [];
this.removeHandlers = [];
this.addTimeds = [];
this.addHandlers = [];
this._idleTimeout = null;
this.authenticated = false;
this.disconnecting = false;
this.connected = false;
this.errors = 0;
this._uniqueId = Math.round(Math.random() * 10000);
// setup onIdle callback every 1/10th of a second
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
// initialize plugins
for (var k in Strophe._connectionPlugins)
{
if (Strophe._connectionPlugins.hasOwnProperty(k)) {
var ptype = Strophe._connectionPlugins[k];
// jslint complaints about the below line, but this is fine
var F = function () {};
F.prototype = ptype;
this[k] = new F();
this[k].init(this);
}
}
}
Openfire.Connection.prototype = {
/** Function: reset
* Reset the connection.
*
* This function should be called after a connection is disconnected
* before that connection is reused.
*/
reset: function ()
{
this.streamId = null;
this.timedHandlers = [];
this.handlers = [];
this.removeTimeds = [];
this.removeHandlers = [];
this.addTimeds = [];
this.addHandlers = [];
this.authenticated = false;
this.disconnecting = false;
this.connected = false;
this.errors = 0;
},
/** Function: pause
* UNUSED with websockets
*/
pause: function ()
{
return;
},
/** Function: resume
* UNUSED with websockets
*/
resume: function ()
{
return;
},
/** Function: getUniqueId
* Generate a unique ID for use in <iq/> elements.
*
* All <iq/> stanzas are required to have unique id attributes. This
* function makes creating these easy. Each connection instance has
* a counter which starts from zero, and the value of this counter
* plus a colon followed by the suffix becomes the unique id. If no
* suffix is supplied, the counter is used as the unique id.
*
* Suffixes are used to make debugging easier when reading the stream
* data, and their use is recommended. The counter resets to 0 for
* every new connection for the same reason. For connections to the
* same server that authenticate the same way, all the ids should be
* the same, which makes it easy to see changes. This is useful for
* automated testing as well.
*
* Parameters:
* (String) suffix - A optional suffix to append to the id.
*
* Returns:
* A unique string to be used for the id attribute.
*/
getUniqueId: function (suffix)
{
if (typeof(suffix) == "string" || typeof(suffix) == "number") {
return ++this._uniqueId + ":" + suffix;
} else {
return ++this._uniqueId + "";
}
},
/** Function: connect
* Starts the connection process.
*
*
* Parameters:
* (String) username - The Openfire username.
* (String) pass - The user's password.
* (String) resource - The user resource for this connection.
* (Function) callback The connect callback function.
*/
connect: function (jid, pass, callback, wait, hold, route)
{
this.jid = jid.indexOf("/") > -1 ? jid : jid + '/' + this.resource;
this.username = jid.indexOf("@") < 0 ? null : jid.split("@")[0];
this.pass = pass == "" ? null : pass;
this.connect_callback = callback;
this.disconnecting = false;
this.connected = false;
this.authenticated = false;
this.errors = 0;
this._changeConnectStatus(Strophe.Status.CONNECTING, null);
this.url = this.protocol + "//" + this.host + "/ws/server?username=" + this.username + "&password=" + this.pass + "&resource=" + this.resource;
this._ws = new WebSocket(this.url, "xmpp");
this._ws.onopen = this._onopen.bind(this);
this._ws.onmessage = this._onmessage.bind(this);
this._ws.onclose = this._onclose.bind(this);
window.openfireWebSocket = this;
this.jid = this.jid.indexOf("@") < 0 ? this.resource + "@" + this.jid : this.jid;
this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
},
/**
*
* Private Function: _onopen websocket event handler
*
*/
_onopen: function()
{
this.connected = true;
this.authenticated = true;
this.resource = Strophe.getResourceFromJid(this.jid);
this.domain = Strophe.getDomainFromJid(this.jid);
try {
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
} catch (e) {
throw Error("User connection callback caused an exception: " + e);
}
this.interval = setInterval (function() {window.openfireWebSocket.sendRaw(" ")}, 10000 );
},
/** Function: attach
* UNUSED, use connect again
*/
attach: function()
{
return
},
/** Function: xmlInput
* User overrideable function that receives XML data coming into the
* connection.
*
* The default function does nothing. User code can override this with
* > Openfire.xmlInput = function (elem) {
* > (user code)
* > };
*
* Parameters:
* (XMLElement) elem - The XML data received by the connection.
*/
xmlInput: function (elem)
{
return;
},
/** Function: xmlOutput
* User overrideable function that receives XML data sent to the
* connection.
*
* The default function does nothing. User code can override this with
* > Openfire.xmlOutput = function (elem) {
* > (user code)
* > };
*
* Parameters:
* (XMLElement) elem - The XMLdata sent by the connection.
*/
xmlOutput: function (elem)
{
return;
},
/** Function: rawInput
* User overrideable function that receives raw data coming into the
* connection.
*
* The default function does nothing. User code can override this with
* > Openfire.rawInput = function (data) {
* > (user code)
* > };
*
* Parameters:
* (String) data - The data received by the connection.
*/
rawInput: function (data)
{
return;
},
/** Function: rawOutput
* User overrideable function that receives raw data sent to the
* connection.
*
* The default function does nothing. User code can override this with
* > Openfire.rawOutput = function (data) {
* > (user code)
* > };
*
* Parameters:
* (String) data - The data sent by the connection.
*/
rawOutput: function (data)
{
return;
},
/** Function: sendRaw
* Send a stanza in raw XML text.
*
* This function is called to push data onto the send queue to
* go out over the wire. Whenever a request is sent to the BOSH
* server, all pending data is sent and the queue is flushed.
*
* Parameters:
* xml - The stanza text XML to send.
*/
sendRaw: function(xml) {
if(!this.connected || this._ws == null) {
throw Error("Not connected, cannot send packets.");
}
if (xml != " ")
{
this.xmlOutput(this._textToXML(xml));
this.rawOutput(xml);
}
this._ws.send(xml);
},
/** Function: send
* Send a stanza.
*
* This function is called to push data onto the send queue to
* go out over the wire. Whenever a request is sent to the BOSH
* server, all pending data is sent and the queue is flushed.
*
* Parameters:
* (XMLElement |
* [XMLElement] |
* Strophe.Builder) elem - The stanza to send.
*/
send: function(elem)
{
if(!this.connected || this._ws == null) {
throw Error("Not connected, cannot send packets.");
}
var toSend = "";
if (elem === null) { return ; }
if (typeof(elem.sort) === "function")
{
for (var i = 0; i < elem.length; i++)
{
toSend += Strophe.serialize(elem[i]);
this.xmlOutput(elem[i]);
}
} else if (typeof(elem.tree) === "function") {
toSend = Strophe.serialize(elem.tree());
this.xmlOutput(elem.tree());
} else {
toSend = Strophe.serialize(elem);
this.xmlOutput(elem);
}
this.rawOutput(toSend);
this._ws.send(toSend);
},
/** Function: flush
* UNUSED
*/
flush: function ()
{
return
},
/** Function: sendIQ
* Helper function to send IQ stanzas.
*
* Parameters:
* (XMLElement) elem - The stanza to send.
* (Function) callback - The callback function for a successful request.
* (Function) errback - The callback function for a failed or timed
* out request. On timeout, the stanza will be null.
* (Integer) timeout - The time specified in milliseconds for a
* timeout to occur.
*
* Returns:
* The id used to send the IQ.
*/
sendIQ: function(elem, callback, errback, timeout) {
var timeoutHandler = null;
var that = this;
if (typeof(elem.tree) === "function") {
elem = elem.tree();
}
var id = elem.getAttribute('id');
// inject id if not found
if (!id) {
id = this.getUniqueId("sendIQ");
elem.setAttribute("id", id);
}
var handler = this.addHandler(function (stanza) {
// remove timeout handler if there is one
if (timeoutHandler) {
that.deleteTimedHandler(timeoutHandler);
}
var iqtype = stanza.getAttribute('type');
if (iqtype == 'result')
{
if (callback) {
callback(stanza);
}
} else if (iqtype == 'error') {
if (errback) {
errback(stanza);
}
} else {
throw {
name: "StropheError",
message: "Got bad IQ type of " + iqtype
};
}
}, null, 'iq', null, id);
// if timeout specified, setup timeout handler.
if (timeout)
{
timeoutHandler = this.addTimedHandler(timeout, function () {
// get rid of normal handler
that.deleteHandler(handler);
// call errback on timeout with null stanza
if (errback) {
errback(null);
}
return false;
});
}
this.send(elem);
return id;
},
/** Function: addTimedHandler
* Add a timed handler to the connection.
*
* This function adds a timed handler. The provided handler will
* be called every period milliseconds until it returns false,
* the connection is terminated, or the handler is removed. Handlers
* that wish to continue being invoked should return true.
*
* Because of method binding it is necessary to save the result of
* this function if you wish to remove a handler with
* deleteTimedHandler().
*
* Note that user handlers are not active until authentication is
* successful.
*
* Parameters:
* (Integer) period - The period of the handler.
* (Function) handler - The callback function.
*
* Returns:
* A reference to the handler that can be used to remove it.
*/
addTimedHandler: function (period, handler)
{
var thand = new Strophe.TimedHandler(period, handler);
this.addTimeds.push(thand);
return thand;
},
/** Function: deleteTimedHandler
* Delete a timed handler for a connection.
*
* This function removes a timed handler from the connection. The
* handRef parameter is *not* the function passed to addTimedHandler(),
* but is the reference returned from addTimedHandler().
*
* Parameters:
* (Strophe.TimedHandler) handRef - The handler reference.
*/
deleteTimedHandler: function (handRef)
{
// this must be done in the Idle loop so that we don't change
// the handlers during iteration
this.removeTimeds.push(handRef);
},
/** Function: addHandler
* Add a stanza handler for the connection.
*
* This function adds a stanza handler to the connection. The
* handler callback will be called for any stanza that matches
* the parameters. Note that if multiple parameters are supplied,
* they must all match for the handler to be invoked.
*
* The handler will receive the stanza that triggered it as its argument.
* The handler should return true if it is to be invoked again;
* returning false will remove the handler after it returns.
*
* As a convenience, the ns parameters applies to the top level element
* and also any of its immediate children. This is primarily to make
* matching /iq/query elements easy.
*
* The options argument contains handler matching flags that affect how
* matches are determined. Currently the only flag is matchBare (a
* boolean). When matchBare is true, the from parameter and the from
* attribute on the stanza will be matched as bare JIDs instead of
* full JIDs. To use this, pass {matchBare: true} as the value of
* options. The default value for matchBare is false.
*
* The return value should be saved if you wish to remove the handler
* with deleteHandler().
*
* Parameters:
* (Function) handler - The user callback.
* (String) ns - The namespace to match.
* (String) name - The stanza name to match.
* (String) type - The stanza type attribute to match.
* (String) id - The stanza id attribute to match.
* (String) from - The stanza from attribute to match.
* (String) options - The handler options
*
* Returns:
* A reference to the handler that can be used to remove it.
*/
addHandler: function (handler, ns, name, type, id, from, options)
{
var hand = new Strophe.Handler(handler, ns, name, type, id, from, options);
this.addHandlers.push(hand);
return hand;
},
/** Function: deleteHandler
* Delete a stanza handler for a connection.
*
* This function removes a stanza handler from the connection. The
* handRef parameter is *not* the function passed to addHandler(),
* but is the reference returned from addHandler().
*
* Parameters:
* (Strophe.Handler) handRef - The handler reference.
*/
deleteHandler: function (handRef)
{
// this must be done in the Idle loop so that we don't change
// the handlers during iteration
this.removeHandlers.push(handRef);
},
/** Function: disconnect
* Start the graceful disconnection process.
*
* This function starts the disconnection process. This process starts
* by sending unavailable presence and sending BOSH body of type
* terminate. A timeout handler makes sure that disconnection happens
* even if the BOSH server does not respond.
*
* The user supplied connection callback will be notified of the
* progress as this process happens.
*
* Parameters:
* (String) reason - The reason the disconnect is occuring.
*/
disconnect: function(reason) {
if(!this.connected || this._ws == null) {
return;
}
this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason);
Strophe.info("Disconnect was called because: " + reason);
this._ws.close();
},
/** PrivateFunction: _onDisconnectTimeout
* _Private_ timeout handler for handling non-graceful disconnection.
*
* If the graceful disconnect process does not complete within the
* time allotted, this handler finishes the disconnect anyway.
*
* Returns:
* false to remove the handler.
*/
_onDisconnectTimeout: function ()
{
Strophe.info("_onDisconnectTimeout was called");
this._doDisconnect();
return false;
},
/** PrivateFunction: _doDisconnect
* _Private_ function to disconnect.
*
* This is the last piece of the disconnection logic. This resets the
* connection and alerts the user's connection callback.
*/
_doDisconnect: function ()
{
Strophe.info("_doDisconnect was called");
this._onclose();
},
/** PrivateFunction: _changeConnectStatus
* _Private_ helper function that makes sure plugins and the user's
* callback are notified of connection status changes.
*
* Parameters:
* (Integer) status - the new connection status, one of the values
* in Strophe.Status
* (String) condition - the error condition or null
*/
_changeConnectStatus: function (status, condition)
{
// notify all plugins listening for status changes
for (var k in Strophe._connectionPlugins)
{
if (Strophe._connectionPlugins.hasOwnProperty(k))
{
var plugin = this[k];
if (plugin.statusChanged)
{
try {
plugin.statusChanged(status, condition);
} catch (err) {
Strophe.error("" + k + " plugin caused an exception changing status: " + err);
}
}
}
}
// notify the user's callback
if (typeof this.connect_callback == 'function')
{
try {
this.connect_callback(status, condition);
} catch (e) {
Strophe.error("User connection callback caused an exception: " + e);
}
}
},
/**
*
* Private Function: _onclose websocket event handler
*
*/
_onclose: function()
{
Strophe.info("websocket closed");
//console.log('_onclose - disconnected');
clearInterval(this.interval);
this.authenticated = false;
this.disconnecting = false;
this.streamId = null;
// tell the parent we disconnected
this._changeConnectStatus(Strophe.Status.DISCONNECTED, null);
this.connected = false;
// delete handlers
this.handlers = [];
this.timedHandlers = [];
this.removeTimeds = [];
this.removeHandlers = [];
this.addTimeds = [];
this.addHandlers = [];
if(this._ws.readyState != this._ws.CLOSED)
{
this._ws.close();
}
},
/**
*
* Private Function: _onmessage websocket event handler
*
*/
_onmessage: function(packet)
{
var elem;
try {
elem = this._textToXML(packet.data);
} catch (e) {
if (e != "parsererror") { throw e; }
this.disconnect("strophe-parsererror");
}
if (elem === null) { return; }
this.xmlInput(elem);
this.rawInput(packet.data);
// remove handlers scheduled for deletion
var i, hand;
while (this.removeHandlers.length > 0)
{
hand = this.removeHandlers.pop();
i = this.handlers.indexOf(hand);
if (i >= 0) {
this.handlers.splice(i, 1);
}
}
// add handlers scheduled for addition
while (this.addHandlers.length > 0)
{
this.handlers.push(this.addHandlers.pop());
}
// send each incoming stanza through the handler chain
var i, newList;
newList = this.handlers;
this.handlers = [];
for (i = 0; i < newList.length; i++)
{
var hand = newList[i];
if (hand.isMatch(elem) && (this.authenticated || !hand.user))
{
if (hand.run(elem))
{
this.handlers.push(hand);
}
} else {
this.handlers.push(hand);
}
}
},
/**
*
* Private Function: _textToXML convert text to DOM Document object
*
*/
_textToXML: function (text) {
var doc = null;
if (window['DOMParser']) {
var parser = new DOMParser();
doc = parser.parseFromString(text, 'text/xml');
} else if (window['ActiveXObject']) {
var doc = new ActiveXObject("MSXML2.DOMDocument");
doc.async = false;
doc.loadXML(text);
} else {
throw Error('No DOMParser object found.');
}
return doc.firstChild;
},
/** PrivateFunction: _onIdle
* _Private_ handler to process events during idle cycle.
*
* This handler is called every 100ms to fire timed handlers that
* are ready and keep poll requests going.
*/
_onIdle: function ()
{
var i, thand, since, newList;
// remove timed handlers that have been scheduled for deletion
while (this.removeTimeds.length > 0)
{
thand = this.removeTimeds.pop();
i = this.timedHandlers.indexOf(thand);
if (i >= 0) {
this.timedHandlers.splice(i, 1);
}
}
// add timed handlers scheduled for addition
while (this.addTimeds.length > 0)
{
this.timedHandlers.push(this.addTimeds.pop());
}
// call ready timed handlers
var now = new Date().getTime();
newList = [];
for (i = 0; i < this.timedHandlers.length; i++)
{
thand = this.timedHandlers[i];
if (this.authenticated || !thand.user) {
since = thand.lastCalled + thand.period;
if (since - now <= 0) {
if (thand.run()) {
newList.push(thand);
}
} else {
newList.push(thand);
}
}
}
this.timedHandlers = newList;
// reactivate the timer
clearTimeout(this._idleTimeout);
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment