var XMPPVideoRoom = (function() {
|
|
|
/**
|
* Interface with Jitsi Video Room and WebRTC-streamer API
|
* @constructor
|
* @param {string} xmppUrl - url of XMPP server
|
* @param {string} srvurl - url of WebRTC-streamer
|
*/
|
var XMPPVideoRoom = function XMPPVideoRoom (xmppUrl, srvurl, bus) {
|
this.xmppUrl = xmppUrl;
|
this.srvurl = srvurl || "//"+window.location.hostname+":"+window.location.port;
|
this.sessionList = {};
|
this.bus = bus;
|
this.connection = {};
|
};
|
|
XMPPVideoRoom.prototype.getConnection = function(roomId, username) {
|
var key = roomId + "/" + username
|
return new Promise( (resolve, reject) => {
|
if (this.connection.hasOwnProperty(key)) {
|
resolve(this.connection[key]);
|
} else {
|
var connection = new Strophe.Connection("https://" + this.xmppUrl + "/http-bind?room="+roomId);
|
connection.connect(this.xmppUrl, null, (status) => {
|
if (status === Strophe.Status.CONNECTING) {
|
console.log('Strophe is connecting.');
|
} else if (status === Strophe.Status.CONNFAIL) {
|
console.log('Strophe failed to connect.');
|
reject('Strophe failed to connect.')
|
} else if (status === Strophe.Status.CONNECTED) {
|
console.log('Strophe is connected.');
|
if (connection.disco) {
|
connection.disco.addIdentity('client', 'http://jitsi.org/jitsimeet');
|
}
|
this.connection[key] = connection;
|
resolve(connection);
|
}
|
});
|
}
|
});
|
}
|
|
/**
|
* Ask to publish a stream from WebRTC-streamer in a XMPP Video Room user
|
* @param {string} roomId - id of the XMPP Video Room to join
|
* @param {string} url - WebRTC stream to publish
|
* @param {string} username - name in Video Room
|
*/
|
XMPPVideoRoom.prototype.join = function(roomId, url, username, password) {
|
|
this.getConnection(roomId, username).then( connection => {
|
if (connection.disco) {
|
connection.disco.addFeature("urn:xmpp:jingle:1");
|
connection.disco.addFeature("urn:xmpp:jingle:errors:1");
|
connection.disco.addFeature("urn:xmpp:jingle:apps:rtp:1");
|
connection.disco.addFeature("urn:xmpp:jingle:apps:rtp:info:1")
|
connection.disco.addFeature("urn:xmpp:jingle:apps:rtp:errors:1")
|
connection.disco.addFeature("urn:xmpp:jingle:apps:rtp:audio");
|
connection.disco.addFeature("urn:xmpp:jingle:apps:rtp:video");
|
connection.disco.addFeature("urn:xmpp:jingle:apps:dtls:0");
|
connection.disco.addFeature("urn:xmpp:jingle:transports:ice-udp:1");
|
connection.disco.addFeature("urn:xmpp:ping");
|
connection.disco.addFeature("urn:ietf:rfc:5761")
|
connection.disco.addFeature("urn:ietf:rfc:5888"); // a=group, e.g. bundle
|
}
|
connection.addHandler(this.OnJingle.bind(this, connection, roomId, username, url), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
|
|
var roomUrl = this.getRoomUrl(roomId);
|
this.emitState(roomId + '/' + username, "joining");
|
|
var extPresence = Strophe.xmlElement('nick', {xmlns:'http://jabber.org/protocol/nick'}, username);
|
connection.muc.join(roomUrl, username, null, null, null, password, null, extPresence);
|
|
var extPresence = Strophe.xmlElement('videomuted', {xmlns:'http://jitsi.org/jitmeet/video'}, false);
|
connection.muc.join(roomUrl, username, null, this.OnPresence.bind(this,connection,roomId), null, password, null, extPresence);
|
|
this.emitState(roomId + '/' + username, "joined");
|
});
|
}
|
|
/**
|
* Ask to leave a XMPP Video Room user
|
* @param {string} roomId - id of the XMPP Video Room to leave
|
* @param {string} name - name in Video Room
|
*/
|
XMPPVideoRoom.prototype.leave = function (roomId, username) {
|
var found = false;
|
Object.entries(this.sessionList).forEach( ([sid, session]) => {
|
if ( (session.roomid === roomId) && (session.name === username) ) {
|
this.leaveSession(sid);
|
found = true;
|
}
|
});
|
if (!found) {
|
var roomUrl = this.getRoomUrl(roomId);
|
this.getConnection(roomId, username).then( connection => {
|
var sessionname = "kicker" + Math.floor(Math.random()*1000000).toString();
|
|
connection.muc.join(roomUrl, sessionname, null, null, null, null, null, null);
|
connection.muc.kick(roomUrl, username, "Unknown session", () => this.emitPresence(roomId + '/' + username, "out") )
|
connection.muc.leave(roomUrl, sessionname);
|
|
connection.flush()
|
})
|
}
|
}
|
|
/**
|
* Ask to leave all XMPP Video Room
|
*/
|
XMPPVideoRoom.prototype.leaveAll = function () {
|
Object.keys(this.sessionList).forEach( (sid) => {
|
this.leaveSession(sid);
|
});
|
this.sessionList = {};
|
}
|
|
/**
|
* Query a XMPP Video Room
|
* @param {string} roomid - id of the XMPP Video Room to join
|
*/
|
XMPPVideoRoom.prototype.query = function(roomId, password) {
|
var sessionname = "query" + Math.floor(Math.random()*1000000).toString();
|
|
this.getConnection(roomId, sessionname).then( connection => {
|
|
var roomUrl = this.getRoomUrl(roomId);
|
|
connection.muc.join(roomUrl, sessionname, null, this.OnPresence.bind(this,connection,roomId), null, password, null, null);
|
|
connection.muc.queryOccupants(roomUrl, (query) => {
|
var occupants = $(query).find(">query>item");
|
occupants.toArray().forEach( (item) => {
|
this.emitPresence(roomId + '/' + item.getAttribute("name"), "in");
|
});
|
}, () => connection.muc.leave(roomUrl, sessionname), () => connection.muc.leave(roomUrl, sessionname));
|
})
|
}
|
|
|
/*
|
/* HTTP callback for /getIceCandidate
|
*/
|
XMPPVideoRoom.prototype.onReceiveCandidate = function(connection, roomid, name, answer, candidateList) {
|
console.log("============candidateList:" + JSON.stringify(candidateList));
|
var jingle = answer.querySelector("jingle");
|
var sid = jingle.getAttribute("sid");
|
var from = answer.getAttribute("to");
|
var to = answer.getAttribute("from");
|
|
candidateList.forEach(function (candidate) {
|
var jsoncandidate = SDPUtil.parse_icecandidate(candidate.candidate);
|
console.log("<=== webrtc candidate:" + JSON.stringify(jsoncandidate));
|
// get ufrag
|
var ufrag = "";
|
var elems = candidate.candidate.split(' ');
|
for (var i = 8; i < elems.length; i += 2) {
|
switch (elems[i]) {
|
case 'ufrag':
|
ufrag = elems[i + 1];
|
break;
|
}
|
}
|
// get pwd
|
var pwd = "";
|
var transports = $(jingle).find('>content>transport');
|
transports.each( (idx,transport) => {
|
if (ufrag == transport.getAttribute('ufrag')) {
|
pwd = transport.getAttribute('pwd');
|
}
|
});
|
|
// convert candidate from webrtc to jingle
|
var iq = $iq({ type: "set", from, to })
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1'})
|
.attrs({ action: "transport-info", sid, ufrag, pwd })
|
.c('candidate', jsoncandidate)
|
.up()
|
.up();
|
|
var id = connection.sendIQ(iq, () => {
|
console.log("===> xmpp transport-info ok sid:" + sid);
|
},(error) => {
|
console.warn("############transport-info error sid:" + sid + " error:" + error.textContent);
|
});
|
});
|
|
var id = connection.sendIQ(answer, () => {
|
console.log("===> xmpp session-accept ok sid:" + sid);
|
this.emitState(roomid + '/' + name, "published");
|
},(error) => {
|
console.warn("############session-accept error sid:" + sid + " error:" + error.textContent);
|
});
|
}
|
|
/*
|
/* HTTP callback for /call
|
*/
|
XMPPVideoRoom.prototype.onCall = function(connection, roomid, name, iq, data) {
|
var jingle = iq.querySelector("jingle");
|
var sid = jingle.getAttribute("sid");
|
console.log("<=== webrtc answer sid:" + sid);
|
|
var earlyCandidates = this.sessionList[sid].earlyCandidates;
|
if (earlyCandidates) {
|
while (earlyCandidates.length) {
|
var candidate = earlyCandidates.shift();
|
console.log("===> webrtc candidate :" + JSON.stringify(candidate));
|
var method = this.srvurl + "/api/addIceCandidate?peerid="+ sid;
|
fetch(method, { method: "POST", body: JSON.stringify(candidate) })
|
.then( (response) => (response.json()) )
|
.then( (response) => console.log("method:"+method+ " answer:" +response) )
|
.catch( (error) => this.onError("addIceCandidate error:" + error ))
|
}
|
}
|
delete this.sessionList[sid].earlyCandidates;
|
|
var jingle = $iq({ type: "set", from: iq.getAttribute("to"), to: iq.getAttribute("from") })
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1'})
|
.attrs({ action: "session-accept", sid, responder:iq.getAttribute("to") });
|
|
var sdp = new SDP(data.sdp);
|
var answer = sdp.toJingle(jingle, 'initiator');
|
fetch(this.srvurl + "/api/getIceCandidate?peerid="+ sid)
|
.then(r => r.json())
|
.then( (response) => this.onReceiveCandidate(connection, roomid, name, answer.node, response) )
|
.catch( error => this.onError("getIceCandidate error:" + error) )
|
}
|
|
XMPPVideoRoom.prototype.emitState = function(name, state) {
|
if (this.bus) {
|
this.bus.emit('state', name, state);
|
}
|
}
|
|
XMPPVideoRoom.prototype.emitPresence = function(name, state) {
|
if (this.bus) {
|
this.bus.emit('presence', name, state);
|
}
|
}
|
|
XMPPVideoRoom.prototype.onError = function (error) {
|
console.warn("############onError:" + error)
|
}
|
|
XMPPVideoRoom.prototype.getRoomUrl = function (roomId) {
|
return roomId + "@" + "conference." + this.xmppUrl
|
}
|
|
/*
|
/* XMPP callback for jingle
|
*/
|
XMPPVideoRoom.prototype.OnJingle = function(connection, roomid, name, url, iq) {
|
var jingle = iq.querySelector("jingle");
|
var sid = jingle.getAttribute("sid");
|
var action = jingle.getAttribute("action");
|
const fromJid = iq.getAttribute('from');
|
const toJid = iq.getAttribute('to');
|
console.log("OnJingle from:" + fromJid + " to:" + toJid + " sid:" + sid + " action:" + action)
|
console.log(iq.innerHTML);
|
|
var id = iq.getAttribute("id");
|
var ack = $iq({ type: "result", to: fromJid, id })
|
|
if (action === "session-initiate") {
|
connection.sendIQ(ack);
|
|
const resource = Strophe.getResourceFromJid(fromJid);
|
const isP2P = (resource !== 'focus');
|
console.log("<=== xmpp offer sid:" + sid + " resource:" + resource + " initiator:" + jingle.getAttribute("initiator"));
|
|
if (!isP2P) {
|
this.emitState(roomid + '/' + name, "publishing");
|
|
var sdp = new SDP('');
|
sdp.fromJingle($(jingle));
|
|
this.sessionList[sid] = { roomid, name, earlyCandidates:[] } ;
|
|
var videourl = url.video || url;
|
var method = this.srvurl + "/api/call?peerid="+ sid +"&url="+encodeURIComponent(videourl);
|
if (url.audio) {
|
method += "&audio="+encodeURIComponent(url.audio);
|
}
|
method += "&options="+encodeURIComponent("rtptransport=tcp&timeout=60");
|
fetch(method, { method: "POST", body: JSON.stringify({type:"offer",sdp:sdp.raw}) })
|
.then( (response) => response.json() )
|
.then( (response) => this.onCall(connection, roomid, name, iq, response ) )
|
.catch( (error) => this.onError("call " + error ))
|
|
}
|
|
} else if (action === "transport-info") {
|
connection.sendIQ(ack);
|
|
console.log("<=== xmpp candidate sid:" + sid);
|
|
if (this.sessionList[sid]) {
|
var contents = $(jingle).find('>content');
|
contents.each( (contentIdx,content) => {
|
var transports = $(content).find('>transport');
|
transports.each( (idx,transport) => {
|
var ufrag = transport.getAttribute('ufrag');
|
var candidates = $(transport).find('>candidate');
|
candidates.each ( (idx,candidate) => {
|
var sdp = SDPUtil.candidateFromJingle(candidate);
|
sdp = sdp.replace("a=candidate","candidate");
|
sdp = sdp.replace("\r\n"," ufrag " + ufrag);
|
var candidate = { candidate:sdp, sdpMid:"", sdpMLineIndex:contentIdx }
|
|
if (this.sessionList[sid].earlyCandidates) {
|
console.log("queue candidate waiting for call answer");
|
this.sessionList[sid].earlyCandidates.push(candidate);
|
} else {
|
console.log("===> webrtc candidate :" + JSON.stringify(candidate));
|
var method = this.srvurl + "/api/addIceCandidate?peerid="+ sid;
|
|
fetch(method, { method: "POST", body: JSON.stringify(candidate) })
|
.then( (response) => (response.json()) )
|
.then( (response) => console.log("method:"+method+ " answer:" +response) )
|
.catch( (error) => this.onError("call " + error ))
|
}
|
});
|
});
|
});
|
}
|
} else if (action === "session-terminate") {
|
connection.sendIQ(ack);
|
console.log("<=== xmpp session-terminate sid:" + sid + " reason:" + jingle.querySelector("reason").textContent);
|
this.leaveSession(sid);
|
} else {
|
connection.sendIQ(ack);
|
console.log("<=== unknown action");
|
}
|
|
return true;
|
}
|
|
/*
|
/* XMPP callback for presence
|
*/
|
XMPPVideoRoom.prototype.OnPresence = function(connection,roomid,pres)
|
{
|
const resource = Strophe.getResourceFromJid(pres.getAttribute('from'));
|
var msg = "resource:" + resource;
|
|
const type = pres.getAttribute('type');
|
if (type) {
|
msg += " type:" + type;
|
}
|
const statusEl = pres.getElementsByTagName('status')[0];
|
if (statusEl) {
|
msg += " status:" + statusEl.getAttribute('code');
|
}
|
|
if (type == "unavailable") {
|
this.emitPresence(roomid + '/' + resource, "out");
|
} else if (statusEl) {
|
var code = statusEl.getAttribute('code');
|
msg += " status:" + code;
|
if (code === "100" || code === "110" || code === "201" || code === "210") {
|
this.emitPresence(roomid + '/' + resource, "in");
|
}
|
else if (code === "301" || code === "307") {
|
this.emitPresence(roomid + '/' + resource, "out");
|
}
|
} else {
|
this.emitPresence(roomid + '/' + resource, "in");
|
}
|
|
const xElement = pres.getElementsByTagNameNS('http://jabber.org/protocol/muc#user', 'x')[0];
|
const mucUserItem = xElement && xElement.getElementsByTagName('item')[0];
|
if (mucUserItem) {
|
msg += " jid:" + mucUserItem.getAttribute('jid')
|
+ " role:" + mucUserItem.getAttribute('role')
|
+ " affiliation:" + mucUserItem.getAttribute('affiliation');
|
}
|
|
const nickEl = pres.getElementsByTagName('nick')[0];
|
if (nickEl) {
|
msg += " nick:" + nickEl.textContent;
|
}
|
console.log ( "OnPresence " + msg);
|
|
return true;
|
}
|
|
/*
|
/* leave a session
|
*/
|
XMPPVideoRoom.prototype.leaveSession = function (sid) {
|
var session = this.sessionList[sid];
|
if (session) {
|
var roomId = session.roomid
|
this.emitState(roomId + '/' + session.name, "leaving");
|
|
this.getConnection(roomId, session.name).then( connection => {
|
var roomUrl = this.getRoomUrl(roomId);
|
|
// close jingle session
|
var iq = $iq({ type: "set", from: roomUrl +"/" + session.name, to: roomUrl })
|
.c('jingle', {xmlns: 'urn:xmpp:jingle:1'})
|
.attrs({ action: "session-terminate", sid})
|
.up();
|
connection.sendIQ(iq);
|
|
// publish on muc
|
connection.muc.leave(roomUrl, session.name, this.OnPresence.bind(this,connection,roomId));
|
connection.flush();
|
|
// close WebRTC session
|
fetch(this.srvurl + "/api/hangup?peerid="+ sid)
|
.then(r => r.json())
|
.then( (response) => {
|
console.log("hangup answer:" +response);
|
})
|
.catch(error => {
|
this.onError("hangup error:" + error);
|
})
|
|
this.emitState(roomId + '/' + session.name, "leaved");
|
|
// unregister session
|
delete this.sessionList[sid];
|
})
|
|
}
|
}
|
|
|
return XMPPVideoRoom;
|
})();
|
|
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
|
window.XMPPVideoRoom = XMPPVideoRoom;
|
}
|
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
module.exports = XMPPVideoRoom;
|
}
|