rtcmulticonnection
Version:
RTCMultiConnection is a WebRTC JavaScript wrapper library runs top over RTCPeerConnection API to support all possible peer-to-peer features.
846 lines (717 loc) • 32.8 kB
JavaScript
// Muaz Khan - www.MuazKhan.com
// MIT License - www.WebRTC-Experiment.com/licence
// Documentation - github.com/muaz-khan/RTCMultiConnection
var listOfUsers = {};
var listOfRooms = {};
var adminSocket;
// for scalable-broadcast demos
var ScalableBroadcast;
// pushLogs is used to write error logs into logs.json
var pushLogs = function(name, error) {
console.log(name, error);
};
try {
pushLogs = require('./pushLogs.js');
} catch (e) {
console.log('Unable to read pushLogs.js', e);
}
module.exports = exports = function(app, socketCallback) {
socketCallback = socketCallback || function() {};
if (!!app.listen) {
var io = require('socket.io');
try {
// use latest socket.io
io = io(app);
io.on('connection', onConnection);
} catch (e) {
// otherwise fallback
io = io.listen(app, {
log: false,
origins: '*:*'
});
io.set('transports', [
'websocket',
'xhr-polling',
'jsonp-polling'
]);
io.sockets.on('connection', onConnection);
}
} else {
onConnection(app);
}
// to secure your socket.io usage: (via: docs/tips-tricks.md)
// io.set('origins', 'https://domain.com');
function appendUser(socket) {
try {
var alreadyExist = listOfUsers[socket.userid];
var extra = {};
if (alreadyExist && alreadyExist.extra) {
extra = alreadyExist.extra;
}
var params = socket.handshake.query;
if (params.extra) {
try {
if (typeof params.extra === 'string') {
params.extra = JSON.parse(params.extra);
}
extra = params.extra;
} catch (e) {
extra = params.extra;
}
}
listOfUsers[socket.userid] = {
socket: socket,
connectedWith: {},
isPublic: false, // means: isPublicModerator
extra: extra || {},
admininfo: {},
maxParticipantsAllowed: params.maxParticipantsAllowed || 1000
};
} catch (e) {
pushLogs('appendUser', e);
}
sendToAdmin();
}
function sendToAdmin() {
try {
if (adminSocket) {
adminSocket.emit('admin', {
listOfRooms: listOfRooms
});
}
} catch (e) {
pushLogs('admin', e);
}
}
function onConnection(socket) {
var params = socket.handshake.query;
var socketMessageEvent = params.msgEvent || 'RTCMultiConnection-Message';
var autoCloseEntireSession = params.autoCloseEntireSession === true || params.autoCloseEntireSession === 'true';
var sessionid = params.sessionid;
var maxParticipantsAllowed = params.maxParticipantsAllowed || 1000;
var enableScalableBroadcast = params.enableScalableBroadcast === true || params.enableScalableBroadcast === 'true';
if (params.userid === 'admin') {
if (!app.request || !app.isAdminAuthorized || !app.config || !app.isAdminAuthorized(app.request, app.config)) {
socket.disconnect();
return;
}
adminSocket = socket;
socket.on('admin', function(message, callback) {
if (!app.request || !app.isAdminAuthorized || !app.config || !app.isAdminAuthorized(app.request, app.config)) {
socket.disconnect();
return;
}
callback = callback || function() {};
if (message.all === true) {
sendToAdmin();
}
if (message.userinfo === true && message.userid) {
try {
var user = listOfUsers[message.userid];
if (user) {
callback(user.socket.admininfo || {});
} else {
callback({
error: 'User-id "' + message.userid + '" does not exist.'
});
}
} catch (e) {
pushLogs('userinfo', e);
}
}
if (message.clearLogs === true) {
// last callback parameter will force to clear logs
pushLogs('', '', callback);
}
if(message.deleteUser === true) {
try {
var user = listOfUsers[message.userid];
if(user) {
user.socket.disconnect();
}
// delete listOfUsers[message.userid];
callback(true);
}
catch(e) {
pushLogs('deleteUser', e);
callback(false);
}
}
if(message.deleteRoom === true) {
try {
var room = listOfRooms[message.roomid];
if(room) {
var participants = room.participants;
delete listOfRooms[message.roomid];
participants.forEach(function(userid) {
var user = listOfUsers[userid];
if(user) {
user.socket.disconnect();
}
});
}
callback(true);
}
catch(e) {
pushLogs('deleteRoom', e);
callback(false);
}
}
});
return;
}
if (enableScalableBroadcast === true) {
try {
if (!ScalableBroadcast) {
// path to scalable broadcast script must be accurate
ScalableBroadcast = require('./Scalable-Broadcast.js');
}
ScalableBroadcast(socket, params.maxRelayLimitPerUser);
} catch (e) {
pushLogs('ScalableBroadcast', e);
}
// scalable-broadcast can ignore rest of the codes
// however it must have evens like open-room, join-room, and socketMessageEvent
// return;
}
// [disabled]
if (false && !!listOfUsers[params.userid]) {
params.dontUpdateUserId = true;
var useridAlreadyTaken = params.userid;
params.userid = (Math.random() * 1000).toString().replace('.', '');
socket.emit('userid-already-taken', useridAlreadyTaken, params.userid);
return;
}
socket.userid = params.userid;
appendUser(socket);
socket.on('extra-data-updated', function(extra) {
try {
if (!listOfUsers[socket.userid]) return;
if (listOfUsers[socket.userid].socket.admininfo) {
listOfUsers[socket.userid].socket.admininfo.extra = extra;
}
// todo: use "admininfo.extra" instead of below one
listOfUsers[socket.userid].extra = extra;
for (var user in listOfUsers[socket.userid].connectedWith) {
listOfUsers[user].socket.emit('extra-data-updated', socket.userid, extra);
}
} catch (e) {
pushLogs('extra-data-updated', e);
}
});
socket.on('get-remote-user-extra-data', function(remoteUserId, callback) {
callback = callback || function() {};
if (!remoteUserId || !listOfUsers[remoteUserId]) {
callback('remoteUserId (' + remoteUserId + ') does NOT exist.');
return;
}
callback(listOfUsers[remoteUserId].extra);
});
socket.on('become-a-public-moderator', function() {
try {
if (!listOfUsers[socket.userid]) return;
listOfUsers[socket.userid].isPublic = true;
} catch (e) {
pushLogs('become-a-public-moderator', e);
}
});
var dontDuplicateListeners = {};
socket.on('set-custom-socket-event-listener', function(customEvent) {
if (dontDuplicateListeners[customEvent]) return;
dontDuplicateListeners[customEvent] = customEvent;
socket.on(customEvent, function(message) {
try {
socket.broadcast.emit(customEvent, message);
} catch (e) {}
});
});
socket.on('dont-make-me-moderator', function() {
try {
if (!listOfUsers[socket.userid]) return;
listOfUsers[socket.userid].isPublic = false;
} catch (e) {
pushLogs('dont-make-me-moderator', e);
}
});
socket.on('get-public-moderators', function(userIdStartsWith, callback) {
try {
userIdStartsWith = userIdStartsWith || '';
var allPublicModerators = [];
for (var moderatorId in listOfUsers) {
if (listOfUsers[moderatorId].isPublic && moderatorId.indexOf(userIdStartsWith) === 0 && moderatorId !== socket.userid) {
var moderator = listOfUsers[moderatorId];
allPublicModerators.push({
userid: moderatorId,
extra: moderator.extra
});
}
}
callback(allPublicModerators);
} catch (e) {
pushLogs('get-public-moderators', e);
}
});
socket.on('changed-uuid', function(newUserId, callback) {
callback = callback || function() {};
if (params.dontUpdateUserId) {
delete params.dontUpdateUserId;
return;
}
try {
if (listOfUsers[socket.userid] && listOfUsers[socket.userid].socket.userid == socket.userid) {
if (newUserId === socket.userid) return;
var oldUserId = socket.userid;
listOfUsers[newUserId] = listOfUsers[oldUserId];
listOfUsers[newUserId].socket.userid = socket.userid = newUserId;
delete listOfUsers[oldUserId];
callback();
return;
}
socket.userid = newUserId;
appendUser(socket);
callback();
} catch (e) {
pushLogs('changed-uuid', e);
}
});
socket.on('set-password', function(password) {
try {
if (listOfRooms[socket.roomid] && listOfRooms[socket.roomid].owner == socket.userid) {
listOfRooms[socket.roomid].password = password;
}
} catch (e) {
pushLogs('set-password', e);
}
});
socket.on('disconnect-with', function(remoteUserId, callback) {
try {
if (listOfUsers[socket.userid] && listOfUsers[socket.userid].connectedWith[remoteUserId]) {
delete listOfUsers[socket.userid].connectedWith[remoteUserId];
socket.emit('user-disconnected', remoteUserId);
sendToAdmin();
}
if (!listOfUsers[remoteUserId]) return callback();
if (listOfUsers[remoteUserId].connectedWith[socket.userid]) {
delete listOfUsers[remoteUserId].connectedWith[socket.userid];
listOfUsers[remoteUserId].socket.emit('user-disconnected', socket.userid);
sendToAdmin();
}
callback();
} catch (e) {
pushLogs('disconnect-with', e);
}
});
socket.on('close-entire-session', function(callback) {
try {
var connectedWith = listOfUsers[socket.userid].connectedWith;
Object.keys(connectedWith).forEach(function(key) {
if (connectedWith[key] && connectedWith[key].emit) {
try {
connectedWith[key].emit('closed-entire-session', socket.userid, listOfUsers[socket.userid].extra);
} catch (e) {}
}
});
callback();
} catch (e) {
pushLogs('close-entire-session', e);
}
});
socket.on('check-presence', function(roomid, callback) {
try {
if (!listOfRooms[roomid]) {
callback(false, roomid, {});
} else {
callback(true, roomid, listOfRooms[roomid].extra);
}
} catch (e) {
pushLogs('check-presence', e);
}
});
function onMessageCallback(message) {
try {
if (!listOfUsers[message.sender]) {
socket.emit('user-not-found', message.sender);
return;
}
if (!message.message.userLeft && !listOfUsers[message.sender].connectedWith[message.remoteUserId] && !!listOfUsers[message.remoteUserId]) {
listOfUsers[message.sender].connectedWith[message.remoteUserId] = listOfUsers[message.remoteUserId].socket;
listOfUsers[message.sender].socket.emit('user-connected', message.remoteUserId);
if (!listOfUsers[message.remoteUserId]) {
listOfUsers[message.remoteUserId] = {
socket: null,
connectedWith: {},
isPublic: false,
extra: {},
admininfo: {},
maxParticipantsAllowed: params.maxParticipantsAllowed || 1000
};
}
listOfUsers[message.remoteUserId].connectedWith[message.sender] = socket;
if (listOfUsers[message.remoteUserId].socket) {
listOfUsers[message.remoteUserId].socket.emit('user-connected', message.sender);
}
sendToAdmin();
}
if (listOfUsers[message.sender].connectedWith[message.remoteUserId] && listOfUsers[socket.userid]) {
message.extra = listOfUsers[socket.userid].extra;
listOfUsers[message.sender].connectedWith[message.remoteUserId].emit(socketMessageEvent, message);
sendToAdmin();
}
} catch (e) {
pushLogs('onMessageCallback', e);
}
}
function joinARoom(message) {
try {
// var roomid = message.remoteUserId;
var roomid = socket.admininfo.sessionid;
if (!listOfRooms[roomid]) return; // find a solution?
if (listOfRooms[roomid].participants.length > params.maxParticipantsAllowed) {
socket.emit('room-full', roomid);
return;
}
if (listOfRooms[roomid].session && (listOfRooms[roomid].session.oneway === true || listOfRooms[roomid].session.broadcast === true)) {
var owner = listOfRooms[roomid].owner;
if (listOfUsers[owner]) {
message.remoteUserId = owner;
if (enableScalableBroadcast === false) {
// only send to owner i.e. only connect with room owner
listOfUsers[owner].socket.emit(socketMessageEvent, message);
}
}
return;
}
// redundant?
// appendToRoom(roomid, socket.userid);
if (enableScalableBroadcast === false) {
// connect with all participants
listOfRooms[roomid].participants.forEach(function(pid) {
if (pid === socket.userid) return;
var user = listOfUsers[pid];
message.remoteUserId = pid;
user.socket.emit(socketMessageEvent, message);
});
}
} catch (e) {
pushLogs('joinARoom', e);
}
sendToAdmin();
}
function appendToRoom(roomid, userid) {
try {
if (!listOfRooms[roomid]) {
listOfRooms[roomid] = {
isPublic: params.isPublic || false,
maxParticipantsAllowed: params.maxParticipantsAllowed || 1000,
owner: userid, // this can change if owner leaves and if control shifts
participants: [userid],
session: {
audio: true,
video: true
}
};
}
if (listOfRooms[roomid].participants.indexOf(userid) !== -1) return;
listOfRooms[roomid].participants.push(userid);
} catch (e) {
pushLogs('appendToRoom', e);
}
}
function closeOrShiftRoom() {
try {
if (!socket.admininfo) {
return;
}
var roomid = socket.admininfo.sessionid;
if (roomid && listOfRooms[roomid]) {
if (socket.userid === listOfRooms[roomid].owner) {
if (autoCloseEntireSession === false && listOfRooms[roomid].participants.length > 1) {
var firstParticipant;
listOfRooms[roomid].participants.forEach(function(pid) {
if (firstParticipant || pid === socket.userid) return;
if (!listOfUsers[pid]) return;
firstParticipant = listOfUsers[pid];
});
if (firstParticipant) {
// reset owner priviliges
listOfRooms[roomid].owner = firstParticipant.socket.userid;
// "become-next-modrator" merely sets "connection.isInitiator=true"
// though it is not important; maybe below line is redundant?
firstParticipant.socket.emit('become-next-modrator', roomid);
// remove moderator from room's participants list
var newParticipantsList = [];
listOfRooms[roomid].participants.forEach(function(pid) {
if (pid != socket.userid) {
newParticipantsList.push(pid);
}
});
listOfRooms[roomid].participants = newParticipantsList;
} else {
delete listOfRooms[roomid];
}
} else {
delete listOfRooms[roomid];
}
} else {
var newParticipantsList = [];
listOfRooms[roomid].participants.forEach(function(pid) {
if (pid && pid != socket.userid && listOfUsers[pid]) {
newParticipantsList.push(pid);
}
});
listOfRooms[roomid].participants = newParticipantsList;
}
}
} catch (e) {
pushLogs('closeOrShiftRoom', e);
}
}
var numberOfPasswordTries = 0;
socket.on(socketMessageEvent, function(message, callback) {
if (message.remoteUserId && message.remoteUserId === socket.userid) {
// remoteUserId MUST be unique
return;
}
try {
if (message.remoteUserId && message.remoteUserId != 'system' && message.message.newParticipationRequest) {
if (listOfRooms[message.remoteUserId] && listOfRooms[message.remoteUserId].password) {
if (numberOfPasswordTries > 3) {
socket.emit('password-max-tries-over', message.remoteUserId);
return;
}
if (!message.password) {
numberOfPasswordTries++;
socket.emit('join-with-password', message.remoteUserId);
return;
}
if (message.password != listOfRooms[message.remoteUserId].password) {
numberOfPasswordTries++;
socket.emit('invalid-password', message.remoteUserId, message.password);
return;
}
}
if (enableScalableBroadcast === true) {
var user = listOfUsers[message.remoteUserId];
if (user) {
user.socket.emit(socketMessageEvent, message);
}
if (listOfUsers[socket.userid].extra.broadcastId) {
// for /admin/ page
appendToRoom(listOfUsers[socket.userid].extra.broadcastId, socket.userid);
}
} else if (listOfRooms[message.remoteUserId]) {
joinARoom(message);
return;
}
}
// for v3 backward compatibility; >v3.3.3 no more uses below block
if (message.remoteUserId == 'system') {
if (message.message.detectPresence) {
if (message.message.userid === socket.userid) {
callback(false, socket.userid);
return;
}
callback(!!listOfUsers[message.message.userid], message.message.userid);
return;
}
}
if (!listOfUsers[message.sender]) {
listOfUsers[message.sender] = {
socket: socket,
connectedWith: {},
isPublic: false,
extra: {},
admininfo: {},
maxParticipantsAllowed: params.maxParticipantsAllowed || 1000
};
}
// if someone tries to join a person who is absent
// -------------------------------------- DISABLED
if (false && message.message.newParticipationRequest) {
var waitFor = 60 * 10; // 10 minutes
var invokedTimes = 0;
(function repeater() {
if (typeof socket == 'undefined' || !listOfUsers[socket.userid]) {
return;
}
invokedTimes++;
if (invokedTimes > waitFor) {
socket.emit('user-not-found', message.remoteUserId);
return;
}
// if user just come online
if (listOfUsers[message.remoteUserId] && listOfUsers[message.remoteUserId].socket) {
joinARoom(message);
return;
}
setTimeout(repeater, 1000);
})();
return;
}
onMessageCallback(message);
} catch (e) {
pushLogs('on-socketMessageEvent', e);
}
});
socket.on('open-room', function(arg, callback) {
callback = callback || function() {};
try {
// if already joined a room, either leave or close it
closeOrShiftRoom();
if (listOfRooms[arg.sessionid]) {
callback(false, 'Room in use. Pease join instead or create another room.');
return;
}
if (enableScalableBroadcast === true) {
arg.session.scalable = true;
arg.sessionid = arg.extra.broadcastId;
}
// maybe redundant?
if (!listOfUsers[socket.userid]) {
listOfUsers[socket.userid] = {
socket: socket,
connectedWith: {},
isPublic: false, // means: isPublicModerator
extra: arg.extra,
admininfo: {},
maxParticipantsAllowed: params.maxParticipantsAllowed || 1000
};
}
listOfUsers[socket.userid].extra = arg.extra;
if (arg.session && (arg.session.oneway === true || arg.session.broadcast === true)) {
autoCloseEntireSession = true;
}
} catch (e) {
pushLogs('open-room', e);
}
// append this user into participants list
appendToRoom(arg.sessionid, socket.userid);
try {
// override owner & session
if (enableScalableBroadcast === true) {
if (Object.keys(listOfRooms[arg.sessionid]).length == 1) {
listOfRooms[arg.sessionid].owner = socket.userid;
listOfRooms[arg.sessionid].session = arg.session;
}
} else {
// for non-scalable-broadcast demos
listOfRooms[arg.sessionid].owner = socket.userid;
listOfRooms[arg.sessionid].session = arg.session;
if (typeof arg.password !== 'undefined' && arg.password.length) {
// password protected room?
listOfRooms[arg.sessionid].password = arg.password;
}
}
// admin info are shared only with /admin/
listOfUsers[socket.userid].socket.admininfo = {
sessionid: arg.sessionid,
session: arg.session,
mediaConstraints: arg.mediaConstraints,
sdpConstraints: arg.sdpConstraints,
streams: arg.streams,
extra: arg.extra
};
} catch (e) {
pushLogs('open-room', e);
}
sendToAdmin();
try {
callback(true);
} catch (e) {
pushLogs('open-room', e);
}
});
socket.on('join-room', function(arg, callback) {
callback = callback || function() {};
try {
// if already joined a room, either leave or close it
closeOrShiftRoom();
if (enableScalableBroadcast === true) {
arg.session.scalable = true;
arg.sessionid = arg.extra.broadcastId;
}
// maybe redundant?
if (!listOfUsers[socket.userid]) {
listOfUsers[socket.userid] = {
socket: socket,
connectedWith: {},
isPublic: false, // means: isPublicModerator
extra: arg.extra,
admininfo: {},
maxParticipantsAllowed: params.maxParticipantsAllowed || 1000
};
}
listOfUsers[socket.userid].extra = arg.extra;
} catch (e) {
pushLogs('join-room', e);
}
try {
if (!listOfRooms[arg.sessionid]) {
callback(false, 'Room does not exist.');
return;
}
if (listOfRooms[arg.sessionid].password && listOfRooms[arg.sessionid].password != arg.password) {
callback(false, 'Invalid password.');
return;
}
} catch (e) {
pushLogs('join-room', e);
}
// append this user into participants list
appendToRoom(arg.sessionid, socket.userid);
try {
// admin info are shared only with /admin/
listOfUsers[socket.userid].socket.admininfo = {
sessionid: arg.sessionid,
session: arg.session,
mediaConstraints: arg.mediaConstraints,
sdpConstraints: arg.sdpConstraints,
streams: arg.streams,
extra: arg.extra
};
} catch (e) {
pushLogs('join-room', e);
}
sendToAdmin();
try {
callback(true);
} catch (e) {
pushLogs('join-room', e);
}
});
socket.on('disconnect', function() {
try {
if (socket && socket.namespace && socket.namespace.sockets) {
delete socket.namespace.sockets[this.id];
}
} catch (e) {
pushLogs('disconnect', e);
}
try {
// inform all connected users
if (listOfUsers[socket.userid]) {
for (var s in listOfUsers[socket.userid].connectedWith) {
listOfUsers[socket.userid].connectedWith[s].emit('user-disconnected', socket.userid);
// sending duplicate message to same socket?
if (listOfUsers[s] && listOfUsers[s].connectedWith[socket.userid]) {
delete listOfUsers[s].connectedWith[socket.userid];
listOfUsers[s].socket.emit('user-disconnected', socket.userid);
}
}
}
} catch (e) {
pushLogs('disconnect', e);
}
closeOrShiftRoom();
delete listOfUsers[socket.userid];
if (socket.ondisconnect) {
// scalable-broadcast.js
socket.ondisconnect();
}
sendToAdmin();
});
if (socketCallback) {
socketCallback(socket);
}
}
};