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 @@ ...@@ -1604,6 +1604,7 @@
<include name="database/**/*.sql"/> <include name="database/**/*.sql"/>
<include name="i18n/*.properties"/> <include name="i18n/*.properties"/>
<include name="web/**/*.*"/> <include name="web/**/*.*"/>
<include name="**/*.*"/>
<exclude name="web/WEB-INF/web.xml"/> <exclude name="web/WEB-INF/web.xml"/>
<exclude name="web/**/*.jsp"/> <exclude name="web/**/*.jsp"/>
<exclude name="web/**/*.jspf"/> <exclude name="web/**/*.jspf"/>
......
...@@ -44,10 +44,10 @@ ...@@ -44,10 +44,10 @@
Jitsi Video Bridge Plugin Changelog Jitsi Video Bridge Plugin Changelog
</h1> </h1>
<p><b>1.0</b> -- Nov 30th, 2013</p> <p><b>1.1</b> -- Dec 4th, 2013</p>
<ul> <ul>
<li>OF-716 Added to Openfire plugins </li> <li>OF-716 Added to Openfire plugins with webrtc demo video application</li>
</ul> </ul>
<p><b>1.0</b> -- Apr 12, 2013</p> <p><b>1.0</b> -- Apr 12, 2013</p>
......
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
<class>org.jitsi.videobridge.openfire.PluginImpl</class> <class>org.jitsi.videobridge.openfire.PluginImpl</class>
<description>Integrates Jitsi Video Bridge into Openfire.</description> <description>Integrates Jitsi Video Bridge into Openfire.</description>
<licenseType>other</licenseType> <licenseType>other</licenseType>
<minServerVersion>3.0.0</minServerVersion> <minServerVersion>3.9.0</minServerVersion>
<name>Jitsi Video Bridge</name> <name>Jitsi Video Bridge</name>
<version>1.0</version> <version>1.1</version>
<adminconsole> <adminconsole>
<tab id="tab-server"> <tab id="tab-server">
......
...@@ -63,6 +63,7 @@ Jitsi Videobridge does not mix the video channels into a composite video stream, ...@@ -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. 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, Therefore, while it does need to run on a server with good network bandwidth,
CPU horsepower is not that critical for performance. CPU horsepower is not that critical for performance.
A demo video conference application using WebRTC is included.
</p> </p>
...@@ -76,5 +77,9 @@ plugin will then be automatically deployed. To upgrade to a new version, copy th ...@@ -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. 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> </body>
</html> </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.*; ...@@ -17,10 +17,17 @@ import org.jitsi.util.*;
import org.jitsi.videobridge.*; import org.jitsi.videobridge.*;
import org.jivesoftware.openfire.container.*; import org.jivesoftware.openfire.container.*;
import org.jivesoftware.util.*; import org.jivesoftware.util.*;
import org.jivesoftware.openfire.http.HttpBindManager;
import org.slf4j.*; import org.slf4j.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.xmpp.component.*; 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 * Implements <tt>org.jivesoftware.openfire.container.Plugin</tt> to integrate
* Jitsi Video Bridge into Openfire. * Jitsi Video Bridge into Openfire.
...@@ -37,6 +44,12 @@ public class PluginImpl ...@@ -37,6 +44,12 @@ public class PluginImpl
*/ */
private static final Logger Log = LoggerFactory.getLogger(PluginImpl.class); 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 * The name of the property that contains the maximum port number that we'd
* like our RTP managers to bind upon. * like our RTP managers to bind upon.
...@@ -122,6 +135,22 @@ public class PluginImpl ...@@ -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_LOCATION", pluginDirectory.getPath());
System.setProperty("net.java.sip.communicator.SC_HOME_DIR_NAME", "."); 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 // Let's check for custom configuration
String maxVal = JiveGlobals.getProperty(MAX_PORT_NUMBER_PROPERTY_NAME); String maxVal = JiveGlobals.getProperty(MAX_PORT_NUMBER_PROPERTY_NAME);
String minVal = JiveGlobals.getProperty(MIN_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